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

Commit aecd71a

Browse files
author
Kerry
authored
Live location sharing - update beacon tile with latest location (#8265)
* add useBeacon hook Signed-off-by: Kerry Archibald <[email protected]> * update message tile types to work with function comp with ref Signed-off-by: Kerry Archibald <[email protected]> * use beacon hook in beacon body Signed-off-by: Kerry Archibald <[email protected]> * update beacon body with (textual) latest locations, test Signed-off-by: Kerry Archibald <[email protected]> * language in comment Signed-off-by: Kerry Archibald <[email protected]> * comments Signed-off-by: Kerry Archibald <[email protected]> * copyright Signed-off-by: Kerry Archibald <[email protected]>
1 parent 7ba991c commit aecd71a

File tree

9 files changed

+424
-29
lines changed

9 files changed

+424
-29
lines changed

src/components/views/messages/IBodyProps.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import React, { LegacyRef } from "react";
1718
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
1819
import { Relations } from "matrix-js-sdk/src/models/relations";
1920

@@ -52,4 +53,6 @@ export interface IBodyProps {
5253

5354
// helper function to access relations for this event
5455
getRelationsForEvent?: (eventId: string, relationType: string, eventType: string) => Relations;
56+
57+
ref?: React.RefObject<any> | LegacyRef<any>;
5558
}

src/components/views/messages/MBeaconBody.tsx

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,41 +15,71 @@ limitations under the License.
1515
*/
1616

1717
import React from 'react';
18-
import { Beacon, getBeaconInfoIdentifier } from 'matrix-js-sdk/src/matrix';
18+
import { BeaconEvent, MatrixEvent } from 'matrix-js-sdk/src/matrix';
19+
import { BeaconLocationState } from 'matrix-js-sdk/src/content-helpers';
1920

20-
import MatrixClientContext from '../../../contexts/MatrixClientContext';
2121
import { IBodyProps } from "./IBodyProps";
22+
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
23+
import { useBeacon } from '../../../utils/beacon';
2224

23-
export default class MLocationBody extends React.Component<IBodyProps> {
24-
public static contextType = MatrixClientContext;
25-
public context!: React.ContextType<typeof MatrixClientContext>;
26-
private beacon: Beacon | undefined;
27-
private roomId: string;
28-
private beaconIdentifier: string;
25+
const useBeaconState = (beaconInfoEvent: MatrixEvent): {
26+
hasBeacon: boolean;
27+
description?: string;
28+
latestLocationState?: BeaconLocationState;
29+
isLive?: boolean;
30+
} => {
31+
const beacon = useBeacon(beaconInfoEvent);
2932

30-
constructor(props: IBodyProps) {
31-
super(props);
33+
const isLive = useEventEmitterState(
34+
beacon,
35+
BeaconEvent.LivenessChange,
36+
() => beacon?.isLive);
3237

33-
this.roomId = props.mxEvent.getRoomId();
38+
const latestLocationState = useEventEmitterState(
39+
beacon,
40+
BeaconEvent.LocationUpdate,
41+
() => beacon?.latestLocationState);
3442

35-
this.beaconIdentifier = getBeaconInfoIdentifier(props.mxEvent);
43+
if (!beacon) {
44+
return {
45+
hasBeacon: false,
46+
};
3647
}
3748

38-
componentDidMount() {
39-
const roomState = this.context.getRoom(this.roomId)?.currentState;
49+
const { description } = beacon.beaconInfo;
4050

41-
const beacon = roomState?.beacons.get(this.beaconIdentifier);
51+
return {
52+
hasBeacon: true,
53+
description,
54+
isLive,
55+
latestLocationState,
56+
};
57+
};
4258

43-
this.beacon = beacon;
44-
}
59+
const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, ...rest }, ref) => {
60+
const {
61+
hasBeacon,
62+
isLive,
63+
description,
64+
latestLocationState,
65+
} = useBeaconState(mxEvent);
4566

46-
render(): React.ReactElement<HTMLDivElement> {
47-
if (!this.beacon) {
48-
// TODO loading and error states
49-
return null;
50-
}
51-
// TODO everything else :~)
52-
const description = this.beacon.beaconInfo.description;
53-
return <div>{ description }</div>;
67+
if (!hasBeacon || !isLive) {
68+
// TODO stopped, error states
69+
return <span ref={ref}>Beacon stopped or replaced</span>;
5470
}
55-
}
71+
72+
return (
73+
// TODO nice map
74+
<div className='mx_MBeaconBody' ref={ref}>
75+
<code>{ mxEvent.getId() }</code>&nbsp;
76+
<span>Beacon "{ description }" </span>
77+
{ latestLocationState ?
78+
<span>{ `${latestLocationState.uri} at ${latestLocationState.timestamp}` }</span> :
79+
<span data-test-id='beacon-waiting-for-location'>Waiting for location</span> }
80+
</div>
81+
);
82+
});
83+
84+
export default MBeaconBody;
85+

src/components/views/messages/MessageEvent.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
9494
};
9595
}
9696

