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 all 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
73 changes: 54 additions & 19 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, { useCallback, useEffect, 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 @@ -26,25 +26,57 @@ import StyledLiveBeaconIcon from './StyledLiveBeaconIcon';
import { formatDuration } from '../../../DateUtils';
import { getBeaconMsUntilExpiry, sortBeaconsByLatestExpiry } from '../../../utils/beacon';
import Spinner from '../elements/Spinner';
import { useInterval } from '../../../hooks/useTimeout';

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(() => getBeaconMsUntilExpiry(beacon));

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

const updateMsRemaining = useCallback(() => {
const ms = getBeaconMsUntilExpiry(beacon);
setMsRemaining(ms);
}, [beacon]);

useInterval(updateMsRemaining, getUpdateInterval(msRemaining));

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 All @@ -57,7 +89,7 @@ const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
}, [liveBeaconIds]);

if (!liveBeaconIds?.length) {
return { liveBeaconIds };
return {};
}

// select the beacon with latest expiry to display expiry time
Expand All @@ -77,40 +109,43 @@ const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
}
};

const msRemaining = getBeaconMsUntilExpiry(beacon);
return { onStopSharing, beacon, stoppingInProgress };
};

const LiveTimeRemaining: React.FC<{ beacon: Beacon }> = ({ beacon }) => {
const msRemaining = useMsRemaining(beacon);

return { liveBeaconIds, onStopSharing, msRemaining, stoppingInProgress };
const timeRemaining = formatDuration(msRemaining);
const liveTimeRemaining = _t(`%(timeRemaining)s left`, { timeRemaining });

return <span
data-test-id='room-live-share-expiry'
className="mx_RoomLiveShareWarning_expiry"
>{ liveTimeRemaining }</span>;
};

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

if (!liveBeaconIds?.length) {
if (!beacon) {
return null;
}

const timeRemaining = formatDuration(msRemaining);
const liveTimeRemaining = _t(`%(timeRemaining)s left`, { timeRemaining });

return <div
className={classNames('mx_RoomLiveShareWarning')}
>
<StyledLiveBeaconIcon className="mx_RoomLiveShareWarning_icon" />
<span className="mx_RoomLiveShareWarning_label">
{ _t('You are sharing %(count)s live locations', { count: liveBeaconIds.length }) }
{ _t('You are sharing your live location') }
</span>

{ stoppingInProgress ?
<span className='mx_RoomLiveShareWarning_spinner'><Spinner h={16} w={16} /></span> :
<span
data-test-id='room-live-share-expiry'
className="mx_RoomLiveShareWarning_expiry"
>{ liveTimeRemaining }</span>
<LiveTimeRemaining beacon={beacon} />
}
<AccessibleButton
data-test-id='room-live-share-stop-sharing'
Expand Down
2 changes: 0 additions & 2 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2966,8 +2966,6 @@
"Join the beta": "Join the beta",
"You are sharing your live location": "You are sharing your live location",
"%(timeRemaining)s left": "%(timeRemaining)s left",
"You are sharing %(count)s live locations|other": "You are sharing %(count)s live locations",
"You are sharing %(count)s live locations|one": "You are sharing your live location",
"Stop sharing": "Stop sharing",
"Avatar": "Avatar",
"This room is public": "This room is public",
Expand Down
57 changes: 46 additions & 11 deletions test/components/views/beacon/RoomLiveShareWarning-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,17 @@ 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 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 });
const room1Beacon1 = makeBeaconInfoEvent(aliceId, room1Id, {
isLive: true,
timeout: HOUR_MS,
}, '$0');
const room2Beacon1 = makeBeaconInfoEvent(aliceId, room2Id, { isLive: true, timeout: HOUR_MS }, '$1');
const room2Beacon2 = makeBeaconInfoEvent(aliceId, room2Id, { isLive: true, timeout: HOUR_MS * 12 }, '$2');
const room3Beacon1 = makeBeaconInfoEvent(aliceId, room3Id, { isLive: true, timeout: HOUR_MS }, '$3');

// make fresh rooms every time
// as we update room state
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,29 @@ 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');
});

it('clears expiry time interval on unmount', () => {
const clearIntervalSpy = jest.spyOn(global, 'clearInterval');
const component = getComponent({ roomId: room1Id });
expect(getExpiryText(component)).toEqual('1h left');

act(() => {
component.unmount();
});

expect(clearIntervalSpy).toHaveBeenCalled();
});

describe('stopping beacons', () => {
it('stops beacon on stop sharing click', () => {
const component = getComponent({ roomId: room2Id });
Expand All @@ -184,25 +216,28 @@ describe('<RoomLiveShareWarning />', () => {

it('displays again with correct state after stopping a beacon', () => {
// make sure the loading state is reset correctly after removing a beacon
const component = getComponent({ roomId: room2Id });
const component = getComponent({ roomId: room1Id });

// stop the beacon
act(() => {
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));
});

const newLiveBeacon = makeBeaconInfoEvent(aliceId, room2Id, { isLive: true });
const newLiveBeacon = makeBeaconInfoEvent(aliceId, room1Id, { isLive: true });
act(() => {
mockClient.emit(BeaconEvent.New, newLiveBeacon, new Beacon(newLiveBeacon));
});

// button not disabled and expiry time shown
expect(findByTestId(component, 'room-live-share-stop-sharing').at(0).props().disabled).toBeFalsy();
expect(findByTestId(component, 'room-live-share-expiry').text()).toEqual('11h left');
expect(findByTestId(component, 'room-live-share-expiry').text()).toEqual('1h left');
});
});
});
Expand Down
Loading