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>
This commit is contained in:
@@ -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',
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
@@ -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
|
||||
@@ -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());
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -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)) {
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user