diff --git a/res/css/components/views/beacon/_StyledLiveBeaconIcon.scss b/res/css/components/views/beacon/_StyledLiveBeaconIcon.scss index 2ff597efc32..e31279c34bf 100644 --- a/res/css/components/views/beacon/_StyledLiveBeaconIcon.scss +++ b/res/css/components/views/beacon/_StyledLiveBeaconIcon.scss @@ -28,3 +28,8 @@ limitations under the License. // colors icon color: white; } + +.mx_StyledLiveBeaconIcon.mx_StyledLiveBeaconIcon_error { + background-color: $alert; + border-color: $alert; +} diff --git a/src/components/views/beacon/RoomLiveShareWarning.tsx b/src/components/views/beacon/RoomLiveShareWarning.tsx index 24c0b345cce..0f6b0ee8090 100644 --- a/src/components/views/beacon/RoomLiveShareWarning.tsx +++ b/src/components/views/beacon/RoomLiveShareWarning.tsx @@ -73,9 +73,11 @@ type LiveBeaconsState = { beacon?: Beacon; onStopSharing?: () => void; stoppingInProgress?: boolean; + hasStopSharingError?: boolean; }; const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => { const [stoppingInProgress, setStoppingInProgress] = useState(false); + const [error, setError] = useState(); // do we have an active geolocation.watchPosition const isMonitoringLiveLocation = useEventEmitterState( @@ -93,6 +95,7 @@ const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => { // reset stopping in progress on change in live ids useEffect(() => { setStoppingInProgress(false); + setError(undefined); }, [liveBeaconIds]); if (!isMonitoringLiveLocation || !liveBeaconIds?.length) { @@ -112,11 +115,12 @@ const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => { // only clear loading in case of error // to avoid flash of not-loading state // after beacons have been stopped but we wait for sync + setError(error); setStoppingInProgress(false); } }; - return { onStopSharing, beacon, stoppingInProgress }; + return { onStopSharing, beacon, stoppingInProgress, hasStopSharingError: !!error }; }; const LiveTimeRemaining: React.FC<{ beacon: Beacon }> = ({ beacon }) => { @@ -136,6 +140,7 @@ const RoomLiveShareWarning: React.FC = ({ roomId }) => { onStopSharing, beacon, stoppingInProgress, + hasStopSharingError, } = useLiveBeacons(roomId); if (!beacon) { @@ -145,15 +150,19 @@ const RoomLiveShareWarning: React.FC = ({ roomId }) => { return
- + - { _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') + } - { stoppingInProgress ? - : - + { stoppingInProgress && + } + { !stoppingInProgress && !hasStopSharingError && } + = ({ roomId }) => { element='button' disabled={stoppingInProgress} > - { _t('Stop sharing') } + { hasStopSharingError ? _t('Retry') : _t('Stop sharing') }
; }; diff --git a/src/components/views/beacon/StyledLiveBeaconIcon.tsx b/src/components/views/beacon/StyledLiveBeaconIcon.tsx index 1628b47edc3..9c011446710 100644 --- a/src/components/views/beacon/StyledLiveBeaconIcon.tsx +++ b/src/components/views/beacon/StyledLiveBeaconIcon.tsx @@ -19,10 +19,14 @@ import classNames from 'classnames'; import { Icon as LiveLocationIcon } from '../../../../res/img/location/live-location.svg'; -const StyledLiveBeaconIcon: React.FC> = ({ className, ...props }) => +interface Props extends React.SVGProps { + // use error styling when true + withError?: boolean; +} +const StyledLiveBeaconIcon: React.FC = ({ className, withError, ...props }) => ; export default StyledLiveBeaconIcon; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 69e2773b535..eee6e7075c1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2898,6 +2898,7 @@ "Join the beta": "Join the beta", "You are sharing your live location": "You are sharing your live location", "%(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", "Avatar": "Avatar", "This room is public": "This room is public", diff --git a/src/stores/OwnBeaconStore.ts b/src/stores/OwnBeaconStore.ts index 34049982879..b6ad63b9c75 100644 --- a/src/stores/OwnBeaconStore.ts +++ b/src/stores/OwnBeaconStore.ts @@ -55,6 +55,7 @@ const STATIC_UPDATE_INTERVAL = 30000; type OwnBeaconStoreState = { beacons: Map; + beaconWireErrors: Map; beaconsByRoomId: Map>; liveBeaconIds: string[]; }; @@ -63,6 +64,10 @@ export class OwnBeaconStore extends AsyncStoreWithClient { // users beacons, keyed by event type public readonly beacons = new Map(); public readonly beaconsByRoomId = new Map>(); + /** + * Track over the wire errors for beacons + */ + public readonly beaconWireErrors = new Map(); private liveBeaconIds = []; private locationInterval: number; private geolocationError: GeolocationError | undefined; @@ -101,6 +106,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient { this.beacons.clear(); this.beaconsByRoomId.clear(); this.liveBeaconIds = []; + this.beaconWireErrors.clear(); } protected async onReady(): Promise { @@ -362,7 +368,6 @@ export class OwnBeaconStore extends AsyncStoreWithClient { private publishCurrentLocationToBeacons = async () => { try { const position = await getCurrentPosition(); - // TODO error handling this.publishLocationToBeacons(mapGeolocationPositionToTimedGeo(position)); } catch (error) { this.onGeolocationError(error?.message); @@ -394,7 +399,6 @@ export class OwnBeaconStore extends AsyncStoreWithClient { */ private publishLocationToBeacons = async (position: TimedGeoUri) => { this.lastPublishedPositionTimestamp = Date.now(); - // TODO handle failure in individual beacon without rejecting rest await Promise.all(this.liveBeaconIds.map(beaconId => this.sendLocationToBeacon(this.beacons.get(beaconId), position)), ); @@ -407,6 +411,11 @@ export class OwnBeaconStore extends AsyncStoreWithClient { */ private sendLocationToBeacon = async (beacon: Beacon, { geoUri, timestamp }: TimedGeoUri) => { 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); + } }; } diff --git a/test/components/views/beacon/RoomLiveShareWarning-test.tsx b/test/components/views/beacon/RoomLiveShareWarning-test.tsx index 93835c21a36..d549e9a51ee 100644 --- a/test/components/views/beacon/RoomLiveShareWarning-test.tsx +++ b/test/components/views/beacon/RoomLiveShareWarning-test.tsx @@ -26,6 +26,7 @@ import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../../src/stores/OwnB import { advanceDateAndTime, findByTestId, + flushPromisesWithFakeTimers, getMockClientWithEventEmitter, makeBeaconInfoEvent, mockGeolocation, @@ -96,7 +97,7 @@ describe('', () => { beforeEach(() => { mockGeolocation(); jest.spyOn(global.Date, 'now').mockReturnValue(now); - mockClient.unstable_setLiveBeacon.mockClear(); + mockClient.unstable_setLiveBeacon.mockReset().mockResolvedValue({ event_id: '1' }); }); afterEach(async () => { @@ -246,6 +247,30 @@ describe('', () => { 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', () => { // make sure the loading state is reset correctly after removing a beacon const component = getComponent({ roomId: room1Id }); diff --git a/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap b/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap index 786827fdea0..0f765124456 100644 --- a/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap @@ -3,3 +3,5 @@ exports[` when user has live beacons and geolocation is available renders correctly with one live beacon in room 1`] = `"
You are sharing your live location1h left
"`; exports[` when user has live beacons and geolocation is available renders correctly with two live beacons in room 1`] = `"
You are sharing your live location12h left
"`; + +exports[` when user has live beacons and geolocation is available stopping beacons displays error when stop sharing fails 1`] = `"
An error occurred while stopping your live location, please try again
"`;