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

Commit b04d31b

Browse files
author
Kerry
authored
Live location sharing: live share warning in room (#8100)
* add duration dropdown to live location picker Signed-off-by: Kerry Archibald <[email protected]> * tidy comments Signed-off-by: Kerry Archibald <[email protected]> * setup component Signed-off-by: Kerry Archibald <[email protected]> * replace references to beaconInfoId with beacon.identifier Signed-off-by: Kerry Archibald <[email protected]> * icon Signed-off-by: Kerry Archibald <[email protected]> * component for styled live beacon icon Signed-off-by: Kerry Archibald <[email protected]> * emit liveness change whenever livebeaconIds changes Signed-off-by: Kerry Archibald <[email protected]> * Handle multiple live beacons in room share warning, test Signed-off-by: Kerry Archibald <[email protected]> * un xdescribe beaconstore tests Signed-off-by: Kerry Archibald <[email protected]> * missed copyrights Signed-off-by: Kerry Archibald <[email protected]> * i18n Signed-off-by: Kerry Archibald <[email protected]> * tidy Signed-off-by: Kerry Archibald <[email protected]>
1 parent c8d3b51 commit b04d31b

File tree

11 files changed

+600
-42
lines changed

11 files changed

+600
-42
lines changed

res/css/_components.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
@import "./_font-weights.scss";
66
@import "./_spacing.scss";
77
@import "./components/views/beacon/_LeftPanelLiveShareWarning.scss";
8+
@import "./components/views/beacon/_RoomLiveShareWarning.scss";
89
@import "./components/views/beacon/_StyledLiveBeaconIcon.scss";
910
@import "./components/views/location/_LiveDurationDropdown.scss";
1011
@import "./components/views/location/_LocationShareMenu.scss";
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
.mx_RoomLiveShareWarning {
18+
width: 100%;
19+
20+
display: flex;
21+
flex-direction: row;
22+
align-items: center;
23+
24+
box-sizing: border-box;
25+
padding: $spacing-12 $spacing-16;
26+
27+
color: $primary-content;
28+
background-color: $system;
29+
}
30+
31+
.mx_RoomLiveShareWarning_icon {
32+
height: 32px;
33+
width: 32px;
34+
margin-right: $spacing-8;
35+
}
36+
37+
.mx_RoomLiveShareWarning_label {
38+
flex: 1;
39+
font-size: $font-15px;
40+
}
41+
42+
.mx_RoomLiveShareWarning_expiry {
43+
color: $secondary-content;
44+
font-size: $font-12px;
45+
margin-right: $spacing-16;
46+
}
47+
48+
.mx_RoomLiveShareWarning_spinner {
49+
margin-right: $spacing-16;
50+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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 React, { useEffect, useState } from 'react';
18+
import classNames from 'classnames';
19+
import { Room } from 'matrix-js-sdk/src/matrix';
20+
21+
import { _t } from '../../../languageHandler';
22+
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
23+
import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../stores/OwnBeaconStore';
24+
import AccessibleButton from '../elements/AccessibleButton';
25+
import StyledLiveBeaconIcon from './StyledLiveBeaconIcon';
26+
import { formatDuration } from '../../../DateUtils';
27+
import { getBeaconMsUntilExpiry, sortBeaconsByLatestExpiry } from '../../../utils/beacon';
28+
import Spinner from '../elements/Spinner';
29+
30+
interface Props {
31+
roomId: Room['roomId'];
32+
}
33+
34+
/**
35+
* It's technically possible to have multiple live beacons in one room
36+
* Select the latest expiry to display,
37+
* and kill all beacons on stop sharing
38+
*/
39+
type LiveBeaconsState = {
40+
liveBeaconIds: string[];
41+
msRemaining?: number;
42+
onStopSharing?: () => void;
43+
stoppingInProgress?: boolean;
44+
};
45+
46+
const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
47+
const [stoppingInProgress, setStoppingInProgress] = useState(false);
48+
const liveBeaconIds = useEventEmitterState(
49+
OwnBeaconStore.instance,
50+
OwnBeaconStoreEvent.LivenessChange,
51+
() => OwnBeaconStore.instance.getLiveBeaconIds(roomId),
52+
);
53+
54+
// reset stopping in progress on change in live ids
55+
useEffect(() => {
56+
setStoppingInProgress(false);
57+
}, [liveBeaconIds]);
58+
59+
if (!liveBeaconIds?.length) {
60+
return { liveBeaconIds };
61+
}
62+
63+
// select the beacon with latest expiry to display expiry time
64+
const beacon = liveBeaconIds.map(beaconId => OwnBeaconStore.instance.getBeaconById(beaconId))
65+
.sort(sortBeaconsByLatestExpiry)
66+
.shift();
67+
68+
const onStopSharing = async () => {
69+
setStoppingInProgress(true);
70+
try {
71+
await Promise.all(liveBeaconIds.map(beaconId => OwnBeaconStore.instance.stopBeacon(beaconId)));
72+
} catch (error) {
73+
// only clear loading in case of error
74+
// to avoid flash of not-loading state
75+
// after beacons have been stopped but we wait for sync
76+
setStoppingInProgress(false);
77+
}
78+
};
79+
80+
const msRemaining = getBeaconMsUntilExpiry(beacon);
81+
82+
return { liveBeaconIds, onStopSharing, msRemaining, stoppingInProgress };
83+
};
84+
85+
const RoomLiveShareWarning: React.FC<Props> = ({ roomId }) => {
86+
const {
87+
liveBeaconIds,
88+
onStopSharing,
89+
msRemaining,
90+
stoppingInProgress,
91+
} = useLiveBeacons(roomId);
92+
93+
if (!liveBeaconIds?.length) {
94+
return null;
95+
}
96+
97+
const timeRemaining = formatDuration(msRemaining);
98+
const liveTimeRemaining = _t(`%(timeRemaining)s left`, { timeRemaining });
99+
100+
return <div
101+
className={classNames('mx_RoomLiveShareWarning')}
102+
>
103+
<StyledLiveBeaconIcon className="mx_RoomLiveShareWarning_icon" />
104+
<span className="mx_RoomLiveShareWarning_label">
105+
{ _t('You are sharing %(count)s live locations', { count: liveBeaconIds.length }) }
106+
</span>
107+
108+
{ stoppingInProgress ?
109+
<span className='mx_RoomLiveShareWarning_spinner'><Spinner h={16} w={16} /></span> :
110+
<span
111+
data-test-id='room-live-share-expiry'
112+
className="mx_RoomLiveShareWarning_expiry"
113+
>{ liveTimeRemaining }</span>
114+
}
115+
<AccessibleButton
116+
data-test-id='room-live-share-stop-sharing'
117+
onClick={onStopSharing}
118+
kind='danger'
119+
element='button'
120+
disabled={stoppingInProgress}
121+
>
122+
{ _t('Stop sharing') }
123+
</AccessibleButton>
124+
</div>;
125+
};
126+
127+
export default RoomLiveShareWarning;

src/components/views/rooms/RoomHeader.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { RoomNotificationStateStore } from '../../../stores/notifications/RoomNo
4141
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
4242
import { NotificationStateEvents } from '../../../stores/notifications/NotificationState';
4343
import RoomContext from "../../../contexts/RoomContext";
44+
import RoomLiveShareWarning from '../beacon/RoomLiveShareWarning';
4445

4546
export interface ISearchInfo {
4647
searchTerm: string;
@@ -273,6 +274,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
273274
{ rightRow }
274275
<RoomHeaderButtons room={this.props.room} excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons} />
275276
</div>
277+
<RoomLiveShareWarning roomId={this.props.room.roomId} />
276278
</div>
277279
);
278280
}

