Live location sharing - update beacon_info implementation to latest MSC (#8256)

* update calls to set and createLiveBeacon

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix stop beacon

Signed-off-by: Kerry Archibald <kerrya@element.io>

* remove variable event type from beacon utils

Signed-off-by: Kerry Archibald <kerrya@element.io>

* use beacon identifier

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix RoomLiveShareWarning tests

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add case for beacon update

Signed-off-by: Kerry Archibald <kerrya@element.io>

* lint

Signed-off-by: Kerry Archibald <kerrya@element.io>

* more lint

Signed-off-by: Kerry Archibald <kerrya@element.io>
pull/21833/head
Kerry 2022-04-08 10:53:06 +02:00 committed by GitHub
parent 610225aef2
commit 03d0969ae3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 123 additions and 108 deletions

View File

@ -16,7 +16,12 @@ limitations under the License.
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Room, Beacon } from 'matrix-js-sdk/src/matrix'; import {
Room,
Beacon,
BeaconEvent,
BeaconIdentifier,
} from 'matrix-js-sdk/src/matrix';
import { formatDuration } from '../../../DateUtils'; import { formatDuration } from '../../../DateUtils';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -45,16 +50,22 @@ const getUpdateInterval = (ms: number) => {
return 1000; return 1000;
}; };
const useMsRemaining = (beacon: Beacon): number => { const useMsRemaining = (beacon: Beacon): number => {
const [msRemaining, setMsRemaining] = useState(() => getBeaconMsUntilExpiry(beacon)); const beaconInfo = useEventEmitterState(
beacon,
BeaconEvent.Update,
() => beacon.beaconInfo,
);
const [msRemaining, setMsRemaining] = useState(() => getBeaconMsUntilExpiry(beaconInfo));
useEffect(() => { useEffect(() => {
setMsRemaining(getBeaconMsUntilExpiry(beacon)); setMsRemaining(getBeaconMsUntilExpiry(beaconInfo));
}, [beacon]); }, [beaconInfo]);
const updateMsRemaining = useCallback(() => { const updateMsRemaining = useCallback(() => {
const ms = getBeaconMsUntilExpiry(beacon); const ms = getBeaconMsUntilExpiry(beaconInfo);
setMsRemaining(ms); setMsRemaining(ms);
}, [beacon]); }, [beaconInfo]);
useInterval(updateMsRemaining, getUpdateInterval(msRemaining)); useInterval(updateMsRemaining, getUpdateInterval(msRemaining));
@ -74,7 +85,7 @@ type LiveBeaconsState = {
hasStopSharingError?: boolean; hasStopSharingError?: boolean;
hasWireError?: boolean; hasWireError?: boolean;
}; };
const useLiveBeacons = (liveBeaconIds: string[], roomId: string): LiveBeaconsState => { const useLiveBeacons = (liveBeaconIds: BeaconIdentifier[], roomId: string): LiveBeaconsState => {
const [stoppingInProgress, setStoppingInProgress] = useState(false); const [stoppingInProgress, setStoppingInProgress] = useState(false);
const [error, setError] = useState<Error>(); const [error, setError] = useState<Error>();

View File

@ -78,8 +78,7 @@ export const shareLiveLocation = (
description, description,
LocationAssetType.Self, LocationAssetType.Self,
), ),
// use timestamp as unique suffix in interim );
`${Date.now()}`);
} catch (error) { } catch (error) {
handleShareError(error, openMenu, LocationShareType.Live); handleShareError(error, openMenu, LocationShareType.Live);
} }

View File

@ -17,6 +17,7 @@ limitations under the License.
import { debounce } from "lodash"; import { debounce } from "lodash";
import { import {
Beacon, Beacon,
BeaconIdentifier,
BeaconEvent, BeaconEvent,
MatrixEvent, MatrixEvent,
Room, Room,
@ -58,22 +59,22 @@ const STATIC_UPDATE_INTERVAL = 30000;
const BAIL_AFTER_CONSECUTIVE_ERROR_COUNT = 2; const BAIL_AFTER_CONSECUTIVE_ERROR_COUNT = 2;
type OwnBeaconStoreState = { type OwnBeaconStoreState = {
beacons: Map<string, Beacon>; beacons: Map<BeaconIdentifier, Beacon>;
beaconWireErrors: Map<string, Beacon>; beaconWireErrors: Map<string, Beacon>;
beaconsByRoomId: Map<Room['roomId'], Set<string>>; beaconsByRoomId: Map<Room['roomId'], Set<BeaconIdentifier>>;
liveBeaconIds: string[]; liveBeaconIds: BeaconIdentifier[];
}; };
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
public readonly beacons = new Map<string, Beacon>(); public readonly beacons = new Map<BeaconIdentifier, Beacon>();
public readonly beaconsByRoomId = new Map<Room['roomId'], Set<string>>(); public readonly beaconsByRoomId = new Map<Room['roomId'], Set<BeaconIdentifier>>();
/** /**
* Track over the wire errors for published positions * Track over the wire errors for published positions
* Counts consecutive wire errors per beacon * Counts consecutive wire errors per beacon
* Reset on successful publish of location * Reset on successful publish of location
*/ */
public readonly beaconWireErrorCounts = new Map<string, number>(); public readonly beaconWireErrorCounts = new Map<BeaconIdentifier, number>();
/** /**
* ids of live beacons * ids of live beacons
* ordered by creation time descending * ordered by creation time descending
@ -108,6 +109,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
protected async onNotReady() { protected async onNotReady() {
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(RoomStateEvent.Members, this.onRoomStateMembers); this.matrixClient.removeListener(RoomStateEvent.Members, this.onRoomStateMembers);
this.beacons.forEach(beacon => beacon.destroy()); this.beacons.forEach(beacon => beacon.destroy());
@ -122,6 +124,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
protected async onReady(): Promise<void> { protected async onReady(): Promise<void> {
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.removeListener(BeaconEvent.Update, this.onUpdateBeacon);
this.matrixClient.on(RoomStateEvent.Members, this.onRoomStateMembers); this.matrixClient.on(RoomStateEvent.Members, this.onRoomStateMembers);
this.initialiseBeaconState(); this.initialiseBeaconState();
@ -177,8 +180,8 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
return this.beacons.get(beaconId); return this.beacons.get(beaconId);
}; };
public stopBeacon = async (beaconInfoType: string): Promise<void> => { public stopBeacon = async (beaconIdentifier: string): Promise<void> => {
const beacon = this.beacons.get(beaconInfoType); const beacon = this.beacons.get(beaconIdentifier);
// if no beacon, or beacon is already explicitly set isLive: false // if no beacon, or beacon is already explicitly set isLive: false
// do nothing // do nothing
if (!beacon?.beaconInfo?.live) { if (!beacon?.beaconInfo?.live) {
@ -200,6 +203,17 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
this.checkLiveness(); this.checkLiveness();
}; };
/**
* This will be called when a beacon is replaced
*/
private onUpdateBeacon = (_event: MatrixEvent, beacon: Beacon): void => {
if (!isOwnBeacon(beacon, this.matrixClient.getUserId())) {
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)) {
@ -439,7 +453,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
assetType, assetType,
timestamp); timestamp);
await this.matrixClient.unstable_setLiveBeacon(beacon.roomId, beacon.beaconInfoEventType, updateContent); await this.matrixClient.unstable_setLiveBeacon(beacon.roomId, updateContent);
}; };
/** /**

View File

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { BeaconInfoState } from "matrix-js-sdk/src/content-helpers";
import { Beacon } from "matrix-js-sdk/src/matrix"; import { Beacon } from "matrix-js-sdk/src/matrix";
/** /**
@ -26,8 +27,8 @@ import { Beacon } from "matrix-js-sdk/src/matrix";
export const msUntilExpiry = (startTimestamp: number, durationMs: number): number => export const msUntilExpiry = (startTimestamp: number, durationMs: number): number =>
Math.max(0, (startTimestamp + durationMs) - Date.now()); Math.max(0, (startTimestamp + durationMs) - Date.now());
export const getBeaconMsUntilExpiry = (beacon: Beacon): number => export const getBeaconMsUntilExpiry = (beaconInfo: BeaconInfoState): number =>
msUntilExpiry(beacon.beaconInfo.timestamp, beacon.beaconInfo.timeout); msUntilExpiry(beaconInfo.timestamp, beaconInfo.timeout);
export const getBeaconExpiryTimestamp = (beacon: Beacon): number => export const getBeaconExpiryTimestamp = (beacon: Beacon): number =>
beacon.beaconInfo.timestamp + beacon.beaconInfo.timeout; beacon.beaconInfo.timestamp + beacon.beaconInfo.timeout;

View File

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { Room, Beacon, BeaconEvent } from 'matrix-js-sdk/src/matrix'; import { Room, Beacon, BeaconEvent, getBeaconInfoIdentifier } from 'matrix-js-sdk/src/matrix';
import { logger } from 'matrix-js-sdk/src/logger'; import { logger } from 'matrix-js-sdk/src/logger';
import RoomLiveShareWarning from '../../../../src/components/views/beacon/RoomLiveShareWarning'; import RoomLiveShareWarning from '../../../../src/components/views/beacon/RoomLiveShareWarning';
@ -221,6 +221,25 @@ describe('<RoomLiveShareWarning />', () => {
expect(getExpiryText(component)).toEqual('35m left'); expect(getExpiryText(component)).toEqual('35m left');
}); });
it('updates beacon time left when beacon updates', () => {
const component = getComponent({ roomId: room1Id });
expect(getExpiryText(component)).toEqual('1h left');
expect(getExpiryText(component)).toEqual('1h left');
act(() => {
const beacon = OwnBeaconStore.instance.getBeaconById(getBeaconInfoIdentifier(room1Beacon1));
const room1Beacon1Update = makeBeaconInfoEvent(aliceId, room1Id, {
isLive: true,
timeout: 3 * HOUR_MS,
}, '$0');
beacon.update(room1Beacon1Update);
});
// update to expiry of new beacon
expect(getExpiryText(component)).toEqual('3h left');
});
it('clears expiry time interval on unmount', () => { it('clears expiry time interval on unmount', () => {
const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); const clearIntervalSpy = jest.spyOn(global, 'clearInterval');
const component = getComponent({ roomId: room1Id }); const component = getComponent({ roomId: room1Id });
@ -242,7 +261,7 @@ describe('<RoomLiveShareWarning />', () => {
component.setProps({}); component.setProps({});
}); });
expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledTimes(2); expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalled();
expect(component.find('Spinner').length).toBeTruthy(); expect(component.find('Spinner').length).toBeTruthy();
expect(findByTestId(component, 'room-live-share-primary-button').at(0).props().disabled).toBeTruthy(); expect(findByTestId(component, 'room-live-share-primary-button').at(0).props().disabled).toBeTruthy();
}); });
@ -314,7 +333,7 @@ describe('<RoomLiveShareWarning />', () => {
// update mock and emit event // update mock and emit event
act(() => { act(() => {
hasWireErrorsSpy.mockReturnValue(true); hasWireErrorsSpy.mockReturnValue(true);
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.WireError, room2Beacon1.getType()); OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.WireError, getBeaconInfoIdentifier(room2Beacon1));
}); });
component.setProps({}); component.setProps({});
@ -332,7 +351,7 @@ describe('<RoomLiveShareWarning />', () => {
// update mock and emit event // update mock and emit event
act(() => { act(() => {
hasWireErrorsSpy.mockReturnValue(false); hasWireErrorsSpy.mockReturnValue(false);
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.WireError, room2Beacon1.getType()); OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.WireError, getBeaconInfoIdentifier(room2Beacon1));
}); });
component.setProps({}); component.setProps({});
@ -353,8 +372,7 @@ describe('<RoomLiveShareWarning />', () => {
findByTestId(component, 'room-live-share-primary-button').at(0).simulate('click'); findByTestId(component, 'room-live-share-primary-button').at(0).simulate('click');
}); });
expect(resetErrorSpy).toHaveBeenCalledWith(room2Beacon1.getType()); expect(resetErrorSpy).toHaveBeenCalledWith(getBeaconInfoIdentifier(room2Beacon1));
expect(resetErrorSpy).toHaveBeenCalledWith(room2Beacon2.getType());
}); });
it('clicking close button stops beacons', async () => { it('clicking close button stops beacons', async () => {
@ -367,8 +385,7 @@ describe('<RoomLiveShareWarning />', () => {
findByTestId(component, 'room-live-share-wire-error-close-button').at(0).simulate('click'); findByTestId(component, 'room-live-share-wire-error-close-button').at(0).simulate('click');
}); });
expect(stopBeaconSpy).toHaveBeenCalledWith(room2Beacon1.getType()); expect(stopBeaconSpy).toHaveBeenCalledWith(getBeaconInfoIdentifier(room2Beacon1));
expect(stopBeaconSpy).toHaveBeenCalledWith(room2Beacon2.getType());
}); });
}); });
}); });

View File

@ -13,8 +13,7 @@ exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is
<RoomLiveShareWarningInner <RoomLiveShareWarningInner
liveBeaconIds={ liveBeaconIds={
Array [ Array [
"org.matrix.msc3489.beacon_info.@alice:server.org.3", "$room2:server.org_@alice:server.org",
"org.matrix.msc3489.beacon_info.@alice:server.org.4",
] ]
} }
roomId="$room2:server.org" roomId="$room2:server.org"

View File

@ -20,7 +20,6 @@ import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import { MatrixClient } from 'matrix-js-sdk/src/client'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import { mocked } from 'jest-mock'; import { mocked } from 'jest-mock';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { M_BEACON_INFO } from 'matrix-js-sdk/src/@types/beacon';
import { M_ASSET, LocationAssetType } from 'matrix-js-sdk/src/@types/location'; import { M_ASSET, LocationAssetType } from 'matrix-js-sdk/src/@types/location';
import { logger } from 'matrix-js-sdk/src/logger'; import { logger } from 'matrix-js-sdk/src/logger';
@ -310,16 +309,13 @@ describe('<LocationShareMenu />', () => {
}); });
expect(onFinished).toHaveBeenCalled(); expect(onFinished).toHaveBeenCalled();
const [eventRoomId, eventContent, eventTypeSuffix] = mockClient.unstable_createLiveBeacon.mock.calls[0]; const [eventRoomId, eventContent] = mockClient.unstable_createLiveBeacon.mock.calls[0];
expect(eventRoomId).toEqual(defaultProps.roomId); expect(eventRoomId).toEqual(defaultProps.roomId);
expect(eventTypeSuffix).toBeTruthy();
expect(eventContent).toEqual(expect.objectContaining({ expect(eventContent).toEqual(expect.objectContaining({
[M_BEACON_INFO.name]: { // default timeout
// default timeout timeout: DEFAULT_DURATION_MS,
timeout: DEFAULT_DURATION_MS, description: `Ernie's live location`,
description: `Ernie's live location`, live: true,
live: true,
},
[M_ASSET.name]: { [M_ASSET.name]: {
type: LocationAssetType.Self, type: LocationAssetType.Self,
}, },

View File

@ -18,12 +18,13 @@ import {
Room, Room,
Beacon, Beacon,
BeaconEvent, BeaconEvent,
getBeaconInfoIdentifier,
MatrixEvent, MatrixEvent,
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 } from "matrix-js-sdk/src/content-helpers";
import { M_BEACON, M_BEACON_INFO } 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";
import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../src/stores/OwnBeaconStore"; import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../src/stores/OwnBeaconStore";
@ -80,32 +81,27 @@ describe('OwnBeaconStore', () => {
const alicesRoom1BeaconInfo = makeBeaconInfoEvent(aliceId, const alicesRoom1BeaconInfo = makeBeaconInfoEvent(aliceId,
room1Id, room1Id,
{ isLive: true }, { isLive: true },
'$alice-room1-1' '$alice-room1-1',
, '$alice-room1-1',
); );
const alicesRoom2BeaconInfo = makeBeaconInfoEvent(aliceId, const alicesRoom2BeaconInfo = makeBeaconInfoEvent(aliceId,
room2Id, room2Id,
{ isLive: true }, { isLive: true },
'$alice-room2-1' '$alice-room2-1',
, '$alice-room2-1',
); );
const alicesOldRoomIdBeaconInfo = makeBeaconInfoEvent(aliceId, const alicesOldRoomIdBeaconInfo = makeBeaconInfoEvent(aliceId,
room1Id, room1Id,
{ isLive: false }, { isLive: false },
'$alice-room1-2' '$alice-room1-2',
, '$alice-room1-2',
); );
const bobsRoom1BeaconInfo = makeBeaconInfoEvent(bobId, const bobsRoom1BeaconInfo = makeBeaconInfoEvent(bobId,
room1Id, room1Id,
{ isLive: true }, { isLive: true },
'$bob-room1-1' '$bob-room1-1',
, '$bob-room1-1',
); );
const bobsOldRoom1BeaconInfo = makeBeaconInfoEvent(bobId, const bobsOldRoom1BeaconInfo = makeBeaconInfoEvent(bobId,
room1Id, room1Id,
{ isLive: false }, { isLive: false },
'$bob-room1-2' '$bob-room1-2',
, '$bob-room1-2',
); );
// make fresh rooms every time // make fresh rooms every time
@ -129,7 +125,7 @@ describe('OwnBeaconStore', () => {
}; };
const expireBeaconAndEmit = (store, beaconInfoEvent: MatrixEvent): void => { const expireBeaconAndEmit = (store, beaconInfoEvent: MatrixEvent): void => {
const beacon = store.getBeaconById(beaconInfoEvent.getType()); const beacon = store.getBeaconById(getBeaconInfoIdentifier(beaconInfoEvent));
// time travel until beacon is expired // time travel until beacon is expired
advanceDateAndTime(beacon.beaconInfo.timeout + 100); advanceDateAndTime(beacon.beaconInfo.timeout + 100);
@ -141,16 +137,14 @@ describe('OwnBeaconStore', () => {
}; };
const updateBeaconLivenessAndEmit = (store, beaconInfoEvent: MatrixEvent, isLive: boolean): void => { const updateBeaconLivenessAndEmit = (store, beaconInfoEvent: MatrixEvent, isLive: boolean): void => {
const beacon = store.getBeaconById(beaconInfoEvent.getType()); const beacon = store.getBeaconById(getBeaconInfoIdentifier(beaconInfoEvent));
// matches original state of event content // matches original state of event content
// except for live property // except for live property
const updateEvent = makeBeaconInfoEvent( const updateEvent = makeBeaconInfoEvent(
beaconInfoEvent.getSender(), beaconInfoEvent.getSender(),
beaconInfoEvent.getRoomId(), beaconInfoEvent.getRoomId(),
{ isLive, timeout: beacon.beaconInfo.timeout }, { isLive, timeout: beacon.beaconInfo.timeout },
undefined,
); );
updateEvent.event.type = beaconInfoEvent.getType();
beacon.update(updateEvent); beacon.update(updateEvent);
mockClient.emit(BeaconEvent.Update, beaconInfoEvent, beacon); mockClient.emit(BeaconEvent.Update, beaconInfoEvent, beacon);
@ -197,15 +191,14 @@ describe('OwnBeaconStore', () => {
makeRoomsWithStateEvents([ makeRoomsWithStateEvents([
alicesRoom1BeaconInfo, alicesRoom1BeaconInfo,
alicesRoom2BeaconInfo, alicesRoom2BeaconInfo,
alicesOldRoomIdBeaconInfo,
bobsRoom1BeaconInfo, bobsRoom1BeaconInfo,
bobsOldRoom1BeaconInfo, bobsOldRoom1BeaconInfo,
]); ]);
const store = await makeOwnBeaconStore(); const store = await makeOwnBeaconStore();
expect(store.hasLiveBeacons()).toBe(true); expect(store.hasLiveBeacons()).toBe(true);
expect(store.getLiveBeaconIds()).toEqual([ expect(store.getLiveBeaconIds()).toEqual([
alicesRoom1BeaconInfo.getType(), getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
alicesRoom2BeaconInfo.getType(), getBeaconInfoIdentifier(alicesRoom2BeaconInfo),
]); ]);
}); });
@ -251,7 +244,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([RoomStateEvent.Members])); expect(removeSpy.mock.calls[2]).toEqual(expect.arrayContaining([BeaconEvent.Update]));
expect(removeSpy.mock.calls[3]).toEqual(expect.arrayContaining([RoomStateEvent.Members]));
}); });
it('destroys beacons', async () => { it('destroys beacons', async () => {
@ -259,7 +253,7 @@ describe('OwnBeaconStore', () => {
alicesRoom1BeaconInfo, alicesRoom1BeaconInfo,
]); ]);
const store = await makeOwnBeaconStore(); const store = await makeOwnBeaconStore();
const beacon = room1.currentState.beacons.get(alicesRoom1BeaconInfo.getType()); const beacon = room1.currentState.beacons.get(getBeaconInfoIdentifier(alicesRoom1BeaconInfo));
const destroySpy = jest.spyOn(beacon, 'destroy'); const destroySpy = jest.spyOn(beacon, 'destroy');
// @ts-ignore // @ts-ignore
store.onNotReady(); store.onNotReady();
@ -273,7 +267,6 @@ describe('OwnBeaconStore', () => {
makeRoomsWithStateEvents([ makeRoomsWithStateEvents([
alicesRoom1BeaconInfo, alicesRoom1BeaconInfo,
alicesRoom2BeaconInfo, alicesRoom2BeaconInfo,
alicesOldRoomIdBeaconInfo,
bobsRoom1BeaconInfo, bobsRoom1BeaconInfo,
bobsOldRoom1BeaconInfo, bobsOldRoom1BeaconInfo,
]); ]);
@ -282,7 +275,6 @@ describe('OwnBeaconStore', () => {
it('returns true when user has live beacons', async () => { it('returns true when user has live beacons', async () => {
makeRoomsWithStateEvents([ makeRoomsWithStateEvents([
alicesRoom1BeaconInfo, alicesRoom1BeaconInfo,
alicesOldRoomIdBeaconInfo,
bobsRoom1BeaconInfo, bobsRoom1BeaconInfo,
bobsOldRoom1BeaconInfo, bobsOldRoom1BeaconInfo,
]); ]);
@ -302,7 +294,6 @@ describe('OwnBeaconStore', () => {
it('returns true when user has live beacons for roomId', async () => { it('returns true when user has live beacons for roomId', async () => {
makeRoomsWithStateEvents([ makeRoomsWithStateEvents([
alicesRoom1BeaconInfo, alicesRoom1BeaconInfo,
alicesOldRoomIdBeaconInfo,
bobsRoom1BeaconInfo, bobsRoom1BeaconInfo,
bobsOldRoom1BeaconInfo, bobsOldRoom1BeaconInfo,
]); ]);
@ -313,7 +304,6 @@ describe('OwnBeaconStore', () => {
it('returns false when user does not have live beacons for roomId', async () => { it('returns false when user does not have live beacons for roomId', async () => {
makeRoomsWithStateEvents([ makeRoomsWithStateEvents([
alicesRoom1BeaconInfo, alicesRoom1BeaconInfo,
alicesOldRoomIdBeaconInfo,
bobsRoom1BeaconInfo, bobsRoom1BeaconInfo,
bobsOldRoom1BeaconInfo, bobsOldRoom1BeaconInfo,
]); ]);
@ -327,7 +317,6 @@ describe('OwnBeaconStore', () => {
makeRoomsWithStateEvents([ makeRoomsWithStateEvents([
alicesRoom1BeaconInfo, alicesRoom1BeaconInfo,
alicesRoom2BeaconInfo, alicesRoom2BeaconInfo,
alicesOldRoomIdBeaconInfo,
bobsRoom1BeaconInfo, bobsRoom1BeaconInfo,
bobsOldRoom1BeaconInfo, bobsOldRoom1BeaconInfo,
]); ]);
@ -336,13 +325,12 @@ describe('OwnBeaconStore', () => {
it('returns live beacons when user has live beacons', async () => { it('returns live beacons when user has live beacons', async () => {
makeRoomsWithStateEvents([ makeRoomsWithStateEvents([
alicesRoom1BeaconInfo, alicesRoom1BeaconInfo,
alicesOldRoomIdBeaconInfo,
bobsRoom1BeaconInfo, bobsRoom1BeaconInfo,
bobsOldRoom1BeaconInfo, bobsOldRoom1BeaconInfo,
]); ]);
const store = await makeOwnBeaconStore(); const store = await makeOwnBeaconStore();
expect(store.getLiveBeaconIds()).toEqual([ expect(store.getLiveBeaconIds()).toEqual([
alicesRoom1BeaconInfo.getType(), getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
]); ]);
}); });
@ -359,23 +347,21 @@ describe('OwnBeaconStore', () => {
makeRoomsWithStateEvents([ makeRoomsWithStateEvents([
alicesRoom1BeaconInfo, alicesRoom1BeaconInfo,
alicesRoom2BeaconInfo, alicesRoom2BeaconInfo,
alicesOldRoomIdBeaconInfo,
bobsRoom1BeaconInfo, bobsRoom1BeaconInfo,
bobsOldRoom1BeaconInfo, bobsOldRoom1BeaconInfo,
]); ]);
const store = await makeOwnBeaconStore(); const store = await makeOwnBeaconStore();
expect(store.getLiveBeaconIds(room1Id)).toEqual([ expect(store.getLiveBeaconIds(room1Id)).toEqual([
alicesRoom1BeaconInfo.getType(), getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
]); ]);
expect(store.getLiveBeaconIds(room2Id)).toEqual([ expect(store.getLiveBeaconIds(room2Id)).toEqual([
alicesRoom2BeaconInfo.getType(), getBeaconInfoIdentifier(alicesRoom2BeaconInfo),
]); ]);
}); });
it('returns empty array when user does not have live beacons for roomId', async () => { it('returns empty array when user does not have live beacons for roomId', async () => {
makeRoomsWithStateEvents([ makeRoomsWithStateEvents([
alicesRoom1BeaconInfo, alicesRoom1BeaconInfo,
alicesOldRoomIdBeaconInfo,
bobsRoom1BeaconInfo, bobsRoom1BeaconInfo,
bobsOldRoom1BeaconInfo, bobsOldRoom1BeaconInfo,
]); ]);
@ -419,7 +405,7 @@ describe('OwnBeaconStore', () => {
mockClient.emit(BeaconEvent.New, alicesRoom1BeaconInfo, alicesLiveBeacon); mockClient.emit(BeaconEvent.New, alicesRoom1BeaconInfo, alicesLiveBeacon);
expect(emitSpy).toHaveBeenCalledWith(OwnBeaconStoreEvent.LivenessChange, [alicesRoom1BeaconInfo.getType()]); expect(emitSpy).toHaveBeenCalledWith(OwnBeaconStoreEvent.LivenessChange, [alicesLiveBeacon.identifier]);
}); });
it('emits a liveness change event when new beacons do not change live state', async () => { it('emits a liveness change event when new beacons do not change live state', async () => {
@ -485,14 +471,10 @@ describe('OwnBeaconStore', () => {
// except for live property // except for live property
const expectedUpdateContent = { const expectedUpdateContent = {
...prevEventContent, ...prevEventContent,
[M_BEACON_INFO.name]: { live: false,
...prevEventContent[M_BEACON_INFO.name],
live: false,
},
}; };
expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledWith( expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledWith(
room1Id, room1Id,
alicesRoom1BeaconInfo.getType(),
expectedUpdateContent, expectedUpdateContent,
); );
}); });
@ -513,7 +495,7 @@ describe('OwnBeaconStore', () => {
expect(store.hasLiveBeacons(room1Id)).toBe(true); expect(store.hasLiveBeacons(room1Id)).toBe(true);
expect(emitSpy).toHaveBeenCalledWith( expect(emitSpy).toHaveBeenCalledWith(
OwnBeaconStoreEvent.LivenessChange, OwnBeaconStoreEvent.LivenessChange,
[alicesOldRoomIdBeaconInfo.getType()], [getBeaconInfoIdentifier(alicesOldRoomIdBeaconInfo)],
); );
}); });
}); });
@ -603,7 +585,7 @@ describe('OwnBeaconStore', () => {
alicesRoom2BeaconInfo, alicesRoom2BeaconInfo,
]); ]);
const store = await makeOwnBeaconStore(); const store = await makeOwnBeaconStore();
const room1BeaconInstance = store.beacons.get(alicesRoom1BeaconInfo.getType()); const room1BeaconInstance = store.beacons.get(getBeaconInfoIdentifier(alicesRoom1BeaconInfo));
const beaconDestroySpy = jest.spyOn(room1BeaconInstance, 'destroy'); const beaconDestroySpy = jest.spyOn(room1BeaconInstance, 'destroy');
const emitSpy = jest.spyOn(store, 'emit'); const emitSpy = jest.spyOn(store, 'emit');
@ -617,7 +599,7 @@ describe('OwnBeaconStore', () => {
expect(emitSpy).toHaveBeenCalledWith( expect(emitSpy).toHaveBeenCalledWith(
OwnBeaconStoreEvent.LivenessChange, OwnBeaconStoreEvent.LivenessChange,
// other rooms beacons still live // other rooms beacons still live
[alicesRoom2BeaconInfo.getType()], [getBeaconInfoIdentifier(alicesRoom2BeaconInfo)],
); );
expect(beaconDestroySpy).toHaveBeenCalledTimes(1); expect(beaconDestroySpy).toHaveBeenCalledTimes(1);
expect(store.getLiveBeaconIds(room1Id)).toEqual([]); expect(store.getLiveBeaconIds(room1Id)).toEqual([]);
@ -640,57 +622,53 @@ describe('OwnBeaconStore', () => {
it('does nothing for a beacon that is already not live', async () => { it('does nothing for a beacon that is already not live', async () => {
const store = await makeOwnBeaconStore(); const store = await makeOwnBeaconStore();
await store.stopBeacon(alicesOldRoomIdBeaconInfo.getId()); await store.stopBeacon(getBeaconInfoIdentifier(alicesOldRoomIdBeaconInfo));
expect(mockClient.unstable_setLiveBeacon).not.toHaveBeenCalled(); expect(mockClient.unstable_setLiveBeacon).not.toHaveBeenCalled();
}); });
it('updates beacon to live:false when it is unexpired', async () => { it('updates beacon to live:false when it is unexpired', async () => {
makeRoomsWithStateEvents([
alicesRoom1BeaconInfo,
]);
const store = await makeOwnBeaconStore(); const store = await makeOwnBeaconStore();
await store.stopBeacon(alicesOldRoomIdBeaconInfo.getType());
const prevEventContent = alicesRoom1BeaconInfo.getContent(); const prevEventContent = alicesRoom1BeaconInfo.getContent();
await store.stopBeacon(alicesRoom1BeaconInfo.getType()); await store.stopBeacon(getBeaconInfoIdentifier(alicesRoom1BeaconInfo));
// matches original state of event content // matches original state of event content
// except for live property // except for live property
const expectedUpdateContent = { const expectedUpdateContent = {
...prevEventContent, ...prevEventContent,
[M_BEACON_INFO.name]: { live: false,
...prevEventContent[M_BEACON_INFO.name],
live: false,
},
}; };
expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledWith( expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledWith(
room1Id, room1Id,
alicesRoom1BeaconInfo.getType(),
expectedUpdateContent, expectedUpdateContent,
); );
}); });
it('updates beacon to live:false when it is expired but live property is true', async () => { it('updates beacon to live:false when it is expired but live property is true', async () => {
makeRoomsWithStateEvents([
alicesRoom1BeaconInfo,
]);
const store = await makeOwnBeaconStore(); const store = await makeOwnBeaconStore();
await store.stopBeacon(alicesOldRoomIdBeaconInfo.getType());
const prevEventContent = alicesRoom1BeaconInfo.getContent(); const prevEventContent = alicesRoom1BeaconInfo.getContent();
// time travel until beacon is expired // time travel until beacon is expired
advanceDateAndTime(HOUR_MS * 3); advanceDateAndTime(HOUR_MS * 3);
await store.stopBeacon(alicesRoom1BeaconInfo.getType()); await store.stopBeacon(getBeaconInfoIdentifier(alicesRoom1BeaconInfo));
// matches original state of event content // matches original state of event content
// except for live property // except for live property
const expectedUpdateContent = { const expectedUpdateContent = {
...prevEventContent, ...prevEventContent,
[M_BEACON_INFO.name]: { live: false,
...prevEventContent[M_BEACON_INFO.name],
live: false,
},
}; };
expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledWith( expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledWith(
room1Id, room1Id,
alicesRoom1BeaconInfo.getType(),
expectedUpdateContent, expectedUpdateContent,
); );
}); });
@ -863,7 +841,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(alicesRoom1BeaconInfo.getType())).toBe(false); expect(store.beaconHasWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo))).toBe(false);
expect(store.hasWireErrors()).toBe(false); expect(store.hasWireErrors()).toBe(false);
}); });
@ -890,10 +868,10 @@ 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(alicesRoom1BeaconInfo.getType())).toBe(false); expect(store.beaconHasWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo))).toBe(false);
expect(store.hasWireErrors()).toBe(false); expect(store.hasWireErrors()).toBe(false);
expect(emitSpy).not.toHaveBeenCalledWith( expect(emitSpy).not.toHaveBeenCalledWith(
OwnBeaconStoreEvent.WireError, alicesRoom1BeaconInfo.getType(), OwnBeaconStoreEvent.WireError, getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
); );
}); });
@ -913,10 +891,10 @@ describe('OwnBeaconStore', () => {
// only two allowed failures // only two allowed failures
expect(mockClient.sendEvent).toHaveBeenCalledTimes(2); expect(mockClient.sendEvent).toHaveBeenCalledTimes(2);
expect(store.beaconHasWireError(alicesRoom1BeaconInfo.getType())).toBe(true); expect(store.beaconHasWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo))).toBe(true);
expect(store.hasWireErrors()).toBe(true); expect(store.hasWireErrors()).toBe(true);
expect(emitSpy).toHaveBeenCalledWith( expect(emitSpy).toHaveBeenCalledWith(
OwnBeaconStoreEvent.WireError, alicesRoom1BeaconInfo.getType(), OwnBeaconStoreEvent.WireError, getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
); );
}); });
@ -936,18 +914,18 @@ describe('OwnBeaconStore', () => {
// only two allowed failures // only two allowed failures
expect(mockClient.sendEvent).toHaveBeenCalledTimes(2); expect(mockClient.sendEvent).toHaveBeenCalledTimes(2);
expect(store.beaconHasWireError(alicesRoom1BeaconInfo.getType())).toBe(true); expect(store.beaconHasWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo))).toBe(true);
expect(store.hasWireErrors()).toBe(true); expect(store.hasWireErrors()).toBe(true);
expect(store.hasWireErrors(room1Id)).toBe(true); expect(store.hasWireErrors(room1Id)).toBe(true);
expect(emitSpy).toHaveBeenCalledWith( expect(emitSpy).toHaveBeenCalledWith(
OwnBeaconStoreEvent.WireError, alicesRoom1BeaconInfo.getType(), OwnBeaconStoreEvent.WireError, getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
); );
// reset emitSpy mock counts to asser on wireError again // reset emitSpy mock counts to asser on wireError again
emitSpy.mockClear(); emitSpy.mockClear();
store.resetWireError(alicesRoom1BeaconInfo.getType()); store.resetWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo));
expect(store.beaconHasWireError(alicesRoom1BeaconInfo.getType())).toBe(false); expect(store.beaconHasWireError(getBeaconInfoIdentifier(alicesRoom1BeaconInfo))).toBe(false);
// 2 more positions from watchPosition in this period // 2 more positions from watchPosition in this period
await advanceAndFlushPromises(10000); await advanceAndFlushPromises(10000);
@ -955,7 +933,7 @@ describe('OwnBeaconStore', () => {
// 2 from before, 2 new ones // 2 from before, 2 new ones
expect(mockClient.sendEvent).toHaveBeenCalledTimes(4); expect(mockClient.sendEvent).toHaveBeenCalledTimes(4);
expect(emitSpy).toHaveBeenCalledWith( expect(emitSpy).toHaveBeenCalledWith(
OwnBeaconStoreEvent.WireError, alicesRoom1BeaconInfo.getType(), OwnBeaconStoreEvent.WireError, getBeaconInfoIdentifier(alicesRoom1BeaconInfo),
); );
}); });
}); });

View File

@ -33,8 +33,6 @@ const DEFAULT_INFO_CONTENT_PROPS: InfoContentProps = {
timeout: 3600000, timeout: 3600000,
}; };
let count = 1;
/** /**
* Create an m.beacon_info event * Create an m.beacon_info event
* all required properties are mocked * all required properties are mocked
@ -45,7 +43,6 @@ export const makeBeaconInfoEvent = (
roomId: string, roomId: string,
contentProps: Partial<InfoContentProps> = {}, contentProps: Partial<InfoContentProps> = {},
eventId?: string, eventId?: string,
eventTypeSuffix?: string,
): MatrixEvent => { ): MatrixEvent => {
const { const {
timeout, timeout,
@ -58,12 +55,15 @@ export const makeBeaconInfoEvent = (
...contentProps, ...contentProps,
}; };
const event = new MatrixEvent({ const event = new MatrixEvent({
type: `${M_BEACON_INFO.name}.${sender}.${eventTypeSuffix || ++count}`, type: M_BEACON_INFO.name,
room_id: roomId, room_id: roomId,
state_key: sender, state_key: sender,
sender,
content: makeBeaconInfoContent(timeout, isLive, description, assetType, timestamp), content: makeBeaconInfoContent(timeout, isLive, description, assetType, timestamp),
}); });
event.event.origin_server_ts = Date.now();
// live beacons use the beacon_info event id // live beacons use the beacon_info event id
// set or default this // set or default this
event.replaceLocalEventId(eventId || `$${Math.random()}-${Math.random()}`); event.replaceLocalEventId(eventId || `$${Math.random()}-${Math.random()}`);