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

Commit 988d300

Browse files
author
Kerry
authored
Live location sharing: only share to beacons created on device (#8378)
* create live beacons in ownbeaconstore and test Signed-off-by: Kerry Archibald <kerrya@element.io> * more mocks in RoomLiveShareWarning Signed-off-by: Kerry Archibald <kerrya@element.io> * extend mocks in components Signed-off-by: Kerry Archibald <kerrya@element.io> * comment Signed-off-by: Kerry Archibald <kerrya@element.io> * remove another comment Signed-off-by: Kerry Archibald <kerrya@element.io> * extra ? hedge in roommembers change Signed-off-by: Kerry Archibald <kerrya@element.io> * listen to destroy and prune local store on stop Signed-off-by: Kerry Archibald <kerrya@element.io> * tests Signed-off-by: Kerry Archibald <kerrya@element.io> * update copy pasted copyright to 2022 Signed-off-by: Kerry Archibald <kerrya@element.io>
1 parent a3a7c60 commit 988d300

File tree

7 files changed

+341
-20
lines changed

7 files changed

+341
-20
lines changed

src/components/views/location/shareLocation.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { _t } from "../../../languageHandler";
2525
import Modal from "../../../Modal";
2626
import QuestionDialog from "../dialogs/QuestionDialog";
2727
import SdkConfig from "../../../SdkConfig";
28+
import { OwnBeaconStore } from "../../../stores/OwnBeaconStore";
2829

2930
export enum LocationShareType {
3031
Own = 'Own',
@@ -70,7 +71,7 @@ export const shareLiveLocation = (
7071
): ShareLocationFn => async ({ timeout }) => {
7172
const description = _t(`%(displayName)s's live location`, { displayName });
7273
try {
73-
await client.unstable_createLiveBeacon(
74+
await OwnBeaconStore.instance.createLiveBeacon(
7475
roomId,
7576
makeBeaconInfoContent(
7677
timeout ?? DEFAULT_LIVE_DURATION,

src/stores/OwnBeaconStore.ts

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
import {
2929
BeaconInfoState, makeBeaconContent, makeBeaconInfoContent,
3030
} from "matrix-js-sdk/src/content-helpers";
31-
import { M_BEACON } from "matrix-js-sdk/src/@types/beacon";
31+
import { MBeaconInfoEventContent, M_BEACON } from "matrix-js-sdk/src/@types/beacon";
3232
import { logger } from "matrix-js-sdk/src/logger";
3333

3434
import defaultDispatcher from "../dispatcher/dispatcher";
@@ -64,6 +64,30 @@ type OwnBeaconStoreState = {
6464
beaconsByRoomId: Map<Room['roomId'], Set<BeaconIdentifier>>;
6565
liveBeaconIds: BeaconIdentifier[];
6666
};
67+
68+
const CREATED_BEACONS_KEY = 'mx_live_beacon_created_id';
69+
const removeLocallyCreateBeaconEventId = (eventId: string): void => {
70+
const ids = getLocallyCreatedBeaconEventIds();
71+
window.localStorage.setItem(CREATED_BEACONS_KEY, JSON.stringify(ids.filter(id => id !== eventId)));
72+
};
73+
const storeLocallyCreateBeaconEventId = (eventId: string): void => {
74+
const ids = getLocallyCreatedBeaconEventIds();
75+
window.localStorage.setItem(CREATED_BEACONS_KEY, JSON.stringify([...ids, eventId]));
76+
};
77+
78+
const getLocallyCreatedBeaconEventIds = (): string[] => {
79+
let ids: string[];
80+
try {
81+
ids = JSON.parse(window.localStorage.getItem(CREATED_BEACONS_KEY) ?? '[]');
82+
if (!Array.isArray(ids)) {
83+
throw new Error('Invalid stored value');
84+
}
85+
} catch (error) {
86+
logger.error('Failed to retrieve locally created beacon event ids', error);
87+
ids = [];
88+
}
89+
return ids;
90+
};
6791
export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
6892
private static internalInstance = new OwnBeaconStore();
6993
// users beacons, keyed by event type
@@ -110,6 +134,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
110134
this.matrixClient.removeListener(BeaconEvent.LivenessChange, this.onBeaconLiveness);
111135
this.matrixClient.removeListener(BeaconEvent.New, this.onNewBeacon);
112136
this.matrixClient.removeListener(BeaconEvent.Update, this.onUpdateBeacon);
137+
this.matrixClient.removeListener(BeaconEvent.Destroy, this.onDestroyBeacon);
113138
this.matrixClient.removeListener(RoomStateEvent.Members, this.onRoomStateMembers);
114139

115140
this.beacons.forEach(beacon => beacon.destroy());
@@ -125,6 +150,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
125150
this.matrixClient.on(BeaconEvent.LivenessChange, this.onBeaconLiveness);
126151
this.matrixClient.on(BeaconEvent.New, this.onNewBeacon);
127152
this.matrixClient.on(BeaconEvent.Update, this.onUpdateBeacon);
153+
this.matrixClient.on(BeaconEvent.Destroy, this.onDestroyBeacon);
128154
this.matrixClient.on(RoomStateEvent.Members, this.onRoomStateMembers);
129155

130156
this.initialiseBeaconState();
@@ -188,7 +214,10 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
188214
return;
189215
}
190216

191-
return await this.updateBeaconEvent(beacon, { live: false });
217+
await this.updateBeaconEvent(beacon, { live: false });
218+
219+
// prune from local store
220+
removeLocallyCreateBeaconEventId(beacon.beaconInfoId);
192221
};
193222

194223
/**
@@ -215,6 +244,15 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
215244
beacon.monitorLiveness();
216245
};
217246

247+
private onDestroyBeacon = (beaconIdentifier: BeaconIdentifier): void => {
248+
// check if we care about this beacon
249+
if (!this.beacons.has(beaconIdentifier)) {
250+
return;
251+
}
252+
253+
this.checkLiveness();
254+
};
255+
218256
private onBeaconLiveness = (isLive: boolean, beacon: Beacon): void => {
219257
// check if we care about this beacon
220258
if (!this.beacons.has(beacon.identifier)) {
@@ -249,7 +287,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
249287

250288
// stop watching beacons in rooms where user is no longer a member
251289
if (member.membership === 'leave' || member.membership === 'ban') {
252-
this.beaconsByRoomId.get(roomState.roomId).forEach(this.removeBeacon);
290+
this.beaconsByRoomId.get(roomState.roomId)?.forEach(this.removeBeacon);
253291
this.beaconsByRoomId.delete(roomState.roomId);
254292
}
255293
};
@@ -308,9 +346,14 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
308346
};
309347

310348
private checkLiveness = (): void => {
349+
const locallyCreatedBeaconEventIds = getLocallyCreatedBeaconEventIds();
311350
const prevLiveBeaconIds = this.getLiveBeaconIds();
312351
this.liveBeaconIds = [...this.beacons.values()]
313-
.filter(beacon => beacon.isLive)
352+
.filter(beacon =>
353+
beacon.isLive &&
354+
// only beacons created on this device should be shared to
355+
locallyCreatedBeaconEventIds.includes(beacon.beaconInfoId),
356+
)
314357
.sort(sortBeaconsByLatestCreation)
315358
.map(beacon => beacon.identifier);
316359

@@ -339,6 +382,32 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
339382
}
340383
};
341384

385+
public createLiveBeacon = async (
386+
roomId: Room['roomId'],
387+
beaconInfoContent: MBeaconInfoEventContent,
388+
): Promise<void> => {
389+
// eslint-disable-next-line camelcase
390+
const { event_id } = await this.matrixClient.unstable_createLiveBeacon(
391+
roomId,
392+
beaconInfoContent,
393+
);
394+
395+
storeLocallyCreateBeaconEventId(event_id);
396+
397+
// try to stop any other live beacons
398+
// in this room
399+
this.beaconsByRoomId.get(roomId)?.forEach(beaconId => {
400+
if (this.getBeaconById(beaconId)?.isLive) {
401+
try {
402+
// don't await, this is best effort
403+
this.stopBeacon(beaconId);
404+
} catch (error) {
405+
logger.error('Failed to stop live beacons', error);
406+
}
407+
}
408+
});
409+
};
410+
342411
/**
343412
* Geolocation
344413
*/
@@ -420,7 +489,6 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
420489

421490
this.stopPollingLocation();
422491
// kill live beacons when location permissions are revoked
423-
// TODO may need adjustment when PSF-797 is done
424492
await Promise.all(this.liveBeaconIds.map(this.stopBeacon));
425493
};
426494

src/utils/beacon/timeline.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2020 The Matrix.org Foundation C.I.C.
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.

test/components/views/beacon/RoomLiveShareWarning-test.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,20 @@ describe('<RoomLiveShareWarning />', () => {
9393
return component;
9494
};
9595

96+
const localStorageSpy = jest.spyOn(localStorage.__proto__, 'getItem').mockReturnValue(undefined);
97+
9698
beforeEach(() => {
9799
mockGeolocation();
98100
jest.spyOn(global.Date, 'now').mockReturnValue(now);
99101
mockClient.unstable_setLiveBeacon.mockReset().mockResolvedValue({ event_id: '1' });
102+
103+
// assume all beacons were created on this device
104+
localStorageSpy.mockReturnValue(JSON.stringify([
105+
room1Beacon1.getId(),
106+
room2Beacon1.getId(),
107+
room2Beacon2.getId(),
108+
room3Beacon1.getId(),
109+
]));
100110
});
101111

102112
afterEach(async () => {
@@ -106,6 +116,7 @@ describe('<RoomLiveShareWarning />', () => {
106116

107117
afterAll(() => {
108118
jest.spyOn(global.Date, 'now').mockRestore();
119+
localStorageSpy.mockRestore();
109120
});
110121

111122
const getExpiryText = wrapper => findByTestId(wrapper, 'room-live-share-expiry').text();

test/components/views/location/LocationShareMenu-test.tsx

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,15 @@ import { ChevronFace } from '../../../../src/components/structures/ContextMenu';
2929
import SettingsStore from '../../../../src/settings/SettingsStore';
3030
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
3131
import { LocationShareType } from '../../../../src/components/views/location/shareLocation';
32-
import { findByTagAndTestId, flushPromises } from '../../../test-utils';
32+
import {
33+
findByTagAndTestId,
34+
flushPromises,
35+
getMockClientWithEventEmitter,
36+
setupAsyncStoreWithClient,
37+
} from '../../../test-utils';
3338
import Modal from '../../../../src/Modal';
3439
import { DEFAULT_DURATION_MS } from '../../../../src/components/views/location/LiveDurationDropdown';
40+
import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore';
3541

3642
jest.mock('../../../../src/utils/location/findMapStyleUrl', () => ({
3743
findMapStyleUrl: jest.fn().mockReturnValue('test'),
@@ -57,17 +63,15 @@ jest.mock('../../../../src/Modal', () => ({
5763

5864
describe('<LocationShareMenu />', () => {
5965
const userId = '@ernie:server.org';
60-
const mockClient = {
61-
on: jest.fn(),
62-
off: jest.fn(),
63-
removeListener: jest.fn(),
66+
const mockClient = getMockClientWithEventEmitter({
6467
getUserId: jest.fn().mockReturnValue(userId),
6568
getClientWellKnown: jest.fn().mockResolvedValue({
6669
map_style_url: 'maps.com',
6770
}),
6871
sendMessage: jest.fn(),
69-
unstable_createLiveBeacon: jest.fn().mockResolvedValue({}),
70-
};
72+
unstable_createLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }),
73+
getVisibleRooms: jest.fn().mockReturnValue([]),
74+
});
7175

7276
const defaultProps = {
7377
menuPosition: {
@@ -90,19 +94,28 @@ describe('<LocationShareMenu />', () => {
9094
type: 'geolocate',
9195
};
9296

97+
const makeOwnBeaconStore = async () => {
98+
const store = OwnBeaconStore.instance;
99+
100+
await setupAsyncStoreWithClient(store, mockClient);
101+
return store;
102+
};
103+
93104
const getComponent = (props = {}) =>
94105
mount(<LocationShareMenu {...defaultProps} {...props} />, {
95106
wrappingComponent: MatrixClientContext.Provider,
96107
wrappingComponentProps: { value: mockClient },
97108
});
98109

99-
beforeEach(() => {
110+
beforeEach(async () => {
100111
jest.spyOn(logger, 'error').mockRestore();
101112
mocked(SettingsStore).getValue.mockReturnValue(false);
102113
mockClient.sendMessage.mockClear();
103-
mockClient.unstable_createLiveBeacon.mockClear().mockResolvedValue(undefined);
114+
mockClient.unstable_createLiveBeacon.mockClear().mockResolvedValue({ event_id: '1' });
104115
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient as unknown as MatrixClient);
105116
mocked(Modal).createTrackedDialog.mockClear();
117+
118+
await makeOwnBeaconStore();
106119
});
107120

108121
const getShareTypeOption = (component: ReactWrapper, shareType: LocationShareType) =>

0 commit comments

Comments
 (0)