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:
parent
be8665af4d
commit
e721c6b0c2
7 changed files with 68 additions and 13 deletions
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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);
|
||||||
|
try {
|
||||||
await this.matrixClient.sendEvent(beacon.roomId, M_BEACON.name, content);
|
await this.matrixClient.sendEvent(beacon.roomId, M_BEACON.name, content);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
this.beaconWireErrors.set(beacon.identifier, error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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>"`;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue