Live location sharing - stop sharing to beacons in rooms you left (#8187)
* remove beacons on membership changes * add addMembershipToMockedRoom test util Signed-off-by: Kerry Archibald <kerrya@element.io> * test remove beacons on membership changes Signed-off-by: Kerry Archibald <kerrya@element.io> * removelistener Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
parent
e161f0b17b
commit
2adc972eec
4 changed files with 269 additions and 60 deletions
|
@ -20,6 +20,9 @@ import {
|
|||
BeaconEvent,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
RoomMember,
|
||||
RoomState,
|
||||
RoomStateEvent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
BeaconInfoState, makeBeaconContent, makeBeaconInfoContent,
|
||||
|
@ -90,6 +93,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
|||
protected async onNotReady() {
|
||||
this.matrixClient.removeListener(BeaconEvent.LivenessChange, this.onBeaconLiveness);
|
||||
this.matrixClient.removeListener(BeaconEvent.New, this.onNewBeacon);
|
||||
this.matrixClient.removeListener(RoomStateEvent.Members, this.onRoomStateMembers);
|
||||
|
||||
this.beacons.forEach(beacon => beacon.destroy());
|
||||
|
||||
|
@ -102,6 +106,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
|||
protected async onReady(): Promise<void> {
|
||||
this.matrixClient.on(BeaconEvent.LivenessChange, this.onBeaconLiveness);
|
||||
this.matrixClient.on(BeaconEvent.New, this.onNewBeacon);
|
||||
this.matrixClient.on(RoomStateEvent.Members, this.onRoomStateMembers);
|
||||
|
||||
this.initialiseBeaconState();
|
||||
}
|
||||
|
@ -136,6 +141,10 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
|||
return await this.updateBeaconEvent(beacon, { live: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* Listeners
|
||||
*/
|
||||
|
||||
private onNewBeacon = (_event: MatrixEvent, beacon: Beacon): void => {
|
||||
if (!isOwnBeacon(beacon, this.matrixClient.getUserId())) {
|
||||
return;
|
||||
|
@ -160,6 +169,33 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
|||
this.emit(OwnBeaconStoreEvent.LivenessChange, this.getLiveBeaconIds());
|
||||
};
|
||||
|
||||
/**
|
||||
* Check for changes in membership in rooms with beacons
|
||||
* and stop monitoring beacons in rooms user is no longer member of
|
||||
*/
|
||||
private onRoomStateMembers = (_event: MatrixEvent, roomState: RoomState, member: RoomMember): void => {
|
||||
// no beacons for this room, ignore
|
||||
if (
|
||||
!this.beaconsByRoomId.has(roomState.roomId) ||
|
||||
member.userId !== this.matrixClient.getUserId()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO check powerlevels here
|
||||
// in PSF-797
|
||||
|
||||
// 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.delete(roomState.roomId);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* State management
|
||||
*/
|
||||
|
||||
private initialiseBeaconState = () => {
|
||||
const userId = this.matrixClient.getUserId();
|
||||
const visibleRooms = this.matrixClient.getVisibleRooms();
|
||||
|
@ -187,6 +223,21 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
|||
beacon.monitorLiveness();
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove listeners for a given beacon
|
||||
* remove from state
|
||||
* and update liveness if changed
|
||||
*/
|
||||
private removeBeacon = (beaconId: string): void => {
|
||||
if (!this.beacons.has(beaconId)) {
|
||||
return;
|
||||
}
|
||||
this.beacons.get(beaconId).destroy();
|
||||
this.beacons.delete(beaconId);
|
||||
|
||||
this.checkLiveness();
|
||||
};
|
||||
|
||||
private checkLiveness = (): void => {
|
||||
const prevLiveBeaconIds = this.getLiveBeaconIds();
|
||||
this.liveBeaconIds = [...this.beacons.values()]
|
||||
|
@ -218,20 +269,9 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
|||
}
|
||||
};
|
||||
|
||||
private updateBeaconEvent = async (beacon: Beacon, update: Partial<BeaconInfoState>): Promise<void> => {
|
||||
const { description, timeout, timestamp, live, assetType } = {
|
||||
...beacon.beaconInfo,
|
||||
...update,
|
||||
};
|
||||
|
||||
const updateContent = makeBeaconInfoContent(timeout,
|
||||
live,
|
||||
description,
|
||||
assetType,
|
||||
timestamp);
|
||||
|
||||
await this.matrixClient.unstable_setLiveBeacon(beacon.roomId, beacon.beaconInfoEventType, updateContent);
|
||||
};
|
||||
/**
|
||||
* Geolocation
|
||||
*/
|
||||
|
||||
private togglePollingLocation = () => {
|
||||
if (!!this.liveBeaconIds.length) {
|
||||
|
@ -270,17 +310,6 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
|||
this.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
|
||||
};
|
||||
|
||||
private onWatchedPosition = (position: GeolocationPosition) => {
|
||||
const timedGeoPosition = mapGeolocationPositionToTimedGeo(position);
|
||||
|
||||
// if this is our first position, publish immediateley
|
||||
if (!this.lastPublishedPositionTimestamp) {
|
||||
this.publishLocationToBeacons(timedGeoPosition);
|
||||
} else {
|
||||
this.debouncedPublishLocationToBeacons(timedGeoPosition);
|
||||
}
|
||||
};
|
||||
|
||||
private stopPollingLocation = () => {
|
||||
clearInterval(this.locationInterval);
|
||||
this.locationInterval = undefined;
|
||||
|
@ -295,6 +324,70 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
|||
this.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
|
||||
};
|
||||
|
||||
private onWatchedPosition = (position: GeolocationPosition) => {
|
||||
const timedGeoPosition = mapGeolocationPositionToTimedGeo(position);
|
||||
|
||||
// if this is our first position, publish immediateley
|
||||
if (!this.lastPublishedPositionTimestamp) {
|
||||
this.publishLocationToBeacons(timedGeoPosition);
|
||||
} else {
|
||||
this.debouncedPublishLocationToBeacons(timedGeoPosition);
|
||||
}
|
||||
};
|
||||
|
||||
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));
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current location
|
||||
* (as opposed to using watched location)
|
||||
* and publishes it to all live beacons
|
||||
*/
|
||||
private publishCurrentLocationToBeacons = async () => {
|
||||
try {
|
||||
const position = await getCurrentPosition();
|
||||
// TODO error handling
|
||||
this.publishLocationToBeacons(mapGeolocationPositionToTimedGeo(position));
|
||||
} catch (error) {
|
||||
this.onGeolocationError(error?.message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* MatrixClient api
|
||||
*/
|
||||
|
||||
private updateBeaconEvent = async (beacon: Beacon, update: Partial<BeaconInfoState>): Promise<void> => {
|
||||
const { description, timeout, timestamp, live, assetType } = {
|
||||
...beacon.beaconInfo,
|
||||
...update,
|
||||
};
|
||||
|
||||
const updateContent = makeBeaconInfoContent(timeout,
|
||||
live,
|
||||
description,
|
||||
assetType,
|
||||
timestamp);
|
||||
|
||||
await this.matrixClient.unstable_setLiveBeacon(beacon.roomId, beacon.beaconInfoEventType, updateContent);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends m.location events to all live beacons
|
||||
* Sets last published beacon
|
||||
|
@ -316,38 +409,4 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
|||
const content = makeBeaconContent(geoUri, timestamp, beacon.beaconInfoId);
|
||||
await this.matrixClient.sendEvent(beacon.roomId, M_BEACON.name, content);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current location
|
||||
* (as opposed to using watched location)
|
||||
* and publishes it to all live beacons
|
||||
*/
|
||||
private publishCurrentLocationToBeacons = async () => {
|
||||
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));
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue