mirror of
https://github.com/element-hq/element-web
synced 2024-11-26 19:26:04 +03:00
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 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.
|
||||
|
|
|
@ -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 () => {
|
||||
|
@ -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();
|
||||
|
|
|
@ -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'),
|
||||
|
@ -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: {
|
||||
|
@ -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) =>
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
RoomStateEvent,
|
||||
RoomMember,
|
||||
} 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 { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
|
@ -64,6 +64,7 @@ describe('OwnBeaconStore', () => {
|
|||
getVisibleRooms: jest.fn().mockReturnValue([]),
|
||||
unstable_setLiveBeacon: 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 room2Id = '$room2:server.org';
|
||||
|
@ -144,6 +145,7 @@ describe('OwnBeaconStore', () => {
|
|||
beaconInfoEvent.getSender(),
|
||||
beaconInfoEvent.getRoomId(),
|
||||
{ isLive, timeout: beacon.beaconInfo.timeout },
|
||||
'update-event-id',
|
||||
);
|
||||
beacon.update(updateEvent);
|
||||
|
||||
|
@ -156,6 +158,9 @@ describe('OwnBeaconStore', () => {
|
|||
mockClient.emit(BeaconEvent.New, beaconInfoEvent, beacon);
|
||||
};
|
||||
|
||||
const localStorageGetSpy = jest.spyOn(localStorage.__proto__, 'getItem').mockReturnValue(undefined);
|
||||
const localStorageSetSpy = jest.spyOn(localStorage.__proto__, 'setItem').mockImplementation(() => {});
|
||||
|
||||
beforeEach(() => {
|
||||
geolocation = mockGeolocation();
|
||||
mockClient.getVisibleRooms.mockReturnValue([]);
|
||||
|
@ -164,6 +169,9 @@ describe('OwnBeaconStore', () => {
|
|||
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
||||
jest.spyOn(OwnBeaconStore.instance, 'emit').mockRestore();
|
||||
jest.spyOn(logger, 'error').mockRestore();
|
||||
|
||||
localStorageGetSpy.mockClear().mockReturnValue(undefined);
|
||||
localStorageSetSpy.mockClear();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
@ -172,6 +180,10 @@ describe('OwnBeaconStore', () => {
|
|||
jest.clearAllTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
localStorageGetSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('onReady()', () => {
|
||||
it('initialises correctly with no beacons', async () => {
|
||||
makeRoomsWithStateEvents();
|
||||
|
@ -195,7 +207,27 @@ describe('OwnBeaconStore', () => {
|
|||
bobsOldRoom1BeaconInfo,
|
||||
]);
|
||||
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([
|
||||
getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
|
||||
getBeaconInfoIdentifier(alicesRoom2BeaconInfo),
|
||||
|
@ -214,6 +246,10 @@ describe('OwnBeaconStore', () => {
|
|||
});
|
||||
|
||||
it('does geolocation and sends location immediatley when user has live beacons', async () => {
|
||||
localStorageGetSpy.mockReturnValue(JSON.stringify([
|
||||
alicesRoom1BeaconInfo.getId(),
|
||||
alicesRoom2BeaconInfo.getId(),
|
||||
]));
|
||||
makeRoomsWithStateEvents([
|
||||
alicesRoom1BeaconInfo,
|
||||
alicesRoom2BeaconInfo,
|
||||
|
@ -245,7 +281,8 @@ describe('OwnBeaconStore', () => {
|
|||
expect(removeSpy.mock.calls[0]).toEqual(expect.arrayContaining([BeaconEvent.LivenessChange]));
|
||||
expect(removeSpy.mock.calls[1]).toEqual(expect.arrayContaining([BeaconEvent.New]));
|
||||
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 () => {
|
||||
|
@ -270,6 +307,10 @@ describe('OwnBeaconStore', () => {
|
|||
bobsRoom1BeaconInfo,
|
||||
bobsOldRoom1BeaconInfo,
|
||||
]);
|
||||
localStorageGetSpy.mockReturnValue(JSON.stringify([
|
||||
alicesRoom1BeaconInfo.getId(),
|
||||
alicesRoom2BeaconInfo.getId(),
|
||||
]));
|
||||
});
|
||||
|
||||
it('returns true when user has live beacons', async () => {
|
||||
|
@ -320,6 +361,10 @@ describe('OwnBeaconStore', () => {
|
|||
bobsRoom1BeaconInfo,
|
||||
bobsOldRoom1BeaconInfo,
|
||||
]);
|
||||
localStorageGetSpy.mockReturnValue(JSON.stringify([
|
||||
alicesRoom1BeaconInfo.getId(),
|
||||
alicesRoom2BeaconInfo.getId(),
|
||||
]));
|
||||
});
|
||||
|
||||
it('returns live beacons when user has live beacons', async () => {
|
||||
|
@ -371,6 +416,13 @@ describe('OwnBeaconStore', () => {
|
|||
});
|
||||
|
||||
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 () => {
|
||||
makeRoomsWithStateEvents([]);
|
||||
const store = await makeOwnBeaconStore();
|
||||
|
@ -425,6 +477,16 @@ describe('OwnBeaconStore', () => {
|
|||
});
|
||||
|
||||
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 () => {
|
||||
makeRoomsWithStateEvents([
|
||||
alicesRoom1BeaconInfo,
|
||||
|
@ -501,6 +563,13 @@ describe('OwnBeaconStore', () => {
|
|||
});
|
||||
|
||||
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 () => {
|
||||
const membershipEvent = makeMembershipEvent(room2Id, aliceId);
|
||||
// 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()', () => {
|
||||
beforeEach(() => {
|
||||
makeRoomsWithStateEvents([
|
||||
|
@ -672,9 +789,38 @@ describe('OwnBeaconStore', () => {
|
|||
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', () => {
|
||||
// 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 () => {
|
||||
// geolocation is only going to emit 1 position
|
||||
geolocation.watchPosition.mockImplementation(
|
||||
|
@ -842,6 +988,7 @@ describe('OwnBeaconStore', () => {
|
|||
// called for each position from watchPosition
|
||||
expect(mockClient.sendEvent).toHaveBeenCalledTimes(5);
|
||||
expect(store.beaconHasWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo))).toBe(false);
|
||||
expect(store.getLiveBeaconIdsWithWireError()).toEqual([]);
|
||||
expect(store.hasWireErrors()).toBe(false);
|
||||
});
|
||||
|
||||
|
@ -892,6 +1039,12 @@ describe('OwnBeaconStore', () => {
|
|||
// only two allowed failures
|
||||
expect(mockClient.sendEvent).toHaveBeenCalledTimes(2);
|
||||
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(emitSpy).toHaveBeenCalledWith(
|
||||
OwnBeaconStoreEvent.WireError, getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
|
||||
|
@ -1055,4 +1208,79 @@ describe('OwnBeaconStore', () => {
|
|||
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");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
Loading…
Reference in a new issue