src/i18n/strings/en_EN.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2965,6 +2965,10 @@
29652965
"Leave the beta": "Leave the beta",
29662966
"Join the beta": "Join the beta",
29672967
"You are sharing your live location": "You are sharing your live location",
2968+
"%(timeRemaining)s left": "%(timeRemaining)s left",
2969+
"You are sharing %(count)s live locations|other": "You are sharing %(count)s live locations",
2970+
"You are sharing %(count)s live locations|one": "You are sharing your live location",
2971+
"Stop sharing": "Stop sharing",
29682972
"Avatar": "Avatar",
29692973
"This room is public": "This room is public",
29702974
"Away": "Away",

src/stores/OwnBeaconStore.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ import {
2727
import defaultDispatcher from "../dispatcher/dispatcher";
2828
import { ActionPayload } from "../dispatcher/payloads";
2929
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
30+
import { arrayHasDiff } from "../utils/arrays";
3031

3132
const isOwnBeacon = (beacon: Beacon, userId: string): boolean => beacon.beaconInfoOwner === userId;
3233

3334
export enum OwnBeaconStoreEvent {
34-
LivenessChange = 'OwnBeaconStore.LivenessChange'
35+
LivenessChange = 'OwnBeaconStore.LivenessChange',
3536
}
3637

3738
type OwnBeaconStoreState = {
@@ -41,6 +42,7 @@ type OwnBeaconStoreState = {
4142
};
4243
export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
4344
private static internalInstance = new OwnBeaconStore();
45+
// users beacons, keyed by event type
4446
public readonly beacons = new Map<string, Beacon>();
4547
public readonly beaconsByRoomId = new Map<Room['roomId'], Set<string>>();
4648
private liveBeaconIds = [];
@@ -86,8 +88,12 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
8688
return this.liveBeaconIds.filter(beaconId => this.beaconsByRoomId.get(roomId)?.has(beaconId));
8789
}
8890

89-
public stopBeacon = async (beaconInfoId: string): Promise<void> => {
90-
const beacon = this.beacons.get(beaconInfoId);
91+
public getBeaconById(beaconId: string): Beacon | undefined {
92+
return this.beacons.get(beaconId);
93+
}
94+
95+
public stopBeacon = async (beaconInfoType: string): Promise<void> => {
96+
const beacon = this.beacons.get(beaconInfoType);
9197
// if no beacon, or beacon is already explicitly set isLive: false
9298
// do nothing
9399
if (!beacon?.beaconInfo?.live) {
@@ -107,27 +113,27 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
107113

108114
private onBeaconLiveness = (isLive: boolean, beacon: Beacon): void => {
109115
// check if we care about this beacon
110-
if (!this.beacons.has(beacon.beaconInfoId)) {
116+
if (!this.beacons.has(beacon.identifier)) {
111117
return;
112118
}
113119

114-
if (!isLive && this.liveBeaconIds.includes(beacon.beaconInfoId)) {
120+
if (!isLive && this.liveBeaconIds.includes(beacon.identifier)) {
115121
this.liveBeaconIds =
116-
this.liveBeaconIds.filter(beaconId => beaconId !== beacon.beaconInfoId);
122+
this.liveBeaconIds.filter(beaconId => beaconId !== beacon.identifier);
117123
}
118124

119-
if (isLive && !this.liveBeaconIds.includes(beacon.beaconInfoId)) {
120-
this.liveBeaconIds.push(beacon.beaconInfoId);
125+
if (isLive && !this.liveBeaconIds.includes(beacon.identifier)) {
126+
this.liveBeaconIds.push(beacon.identifier);
121127
}
122128

123129
// beacon expired, update beacon to un-alive state
124130
if (!isLive) {
125-
this.stopBeacon(beacon.beaconInfoId);
131+
this.stopBeacon(beacon.identifier);
126132
}
127133

128134
// TODO start location polling here
129135

130-
this.emit(OwnBeaconStoreEvent.LivenessChange, this.hasLiveBeacons());
136+
this.emit(OwnBeaconStoreEvent.LivenessChange, this.getLiveBeaconIds());
131137
};
132138

133139
private initialiseBeaconState = () => {
@@ -146,27 +152,25 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
146152
};
147153

148154
private addBeacon = (beacon: Beacon): void => {
149-
this.beacons.set(beacon.beaconInfoId, beacon);
155+
this.beacons.set(beacon.identifier, beacon);
150156

151157
if (!this.beaconsByRoomId.has(beacon.roomId)) {
152158
this.beaconsByRoomId.set(beacon.roomId, new Set<string>());
153159
}
154160

155-
this.beaconsByRoomId.get(beacon.roomId).add(beacon.beaconInfoId);
161+
this.beaconsByRoomId.get(beacon.roomId).add(beacon.identifier);
156162

157163
beacon.monitorLiveness();
158164
};
159165

160166
private checkLiveness = (): void => {
161-
const prevLiveness = this.hasLiveBeacons();
167+
const prevLiveBeaconIds = this.getLiveBeaconIds();
162168
this.liveBeaconIds = [...this.beacons.values()]
163169
.filter(beacon => beacon.isLive)
164-
.map(beacon => beacon.beaconInfoId);
165-
166-
const newLiveness = this.hasLiveBeacons();
170+
.map(beacon => beacon.identifier);
167171

168-
if (prevLiveness !== newLiveness) {
169-
this.emit(OwnBeaconStoreEvent.LivenessChange, newLiveness);
172+
if (arrayHasDiff(prevLiveBeaconIds, this.liveBeaconIds)) {
173+
this.emit(OwnBeaconStoreEvent.LivenessChange, this.liveBeaconIds);
170174
}
171175
};
172176

0 commit comments

Comments
 (0)