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

Commit

Permalink
Live location sharing - handle geolocation errors (#8179)
Browse files Browse the repository at this point in the history
* display live share warning only when geolocation is happening

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

* kill beacons when geolocation is unavailable or permissions denied

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

* polish and comments

Signed-off-by: Kerry Archibald <kerrya@element.io>
  • Loading branch information
Kerry authored Mar 28, 2022
1 parent 2520d81 commit d2b97e2
Show file tree
Hide file tree
Showing 12 changed files with 291 additions and 64 deletions.
8 changes: 4 additions & 4 deletions src/components/views/beacon/LeftPanelLiveShareWarning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ interface Props {
}

const LeftPanelLiveShareWarning: React.FC<Props> = ({ isMinimized }) => {
const hasLiveBeacons = useEventEmitterState(
const isMonitoringLiveLocation = useEventEmitterState(
OwnBeaconStore.instance,
OwnBeaconStoreEvent.LivenessChange,
() => OwnBeaconStore.instance.hasLiveBeacons(),
OwnBeaconStoreEvent.MonitoringLivePosition,
() => OwnBeaconStore.instance.isMonitoringLiveLocation,
);

if (!hasLiveBeacons) {
if (!isMonitoringLiveLocation) {
return null;
}

Expand Down
9 changes: 8 additions & 1 deletion src/components/views/beacon/RoomLiveShareWarning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ type LiveBeaconsState = {
const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
const [stoppingInProgress, setStoppingInProgress] = useState(false);

// do we have an active geolocation.watchPosition
const isMonitoringLiveLocation = useEventEmitterState(
OwnBeaconStore.instance,
OwnBeaconStoreEvent.MonitoringLivePosition,
() => OwnBeaconStore.instance.isMonitoringLiveLocation,
);

const liveBeaconIds = useEventEmitterState(
OwnBeaconStore.instance,
OwnBeaconStoreEvent.LivenessChange,
Expand All @@ -88,7 +95,7 @@ const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
setStoppingInProgress(false);
}, [liveBeaconIds]);

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

Expand Down
57 changes: 45 additions & 12 deletions src/stores/OwnBeaconStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const isOwnBeacon = (beacon: Beacon, userId: string): boolean => beacon.beaconIn

export enum OwnBeaconStoreEvent {
LivenessChange = 'OwnBeaconStore.LivenessChange',
MonitoringLivePosition = 'OwnBeaconStore.MonitoringLivePosition',
}

const MOVING_UPDATE_INTERVAL = 2000;
Expand Down Expand Up @@ -232,18 +233,28 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
await this.matrixClient.unstable_setLiveBeacon(beacon.roomId, beacon.beaconInfoEventType, updateContent);
};

private togglePollingLocation = async (): Promise<void> => {
private togglePollingLocation = () => {
if (!!this.liveBeaconIds.length) {
return this.startPollingLocation();
this.startPollingLocation();
} else {
this.stopPollingLocation();
}
return this.stopPollingLocation();
};

private startPollingLocation = async () => {
// clear any existing interval
this.stopPollingLocation();

this.clearPositionWatch = await watchPosition(this.onWatchedPosition, this.onWatchedPositionError);
try {
this.clearPositionWatch = await watchPosition(
this.onWatchedPosition,
this.onGeolocationError,
);
} catch (error) {
this.onGeolocationError(error?.message);
// don't set locationInterval if geolocation failed to setup
return;
}

this.locationInterval = setInterval(() => {
if (!this.lastPublishedPositionTimestamp) {
Expand All @@ -255,6 +266,8 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
this.publishCurrentLocationToBeacons();
}
}, STATIC_UPDATE_INTERVAL);

this.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
};

private onWatchedPosition = (position: GeolocationPosition) => {
Expand All @@ -268,11 +281,6 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
}
};

private onWatchedPositionError = (error: GeolocationError) => {
this.geolocationError = error;
logger.error(this.geolocationError);
};

private stopPollingLocation = () => {
clearInterval(this.locationInterval);
this.locationInterval = undefined;
Expand All @@ -283,6 +291,8 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
this.clearPositionWatch();
this.clearPositionWatch = undefined;
}

this.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
};

/**
Expand Down Expand Up @@ -313,8 +323,31 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
* and publishes it to all live beacons
*/
private publishCurrentLocationToBeacons = async () => {
const position = await getCurrentPosition();
// TODO error handling
this.publishLocationToBeacons(mapGeolocationPositionToTimedGeo(position));
try {
const position = await getCurrentPosition();
// TODO error handling
this.publishLocationToBeacons(mapGeolocationPositionToTimedGeo(position));
} catch (error) {
this.onGeolocationError(error?.message);
}
};

private onGeolocationError = async (error: GeolocationError): Promise<void> => {
this.geolocationError = error;
logger.error('Geolocation failed', this.geolocationError);

// other errors are considered non-fatal
// and self recovering
if (![
GeolocationError.Unavailable,
GeolocationError.PermissionDenied,
].includes(error)) {
return;
}

this.stopPollingLocation();
// kill live beacons when location permissions are revoked
// TODO may need adjustment when PSF-797 is done
await Promise.all(this.liveBeaconIds.map(this.stopBeacon));
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ describe('<LeftPanelLiveShareWarning />', () => {
expect(component.html()).toBe(null);
});

describe('when user has live beacons', () => {
describe('when user has live location monitor', () => {
beforeEach(() => {
mocked(OwnBeaconStore.instance).hasLiveBeacons.mockReturnValue(true);
mocked(OwnBeaconStore.instance).isMonitoringLiveLocation = true;
});
it('renders correctly when not minimized', () => {
const component = getComponent();
Expand All @@ -68,8 +68,8 @@ describe('<LeftPanelLiveShareWarning />', () => {
// started out rendered
expect(component.html()).toBeTruthy();

mocked(OwnBeaconStore.instance).hasLiveBeacons.mockReturnValue(false);
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.LivenessChange, false);
mocked(OwnBeaconStore.instance).isMonitoringLiveLocation = false;
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.MonitoringLivePosition);

await flushPromises();
component.setProps({});
Expand Down
38 changes: 35 additions & 3 deletions test/components/views/beacon/RoomLiveShareWarning-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import { mount } from 'enzyme';
import { Room, Beacon, BeaconEvent } from 'matrix-js-sdk/src/matrix';
import { logger } from 'matrix-js-sdk/src/logger';

import '../../../skinned-sdk';
import RoomLiveShareWarning from '../../../../src/components/views/beacon/RoomLiveShareWarning';
import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore';
import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../../src/stores/OwnBeaconStore';
import {
advanceDateAndTime,
findByTestId,
Expand All @@ -33,7 +34,6 @@ import {
} from '../../../test-utils';

jest.useFakeTimers();
mockGeolocation();
describe('<RoomLiveShareWarning />', () => {
const aliceId = '@alice:server.org';
const room1Id = '$room1:server.org';
Expand Down Expand Up @@ -94,6 +94,7 @@ describe('<RoomLiveShareWarning />', () => {
};

beforeEach(() => {
mockGeolocation();
jest.spyOn(global.Date, 'now').mockReturnValue(now);
mockClient.unstable_setLiveBeacon.mockClear();
});
Expand Down Expand Up @@ -123,7 +124,22 @@ describe('<RoomLiveShareWarning />', () => {
expect(component.html()).toBe(null);
});

describe('when user has live beacons', () => {
it('does not render when geolocation is not working', async () => {
jest.spyOn(logger, 'error').mockImplementation(() => { });
// @ts-ignore
navigator.geolocation = undefined;
await act(async () => {
await makeRoomsWithStateEvents([room1Beacon1, room2Beacon1, room2Beacon2]);
await makeOwnBeaconStore();
});
const component = getComponent({ roomId: room1Id });

// beacons have generated ids that break snapshots
// assert on html
expect(component.html()).toBeNull();
});

describe('when user has live beacons and geolocation is available', () => {
beforeEach(async () => {
await act(async () => {
await makeRoomsWithStateEvents([room1Beacon1, room2Beacon1, room2Beacon2]);
Expand Down Expand Up @@ -164,6 +180,22 @@ describe('<RoomLiveShareWarning />', () => {
expect(component.html()).toBe(null);
});

it('removes itself when user stops monitoring live position', async () => {
const component = getComponent({ roomId: room1Id });
// started out rendered
expect(component.html()).toBeTruthy();

act(() => {
// cheat to clear this
// @ts-ignore
OwnBeaconStore.instance.clearPositionWatch = undefined;
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
component.setProps({});
});

expect(component.html()).toBe(null);
});

it('renders when user adds a live beacon', async () => {
const component = getComponent({ roomId: room3Id });
// started out not rendered
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<LeftPanelLiveShareWarning /> when user has live beacons renders correctly when minimized 1`] = `
exports[`<LeftPanelLiveShareWarning /> when user has live location monitor renders correctly when minimized 1`] = `
<LeftPanelLiveShareWarning
isMinimized={true}
>
Expand All @@ -15,7 +15,7 @@ exports[`<LeftPanelLiveShareWarning /> when user has live beacons renders correc
</LeftPanelLiveShareWarning>
`;

exports[`<LeftPanelLiveShareWarning /> when user has live beacons renders correctly when not minimized 1`] = `
exports[`<LeftPanelLiveShareWarning /> when user has live location monitor renders correctly when not minimized 1`] = `
<LeftPanelLiveShareWarning>
<div
className="mx_LeftPanelLiveShareWarning"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<RoomLiveShareWarning /> when user has live beacons 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 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>"`;
Loading

0 comments on commit d2b97e2

Please sign in to comment.