Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Live location sharing - refresh beacon expiry in room #8116

Merged
merged 20 commits into from
Mar 23, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions res/css/components/views/beacon/_RoomLiveShareWarning.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,10 @@ limitations under the License.
.mx_RoomLiveShareWarning_spinner {
margin-right: $spacing-16;
}


.mx_RoomLiveShareWarning_expiry {
color: $secondary-content;
font-size: $font-12px;
margin-right: $spacing-16;
}
54 changes: 46 additions & 8 deletions src/components/views/beacon/RoomLiveShareWarning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import { Room } from 'matrix-js-sdk/src/matrix';
import { Room, Beacon } from 'matrix-js-sdk/src/matrix';

import { _t } from '../../../languageHandler';
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
Expand All @@ -31,20 +31,58 @@ interface Props {
roomId: Room['roomId'];
}

const MINUTE_MS = 60000;
const HOUR_MS = MINUTE_MS * 60;

const getUpdateInterval = (ms: number) => {
// every 10 mins when more than an hour
if (ms > HOUR_MS) {
return MINUTE_MS * 10;
}
// every minute when more than a minute
if (ms > MINUTE_MS) {
return MINUTE_MS;
}
// otherwise every second
return 1000;
};
const useMsRemaining = (beacon?: Beacon): number => {
const [msRemaining, setMsRemaining] = useState(() => beacon ? getBeaconMsUntilExpiry(beacon) : 0);
const updateExpiryTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);

useEffect(() => {
setMsRemaining(beacon ? getBeaconMsUntilExpiry(beacon) : 0);
}, [beacon]);

useEffect(() => {
clearTimeout(updateExpiryTimeoutRef.current);
if (beacon) {
updateExpiryTimeoutRef.current = window.setTimeout(() => {
const ms = getBeaconMsUntilExpiry(beacon);
setMsRemaining(ms);
},
getUpdateInterval(msRemaining),
);
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
}
}, [msRemaining, setMsRemaining, beacon]);
t3chguy marked this conversation as resolved.
Show resolved Hide resolved

return msRemaining;
};

/**
* It's technically possible to have multiple live beacons in one room
* Select the latest expiry to display,
* and kill all beacons on stop sharing
*/
type LiveBeaconsState = {
liveBeaconIds: string[];
msRemaining?: number;
beacon?: Beacon;
onStopSharing?: () => void;
stoppingInProgress?: boolean;
};

const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
const [stoppingInProgress, setStoppingInProgress] = useState(false);

const liveBeaconIds = useEventEmitterState(
OwnBeaconStore.instance,
OwnBeaconStoreEvent.LivenessChange,
Expand Down Expand Up @@ -77,19 +115,19 @@ const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
}
};

const msRemaining = getBeaconMsUntilExpiry(beacon);

return { liveBeaconIds, onStopSharing, msRemaining, stoppingInProgress };
return { liveBeaconIds, onStopSharing, beacon, stoppingInProgress };
};

const RoomLiveShareWarning: React.FC<Props> = ({ roomId }) => {
const {
liveBeaconIds,
onStopSharing,
msRemaining,
beacon,
stoppingInProgress,
} = useLiveBeacons(roomId);

const msRemaining = useMsRemaining(beacon);

if (!liveBeaconIds?.length) {
return null;
}
Expand Down
32 changes: 27 additions & 5 deletions test/components/views/beacon/RoomLiveShareWarning-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,14 @@ describe('<RoomLiveShareWarning />', () => {

// 14.03.2022 16:15
const now = 1647270879403;
const MINUTE_MS = 60000;
const HOUR_MS = 3600000;
// mock the date so events are stable for snapshots etc
jest.spyOn(global.Date, 'now').mockReturnValue(now);
const room1Beacon1 = makeBeaconInfoEvent(aliceId, room1Id, { isLive: true, timeout: HOUR_MS });
const room1Beacon1 = makeBeaconInfoEvent(aliceId, room1Id, {
isLive: true,
timeout: HOUR_MS,
});
const room2Beacon1 = makeBeaconInfoEvent(aliceId, room2Id, { isLive: true, timeout: HOUR_MS });
const room2Beacon2 = makeBeaconInfoEvent(aliceId, room2Id, { isLive: true, timeout: HOUR_MS * 12 });
const room3Beacon1 = makeBeaconInfoEvent(aliceId, room3Id, { isLive: true, timeout: HOUR_MS });
Expand All @@ -67,7 +71,8 @@ describe('<RoomLiveShareWarning />', () => {

const advanceDateAndTime = (ms: number) => {
// bc liveness check uses Date.now we have to advance this mock
jest.spyOn(global.Date, 'now').mockReturnValue(now + ms);
jest.spyOn(global.Date, 'now').mockReturnValue(Date.now() + ms);

// then advance time for the interval by the same amount
jest.advanceTimersByTime(ms);
};
Expand Down Expand Up @@ -105,6 +110,8 @@ describe('<RoomLiveShareWarning />', () => {
jest.spyOn(global.Date, 'now').mockRestore();
});

const getExpiryText = wrapper => findByTestId(wrapper, 'room-live-share-expiry').text();

it('renders nothing when user has no live beacons at all', async () => {
await makeOwnBeaconStore();
const component = getComponent();
Expand Down Expand Up @@ -137,7 +144,7 @@ describe('<RoomLiveShareWarning />', () => {
const component = getComponent({ roomId: room2Id });
expect(component).toMatchSnapshot();
// later expiry displayed
expect(findByTestId(component, 'room-live-share-expiry').text()).toEqual('12h left');
expect(getExpiryText(component)).toEqual('12h left');
});

it('removes itself when user stops having live beacons', async () => {
Expand All @@ -146,7 +153,9 @@ describe('<RoomLiveShareWarning />', () => {
expect(component.html()).toBeTruthy();

// time travel until room1Beacon1 is expired
advanceDateAndTime(HOUR_MS + 1);
act(() => {
advanceDateAndTime(HOUR_MS + 1);
});
act(() => {
mockClient.emit(BeaconEvent.LivenessChange, false, new Beacon(room1Beacon1));
component.setProps({});
Expand All @@ -168,6 +177,17 @@ describe('<RoomLiveShareWarning />', () => {
expect(component.html()).toBeTruthy();
});

it('updates beacon time left periodically', () => {
const component = getComponent({ roomId: room1Id });
expect(getExpiryText(component)).toEqual('1h left');

act(() => {
advanceDateAndTime(MINUTE_MS * 25);
});

expect(getExpiryText(component)).toEqual('35m left');
});

describe('stopping beacons', () => {
it('stops beacon on stop sharing click', () => {
const component = getComponent({ roomId: room2Id });
Expand All @@ -190,7 +210,9 @@ describe('<RoomLiveShareWarning />', () => {
findByTestId(component, 'room-live-share-stop-sharing').at(0).simulate('click');
});
// time travel until room1Beacon1 is expired
advanceDateAndTime(HOUR_MS + 1);
act(() => {
advanceDateAndTime(HOUR_MS + 1);
});
act(() => {
mockClient.emit(BeaconEvent.LivenessChange, false, new Beacon(room1Beacon1));
});
Expand Down