Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

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

Merged
merged 8 commits into from
Apr 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions src/components/views/beacon/RoomLiveShareWarning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ limitations under the License.

import React, { useCallback, useEffect, useState } from 'react';
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 { _t } from '../../../languageHandler';
Expand Down Expand Up @@ -45,16 +50,22 @@ const getUpdateInterval = (ms: number) => {
return 1000;
};
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(() => {
setMsRemaining(getBeaconMsUntilExpiry(beacon));
}, [beacon]);
setMsRemaining(getBeaconMsUntilExpiry(beaconInfo));
}, [beaconInfo]);

const updateMsRemaining = useCallback(() => {
const ms = getBeaconMsUntilExpiry(beacon);
const ms = getBeaconMsUntilExpiry(beaconInfo);
setMsRemaining(ms);
}, [beacon]);
}, [beaconInfo]);

useInterval(updateMsRemaining, getUpdateInterval(msRemaining));

Expand All @@ -74,7 +85,7 @@ type LiveBeaconsState = {
hasStopSharingError?: boolean;
hasWireError?: boolean;
};
const useLiveBeacons = (liveBeaconIds: string[], roomId: string): LiveBeaconsState => {
const useLiveBeacons = (liveBeaconIds: BeaconIdentifier[], roomId: string): LiveBeaconsState => {
const [stoppingInProgress, setStoppingInProgress] = useState(false);
const [error, setError] = useState<Error>();

Expand Down
3 changes: 1 addition & 2 deletions src/components/views/location/shareLocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ export const shareLiveLocation = (
description,
LocationAssetType.Self,
),
// use timestamp as unique suffix in interim
`${Date.now()}`);
);
} catch (error) {
handleShareError(error, openMenu, LocationShareType.Live);
}
Expand Down
32 changes: 23 additions & 9 deletions src/stores/OwnBeaconStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
import { debounce } from "lodash";
import {
Beacon,
BeaconIdentifier,
BeaconEvent,
MatrixEvent,
Room,
Expand Down Expand Up @@ -58,22 +59,22 @@ const STATIC_UPDATE_INTERVAL = 30000;
const BAIL_AFTER_CONSECUTIVE_ERROR_COUNT = 2;

type OwnBeaconStoreState = {
beacons: Map<string, Beacon>;
beacons: Map<BeaconIdentifier, Beacon>;
beaconWireErrors: Map<string, Beacon>;
beaconsByRoomId: Map<Room['roomId'], Set<string>>;
liveBeaconIds: string[];
beaconsByRoomId: Map<Room['roomId'], Set<BeaconIdentifier>>;
liveBeaconIds: BeaconIdentifier[];
};
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>>();
public readonly beacons = new Map<BeaconIdentifier, Beacon>();
public readonly beaconsByRoomId = new Map<Room['roomId'], Set<BeaconIdentifier>>();
/**
* Track over the wire errors for published positions
* Counts consecutive wire errors per beacon
* Reset on successful publish of location
*/
public readonly beaconWireErrorCounts = new Map<string, number>();
public readonly beaconWireErrorCounts = new Map<BeaconIdentifier, number>();
/**
* ids of live beacons
* ordered by creation time descending
Expand Down Expand Up @@ -108,6 +109,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
protected async onNotReady() {
this.matrixClient.removeListener(BeaconEvent.LivenessChange, this.onBeaconLiveness);
this.matrixClient.removeListener(BeaconEvent.New, this.onNewBeacon);
this.matrixClient.removeListener(BeaconEvent.Update, this.onUpdateBeacon);
this.matrixClient.removeListener(RoomStateEvent.Members, this.onRoomStateMembers);

this.beacons.forEach(beacon => beacon.destroy());
Expand All @@ -122,6 +124,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
protected async onReady(): Promise<void> {
this.matrixClient.on(BeaconEvent.LivenessChange, this.onBeaconLiveness);
this.matrixClient.on(BeaconEvent.New, this.onNewBeacon);
this.matrixClient.removeListener(BeaconEvent.Update, this.onUpdateBeacon);
this.matrixClient.on(RoomStateEvent.Members, this.onRoomStateMembers);

this.initialiseBeaconState();
Expand Down Expand Up @@ -177,8 +180,8 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
return this.beacons.get(beaconId);
};

public stopBeacon = async (beaconInfoType: string): Promise<void> => {
const beacon = this.beacons.get(beaconInfoType);
public stopBeacon = async (beaconIdentifier: string): Promise<void> => {
const beacon = this.beacons.get(beaconIdentifier);
// if no beacon, or beacon is already explicitly set isLive: false
// do nothing
if (!beacon?.beaconInfo?.live) {
Expand All @@ -200,6 +203,17 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
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 => {
// check if we care about this beacon
if (!this.beacons.has(beacon.identifier)) {
Expand Down Expand Up @@ -439,7 +453,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
assetType,
timestamp);

await this.matrixClient.unstable_setLiveBeacon(beacon.roomId, beacon.beaconInfoEventType, updateContent);
await this.matrixClient.unstable_setLiveBeacon(beacon.roomId, updateContent);
};

/**
Expand Down
5 changes: 3 additions & 2 deletions src/utils/beacon/duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { BeaconInfoState } from "matrix-js-sdk/src/content-helpers";
import { Beacon } from "matrix-js-sdk/src/matrix";

/**
Expand All @@ -26,8 +27,8 @@ import { Beacon } from "matrix-js-sdk/src/matrix";
export const msUntilExpiry = (startTimestamp: number, durationMs: number): number =>
Math.max(0, (startTimestamp + durationMs) - Date.now());

export const getBeaconMsUntilExpiry = (beacon: Beacon): number =>
msUntilExpiry(beacon.beaconInfo.timestamp, beacon.beaconInfo.timeout);
export const getBeaconMsUntilExpiry = (beaconInfo: BeaconInfoState): number =>
msUntilExpiry(beaconInfo.timestamp, beaconInfo.timeout);

export const getBeaconExpiryTimestamp = (beacon: Beacon): number =>
beacon.beaconInfo.timestamp + beacon.beaconInfo.timeout;
Expand Down
33 changes: 25 additions & 8 deletions test/components/views/beacon/RoomLiveShareWarning-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ 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 { Room, Beacon, BeaconEvent, getBeaconInfoIdentifier } from 'matrix-js-sdk/src/matrix';
import { logger } from 'matrix-js-sdk/src/logger';

import RoomLiveShareWarning from '../../../../src/components/views/beacon/RoomLiveShareWarning';
Expand Down Expand Up @@ -221,6 +221,25 @@ describe('<RoomLiveShareWarning />', () => {
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', () => {
const clearIntervalSpy = jest.spyOn(global, 'clearInterval');
const component = getComponent({ roomId: room1Id });
Expand All @@ -242,7 +261,7 @@ describe('<RoomLiveShareWarning />', () => {
component.setProps({});
});

expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledTimes(2);
expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalled();
expect(component.find('Spinner').length).toBeTruthy();
expect(findByTestId(component, 'room-live-share-primary-button').at(0).props().disabled).toBeTruthy();
});
Expand Down Expand Up @@ -314,7 +333,7 @@ describe('<RoomLiveShareWarning />', () => {
// update mock and emit event
act(() => {
hasWireErrorsSpy.mockReturnValue(true);
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.WireError, room2Beacon1.getType());
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.WireError, getBeaconInfoIdentifier(room2Beacon1));
});
component.setProps({});

Expand All @@ -332,7 +351,7 @@ describe('<RoomLiveShareWarning />', () => {
// update mock and emit event
act(() => {
hasWireErrorsSpy.mockReturnValue(false);
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.WireError, room2Beacon1.getType());
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.WireError, getBeaconInfoIdentifier(room2Beacon1));
});
component.setProps({});

Expand All @@ -353,8 +372,7 @@ describe('<RoomLiveShareWarning />', () => {
findByTestId(component, 'room-live-share-primary-button').at(0).simulate('click');
});

expect(resetErrorSpy).toHaveBeenCalledWith(room2Beacon1.getType());
expect(resetErrorSpy).toHaveBeenCalledWith(room2Beacon2.getType());
expect(resetErrorSpy).toHaveBeenCalledWith(getBeaconInfoIdentifier(room2Beacon1));
});

it('clicking close button stops beacons', async () => {
Expand All @@ -367,8 +385,7 @@ describe('<RoomLiveShareWarning />', () => {
findByTestId(component, 'room-live-share-wire-error-close-button').at(0).simulate('click');
});

expect(stopBeaconSpy).toHaveBeenCalledWith(room2Beacon1.getType());
expect(stopBeaconSpy).toHaveBeenCalledWith(room2Beacon2.getType());
expect(stopBeaconSpy).toHaveBeenCalledWith(getBeaconInfoIdentifier(room2Beacon1));
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is
<RoomLiveShareWarningInner
liveBeaconIds={
Array [
"org.matrix.msc3489.beacon_info.@alice:server.org.3",
"org.matrix.msc3489.beacon_info.@alice:server.org.4",
"$room2:server.org_@alice:server.org",
]
}
roomId="$room2:server.org"
Expand Down
14 changes: 5 additions & 9 deletions test/components/views/location/LocationShareMenu-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { mocked } from 'jest-mock';
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 { logger } from 'matrix-js-sdk/src/logger';

Expand Down Expand Up @@ -310,16 +309,13 @@ describe('<LocationShareMenu />', () => {
});

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(eventTypeSuffix).toBeTruthy();
expect(eventContent).toEqual(expect.objectContaining({
[M_BEACON_INFO.name]: {
// default timeout
timeout: DEFAULT_DURATION_MS,
description: `Ernie's live location`,
live: true,
},
// default timeout
timeout: DEFAULT_DURATION_MS,
description: `Ernie's live location`,
live: true,
[M_ASSET.name]: {
type: LocationAssetType.Self,
},
Expand Down
Loading