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: only share to beacons created on device (#8378)
Browse files Browse the repository at this point in the history
* create live beacons in ownbeaconstore and test

Signed-off-by: Kerry Archibald <[email protected]>

* more mocks in RoomLiveShareWarning

Signed-off-by: Kerry Archibald <[email protected]>

* extend mocks in components

Signed-off-by: Kerry Archibald <[email protected]>

* comment

Signed-off-by: Kerry Archibald <[email protected]>

* remove another comment

Signed-off-by: Kerry Archibald <[email protected]>

* extra ? hedge in roommembers change

Signed-off-by: Kerry Archibald <[email protected]>

* listen to destroy and prune local store on stop

Signed-off-by: Kerry Archibald <[email protected]>

* tests

Signed-off-by: Kerry Archibald <[email protected]>

* update copy pasted copyright to 2022

Signed-off-by: Kerry Archibald <[email protected]>
  • Loading branch information
Kerry authored Apr 22, 2022
1 parent a3a7c60 commit 988d300
Show file tree
Hide file tree
Showing 7 changed files with 341 additions and 20 deletions.
3 changes: 2 additions & 1 deletion src/components/views/location/shareLocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import QuestionDialog from "../dialogs/QuestionDialog";
import SdkConfig from "../../../SdkConfig";
import { OwnBeaconStore } from "../../../stores/OwnBeaconStore";

export enum LocationShareType {
Own = 'Own',
Expand Down Expand Up @@ -70,7 +71,7 @@ export const shareLiveLocation = (
): ShareLocationFn => async ({ timeout }) => {
const description = _t(`%(displayName)s's live location`, { displayName });
try {
await client.unstable_createLiveBeacon(
await OwnBeaconStore.instance.createLiveBeacon(
roomId,
makeBeaconInfoContent(
timeout ?? DEFAULT_LIVE_DURATION,
Expand Down
78 changes: 73 additions & 5 deletions src/stores/OwnBeaconStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
import {
BeaconInfoState, makeBeaconContent, makeBeaconInfoContent,
} from "matrix-js-sdk/src/content-helpers";
import { M_BEACON } from "matrix-js-sdk/src/@types/beacon";
import { MBeaconInfoEventContent, M_BEACON } from "matrix-js-sdk/src/@types/beacon";
import { logger } from "matrix-js-sdk/src/logger";

import defaultDispatcher from "../dispatcher/dispatcher";
Expand Down Expand Up @@ -64,6 +64,30 @@ type OwnBeaconStoreState = {
beaconsByRoomId: Map<Room['roomId'], Set<BeaconIdentifier>>;
liveBeaconIds: BeaconIdentifier[];
};

const CREATED_BEACONS_KEY = 'mx_live_beacon_created_id';
const removeLocallyCreateBeaconEventId = (eventId: string): void => {
const ids = getLocallyCreatedBeaconEventIds();
window.localStorage.setItem(CREATED_BEACONS_KEY, JSON.stringify(ids.filter(id => id !== eventId)));
};
const storeLocallyCreateBeaconEventId = (eventId: string): void => {
const ids = getLocallyCreatedBeaconEventIds();
window.localStorage.setItem(CREATED_BEACONS_KEY, JSON.stringify([...ids, eventId]));
};

const getLocallyCreatedBeaconEventIds = (): string[] => {
let ids: string[];
try {
ids = JSON.parse(window.localStorage.getItem(CREATED_BEACONS_KEY) ?? '[]');
if (!Array.isArray(ids)) {
throw new Error('Invalid stored value');
}
} catch (error) {
logger.error('Failed to retrieve locally created beacon event ids', error);
ids = [];
}
return ids;
};
export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
private static internalInstance = new OwnBeaconStore();
// users beacons, keyed by event type
Expand Down Expand Up @@ -110,6 +134,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
this.matrixClient.removeListener(BeaconEvent.LivenessChange, this.onBeaconLiveness);
this.matrixClient.removeListener(BeaconEvent.New, this.onNewBeacon);
this.matrixClient.removeListener(BeaconEvent.Update, this.onUpdateBeacon);
this.matrixClient.removeListener(BeaconEvent.Destroy, this.onDestroyBeacon);
this.matrixClient.removeListener(RoomStateEvent.Members, this.onRoomStateMembers);

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

this.initialiseBeaconState();
Expand Down Expand Up @@ -188,7 +214,10 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
return;
}

return await this.updateBeaconEvent(beacon, { live: false });
await this.updateBeaconEvent(beacon, { live: false });

// prune from local store
removeLocallyCreateBeaconEventId(beacon.beaconInfoId);
};

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

private onDestroyBeacon = (beaconIdentifier: BeaconIdentifier): void => {
// check if we care about this beacon
if (!this.beacons.has(beaconIdentifier)) {
return;
}

this.checkLiveness();
};

private onBeaconLiveness = (isLive: boolean, beacon: Beacon): void => {
// check if we care about this beacon
if (!this.beacons.has(beacon.identifier)) {
Expand Down Expand Up @@ -249,7 +287,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {

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

private checkLiveness = (): void => {
const locallyCreatedBeaconEventIds = getLocallyCreatedBeaconEventIds();
const prevLiveBeaconIds = this.getLiveBeaconIds();
this.liveBeaconIds = [...this.beacons.values()]
.filter(beacon => beacon.isLive)
.filter(beacon =>
beacon.isLive &&
// only beacons created on this device should be shared to
locallyCreatedBeaconEventIds.includes(beacon.beaconInfoId),
)
.sort(sortBeaconsByLatestCreation)
.map(beacon => beacon.identifier);

Expand Down Expand Up @@ -339,6 +382,32 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
}
};

public createLiveBeacon = async (
roomId: Room['roomId'],
beaconInfoContent: MBeaconInfoEventContent,
): Promise<void> => {
// eslint-disable-next-line camelcase
const { event_id } = await this.matrixClient.unstable_createLiveBeacon(
roomId,
beaconInfoContent,
);

storeLocallyCreateBeaconEventId(event_id);

// try to stop any other live beacons
// in this room
this.beaconsByRoomId.get(roomId)?.forEach(beaconId => {
if (this.getBeaconById(beaconId)?.isLive) {
try {
// don't await, this is best effort
this.stopBeacon(beaconId);
} catch (error) {
logger.error('Failed to stop live beacons', error);
}
}
});
};

/**
* Geolocation
*/
Expand Down Expand Up @@ -420,7 +489,6 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {

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));
};

Expand Down
2 changes: 1 addition & 1 deletion src/utils/beacon/timeline.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
11 changes: 11 additions & 0 deletions test/components/views/beacon/RoomLiveShareWarning-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,20 @@ describe('<RoomLiveShareWarning />', () => {
return component;
};

const localStorageSpy = jest.spyOn(localStorage.__proto__, 'getItem').mockReturnValue(undefined);

beforeEach(() => {
mockGeolocation();
jest.spyOn(global.Date, 'now').mockReturnValue(now);
mockClient.unstable_setLiveBeacon.mockReset().mockResolvedValue({ event_id: '1' });

// assume all beacons were created on this device
localStorageSpy.mockReturnValue(JSON.stringify([
room1Beacon1.getId(),
room2Beacon1.getId(),
room2Beacon2.getId(),
room3Beacon1.getId(),
]));
});

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

afterAll(() => {
jest.spyOn(global.Date, 'now').mockRestore();
localStorageSpy.mockRestore();
});

const getExpiryText = wrapper => findByTestId(wrapper, 'room-live-share-expiry').text();
Expand Down
31 changes: 22 additions & 9 deletions test/components/views/location/LocationShareMenu-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@ import { ChevronFace } from '../../../../src/components/structures/ContextMenu';
import SettingsStore from '../../../../src/settings/SettingsStore';
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
import { LocationShareType } from '../../../../src/components/views/location/shareLocation';
import { findByTagAndTestId, flushPromises } from '../../../test-utils';
import {
findByTagAndTestId,
flushPromises,
getMockClientWithEventEmitter,
setupAsyncStoreWithClient,
} from '../../../test-utils';
import Modal from '../../../../src/Modal';
import { DEFAULT_DURATION_MS } from '../../../../src/components/views/location/LiveDurationDropdown';
import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore';

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

describe('<LocationShareMenu />', () => {
const userId = '@ernie:server.org';
const mockClient = {
on: jest.fn(),
off: jest.fn(),
removeListener: jest.fn(),
const mockClient = getMockClientWithEventEmitter({
getUserId: jest.fn().mockReturnValue(userId),
getClientWellKnown: jest.fn().mockResolvedValue({
map_style_url: 'maps.com',
}),
sendMessage: jest.fn(),
unstable_createLiveBeacon: jest.fn().mockResolvedValue({}),
};
unstable_createLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }),
getVisibleRooms: jest.fn().mockReturnValue([]),
});

const defaultProps = {
menuPosition: {
Expand All @@ -90,19 +94,28 @@ describe('<LocationShareMenu />', () => {
type: 'geolocate',
};

const makeOwnBeaconStore = async () => {
const store = OwnBeaconStore.instance;

await setupAsyncStoreWithClient(store, mockClient);
return store;
};

const getComponent = (props = {}) =>
mount(<LocationShareMenu {...defaultProps} {...props} />, {
wrappingComponent: MatrixClientContext.Provider,
wrappingComponentProps: { value: mockClient },
});

beforeEach(() => {
beforeEach(async () => {
jest.spyOn(logger, 'error').mockRestore();
mocked(SettingsStore).getValue.mockReturnValue(false);
mockClient.sendMessage.mockClear();
mockClient.unstable_createLiveBeacon.mockClear().mockResolvedValue(undefined);
mockClient.unstable_createLiveBeacon.mockClear().mockResolvedValue({ event_id: '1' });
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient as unknown as MatrixClient);
mocked(Modal).createTrackedDialog.mockClear();

await makeOwnBeaconStore();
});

const getShareTypeOption = (component: ReactWrapper, shareType: LocationShareType) =>
Expand Down
Loading

0 comments on commit 988d300

Please sign in to comment.