diff --git a/res/css/_components.scss b/res/css/_components.scss index f0d78df5bc..e607deffe8 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -5,6 +5,7 @@ @import "./_font-weights.scss"; @import "./_spacing.scss"; @import "./components/views/beacon/_LeftPanelLiveShareWarning.scss"; +@import "./components/views/beacon/_RoomLiveShareWarning.scss"; @import "./components/views/beacon/_StyledLiveBeaconIcon.scss"; @import "./components/views/location/_LiveDurationDropdown.scss"; @import "./components/views/location/_LocationShareMenu.scss"; diff --git a/res/css/components/views/beacon/_RoomLiveShareWarning.scss b/res/css/components/views/beacon/_RoomLiveShareWarning.scss new file mode 100644 index 0000000000..c0d5ea47fe --- /dev/null +++ b/res/css/components/views/beacon/_RoomLiveShareWarning.scss @@ -0,0 +1,50 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_RoomLiveShareWarning { + width: 100%; + + display: flex; + flex-direction: row; + align-items: center; + + box-sizing: border-box; + padding: $spacing-12 $spacing-16; + + color: $primary-content; + background-color: $system; +} + +.mx_RoomLiveShareWarning_icon { + height: 32px; + width: 32px; + margin-right: $spacing-8; +} + +.mx_RoomLiveShareWarning_label { + flex: 1; + font-size: $font-15px; +} + +.mx_RoomLiveShareWarning_expiry { + color: $secondary-content; + font-size: $font-12px; + margin-right: $spacing-16; +} + +.mx_RoomLiveShareWarning_spinner { + margin-right: $spacing-16; +} diff --git a/src/components/views/beacon/RoomLiveShareWarning.tsx b/src/components/views/beacon/RoomLiveShareWarning.tsx new file mode 100644 index 0000000000..5eb85e71b4 --- /dev/null +++ b/src/components/views/beacon/RoomLiveShareWarning.tsx @@ -0,0 +1,127 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useEffect, useState } from 'react'; +import classNames from 'classnames'; +import { Room } from 'matrix-js-sdk/src/matrix'; + +import { _t } from '../../../languageHandler'; +import { useEventEmitterState } from '../../../hooks/useEventEmitter'; +import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../stores/OwnBeaconStore'; +import AccessibleButton from '../elements/AccessibleButton'; +import StyledLiveBeaconIcon from './StyledLiveBeaconIcon'; +import { formatDuration } from '../../../DateUtils'; +import { getBeaconMsUntilExpiry, sortBeaconsByLatestExpiry } from '../../../utils/beacon'; +import Spinner from '../elements/Spinner'; + +interface Props { + roomId: Room['roomId']; +} + +/** + * It's technically possible to have multiple live beacons in one room + * Select the latest expiry to display, + * and kill all beacons on stop sharing + */ +type LiveBeaconsState = { + liveBeaconIds: string[]; + msRemaining?: number; + onStopSharing?: () => void; + stoppingInProgress?: boolean; +}; + +const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => { + const [stoppingInProgress, setStoppingInProgress] = useState(false); + const liveBeaconIds = useEventEmitterState( + OwnBeaconStore.instance, + OwnBeaconStoreEvent.LivenessChange, + () => OwnBeaconStore.instance.getLiveBeaconIds(roomId), + ); + + // reset stopping in progress on change in live ids + useEffect(() => { + setStoppingInProgress(false); + }, [liveBeaconIds]); + + if (!liveBeaconIds?.length) { + return { liveBeaconIds }; + } + + // select the beacon with latest expiry to display expiry time + const beacon = liveBeaconIds.map(beaconId => OwnBeaconStore.instance.getBeaconById(beaconId)) + .sort(sortBeaconsByLatestExpiry) + .shift(); + + const onStopSharing = async () => { + setStoppingInProgress(true); + try { + await Promise.all(liveBeaconIds.map(beaconId => OwnBeaconStore.instance.stopBeacon(beaconId))); + } catch (error) { + // only clear loading in case of error + // to avoid flash of not-loading state + // after beacons have been stopped but we wait for sync + setStoppingInProgress(false); + } + }; + + const msRemaining = getBeaconMsUntilExpiry(beacon); + + return { liveBeaconIds, onStopSharing, msRemaining, stoppingInProgress }; +}; + +const RoomLiveShareWarning: React.FC = ({ roomId }) => { + const { + liveBeaconIds, + onStopSharing, + msRemaining, + stoppingInProgress, + } = useLiveBeacons(roomId); + + if (!liveBeaconIds?.length) { + return null; + } + + const timeRemaining = formatDuration(msRemaining); + const liveTimeRemaining = _t(`%(timeRemaining)s left`, { timeRemaining }); + + return
+ + + { _t('You are sharing %(count)s live locations', { count: liveBeaconIds.length }) } + + + { stoppingInProgress ? + : + { liveTimeRemaining } + } + + { _t('Stop sharing') } + +
; +}; + +export default RoomLiveShareWarning; diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index c0a0718111..9df1ba663c 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -41,6 +41,7 @@ import { RoomNotificationStateStore } from '../../../stores/notifications/RoomNo import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases'; import { NotificationStateEvents } from '../../../stores/notifications/NotificationState'; import RoomContext from "../../../contexts/RoomContext"; +import RoomLiveShareWarning from '../beacon/RoomLiveShareWarning'; export interface ISearchInfo { searchTerm: string; @@ -273,6 +274,7 @@ export default class RoomHeader extends React.Component { { rightRow } + ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6b94b7a85d..ddda953227 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2965,6 +2965,10 @@ "Leave the beta": "Leave the beta", "Join the beta": "Join the beta", "You are sharing your live location": "You are sharing your live location", + "%(timeRemaining)s left": "%(timeRemaining)s left", + "You are sharing %(count)s live locations|other": "You are sharing %(count)s live locations", + "You are sharing %(count)s live locations|one": "You are sharing your live location", + "Stop sharing": "Stop sharing", "Avatar": "Avatar", "This room is public": "This room is public", "Away": "Away", diff --git a/src/stores/OwnBeaconStore.ts b/src/stores/OwnBeaconStore.ts index 6bd256be2c..4ac2bcadde 100644 --- a/src/stores/OwnBeaconStore.ts +++ b/src/stores/OwnBeaconStore.ts @@ -27,11 +27,12 @@ import { import defaultDispatcher from "../dispatcher/dispatcher"; import { ActionPayload } from "../dispatcher/payloads"; import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; +import { arrayHasDiff } from "../utils/arrays"; const isOwnBeacon = (beacon: Beacon, userId: string): boolean => beacon.beaconInfoOwner === userId; export enum OwnBeaconStoreEvent { - LivenessChange = 'OwnBeaconStore.LivenessChange' + LivenessChange = 'OwnBeaconStore.LivenessChange', } type OwnBeaconStoreState = { @@ -41,6 +42,7 @@ type OwnBeaconStoreState = { }; export class OwnBeaconStore extends AsyncStoreWithClient { private static internalInstance = new OwnBeaconStore(); + // users beacons, keyed by event type public readonly beacons = new Map(); public readonly beaconsByRoomId = new Map>(); private liveBeaconIds = []; @@ -86,8 +88,12 @@ export class OwnBeaconStore extends AsyncStoreWithClient { return this.liveBeaconIds.filter(beaconId => this.beaconsByRoomId.get(roomId)?.has(beaconId)); } - public stopBeacon = async (beaconInfoId: string): Promise => { - const beacon = this.beacons.get(beaconInfoId); + public getBeaconById(beaconId: string): Beacon | undefined { + return this.beacons.get(beaconId); + } + + public stopBeacon = async (beaconInfoType: string): Promise => { + const beacon = this.beacons.get(beaconInfoType); // if no beacon, or beacon is already explicitly set isLive: false // do nothing if (!beacon?.beaconInfo?.live) { @@ -107,27 +113,27 @@ export class OwnBeaconStore extends AsyncStoreWithClient { private onBeaconLiveness = (isLive: boolean, beacon: Beacon): void => { // check if we care about this beacon - if (!this.beacons.has(beacon.beaconInfoId)) { + if (!this.beacons.has(beacon.identifier)) { return; } - if (!isLive && this.liveBeaconIds.includes(beacon.beaconInfoId)) { + if (!isLive && this.liveBeaconIds.includes(beacon.identifier)) { this.liveBeaconIds = - this.liveBeaconIds.filter(beaconId => beaconId !== beacon.beaconInfoId); + this.liveBeaconIds.filter(beaconId => beaconId !== beacon.identifier); } - if (isLive && !this.liveBeaconIds.includes(beacon.beaconInfoId)) { - this.liveBeaconIds.push(beacon.beaconInfoId); + if (isLive && !this.liveBeaconIds.includes(beacon.identifier)) { + this.liveBeaconIds.push(beacon.identifier); } // beacon expired, update beacon to un-alive state if (!isLive) { - this.stopBeacon(beacon.beaconInfoId); + this.stopBeacon(beacon.identifier); } // TODO start location polling here - this.emit(OwnBeaconStoreEvent.LivenessChange, this.hasLiveBeacons()); + this.emit(OwnBeaconStoreEvent.LivenessChange, this.getLiveBeaconIds()); }; private initialiseBeaconState = () => { @@ -146,27 +152,25 @@ export class OwnBeaconStore extends AsyncStoreWithClient { }; private addBeacon = (beacon: Beacon): void => { - this.beacons.set(beacon.beaconInfoId, beacon); + this.beacons.set(beacon.identifier, beacon); if (!this.beaconsByRoomId.has(beacon.roomId)) { this.beaconsByRoomId.set(beacon.roomId, new Set()); } - this.beaconsByRoomId.get(beacon.roomId).add(beacon.beaconInfoId); + this.beaconsByRoomId.get(beacon.roomId).add(beacon.identifier); beacon.monitorLiveness(); }; private checkLiveness = (): void => { - const prevLiveness = this.hasLiveBeacons(); + const prevLiveBeaconIds = this.getLiveBeaconIds(); this.liveBeaconIds = [...this.beacons.values()] .filter(beacon => beacon.isLive) - .map(beacon => beacon.beaconInfoId); + .map(beacon => beacon.identifier); - const newLiveness = this.hasLiveBeacons(); - - if (prevLiveness !== newLiveness) { - this.emit(OwnBeaconStoreEvent.LivenessChange, newLiveness); + if (arrayHasDiff(prevLiveBeaconIds, this.liveBeaconIds)) { + this.emit(OwnBeaconStoreEvent.LivenessChange, this.liveBeaconIds); } }; diff --git a/test/components/views/beacon/RoomLiveShareWarning-test.tsx b/test/components/views/beacon/RoomLiveShareWarning-test.tsx new file mode 100644 index 0000000000..57095bba4e --- /dev/null +++ b/test/components/views/beacon/RoomLiveShareWarning-test.tsx @@ -0,0 +1,209 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mount } from 'enzyme'; +import { Room, Beacon, BeaconEvent } from 'matrix-js-sdk/src/matrix'; + +import '../../../skinned-sdk'; +import RoomLiveShareWarning from '../../../../src/components/views/beacon/RoomLiveShareWarning'; +import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore'; +import { + findByTestId, + getMockClientWithEventEmitter, + makeBeaconInfoEvent, + resetAsyncStoreWithClient, + setupAsyncStoreWithClient, +} from '../../../test-utils'; + +jest.useFakeTimers(); +describe('', () => { + const aliceId = '@alice:server.org'; + const room1Id = '$room1:server.org'; + const room2Id = '$room2:server.org'; + const room3Id = '$room3:server.org'; + const mockClient = getMockClientWithEventEmitter({ + getVisibleRooms: jest.fn().mockReturnValue([]), + getUserId: jest.fn().mockReturnValue(aliceId), + unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }), + }); + + // 14.03.2022 16:15 + const now = 1647270879403; + const HOUR_MS = 3600000; + // mock the date so events are stable for snapshots etc + jest.spyOn(global.Date, 'now').mockReturnValue(now); + const room1Beacon1 = makeBeaconInfoEvent(aliceId, room1Id, { isLive: true, timeout: HOUR_MS }); + const room2Beacon1 = makeBeaconInfoEvent(aliceId, room2Id, { isLive: true, timeout: HOUR_MS }); + const room2Beacon2 = makeBeaconInfoEvent(aliceId, room2Id, { isLive: true, timeout: HOUR_MS * 12 }); + const room3Beacon1 = makeBeaconInfoEvent(aliceId, room3Id, { isLive: true, timeout: HOUR_MS }); + + // make fresh rooms every time + // as we update room state + const makeRoomsWithStateEvents = (stateEvents = []): [Room, Room] => { + const room1 = new Room(room1Id, mockClient, aliceId); + const room2 = new Room(room2Id, mockClient, aliceId); + + room1.currentState.setStateEvents(stateEvents); + room2.currentState.setStateEvents(stateEvents); + mockClient.getVisibleRooms.mockReturnValue([room1, room2]); + + return [room1, room2]; + }; + + const advanceDateAndTime = (ms: number) => { + // bc liveness check uses Date.now we have to advance this mock + jest.spyOn(global.Date, 'now').mockReturnValue(now + ms); + // then advance time for the interval by the same amount + jest.advanceTimersByTime(ms); + }; + + const makeOwnBeaconStore = async () => { + const store = OwnBeaconStore.instance; + + await setupAsyncStoreWithClient(store, mockClient); + return store; + }; + + const defaultProps = { + roomId: room1Id, + }; + const getComponent = (props = {}) => { + let component; + // component updates on render + // wrap in act + act(() => { + component = mount(); + }); + return component; + }; + + beforeEach(() => { + jest.spyOn(global.Date, 'now').mockReturnValue(now); + mockClient.unstable_setLiveBeacon.mockClear(); + }); + + afterEach(async () => { + await resetAsyncStoreWithClient(OwnBeaconStore.instance); + }); + + afterAll(() => { + jest.spyOn(global.Date, 'now').mockRestore(); + }); + + it('renders nothing when user has no live beacons at all', async () => { + await makeOwnBeaconStore(); + const component = getComponent(); + expect(component.html()).toBe(null); + }); + + it('renders nothing when user has no live beacons in room', async () => { + await act(async () => { + await makeRoomsWithStateEvents([room2Beacon1]); + await makeOwnBeaconStore(); + }); + const component = getComponent({ roomId: room1Id }); + expect(component.html()).toBe(null); + }); + + describe('when user has live beacons', () => { + beforeEach(async () => { + await act(async () => { + await makeRoomsWithStateEvents([room1Beacon1, room2Beacon1, room2Beacon2]); + await makeOwnBeaconStore(); + }); + }); + + it('renders correctly with one live beacon in room', () => { + const component = getComponent({ roomId: room1Id }); + expect(component).toMatchSnapshot(); + }); + + it('renders correctly with two live beacons in room', () => { + const component = getComponent({ roomId: room2Id }); + expect(component).toMatchSnapshot(); + // later expiry displayed + expect(findByTestId(component, 'room-live-share-expiry').text()).toEqual('12h left'); + }); + + it('removes itself when user stops having live beacons', async () => { + const component = getComponent({ roomId: room1Id }); + // started out rendered + expect(component.html()).toBeTruthy(); + + // time travel until room1Beacon1 is expired + advanceDateAndTime(HOUR_MS + 1); + act(() => { + mockClient.emit(BeaconEvent.LivenessChange, false, new Beacon(room1Beacon1)); + component.setProps({}); + }); + + expect(component.html()).toBe(null); + }); + + it('renders when user adds a live beacon', async () => { + const component = getComponent({ roomId: room3Id }); + // started out not rendered + expect(component.html()).toBeFalsy(); + + act(() => { + mockClient.emit(BeaconEvent.New, room3Beacon1, new Beacon(room3Beacon1)); + component.setProps({}); + }); + + expect(component.html()).toBeTruthy(); + }); + + describe('stopping beacons', () => { + it('stops beacon on stop sharing click', () => { + const component = getComponent({ roomId: room2Id }); + + act(() => { + findByTestId(component, 'room-live-share-stop-sharing').at(0).simulate('click'); + component.setProps({}); + }); + + expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledTimes(2); + expect(component.find('Spinner').length).toBeTruthy(); + expect(findByTestId(component, 'room-live-share-stop-sharing').at(0).props().disabled).toBeTruthy(); + }); + + it('displays again with correct state after stopping a beacon', () => { + // make sure the loading state is reset correctly after removing a beacon + const component = getComponent({ roomId: room2Id }); + + act(() => { + findByTestId(component, 'room-live-share-stop-sharing').at(0).simulate('click'); + }); + // time travel until room1Beacon1 is expired + advanceDateAndTime(HOUR_MS + 1); + act(() => { + mockClient.emit(BeaconEvent.LivenessChange, false, new Beacon(room1Beacon1)); + }); + + const newLiveBeacon = makeBeaconInfoEvent(aliceId, room2Id, { isLive: true }); + act(() => { + mockClient.emit(BeaconEvent.New, newLiveBeacon, new Beacon(newLiveBeacon)); + }); + + // button not disabled and expiry time shown + expect(findByTestId(component, 'room-live-share-stop-sharing').at(0).props().disabled).toBeFalsy(); + expect(findByTestId(component, 'room-live-share-expiry').text()).toEqual('11h left'); + }); + }); + }); +}); diff --git a/test/components/views/beacon/StyledLiveBeaconIcon-test.tsx b/test/components/views/beacon/StyledLiveBeaconIcon-test.tsx new file mode 100644 index 0000000000..38fc8391d5 --- /dev/null +++ b/test/components/views/beacon/StyledLiveBeaconIcon-test.tsx @@ -0,0 +1,32 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { mount } from 'enzyme'; + +import '../../../skinned-sdk'; +import StyledLiveBeaconIcon from '../../../../src/components/views/beacon/StyledLiveBeaconIcon'; + +describe('', () => { + const defaultProps = {}; + const getComponent = (props = {}) => + mount(); + + it('renders', () => { + const component = getComponent(); + expect(component).toBeTruthy(); + }); +}); diff --git a/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap b/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap new file mode 100644 index 0000000000..3079670fc5 --- /dev/null +++ b/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` when user has live beacons renders correctly with one live beacon in room 1`] = ` + +
+ +
+ + + You are sharing your live location + + + 1h left + + + + +
+ +`; + +exports[` when user has live beacons renders correctly with two live beacons in room 1`] = ` + +
+ +
+ + + You are sharing 2 live locations + + + 12h left + + + + +
+ +`; diff --git a/test/stores/OwnBeaconStore-test.ts b/test/stores/OwnBeaconStore-test.ts index 6c8cacd3df..14b87d822d 100644 --- a/test/stores/OwnBeaconStore-test.ts +++ b/test/stores/OwnBeaconStore-test.ts @@ -24,8 +24,7 @@ import { getMockClientWithEventEmitter } from "../test-utils/client"; jest.useFakeTimers(); -// xdescribing while mismatch with matrix-js-sdk -xdescribe('OwnBeaconStore', () => { +describe('OwnBeaconStore', () => { // 14.03.2022 16:15 const now = 1647270879403; const HOUR_MS = 3600000; @@ -46,11 +45,36 @@ xdescribe('OwnBeaconStore', () => { // event creation sets timestamp to Date.now() jest.spyOn(global.Date, 'now').mockReturnValue(now - HOUR_MS); - const alicesRoom1BeaconInfo = makeBeaconInfoEvent(aliceId, room1Id, { isLive: true }, '$alice-room1-1'); - const alicesRoom2BeaconInfo = makeBeaconInfoEvent(aliceId, room2Id, { isLive: true }, '$alice-room2-1'); - const alicesOldRoomIdBeaconInfo = makeBeaconInfoEvent(aliceId, room1Id, { isLive: false }, '$alice-room1-2'); - const bobsRoom1BeaconInfo = makeBeaconInfoEvent(bobId, room1Id, { isLive: true }, '$bob-room1-1'); - const bobsOldRoom1BeaconInfo = makeBeaconInfoEvent(bobId, room1Id, { isLive: false }, '$bob-room1-2'); + const alicesRoom1BeaconInfo = makeBeaconInfoEvent(aliceId, + room1Id, + { isLive: true }, + '$alice-room1-1' + , '$alice-room1-1', + ); + const alicesRoom2BeaconInfo = makeBeaconInfoEvent(aliceId, + room2Id, + { isLive: true }, + '$alice-room2-1' + , '$alice-room2-1', + ); + const alicesOldRoomIdBeaconInfo = makeBeaconInfoEvent(aliceId, + room1Id, + { isLive: false }, + '$alice-room1-2' + , '$alice-room1-2', + ); + const bobsRoom1BeaconInfo = makeBeaconInfoEvent(bobId, + room1Id, + { isLive: true }, + '$bob-room1-1' + , '$bob-room1-1', + ); + const bobsOldRoom1BeaconInfo = makeBeaconInfoEvent(bobId, + room1Id, + { isLive: false }, + '$bob-room1-2' + , '$bob-room1-2', + ); // make fresh rooms every time // as we update room state @@ -121,8 +145,8 @@ xdescribe('OwnBeaconStore', () => { const store = await makeOwnBeaconStore(); expect(store.hasLiveBeacons()).toBe(true); expect(store.getLiveBeaconIds()).toEqual([ - alicesRoom1BeaconInfo.getId(), - alicesRoom2BeaconInfo.getId(), + alicesRoom1BeaconInfo.getType(), + alicesRoom2BeaconInfo.getType(), ]); }); }); @@ -143,7 +167,7 @@ xdescribe('OwnBeaconStore', () => { alicesRoom1BeaconInfo, ]); const store = await makeOwnBeaconStore(); - const beacon = room1.currentState.beacons.get(alicesRoom1BeaconInfo.getId()); + const beacon = room1.currentState.beacons.get(alicesRoom1BeaconInfo.getType()); const destroySpy = jest.spyOn(beacon, 'destroy'); // @ts-ignore store.onNotReady(); @@ -226,7 +250,7 @@ xdescribe('OwnBeaconStore', () => { ]); const store = await makeOwnBeaconStore(); expect(store.getLiveBeaconIds()).toEqual([ - alicesRoom1BeaconInfo.getId(), + alicesRoom1BeaconInfo.getType(), ]); }); @@ -249,10 +273,10 @@ xdescribe('OwnBeaconStore', () => { ]); const store = await makeOwnBeaconStore(); expect(store.getLiveBeaconIds(room1Id)).toEqual([ - alicesRoom1BeaconInfo.getId(), + alicesRoom1BeaconInfo.getType(), ]); expect(store.getLiveBeaconIds(room2Id)).toEqual([ - alicesRoom2BeaconInfo.getId(), + alicesRoom2BeaconInfo.getType(), ]); }); @@ -303,10 +327,10 @@ xdescribe('OwnBeaconStore', () => { mockClient.emit(BeaconEvent.New, alicesRoom1BeaconInfo, alicesLiveBeacon); - expect(emitSpy).toHaveBeenCalledWith(OwnBeaconStoreEvent.LivenessChange, true); + expect(emitSpy).toHaveBeenCalledWith(OwnBeaconStoreEvent.LivenessChange, [alicesRoom1BeaconInfo.getType()]); }); - it('does not emit 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 () => { makeRoomsWithStateEvents([ alicesRoom2BeaconInfo, ]); @@ -318,7 +342,7 @@ xdescribe('OwnBeaconStore', () => { mockClient.emit(BeaconEvent.New, alicesRoom1BeaconInfo, alicesLiveBeacon); - expect(emitSpy).not.toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalled(); }); }); @@ -357,7 +381,7 @@ xdescribe('OwnBeaconStore', () => { expect(store.hasLiveBeacons()).toBe(false); expect(store.hasLiveBeacons(room1Id)).toBe(false); - expect(emitSpy).toHaveBeenCalledWith(OwnBeaconStoreEvent.LivenessChange, false); + expect(emitSpy).toHaveBeenCalledWith(OwnBeaconStoreEvent.LivenessChange, []); }); it('stops beacon when liveness changes from true to false and beacon is expired', async () => { @@ -400,7 +424,7 @@ xdescribe('OwnBeaconStore', () => { const emitSpy = jest.spyOn(store, 'emit'); const alicesBeacon = new Beacon(alicesOldRoomIdBeaconInfo); const liveUpdate = makeBeaconInfoEvent( - aliceId, room1Id, { isLive: true }, alicesOldRoomIdBeaconInfo.getId(), + aliceId, room1Id, { isLive: true }, alicesOldRoomIdBeaconInfo.getId(), '$alice-room1-2', ); // bring the beacon back to life @@ -410,7 +434,10 @@ xdescribe('OwnBeaconStore', () => { expect(store.hasLiveBeacons()).toBe(true); expect(store.hasLiveBeacons(room1Id)).toBe(true); - expect(emitSpy).toHaveBeenCalledWith(OwnBeaconStoreEvent.LivenessChange, true); + expect(emitSpy).toHaveBeenCalledWith( + OwnBeaconStoreEvent.LivenessChange, + [alicesOldRoomIdBeaconInfo.getType()], + ); }); }); @@ -437,10 +464,10 @@ xdescribe('OwnBeaconStore', () => { it('updates beacon to live:false when it is unexpired', async () => { const store = await makeOwnBeaconStore(); - await store.stopBeacon(alicesOldRoomIdBeaconInfo.getId()); + await store.stopBeacon(alicesOldRoomIdBeaconInfo.getType()); const prevEventContent = alicesRoom1BeaconInfo.getContent(); - await store.stopBeacon(alicesRoom1BeaconInfo.getId()); + await store.stopBeacon(alicesRoom1BeaconInfo.getType()); // matches original state of event content // except for live property @@ -461,13 +488,13 @@ xdescribe('OwnBeaconStore', () => { it('updates beacon to live:false when it is expired but live property is true', async () => { const store = await makeOwnBeaconStore(); - await store.stopBeacon(alicesOldRoomIdBeaconInfo.getId()); + await store.stopBeacon(alicesOldRoomIdBeaconInfo.getType()); const prevEventContent = alicesRoom1BeaconInfo.getContent(); // time travel until beacon is expired advanceDateAndTime(HOUR_MS * 3); - await store.stopBeacon(alicesRoom1BeaconInfo.getId()); + await store.stopBeacon(alicesRoom1BeaconInfo.getType()); // matches original state of event content // except for live property diff --git a/test/test-utils/beacon.ts b/test/test-utils/beacon.ts index d0e63e6b4b..d156baaa5e 100644 --- a/test/test-utils/beacon.ts +++ b/test/test-utils/beacon.ts @@ -42,6 +42,7 @@ export const makeBeaconInfoEvent = ( roomId: string, contentProps: Partial = {}, eventId?: string, + eventTypeSuffix?: string, ): MatrixEvent => { const { timeout, @@ -54,7 +55,7 @@ export const makeBeaconInfoEvent = ( ...contentProps, }; const event = new MatrixEvent({ - type: `${M_BEACON_INFO.name}.${sender}.${++count}`, + type: `${M_BEACON_INFO.name}.${sender}.${eventTypeSuffix || ++count}`, room_id: roomId, state_key: sender, content: makeBeaconInfoContent(timeout, isLive, description, assetType, timestamp),