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:
parent
a3a7c60dd7
commit
988d300258
7 changed files with 341 additions and 20 deletions
|
@ -25,6 +25,7 @@ import { _t } from "../../../languageHandler";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
|
import { OwnBeaconStore } from "../../../stores/OwnBeaconStore";
|
||||||
|
|
||||||
export enum LocationShareType {
|
export enum LocationShareType {
|
||||||
Own = 'Own',
|
Own = 'Own',
|
||||||
|
@ -70,7 +71,7 @@ export const shareLiveLocation = (
|
||||||
): ShareLocationFn => async ({ timeout }) => {
|
): ShareLocationFn => async ({ timeout }) => {
|
||||||
const description = _t(`%(displayName)s's live location`, { displayName });
|
const description = _t(`%(displayName)s's live location`, { displayName });
|
||||||
try {
|
try {
|
||||||
await client.unstable_createLiveBeacon(
|
await OwnBeaconStore.instance.createLiveBeacon(
|
||||||
roomId,
|
roomId,
|
||||||
makeBeaconInfoContent(
|
makeBeaconInfoContent(
|
||||||
timeout ?? DEFAULT_LIVE_DURATION,
|
timeout ?? DEFAULT_LIVE_DURATION,
|
||||||
|
|
|
@ -28,7 +28,7 @@ import {
|
||||||
import {
|
import {
|
||||||
BeaconInfoState, makeBeaconContent, makeBeaconInfoContent,
|
BeaconInfoState, makeBeaconContent, makeBeaconInfoContent,
|
||||||
} from "matrix-js-sdk/src/content-helpers";
|
} 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 { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||||
|
@ -64,6 +64,30 @@ type OwnBeaconStoreState = {
|
||||||
beaconsByRoomId: Map<Room['roomId'], Set<BeaconIdentifier>>;
|
beaconsByRoomId: Map<Room['roomId'], Set<BeaconIdentifier>>;
|
||||||
liveBeaconIds: 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> {
|
export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
private static internalInstance = new OwnBeaconStore();
|
private static internalInstance = new OwnBeaconStore();
|
||||||
// users beacons, keyed by event type
|
// 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.LivenessChange, this.onBeaconLiveness);
|
||||||
this.matrixClient.removeListener(BeaconEvent.New, this.onNewBeacon);
|
this.matrixClient.removeListener(BeaconEvent.New, this.onNewBeacon);
|
||||||
this.matrixClient.removeListener(BeaconEvent.Update, this.onUpdateBeacon);
|
this.matrixClient.removeListener(BeaconEvent.Update, this.onUpdateBeacon);
|
||||||
|
this.matrixClient.removeListener(BeaconEvent.Destroy, this.onDestroyBeacon);
|
||||||
this.matrixClient.removeListener(RoomStateEvent.Members, this.onRoomStateMembers);
|
this.matrixClient.removeListener(RoomStateEvent.Members, this.onRoomStateMembers);
|
||||||
|
|
||||||
this.beacons.forEach(beacon => beacon.destroy());
|
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.LivenessChange, this.onBeaconLiveness);
|
||||||
this.matrixClient.on(BeaconEvent.New, this.onNewBeacon);
|
this.matrixClient.on(BeaconEvent.New, this.onNewBeacon);
|
||||||
this.matrixClient.on(BeaconEvent.Update, this.onUpdateBeacon);
|
this.matrixClient.on(BeaconEvent.Update, this.onUpdateBeacon);
|
||||||
|
this.matrixClient.on(BeaconEvent.Destroy, this.onDestroyBeacon);
|
||||||
this.matrixClient.on(RoomStateEvent.Members, this.onRoomStateMembers);
|
this.matrixClient.on(RoomStateEvent.Members, this.onRoomStateMembers);
|
||||||
|
|
||||||
this.initialiseBeaconState();
|
this.initialiseBeaconState();
|
||||||
|
@ -188,7 +214,10 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
return;
|
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();
|
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 => {
|
private onBeaconLiveness = (isLive: boolean, beacon: Beacon): void => {
|
||||||
// check if we care about this beacon
|
// check if we care about this beacon
|
||||||
if (!this.beacons.has(beacon.identifier)) {
|
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
|
// stop watching beacons in rooms where user is no longer a member
|
||||||
if (member.membership === 'leave' || member.membership === 'ban') {
|
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);
|
this.beaconsByRoomId.delete(roomState.roomId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -308,9 +346,14 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private checkLiveness = (): void => {
|
private checkLiveness = (): void => {
|
||||||
|
const locallyCreatedBeaconEventIds = getLocallyCreatedBeaconEventIds();
|
||||||
const prevLiveBeaconIds = this.getLiveBeaconIds();
|
const prevLiveBeaconIds = this.getLiveBeaconIds();
|
||||||
this.liveBeaconIds = [...this.beacons.values()]
|
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)
|
.sort(sortBeaconsByLatestCreation)
|
||||||
.map(beacon => beacon.identifier);
|
.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
|
* Geolocation
|
||||||
*/
|
*/
|
||||||
|
@ -420,7 +489,6 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
|
|
||||||
this.stopPollingLocation();
|
this.stopPollingLocation();
|
||||||
// kill live beacons when location permissions are revoked
|
// 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));
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -93,10 +93,20 @@ describe('<RoomLiveShareWarning />', () => {
|
||||||
return component;
|
return component;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const localStorageSpy = jest.spyOn(localStorage.__proto__, 'getItem').mockReturnValue(undefined);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockGeolocation();
|
mockGeolocation();
|
||||||
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
||||||
mockClient.unstable_setLiveBeacon.mockReset().mockResolvedValue({ event_id: '1' });
|
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 () => {
|
afterEach(async () => {
|
||||||
|
@ -106,6 +116,7 @@ describe('<RoomLiveShareWarning />', () => {
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
jest.spyOn(global.Date, 'now').mockRestore();
|
jest.spyOn(global.Date, 'now').mockRestore();
|
||||||
|
localStorageSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
const getExpiryText = wrapper => findByTestId(wrapper, 'room-live-share-expiry').text();
|
const getExpiryText = wrapper => findByTestId(wrapper, 'room-live-share-expiry').text();
|
||||||
|
|
|
@ -29,9 +29,15 @@ import { ChevronFace } from '../../../../src/components/structures/ContextMenu';
|
||||||
import SettingsStore from '../../../../src/settings/SettingsStore';
|
import SettingsStore from '../../../../src/settings/SettingsStore';
|
||||||
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
|
||||||
import { LocationShareType } from '../../../../src/components/views/location/shareLocation';
|
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 Modal from '../../../../src/Modal';
|
||||||
import { DEFAULT_DURATION_MS } from '../../../../src/components/views/location/LiveDurationDropdown';
|
import { DEFAULT_DURATION_MS } from '../../../../src/components/views/location/LiveDurationDropdown';
|
||||||
|
import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore';
|
||||||
|
|
||||||
jest.mock('../../../../src/utils/location/findMapStyleUrl', () => ({
|
jest.mock('../../../../src/utils/location/findMapStyleUrl', () => ({
|
||||||
findMapStyleUrl: jest.fn().mockReturnValue('test'),
|
findMapStyleUrl: jest.fn().mockReturnValue('test'),
|
||||||
|
@ -57,17 +63,15 @@ jest.mock('../../../../src/Modal', () => ({
|
||||||
|
|
||||||
describe('<LocationShareMenu />', () => {
|
describe('<LocationShareMenu />', () => {
|
||||||
const userId = '@ernie:server.org';
|
const userId = '@ernie:server.org';
|
||||||
const mockClient = {
|
const mockClient = getMockClientWithEventEmitter({
|
||||||
on: jest.fn(),
|
|
||||||
off: jest.fn(),
|
|
||||||
removeListener: jest.fn(),
|
|
||||||
getUserId: jest.fn().mockReturnValue(userId),
|
getUserId: jest.fn().mockReturnValue(userId),
|
||||||
getClientWellKnown: jest.fn().mockResolvedValue({
|
getClientWellKnown: jest.fn().mockResolvedValue({
|
||||||
map_style_url: 'maps.com',
|
map_style_url: 'maps.com',
|
||||||
}),
|
}),
|
||||||
sendMessage: jest.fn(),
|
sendMessage: jest.fn(),
|
||||||
unstable_createLiveBeacon: jest.fn().mockResolvedValue({}),
|
unstable_createLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }),
|
||||||
};
|
getVisibleRooms: jest.fn().mockReturnValue([]),
|
||||||
|
});
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
menuPosition: {
|
menuPosition: {
|
||||||
|
@ -90,19 +94,28 @@ describe('<LocationShareMenu />', () => {
|
||||||
type: 'geolocate',
|
type: 'geolocate',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const makeOwnBeaconStore = async () => {
|
||||||
|
const store = OwnBeaconStore.instance;
|
||||||
|
|
||||||
|
await setupAsyncStoreWithClient(store, mockClient);
|
||||||
|
return store;
|
||||||
|
};
|
||||||
|
|
||||||
const getComponent = (props = {}) =>
|
const getComponent = (props = {}) =>
|
||||||
mount(<LocationShareMenu {...defaultProps} {...props} />, {
|
mount(<LocationShareMenu {...defaultProps} {...props} />, {
|
||||||
wrappingComponent: MatrixClientContext.Provider,
|
wrappingComponent: MatrixClientContext.Provider,
|
||||||
wrappingComponentProps: { value: mockClient },
|
wrappingComponentProps: { value: mockClient },
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
jest.spyOn(logger, 'error').mockRestore();
|
jest.spyOn(logger, 'error').mockRestore();
|
||||||
mocked(SettingsStore).getValue.mockReturnValue(false);
|
mocked(SettingsStore).getValue.mockReturnValue(false);
|
||||||
mockClient.sendMessage.mockClear();
|
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);
|
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient as unknown as MatrixClient);
|
||||||
mocked(Modal).createTrackedDialog.mockClear();
|
mocked(Modal).createTrackedDialog.mockClear();
|
||||||
|
|
||||||
|
await makeOwnBeaconStore();
|
||||||
});
|
});
|
||||||
|
|
||||||
const getShareTypeOption = (component: ReactWrapper, shareType: LocationShareType) =>
|
const getShareTypeOption = (component: ReactWrapper, shareType: LocationShareType) =>
|
||||||
|
|
|
@ -23,7 +23,7 @@ import {
|
||||||
RoomStateEvent,
|
RoomStateEvent,
|
||||||
RoomMember,
|
RoomMember,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { makeBeaconContent } from "matrix-js-sdk/src/content-helpers";
|
import { makeBeaconContent, makeBeaconInfoContent } from "matrix-js-sdk/src/content-helpers";
|
||||||
import { M_BEACON } from "matrix-js-sdk/src/@types/beacon";
|
import { M_BEACON } from "matrix-js-sdk/src/@types/beacon";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
@ -64,6 +64,7 @@ describe('OwnBeaconStore', () => {
|
||||||
getVisibleRooms: jest.fn().mockReturnValue([]),
|
getVisibleRooms: jest.fn().mockReturnValue([]),
|
||||||
unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }),
|
unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }),
|
||||||
sendEvent: jest.fn().mockResolvedValue({ event_id: '1' }),
|
sendEvent: jest.fn().mockResolvedValue({ event_id: '1' }),
|
||||||
|
unstable_createLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }),
|
||||||
});
|
});
|
||||||
const room1Id = '$room1:server.org';
|
const room1Id = '$room1:server.org';
|
||||||
const room2Id = '$room2:server.org';
|
const room2Id = '$room2:server.org';
|
||||||
|
@ -144,6 +145,7 @@ describe('OwnBeaconStore', () => {
|
||||||
beaconInfoEvent.getSender(),
|
beaconInfoEvent.getSender(),
|
||||||
beaconInfoEvent.getRoomId(),
|
beaconInfoEvent.getRoomId(),
|
||||||
{ isLive, timeout: beacon.beaconInfo.timeout },
|
{ isLive, timeout: beacon.beaconInfo.timeout },
|
||||||
|
'update-event-id',
|
||||||
);
|
);
|
||||||
beacon.update(updateEvent);
|
beacon.update(updateEvent);
|
||||||
|
|
||||||
|
@ -156,6 +158,9 @@ describe('OwnBeaconStore', () => {
|
||||||
mockClient.emit(BeaconEvent.New, beaconInfoEvent, beacon);
|
mockClient.emit(BeaconEvent.New, beaconInfoEvent, beacon);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const localStorageGetSpy = jest.spyOn(localStorage.__proto__, 'getItem').mockReturnValue(undefined);
|
||||||
|
const localStorageSetSpy = jest.spyOn(localStorage.__proto__, 'setItem').mockImplementation(() => {});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
geolocation = mockGeolocation();
|
geolocation = mockGeolocation();
|
||||||
mockClient.getVisibleRooms.mockReturnValue([]);
|
mockClient.getVisibleRooms.mockReturnValue([]);
|
||||||
|
@ -164,6 +169,9 @@ describe('OwnBeaconStore', () => {
|
||||||
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
||||||
jest.spyOn(OwnBeaconStore.instance, 'emit').mockRestore();
|
jest.spyOn(OwnBeaconStore.instance, 'emit').mockRestore();
|
||||||
jest.spyOn(logger, 'error').mockRestore();
|
jest.spyOn(logger, 'error').mockRestore();
|
||||||
|
|
||||||
|
localStorageGetSpy.mockClear().mockReturnValue(undefined);
|
||||||
|
localStorageSetSpy.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
@ -172,6 +180,10 @@ describe('OwnBeaconStore', () => {
|
||||||
jest.clearAllTimers();
|
jest.clearAllTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
localStorageGetSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
describe('onReady()', () => {
|
describe('onReady()', () => {
|
||||||
it('initialises correctly with no beacons', async () => {
|
it('initialises correctly with no beacons', async () => {
|
||||||
makeRoomsWithStateEvents();
|
makeRoomsWithStateEvents();
|
||||||
|
@ -195,7 +207,27 @@ describe('OwnBeaconStore', () => {
|
||||||
bobsOldRoom1BeaconInfo,
|
bobsOldRoom1BeaconInfo,
|
||||||
]);
|
]);
|
||||||
const store = await makeOwnBeaconStore();
|
const store = await makeOwnBeaconStore();
|
||||||
expect(store.hasLiveBeacons()).toBe(true);
|
expect(store.beaconsByRoomId.get(room1Id)).toEqual(new Set([
|
||||||
|
getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
|
||||||
|
]));
|
||||||
|
expect(store.beaconsByRoomId.get(room2Id)).toEqual(new Set([
|
||||||
|
getBeaconInfoIdentifier(alicesRoom2BeaconInfo),
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates live beacon ids when users own beacons were created on device', async () => {
|
||||||
|
localStorageGetSpy.mockReturnValue(JSON.stringify([
|
||||||
|
alicesRoom1BeaconInfo.getId(),
|
||||||
|
alicesRoom2BeaconInfo.getId(),
|
||||||
|
]));
|
||||||
|
makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
alicesRoom2BeaconInfo,
|
||||||
|
bobsRoom1BeaconInfo,
|
||||||
|
bobsOldRoom1BeaconInfo,
|
||||||
|
]);
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
expect(store.hasLiveBeacons(room1Id)).toBeTruthy();
|
||||||
expect(store.getLiveBeaconIds()).toEqual([
|
expect(store.getLiveBeaconIds()).toEqual([
|
||||||
getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
|
getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
|
||||||
getBeaconInfoIdentifier(alicesRoom2BeaconInfo),
|
getBeaconInfoIdentifier(alicesRoom2BeaconInfo),
|
||||||
|
@ -214,6 +246,10 @@ describe('OwnBeaconStore', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does geolocation and sends location immediatley when user has live beacons', async () => {
|
it('does geolocation and sends location immediatley when user has live beacons', async () => {
|
||||||
|
localStorageGetSpy.mockReturnValue(JSON.stringify([
|
||||||
|
alicesRoom1BeaconInfo.getId(),
|
||||||
|
alicesRoom2BeaconInfo.getId(),
|
||||||
|
]));
|
||||||
makeRoomsWithStateEvents([
|
makeRoomsWithStateEvents([
|
||||||
alicesRoom1BeaconInfo,
|
alicesRoom1BeaconInfo,
|
||||||
alicesRoom2BeaconInfo,
|
alicesRoom2BeaconInfo,
|
||||||
|
@ -245,7 +281,8 @@ describe('OwnBeaconStore', () => {
|
||||||
expect(removeSpy.mock.calls[0]).toEqual(expect.arrayContaining([BeaconEvent.LivenessChange]));
|
expect(removeSpy.mock.calls[0]).toEqual(expect.arrayContaining([BeaconEvent.LivenessChange]));
|
||||||
expect(removeSpy.mock.calls[1]).toEqual(expect.arrayContaining([BeaconEvent.New]));
|
expect(removeSpy.mock.calls[1]).toEqual(expect.arrayContaining([BeaconEvent.New]));
|
||||||
expect(removeSpy.mock.calls[2]).toEqual(expect.arrayContaining([BeaconEvent.Update]));
|
expect(removeSpy.mock.calls[2]).toEqual(expect.arrayContaining([BeaconEvent.Update]));
|
||||||
expect(removeSpy.mock.calls[3]).toEqual(expect.arrayContaining([RoomStateEvent.Members]));
|
expect(removeSpy.mock.calls[3]).toEqual(expect.arrayContaining([BeaconEvent.Destroy]));
|
||||||
|
expect(removeSpy.mock.calls[4]).toEqual(expect.arrayContaining([RoomStateEvent.Members]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('destroys beacons', async () => {
|
it('destroys beacons', async () => {
|
||||||
|
@ -270,6 +307,10 @@ describe('OwnBeaconStore', () => {
|
||||||
bobsRoom1BeaconInfo,
|
bobsRoom1BeaconInfo,
|
||||||
bobsOldRoom1BeaconInfo,
|
bobsOldRoom1BeaconInfo,
|
||||||
]);
|
]);
|
||||||
|
localStorageGetSpy.mockReturnValue(JSON.stringify([
|
||||||
|
alicesRoom1BeaconInfo.getId(),
|
||||||
|
alicesRoom2BeaconInfo.getId(),
|
||||||
|
]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns true when user has live beacons', async () => {
|
it('returns true when user has live beacons', async () => {
|
||||||
|
@ -320,6 +361,10 @@ describe('OwnBeaconStore', () => {
|
||||||
bobsRoom1BeaconInfo,
|
bobsRoom1BeaconInfo,
|
||||||
bobsOldRoom1BeaconInfo,
|
bobsOldRoom1BeaconInfo,
|
||||||
]);
|
]);
|
||||||
|
localStorageGetSpy.mockReturnValue(JSON.stringify([
|
||||||
|
alicesRoom1BeaconInfo.getId(),
|
||||||
|
alicesRoom2BeaconInfo.getId(),
|
||||||
|
]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns live beacons when user has live beacons', async () => {
|
it('returns live beacons when user has live beacons', async () => {
|
||||||
|
@ -371,6 +416,13 @@ describe('OwnBeaconStore', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('on new beacon event', () => {
|
describe('on new beacon event', () => {
|
||||||
|
// assume all beacons were created on this device
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorageGetSpy.mockReturnValue(JSON.stringify([
|
||||||
|
alicesRoom1BeaconInfo.getId(),
|
||||||
|
alicesRoom2BeaconInfo.getId(),
|
||||||
|
]));
|
||||||
|
});
|
||||||
it('ignores events for irrelevant beacons', async () => {
|
it('ignores events for irrelevant beacons', async () => {
|
||||||
makeRoomsWithStateEvents([]);
|
makeRoomsWithStateEvents([]);
|
||||||
const store = await makeOwnBeaconStore();
|
const store = await makeOwnBeaconStore();
|
||||||
|
@ -425,6 +477,16 @@ describe('OwnBeaconStore', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('on liveness change event', () => {
|
describe('on liveness change event', () => {
|
||||||
|
// assume all beacons were created on this device
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorageGetSpy.mockReturnValue(JSON.stringify([
|
||||||
|
alicesRoom1BeaconInfo.getId(),
|
||||||
|
alicesRoom2BeaconInfo.getId(),
|
||||||
|
alicesOldRoomIdBeaconInfo.getId(),
|
||||||
|
'update-event-id',
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
it('ignores events for irrelevant beacons', async () => {
|
it('ignores events for irrelevant beacons', async () => {
|
||||||
makeRoomsWithStateEvents([
|
makeRoomsWithStateEvents([
|
||||||
alicesRoom1BeaconInfo,
|
alicesRoom1BeaconInfo,
|
||||||
|
@ -501,6 +563,13 @@ describe('OwnBeaconStore', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('on room membership changes', () => {
|
describe('on room membership changes', () => {
|
||||||
|
// assume all beacons were created on this device
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorageGetSpy.mockReturnValue(JSON.stringify([
|
||||||
|
alicesRoom1BeaconInfo.getId(),
|
||||||
|
alicesRoom2BeaconInfo.getId(),
|
||||||
|
]));
|
||||||
|
});
|
||||||
it('ignores events for rooms without beacons', async () => {
|
it('ignores events for rooms without beacons', async () => {
|
||||||
const membershipEvent = makeMembershipEvent(room2Id, aliceId);
|
const membershipEvent = makeMembershipEvent(room2Id, aliceId);
|
||||||
// no beacons for room2
|
// no beacons for room2
|
||||||
|
@ -606,6 +675,54 @@ describe('OwnBeaconStore', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('on destroy event', () => {
|
||||||
|
// assume all beacons were created on this device
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorageGetSpy.mockReturnValue(JSON.stringify([
|
||||||
|
alicesRoom1BeaconInfo.getId(),
|
||||||
|
alicesRoom2BeaconInfo.getId(),
|
||||||
|
alicesOldRoomIdBeaconInfo.getId(),
|
||||||
|
'update-event-id',
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores events for irrelevant beacons', async () => {
|
||||||
|
makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
]);
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
const emitSpy = jest.spyOn(store, 'emit');
|
||||||
|
const oldLiveBeaconIds = store.getLiveBeaconIds();
|
||||||
|
const bobsLiveBeacon = new Beacon(bobsRoom1BeaconInfo);
|
||||||
|
|
||||||
|
mockClient.emit(BeaconEvent.Destroy, bobsLiveBeacon.identifier);
|
||||||
|
|
||||||
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
|
// strictly equal
|
||||||
|
expect(store.getLiveBeaconIds()).toBe(oldLiveBeaconIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates state and emits beacon liveness changes from true to false', async () => {
|
||||||
|
makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
]);
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
|
||||||
|
// live before
|
||||||
|
expect(store.hasLiveBeacons()).toBe(true);
|
||||||
|
const emitSpy = jest.spyOn(store, 'emit');
|
||||||
|
|
||||||
|
const beacon = store.getBeaconById(getBeaconInfoIdentifier(alicesRoom1BeaconInfo));
|
||||||
|
|
||||||
|
beacon.destroy();
|
||||||
|
mockClient.emit(BeaconEvent.Destroy, beacon.identifier);
|
||||||
|
|
||||||
|
expect(store.hasLiveBeacons()).toBe(false);
|
||||||
|
expect(store.hasLiveBeacons(room1Id)).toBe(false);
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith(OwnBeaconStoreEvent.LivenessChange, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('stopBeacon()', () => {
|
describe('stopBeacon()', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
makeRoomsWithStateEvents([
|
makeRoomsWithStateEvents([
|
||||||
|
@ -672,9 +789,38 @@ describe('OwnBeaconStore', () => {
|
||||||
expectedUpdateContent,
|
expectedUpdateContent,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('removes beacon event id from local store', async () => {
|
||||||
|
localStorageGetSpy.mockReturnValue(JSON.stringify([
|
||||||
|
alicesRoom1BeaconInfo.getId(),
|
||||||
|
alicesRoom2BeaconInfo.getId(),
|
||||||
|
]));
|
||||||
|
makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
]);
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
|
||||||
|
await store.stopBeacon(getBeaconInfoIdentifier(alicesRoom1BeaconInfo));
|
||||||
|
|
||||||
|
expect(localStorageSetSpy).toHaveBeenCalledWith(
|
||||||
|
'mx_live_beacon_created_id',
|
||||||
|
// stopped beacon's event_id was removed
|
||||||
|
JSON.stringify([alicesRoom2BeaconInfo.getId()]),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('publishing positions', () => {
|
describe('publishing positions', () => {
|
||||||
|
// assume all beacons were created on this device
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorageGetSpy.mockReturnValue(JSON.stringify([
|
||||||
|
alicesRoom1BeaconInfo.getId(),
|
||||||
|
alicesRoom2BeaconInfo.getId(),
|
||||||
|
alicesOldRoomIdBeaconInfo.getId(),
|
||||||
|
'update-event-id',
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
it('stops watching position when user has no more live beacons', async () => {
|
it('stops watching position when user has no more live beacons', async () => {
|
||||||
// geolocation is only going to emit 1 position
|
// geolocation is only going to emit 1 position
|
||||||
geolocation.watchPosition.mockImplementation(
|
geolocation.watchPosition.mockImplementation(
|
||||||
|
@ -842,6 +988,7 @@ describe('OwnBeaconStore', () => {
|
||||||
// called for each position from watchPosition
|
// called for each position from watchPosition
|
||||||
expect(mockClient.sendEvent).toHaveBeenCalledTimes(5);
|
expect(mockClient.sendEvent).toHaveBeenCalledTimes(5);
|
||||||
expect(store.beaconHasWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo))).toBe(false);
|
expect(store.beaconHasWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo))).toBe(false);
|
||||||
|
expect(store.getLiveBeaconIdsWithWireError()).toEqual([]);
|
||||||
expect(store.hasWireErrors()).toBe(false);
|
expect(store.hasWireErrors()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -892,6 +1039,12 @@ describe('OwnBeaconStore', () => {
|
||||||
// only two allowed failures
|
// only two allowed failures
|
||||||
expect(mockClient.sendEvent).toHaveBeenCalledTimes(2);
|
expect(mockClient.sendEvent).toHaveBeenCalledTimes(2);
|
||||||
expect(store.beaconHasWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo))).toBe(true);
|
expect(store.beaconHasWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo))).toBe(true);
|
||||||
|
expect(store.getLiveBeaconIdsWithWireError()).toEqual(
|
||||||
|
[getBeaconInfoIdentifier(alicesRoom1BeaconInfo)],
|
||||||
|
);
|
||||||
|
expect(store.getLiveBeaconIdsWithWireError(room1Id)).toEqual(
|
||||||
|
[getBeaconInfoIdentifier(alicesRoom1BeaconInfo)],
|
||||||
|
);
|
||||||
expect(store.hasWireErrors()).toBe(true);
|
expect(store.hasWireErrors()).toBe(true);
|
||||||
expect(emitSpy).toHaveBeenCalledWith(
|
expect(emitSpy).toHaveBeenCalledWith(
|
||||||
OwnBeaconStoreEvent.WireError, getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
|
OwnBeaconStoreEvent.WireError, getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
|
||||||
|
@ -1055,4 +1208,79 @@ describe('OwnBeaconStore', () => {
|
||||||
expect(mockClient.sendEvent).not.toHaveBeenCalled();
|
expect(mockClient.sendEvent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('createLiveBeacon', () => {
|
||||||
|
const newEventId = 'new-beacon-event-id';
|
||||||
|
const loggerErrorSpy = jest.spyOn(logger, 'error').mockImplementation(() => {});
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorageGetSpy.mockReturnValue(JSON.stringify([
|
||||||
|
alicesRoom1BeaconInfo.getId(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
localStorageSetSpy.mockClear();
|
||||||
|
|
||||||
|
mockClient.unstable_createLiveBeacon.mockResolvedValue({ event_id: newEventId });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a live beacon', async () => {
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
const content = makeBeaconInfoContent(100);
|
||||||
|
await store.createLiveBeacon(room1Id, content);
|
||||||
|
expect(mockClient.unstable_createLiveBeacon).toHaveBeenCalledWith(room1Id, content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets new beacon event id in local storage', async () => {
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
const content = makeBeaconInfoContent(100);
|
||||||
|
await store.createLiveBeacon(room1Id, content);
|
||||||
|
|
||||||
|
expect(localStorageSetSpy).toHaveBeenCalledWith(
|
||||||
|
'mx_live_beacon_created_id',
|
||||||
|
JSON.stringify([
|
||||||
|
alicesRoom1BeaconInfo.getId(),
|
||||||
|
newEventId,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles saving beacon event id when local storage has bad value', async () => {
|
||||||
|
localStorageGetSpy.mockReturnValue(JSON.stringify({ id: '1' }));
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
const content = makeBeaconInfoContent(100);
|
||||||
|
await store.createLiveBeacon(room1Id, content);
|
||||||
|
|
||||||
|
// stored successfully
|
||||||
|
expect(localStorageSetSpy).toHaveBeenCalledWith(
|
||||||
|
'mx_live_beacon_created_id',
|
||||||
|
JSON.stringify([
|
||||||
|
newEventId,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a live beacon without error when no beacons exist for room', async () => {
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
const content = makeBeaconInfoContent(100);
|
||||||
|
await store.createLiveBeacon(room1Id, content);
|
||||||
|
|
||||||
|
// didn't throw, no error log
|
||||||
|
expect(loggerErrorSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stops live beacons for room after creating new beacon', async () => {
|
||||||
|
// room1 already has a beacon
|
||||||
|
makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
]);
|
||||||
|
// but it was not created on this device
|
||||||
|
localStorageGetSpy.mockReturnValue(undefined);
|
||||||
|
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
const content = makeBeaconInfoContent(100);
|
||||||
|
await store.createLiveBeacon(room1Id, content);
|
||||||
|
|
||||||
|
// update beacon called
|
||||||
|
expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
Loading…
Reference in a new issue