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

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

Merged
merged 2 commits into from
Mar 30, 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
5 changes: 5 additions & 0 deletions res/css/components/views/beacon/_StyledLiveBeaconIcon.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@ limitations under the License.
// colors icon
color: white;
}

.mx_StyledLiveBeaconIcon.mx_StyledLiveBeaconIcon_error {
background-color: $alert;
border-color: $alert;
}
23 changes: 16 additions & 7 deletions src/components/views/beacon/RoomLiveShareWarning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Error>();

// do we have an active geolocation.watchPosition
const isMonitoringLiveLocation = useEventEmitterState(
Expand All @@ -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) {
Expand All @@ -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 }) => {
Expand All @@ -136,6 +140,7 @@ const RoomLiveShareWarning: React.FC<Props> = ({ roomId }) => {
onStopSharing,
beacon,
stoppingInProgress,
hasStopSharingError,
} = useLiveBeacons(roomId);

if (!beacon) {
Expand All @@ -145,23 +150,27 @@ const RoomLiveShareWarning: React.FC<Props> = ({ roomId }) => {
return <div
className={classNames('mx_RoomLiveShareWarning')}
>
<StyledLiveBeaconIcon className="mx_RoomLiveShareWarning_icon" />
<StyledLiveBeaconIcon className="mx_RoomLiveShareWarning_icon" withError={hasStopSharingError} />
<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>

{ stoppingInProgress ?
<span className='mx_RoomLiveShareWarning_spinner'><Spinner h={16} w={16} /></span> :
<LiveTimeRemaining beacon={beacon} />
{ stoppingInProgress &&
<span className='mx_RoomLiveShareWarning_spinner'><Spinner h={16} w={16} /></span>
}
{ !stoppingInProgress && !hasStopSharingError && <LiveTimeRemaining beacon={beacon} /> }

<AccessibleButton
data-test-id='room-live-share-stop-sharing'
onClick={onStopSharing}
kind='danger'
element='button'
disabled={stoppingInProgress}
>
{ _t('Stop sharing') }
{ hasStopSharingError ? _t('Retry') : _t('Stop sharing') }
</AccessibleButton>
</div>;
};
Expand Down
8 changes: 6 additions & 2 deletions src/components/views/beacon/StyledLiveBeaconIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ import classNames from 'classnames';

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
{...props}
className={classNames('mx_StyledLiveBeaconIcon', className)}
className={classNames('mx_StyledLiveBeaconIcon', className, { 'mx_StyledLiveBeaconIcon_error': withError })}
/>;

export default StyledLiveBeaconIcon;
1 change: 1 addition & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 12 additions & 3 deletions src/stores/OwnBeaconStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const STATIC_UPDATE_INTERVAL = 30000;

type OwnBeaconStoreState = {
beacons: Map<string, Beacon>;
beaconWireErrors: Map<string, Beacon>;
beaconsByRoomId: Map<Room['roomId'], Set<string>>;
liveBeaconIds: string[];
};
Expand All @@ -63,6 +64,10 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
// users beacons, keyed by event type
public readonly beacons = new Map<string, Beacon>();
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 locationInterval: number;
private geolocationError: GeolocationError | undefined;
Expand Down Expand Up @@ -101,6 +106,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
this.beacons.clear();
this.beaconsByRoomId.clear();
this.liveBeaconIds = [];
this.beaconWireErrors.clear();
}

protected async onReady(): Promise<void> {
Expand Down Expand Up @@ -362,7 +368,6 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
private publishCurrentLocationToBeacons = async () => {
try {
const position = await getCurrentPosition();
// TODO error handling
this.publishLocationToBeacons(mapGeolocationPositionToTimedGeo(position));
} catch (error) {
this.onGeolocationError(error?.message);
Expand Down Expand Up @@ -394,7 +399,6 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
*/
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)),
);
Expand All @@ -407,6 +411,11 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
*/
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);
}
};
}
27 changes: 26 additions & 1 deletion test/components/views/beacon/RoomLiveShareWarning-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../../src/stores/OwnB
import {
advanceDateAndTime,
findByTestId,
flushPromisesWithFakeTimers,
getMockClientWithEventEmitter,
makeBeaconInfoEvent,
mockGeolocation,
Expand Down Expand Up @@ -96,7 +97,7 @@ describe('<RoomLiveShareWarning />', () => {
beforeEach(() => {
mockGeolocation();
jest.spyOn(global.Date, 'now').mockReturnValue(now);
mockClient.unstable_setLiveBeacon.mockClear();
mockClient.unstable_setLiveBeacon.mockReset().mockResolvedValue({ event_id: '1' });
});

afterEach(async () => {
Expand Down Expand Up @@ -246,6 +247,30 @@ describe('<RoomLiveShareWarning />', () => {
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 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 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>"`;