Live location sharing: allow retry when stop sharing fails (#8193)

* allow retry when stop sharing fails

Signed-off-by: Kerry Archibald <kerrya@element.io>

* tidy

Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
Kerry 2022-03-30 14:31:19 +02:00 committed by GitHub
parent be8665af4d
commit e721c6b0c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 68 additions and 13 deletions

View file

@ -28,3 +28,8 @@ limitations under the License.
// colors icon // colors icon
color: white; color: white;
} }
.mx_StyledLiveBeaconIcon.mx_StyledLiveBeaconIcon_error {
background-color: $alert;
border-color: $alert;
}

View file

@ -73,9 +73,11 @@ type LiveBeaconsState = {
beacon?: Beacon; beacon?: Beacon;
onStopSharing?: () => void; onStopSharing?: () => void;
stoppingInProgress?: boolean; stoppingInProgress?: boolean;
hasStopSharingError?: boolean;
}; };
const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => { const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
const [stoppingInProgress, setStoppingInProgress] = useState(false); const [stoppingInProgress, setStoppingInProgress] = useState(false);
const [error, setError] = useState<Error>();
// do we have an active geolocation.watchPosition // do we have an active geolocation.watchPosition
const isMonitoringLiveLocation = useEventEmitterState( const isMonitoringLiveLocation = useEventEmitterState(
@ -93,6 +95,7 @@ const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
// reset stopping in progress on change in live ids // reset stopping in progress on change in live ids
useEffect(() => { useEffect(() => {
setStoppingInProgress(false); setStoppingInProgress(false);
setError(undefined);
}, [liveBeaconIds]); }, [liveBeaconIds]);
if (!isMonitoringLiveLocation || !liveBeaconIds?.length) { if (!isMonitoringLiveLocation || !liveBeaconIds?.length) {
@ -112,11 +115,12 @@ const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
// only clear loading in case of error // only clear loading in case of error
// to avoid flash of not-loading state // to avoid flash of not-loading state
// after beacons have been stopped but we wait for sync // after beacons have been stopped but we wait for sync
setError(error);
setStoppingInProgress(false); setStoppingInProgress(false);
} }
}; };
return { onStopSharing, beacon, stoppingInProgress }; return { onStopSharing, beacon, stoppingInProgress, hasStopSharingError: !!error };
}; };
const LiveTimeRemaining: React.FC<{ beacon: Beacon }> = ({ beacon }) => { const LiveTimeRemaining: React.FC<{ beacon: Beacon }> = ({ beacon }) => {
@ -136,6 +140,7 @@ const RoomLiveShareWarning: React.FC<Props> = ({ roomId }) => {
onStopSharing, onStopSharing,
beacon, beacon,
stoppingInProgress, stoppingInProgress,
hasStopSharingError,
} = useLiveBeacons(roomId); } = useLiveBeacons(roomId);
if (!beacon) { if (!beacon) {
@ -145,15 +150,19 @@ const RoomLiveShareWarning: React.FC<Props> = ({ roomId }) => {
return <div return <div
className={classNames('mx_RoomLiveShareWarning')} className={classNames('mx_RoomLiveShareWarning')}
> >
<StyledLiveBeaconIcon className="mx_RoomLiveShareWarning_icon" /> <StyledLiveBeaconIcon className="mx_RoomLiveShareWarning_icon" withError={hasStopSharingError} />
<span className="mx_RoomLiveShareWarning_label"> <span className="mx_RoomLiveShareWarning_label">
{ _t('You are sharing your live location') } { hasStopSharingError ?
_t('An error occurred while stopping your live location, please try again') :
_t('You are sharing your live location')
}
</span> </span>
{ stoppingInProgress ? { stoppingInProgress &&
<span className='mx_RoomLiveShareWarning_spinner'><Spinner h={16} w={16} /></span> : <span className='mx_RoomLiveShareWarning_spinner'><Spinner h={16} w={16} /></span>
<LiveTimeRemaining beacon={beacon} />
} }
{ !stoppingInProgress && !hasStopSharingError && <LiveTimeRemaining beacon={beacon} /> }
<AccessibleButton <AccessibleButton
data-test-id='room-live-share-stop-sharing' data-test-id='room-live-share-stop-sharing'
onClick={onStopSharing} onClick={onStopSharing}
@ -161,7 +170,7 @@ const RoomLiveShareWarning: React.FC<Props> = ({ roomId }) => {
element='button' element='button'
disabled={stoppingInProgress} disabled={stoppingInProgress}
> >
{ _t('Stop sharing') } { hasStopSharingError ? _t('Retry') : _t('Stop sharing') }
</AccessibleButton> </AccessibleButton>
</div>; </div>;
}; };

View file

@ -19,10 +19,14 @@ import classNames from 'classnames';
import { Icon as LiveLocationIcon } from '../../../../res/img/location/live-location.svg'; import { Icon as LiveLocationIcon } from '../../../../res/img/location/live-location.svg';
const StyledLiveBeaconIcon: React.FC<React.SVGProps<SVGSVGElement>> = ({ className, ...props }) => interface Props extends React.SVGProps<SVGSVGElement> {
// use error styling when true
withError?: boolean;
}
const StyledLiveBeaconIcon: React.FC<Props> = ({ className, withError, ...props }) =>
<LiveLocationIcon <LiveLocationIcon
{...props} {...props}
className={classNames('mx_StyledLiveBeaconIcon', className)} className={classNames('mx_StyledLiveBeaconIcon', className, { 'mx_StyledLiveBeaconIcon_error': withError })}
/>; />;
export default StyledLiveBeaconIcon; export default StyledLiveBeaconIcon;

View file

@ -2898,6 +2898,7 @@
"Join the beta": "Join the beta", "Join the beta": "Join the beta",
"You are sharing your live location": "You are sharing your live location", "You are sharing your live location": "You are sharing your live location",
"%(timeRemaining)s left": "%(timeRemaining)s left", "%(timeRemaining)s left": "%(timeRemaining)s left",
"An error occurred while stopping your live location, please try again": "An error occurred while stopping your live location, please try again",
"Stop sharing": "Stop sharing", "Stop sharing": "Stop sharing",
"Avatar": "Avatar", "Avatar": "Avatar",
"This room is public": "This room is public", "This room is public": "This room is public",

View file

@ -55,6 +55,7 @@ const STATIC_UPDATE_INTERVAL = 30000;
type OwnBeaconStoreState = { type OwnBeaconStoreState = {
beacons: Map<string, Beacon>; beacons: Map<string, Beacon>;
beaconWireErrors: Map<string, Beacon>;
beaconsByRoomId: Map<Room['roomId'], Set<string>>; beaconsByRoomId: Map<Room['roomId'], Set<string>>;
liveBeaconIds: string[]; liveBeaconIds: string[];
}; };
@ -63,6 +64,10 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
// users beacons, keyed by event type // users beacons, keyed by event type
public readonly beacons = new Map<string, Beacon>(); public readonly beacons = new Map<string, Beacon>();
public readonly beaconsByRoomId = new Map<Room['roomId'], Set<string>>(); public readonly beaconsByRoomId = new Map<Room['roomId'], Set<string>>();
/**
* Track over the wire errors for beacons
*/
public readonly beaconWireErrors = new Map<string, Error>();
private liveBeaconIds = []; private liveBeaconIds = [];
private locationInterval: number; private locationInterval: number;
private geolocationError: GeolocationError | undefined; private geolocationError: GeolocationError | undefined;
@ -101,6 +106,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
this.beacons.clear(); this.beacons.clear();
this.beaconsByRoomId.clear(); this.beaconsByRoomId.clear();
this.liveBeaconIds = []; this.liveBeaconIds = [];
this.beaconWireErrors.clear();
} }
protected async onReady(): Promise<void> { protected async onReady(): Promise<void> {
@ -362,7 +368,6 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
private publishCurrentLocationToBeacons = async () => { private publishCurrentLocationToBeacons = async () => {
try { try {
const position = await getCurrentPosition(); const position = await getCurrentPosition();
// TODO error handling
this.publishLocationToBeacons(mapGeolocationPositionToTimedGeo(position)); this.publishLocationToBeacons(mapGeolocationPositionToTimedGeo(position));
} catch (error) { } catch (error) {
this.onGeolocationError(error?.message); this.onGeolocationError(error?.message);
@ -394,7 +399,6 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
*/ */
private publishLocationToBeacons = async (position: TimedGeoUri) => { private publishLocationToBeacons = async (position: TimedGeoUri) => {
this.lastPublishedPositionTimestamp = Date.now(); this.lastPublishedPositionTimestamp = Date.now();
// TODO handle failure in individual beacon without rejecting rest
await Promise.all(this.liveBeaconIds.map(beaconId => await Promise.all(this.liveBeaconIds.map(beaconId =>
this.sendLocationToBeacon(this.beacons.get(beaconId), position)), this.sendLocationToBeacon(this.beacons.get(beaconId), position)),
); );
@ -407,6 +411,11 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
*/ */
private sendLocationToBeacon = async (beacon: Beacon, { geoUri, timestamp }: TimedGeoUri) => { private sendLocationToBeacon = async (beacon: Beacon, { geoUri, timestamp }: TimedGeoUri) => {
const content = makeBeaconContent(geoUri, timestamp, beacon.beaconInfoId); const content = makeBeaconContent(geoUri, timestamp, beacon.beaconInfoId);
await this.matrixClient.sendEvent(beacon.roomId, M_BEACON.name, content); try {
await this.matrixClient.sendEvent(beacon.roomId, M_BEACON.name, content);
} catch (error) {
logger.error(error);
this.beaconWireErrors.set(beacon.identifier, error);
}
}; };
} }

View file

@ -26,6 +26,7 @@ import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../../src/stores/OwnB
import { import {
advanceDateAndTime, advanceDateAndTime,
findByTestId, findByTestId,
flushPromisesWithFakeTimers,
getMockClientWithEventEmitter, getMockClientWithEventEmitter,
makeBeaconInfoEvent, makeBeaconInfoEvent,
mockGeolocation, mockGeolocation,
@ -96,7 +97,7 @@ describe('<RoomLiveShareWarning />', () => {
beforeEach(() => { beforeEach(() => {
mockGeolocation(); mockGeolocation();
jest.spyOn(global.Date, 'now').mockReturnValue(now); jest.spyOn(global.Date, 'now').mockReturnValue(now);
mockClient.unstable_setLiveBeacon.mockClear(); mockClient.unstable_setLiveBeacon.mockReset().mockResolvedValue({ event_id: '1' });
}); });
afterEach(async () => { afterEach(async () => {
@ -246,6 +247,30 @@ describe('<RoomLiveShareWarning />', () => {
expect(findByTestId(component, 'room-live-share-stop-sharing').at(0).props().disabled).toBeTruthy(); expect(findByTestId(component, 'room-live-share-stop-sharing').at(0).props().disabled).toBeTruthy();
}); });
it('displays error when stop sharing fails', async () => {
const component = getComponent({ roomId: room1Id });
// fail first time
mockClient.unstable_setLiveBeacon
.mockRejectedValueOnce(new Error('oups'))
.mockResolvedValue(({ event_id: '1' }));
await act(async () => {
findByTestId(component, 'room-live-share-stop-sharing').at(0).simulate('click');
await flushPromisesWithFakeTimers();
});
component.setProps({});
expect(component.html()).toMatchSnapshot();
act(() => {
findByTestId(component, 'room-live-share-stop-sharing').at(0).simulate('click');
component.setProps({});
});
expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledTimes(2);
});
it('displays again with correct state after stopping a beacon', () => { it('displays again with correct state after stopping a beacon', () => {
// make sure the loading state is reset correctly after removing a beacon // make sure the loading state is reset correctly after removing a beacon
const component = getComponent({ roomId: room1Id }); const component = getComponent({ roomId: room1Id });

View file

@ -3,3 +3,5 @@
exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is available renders correctly with one live beacon in room 1`] = `"<div class=\\"mx_RoomLiveShareWarning\\"><div class=\\"mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon\\"></div><span class=\\"mx_RoomLiveShareWarning_label\\">You are sharing your live location</span><span data-test-id=\\"room-live-share-expiry\\" class=\\"mx_RoomLiveShareWarning_expiry\\">1h left</span><button data-test-id=\\"room-live-share-stop-sharing\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger\\">Stop sharing</button></div>"`; exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is available renders correctly with one live beacon in room 1`] = `"<div class=\\"mx_RoomLiveShareWarning\\"><div class=\\"mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon\\"></div><span class=\\"mx_RoomLiveShareWarning_label\\">You are sharing your live location</span><span data-test-id=\\"room-live-share-expiry\\" class=\\"mx_RoomLiveShareWarning_expiry\\">1h left</span><button data-test-id=\\"room-live-share-stop-sharing\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger\\">Stop sharing</button></div>"`;
exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is available renders correctly with two live beacons in room 1`] = `"<div class=\\"mx_RoomLiveShareWarning\\"><div class=\\"mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon\\"></div><span class=\\"mx_RoomLiveShareWarning_label\\">You are sharing your live location</span><span data-test-id=\\"room-live-share-expiry\\" class=\\"mx_RoomLiveShareWarning_expiry\\">12h left</span><button data-test-id=\\"room-live-share-stop-sharing\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger\\">Stop sharing</button></div>"`; exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is available renders correctly with two live beacons in room 1`] = `"<div class=\\"mx_RoomLiveShareWarning\\"><div class=\\"mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon\\"></div><span class=\\"mx_RoomLiveShareWarning_label\\">You are sharing your live location</span><span data-test-id=\\"room-live-share-expiry\\" class=\\"mx_RoomLiveShareWarning_expiry\\">12h left</span><button data-test-id=\\"room-live-share-stop-sharing\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger\\">Stop sharing</button></div>"`;
exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is available stopping beacons displays error when stop sharing fails 1`] = `"<div class=\\"mx_RoomLiveShareWarning\\"><div class=\\"mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon mx_StyledLiveBeaconIcon_error\\"></div><span class=\\"mx_RoomLiveShareWarning_label\\">An error occurred while stopping your live location, please try again</span><button data-test-id=\\"room-live-share-stop-sharing\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger\\">Retry</button></div>"`;