97-
private get evTypes(): Record<string, typeof React.Component> {
97+
private get evTypes(): Record<string, React.ComponentType<Partial<IBodyProps>>> {
9898
return {
9999
[EventType.Sticker]: MStickerBody,
100100
[M_POLL_START.name]: MPollBody,
@@ -122,7 +122,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
122122
const content = this.props.mxEvent.getContent();
123123
const type = this.props.mxEvent.getType();
124124
const msgtype = content.msgtype;
125-
let BodyType: typeof React.Component | ReactAnyComponent = RedactedBody;
125+
let BodyType: React.ComponentType<Partial<IBodyProps>> | ReactAnyComponent = RedactedBody;
126126
if (!this.props.mxEvent.isRedacted()) {
127127
// only resolve BodyType if event is not redacted
128128
if (type && this.evTypes[type]) {

src/stores/OwnBeaconStore.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
125125
protected async onReady(): Promise<void> {
126126
this.matrixClient.on(BeaconEvent.LivenessChange, this.onBeaconLiveness);
127127
this.matrixClient.on(BeaconEvent.New, this.onNewBeacon);
128-
this.matrixClient.removeListener(BeaconEvent.Update, this.onUpdateBeacon);
128+
this.matrixClient.on(BeaconEvent.Update, this.onUpdateBeacon);
129129
this.matrixClient.on(RoomStateEvent.Members, this.onRoomStateMembers);
130130

131131
this.initialiseBeaconState();
@@ -213,6 +213,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
213213
}
214214

215215
this.checkLiveness();
216+
beacon.monitorLiveness();
216217
};
217218

218219
private onBeaconLiveness = (isLive: boolean, beacon: Beacon): void => {

src/utils/beacon/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ limitations under the License.
1616

1717
export * from './duration';
1818
export * from './geolocation';
19+
export * from './useBeacon';

src/utils/beacon/useBeacon.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { useContext, useEffect, useState } from "react";
18+
import {
19+
Beacon,
20+
BeaconEvent,
21+
MatrixEvent,
22+
getBeaconInfoIdentifier,
23+
} from "matrix-js-sdk/src/matrix";
24+
25+
import MatrixClientContext from "../../contexts/MatrixClientContext";
26+
import { useEventEmitterState } from "../../hooks/useEventEmitter";
27+
28+
export const useBeacon = (beaconInfoEvent: MatrixEvent): Beacon | undefined => {
29+
const matrixClient = useContext(MatrixClientContext);
30+
const [beacon, setBeacon] = useState<Beacon>();
31+
32+
useEffect(() => {
33+
const roomId = beaconInfoEvent.getRoomId();
34+
const beaconIdentifier = getBeaconInfoIdentifier(beaconInfoEvent);
35+
36+
const room = matrixClient.getRoom(roomId);
37+
const beaconInstance = room.currentState.beacons.get(beaconIdentifier);
38+
39+
// TODO could this be less stupid?
40+
41+
// Beacons are identified by their `state_key`,
42+
// where `state_key` is always owner mxid for access control.
43+
// Thus, only one beacon is allowed per-user per-room.
44+
// See https://github.com/matrix-org/matrix-spec-proposals/pull/3672
45+
// When a user creates a new beacon any previous
46+
// beacon is replaced and should assume a 'stopped' state
47+
// Here we check that this event is the latest beacon for this user
48+
// If it is not the beacon instance is set to undefined.
49+
// Retired beacons don't get a beacon instance.
50+
if (beaconInstance?.beaconInfoId === beaconInfoEvent.getId()) {
51+
setBeacon(beaconInstance);
52+
} else {
53+
setBeacon(undefined);
54+
}
55+
}, [beaconInfoEvent, matrixClient]);
56+
57+
// beacon update will fire when this beacon is superceded
58+
// check the updated event id for equality to the matrix event
59+
const beaconInstanceEventId = useEventEmitterState(
60+
beacon,
61+
BeaconEvent.Update,
62+
() => beacon?.beaconInfoId,
63+
);
64+
65+
useEffect(() => {
66+
if (beaconInstanceEventId && beaconInstanceEventId !== beaconInfoEvent.getId()) {
67+
setBeacon(undefined);
68+
}
69+
}, [beaconInstanceEventId, beaconInfoEvent]);
70+
71+
return beacon;
72+
};

test/components/views/location/__snapshots__/SmartMarker-test.tsx.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ exports[`<SmartMarker /> creates a marker on mount 1`] = `
1010
"_maxListeners": undefined,
1111
"addControl": [MockFunction],
1212
"removeControl": [MockFunction],
13+
"zoomIn": [MockFunction],
14+
"zoomOut": [MockFunction],
1315
Symbol(kCapture): false,
1416
}
1517
}
@@ -40,6 +42,8 @@ exports[`<SmartMarker /> removes marker on unmount 1`] = `
4042
"_maxListeners": undefined,
4143
"addControl": [MockFunction],
4244
"removeControl": [MockFunction],
45+
"zoomIn": [MockFunction],
46+
"zoomOut": [MockFunction],
4347
Symbol(kCapture): false,
4448
}
4549
}

0 commit comments

Comments
 (0)