Live location sharing: live share warning in room (#8100)

* add duration dropdown to live location picker

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

* tidy comments

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

* setup component

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

* replace references to beaconInfoId with beacon.identifier

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

* icon

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

* component for styled live beacon icon

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

* emit liveness change whenever livebeaconIds changes

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

* Handle multiple live beacons in room share warning, test

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

* un xdescribe beaconstore tests

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

* missed copyrights

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

* i18n

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

* tidy

Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
Kerry 2022-03-22 14:57:12 +01:00 committed by GitHub
parent c8d3b51640
commit b04d31b5be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 600 additions and 42 deletions

View file

@ -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";

View file

@ -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;
}

View file

@ -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<Props> = ({ 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 <div
className={classNames('mx_RoomLiveShareWarning')}
>
<StyledLiveBeaconIcon className="mx_RoomLiveShareWarning_icon" />
<span className="mx_RoomLiveShareWarning_label">
{ _t('You are sharing %(count)s live locations', { count: liveBeaconIds.length }) }
</span>
{ stoppingInProgress ?
<span className='mx_RoomLiveShareWarning_spinner'><Spinner h={16} w={16} /></span> :
<span
data-test-id='room-live-share-expiry'
className="mx_RoomLiveShareWarning_expiry"
>{ liveTimeRemaining }</span>
}
<AccessibleButton
data-test-id='room-live-share-stop-sharing'
onClick={onStopSharing}
kind='danger'
element='button'
disabled={stoppingInProgress}
>
{ _t('Stop sharing') }
</AccessibleButton>
</div>;
};
export default RoomLiveShareWarning;

View file

@ -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<IProps, IState> {
{ rightRow }
<RoomHeaderButtons room={this.props.room} excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons} />
</div>
<RoomLiveShareWarning roomId={this.props.room.roomId} />
</div>
);
}

View file

@ -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",

View file

@ -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<OwnBeaconStoreState> {
private static internalInstance = new OwnBeaconStore();
// users beacons, keyed by event type
public readonly beacons = new Map<string, Beacon>();
public readonly beaconsByRoomId = new Map<Room['roomId'], Set<string>>();
private liveBeaconIds = [];
@ -86,8 +88,12 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
return this.liveBeaconIds.filter(beaconId => this.beaconsByRoomId.get(roomId)?.has(beaconId));
}
public stopBeacon = async (beaconInfoId: string): Promise<void> => {
const beacon = this.beacons.get(beaconInfoId);
public getBeaconById(beaconId: string): Beacon | undefined {
return this.beacons.get(beaconId);
}
public stopBeacon = async (beaconInfoType: string): Promise<void> => {
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<OwnBeaconStoreState> {
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<OwnBeaconStoreState> {
};
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<string>());
}
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);
}
};

View file

@ -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('<RoomLiveShareWarning />', () => {
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(<RoomLiveShareWarning {...defaultProps} {...props} />);
});
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');
});
});
});
});

View file

@ -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('<StyledLiveBeaconIcon />', () => {
const defaultProps = {};
const getComponent = (props = {}) =>
mount(<StyledLiveBeaconIcon {...defaultProps} {...props} />);
it('renders', () => {
const component = getComponent();
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,101 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RoomLiveShareWarning /> when user has live beacons renders correctly with one live beacon in room 1`] = `
<RoomLiveShareWarning
roomId="$room1:server.org"
>
<div
className="mx_RoomLiveShareWarning"
>
<StyledLiveBeaconIcon
className="mx_RoomLiveShareWarning_icon"
>
<div
className="mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon"
/>
</StyledLiveBeaconIcon>
<span
className="mx_RoomLiveShareWarning_label"
>
You are sharing your live location
</span>
<span
className="mx_RoomLiveShareWarning_expiry"
data-test-id="room-live-share-expiry"
>
1h left
</span>
<AccessibleButton
data-test-id="room-live-share-stop-sharing"
disabled={false}
element="button"
kind="danger"
onClick={[Function]}
role="button"
tabIndex={0}
>
<button
className="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
data-test-id="room-live-share-stop-sharing"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="button"
tabIndex={0}
>
Stop sharing
</button>
</AccessibleButton>
</div>
</RoomLiveShareWarning>
`;
exports[`<RoomLiveShareWarning /> when user has live beacons renders correctly with two live beacons in room 1`] = `
<RoomLiveShareWarning
roomId="$room2:server.org"
>
<div
className="mx_RoomLiveShareWarning"
>
<StyledLiveBeaconIcon
className="mx_RoomLiveShareWarning_icon"
>
<div
className="mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon"
/>
</StyledLiveBeaconIcon>
<span
className="mx_RoomLiveShareWarning_label"
>
You are sharing 2 live locations
</span>
<span
className="mx_RoomLiveShareWarning_expiry"
data-test-id="room-live-share-expiry"
>
12h left
</span>
<AccessibleButton
data-test-id="room-live-share-stop-sharing"
disabled={false}
element="button"
kind="danger"
onClick={[Function]}
role="button"
tabIndex={0}
>
<button
className="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
data-test-id="room-live-share-stop-sharing"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="button"
tabIndex={0}
>
Stop sharing
</button>
</AccessibleButton>
</div>
</RoomLiveShareWarning>
`;

View file

@ -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

View file

@ -42,6 +42,7 @@ export const makeBeaconInfoEvent = (
roomId: string,
contentProps: Partial<InfoContentProps> = {},
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),