diff --git a/src/components/views/location/shareLocation.ts b/src/components/views/location/shareLocation.ts index 6654a389a0..895f2c23c6 100644 --- a/src/components/views/location/shareLocation.ts +++ b/src/components/views/location/shareLocation.ts @@ -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, diff --git a/src/stores/OwnBeaconStore.ts b/src/stores/OwnBeaconStore.ts index 1bef120ae7..b4b4a4328a 100644 --- a/src/stores/OwnBeaconStore.ts +++ b/src/stores/OwnBeaconStore.ts @@ -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>; 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 { private static internalInstance = new OwnBeaconStore(); // users beacons, keyed by event type @@ -110,6 +134,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient { 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 { 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 { 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 { 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 { // 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 { }; 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 { } }; + public createLiveBeacon = async ( + roomId: Room['roomId'], + beaconInfoContent: MBeaconInfoEventContent, + ): Promise => { + // 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 { 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)); }; diff --git a/src/utils/beacon/timeline.ts b/src/utils/beacon/timeline.ts index e6e8f2311d..d00872f865 100644 --- a/src/utils/beacon/timeline.ts +++ b/src/utils/beacon/timeline.ts @@ -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. diff --git a/test/components/views/beacon/RoomLiveShareWarning-test.tsx b/test/components/views/beacon/RoomLiveShareWarning-test.tsx index 6fa6098f2c..030ac8ea4a 100644 --- a/test/components/views/beacon/RoomLiveShareWarning-test.tsx +++ b/test/components/views/beacon/RoomLiveShareWarning-test.tsx @@ -93,10 +93,20 @@ describe('', () => { 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('', () => { afterAll(() => { jest.spyOn(global.Date, 'now').mockRestore(); + localStorageSpy.mockRestore(); }); const getExpiryText = wrapper => findByTestId(wrapper, 'room-live-share-expiry').text(); diff --git a/test/components/views/location/LocationShareMenu-test.tsx b/test/components/views/location/LocationShareMenu-test.tsx index e873b9db89..ce52125f86 100644 --- a/test/components/views/location/LocationShareMenu-test.tsx +++ b/test/components/views/location/LocationShareMenu-test.tsx @@ -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('', () => { 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('', () => { type: 'geolocate', }; + const makeOwnBeaconStore = async () => { + const store = OwnBeaconStore.instance; + + await setupAsyncStoreWithClient(store, mockClient); + return store; + }; + const getComponent = (props = {}) => mount(, { 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) => diff --git a/test/stores/OwnBeaconStore-test.ts b/test/stores/OwnBeaconStore-test.ts index 6a95c82ad0..df1d868e40 100644 --- a/test/stores/OwnBeaconStore-test.ts +++ b/test/stores/OwnBeaconStore-test.ts @@ -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(); + }); + }); }); diff --git a/test/utils/beacon/timeline-test.ts b/test/utils/beacon/timeline-test.ts index 96be565de7..59217d2459 100644 --- a/test/utils/beacon/timeline-test.ts +++ b/test/utils/beacon/timeline-test.ts @@ -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.