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

Commit 14653d1

Browse files
author
Kerry
authored
Live location share - set time limit (#8082)
* add mocking helpers for platform peg Signed-off-by: Kerry Archibald <[email protected]> * basic working live duration dropdown Signed-off-by: Kerry Archibald <[email protected]> * add duration format utility Signed-off-by: Kerry Archibald <[email protected]> * add duration dropdown to live location picker Signed-off-by: Kerry Archibald <[email protected]> * adjust style to allow overflow and variable height chin Signed-off-by: Kerry Archibald <[email protected]> * tidy comments Signed-off-by: Kerry Archibald <[email protected]> * arrow fn change Signed-off-by: Kerry Archibald <[email protected]> * lint Signed-off-by: Kerry Archibald <[email protected]>
1 parent 8418b4f commit 14653d1

File tree

13 files changed

+365
-36
lines changed

13 files changed

+365
-36
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/location/_LiveDurationDropdown.scss";
89
@import "./components/views/location/_LocationShareMenu.scss";
910
@import "./components/views/location/_MapError.scss";
1011
@import "./components/views/location/_ShareDialogButtons.scss";
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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_LiveDurationDropdown {
18+
margin-bottom: $spacing-16;
19+
}

res/css/views/location/_LocationPicker.scss

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ limitations under the License.
1919

2020
height: 100%;
2121
position: relative;
22-
overflow: hidden;
22+
display: flex;
23+
flex-direction: column;
2324

2425
// when there are errors loading the map
2526
// the canvas is still inserted
@@ -32,8 +33,9 @@ limitations under the License.
3233
}
3334

3435
#mx_LocationPicker_map {
35-
height: 100%;
36-
border-radius: 8px;
36+
border-top-left-radius: inherit;
37+
border-top-right-radius: inherit;
38+
flex: 1;
3739

3840
.maplibregl-ctrl.maplibregl-ctrl-group,
3941
.maplibregl-ctrl.maplibregl-ctrl-attrib {
@@ -46,10 +48,6 @@ limitations under the License.
4648
margin-top: 50px;
4749
}
4850

49-
.maplibregl-ctrl-bottom-right {
50-
bottom: 80px;
51-
}
52-
5351
.maplibregl-user-location-accuracy-circle {
5452
display: none;
5553
}
@@ -93,15 +91,17 @@ limitations under the License.
9391
}
9492

9593
.mx_LocationPicker_footer {
96-
position: absolute;
97-
bottom: 0px;
94+
flex: 0;
9895
width: 100%;
9996
box-sizing: border-box;
10097
padding: $spacing-16;
10198
display: flex;
10299
flex-direction: column;
103100
justify-content: stretch;
104101

102+
border-bottom-left-radius: inherit;
103+
border-bottom-right-radius: inherit;
104+
105105
background-color: $header-panel-bg-color;
106106
}
107107
}

src/DateUtils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,25 @@ export function formatRelativeTime(date: Date, showTwelveHour = false): string {
201201
return relativeDate;
202202
}
203203
}
204+
205+
/**
206+
* Formats duration in ms to human readable string
207+
* Returns value in biggest possible unit (day, hour, min, second)
208+
* Rounds values up until unit threshold
209+
* ie. 23:13:57 -> 23h, 24:13:57 -> 1d, 44:56:56 -> 2d
210+
*/
211+
const MINUTE_MS = 60000;
212+
const HOUR_MS = MINUTE_MS * 60;
213+
const DAY_MS = HOUR_MS * 24;
214+
export function formatDuration(durationMs: number): string {
215+
if (durationMs >= DAY_MS) {
216+
return _t('%(value)sd', { value: Math.round(durationMs / DAY_MS) });
217+
}
218+
if (durationMs >= HOUR_MS) {
219+
return _t('%(value)sh', { value: Math.round(durationMs / HOUR_MS) });
220+
}
221+
if (durationMs >= MINUTE_MS) {
222+
return _t('%(value)sm', { value: Math.round(durationMs / MINUTE_MS) });
223+
}
224+
return _t('%(value)ss', { value: Math.round(durationMs / 1000) });
225+
}
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 React from 'react';
18+
19+
import { formatDuration } from '../../../DateUtils';
20+
import { _t } from '../../../languageHandler';
21+
import Dropdown from '../elements/Dropdown';
22+
23+
const DURATION_MS = {
24+
fifteenMins: 900000,
25+
oneHour: 3600000,
26+
eightHours: 28800000,
27+
};
28+
29+
export const DEFAULT_DURATION_MS = DURATION_MS.fifteenMins;
30+
31+
interface Props {
32+
timeout: number;
33+
onChange: (timeout: number) => void;
34+
}
35+
36+
const getLabel = (durationMs: number) => {
37+
return _t('Share for %(duration)s', { duration: formatDuration(durationMs) });
38+
};
39+
40+
const LiveDurationDropdown: React.FC<Props> = ({ timeout, onChange }) => {
41+
const options = Object.values(DURATION_MS).map((duration) =>
42+
({ key: duration.toString(), duration, label: getLabel(duration) }),
43+
);
44+
45+
// timeout is not one of our default values
46+
// eg it was set by another client
47+
if (!Object.values(DURATION_MS).includes(timeout)) {
48+
options.push({
49+
key: timeout.toString(), duration: timeout, label: getLabel(timeout),
50+
});
51+
}
52+
53+
const onOptionChange = (key: string) => {
54+
// stringified value back to number
55+
onChange(+key);
56+
};
57+
58+
return <Dropdown
59+
id='live-duration'
60+
data-test-id='live-duration-dropdown'
61+
label={getLabel(timeout)}
62+
value={timeout.toString()}
63+
onOptionChange={onOptionChange}
64+
className='mx_LiveDurationDropdown'
65+
>
66+
{ options.map(({ key, label }) =>
67+
<div data-test-id={`live-duration-option-${key}`} key={key}>{ label }</div>,
68+
) }
69+
</Dropdown>;
70+
};
71+
72+
export default LiveDurationDropdown;

src/components/views/location/LocationPicker.tsx

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { LocationShareError } from './LocationShareErrors';
3535
import AccessibleButton from '../elements/AccessibleButton';
3636
import { MapError } from './MapError';
3737
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
38+
import LiveDurationDropdown, { DEFAULT_DURATION_MS } from './LiveDurationDropdown';
3839
export interface ILocationPickerProps {
3940
sender: RoomMember;
4041
shareType: LocationShareType;
@@ -50,6 +51,7 @@ interface IPosition {
5051
timestamp: number;
5152
}
5253
interface IState {
54+
timeout: number;
5355
position?: IPosition;
5456
error?: LocationShareError;
5557
}
@@ -70,6 +72,7 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
7072

7173
this.state = {
7274
position: undefined,
75+
timeout: DEFAULT_DURATION_MS,
7376
error: undefined,
7477
};
7578
}
@@ -206,10 +209,17 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
206209
}
207210
};
208211

212+
private onTimeoutChange = (timeout: number): void => {
213+
this.setState({ timeout });
214+
};
215+
209216
private onOk = () => {
210-
const position = this.state.position;
217+
const { timeout, position } = this.state;
211218

212-
this.props.onChoose(position ? { uri: getGeoUri(position), timestamp: position.timestamp } : {});
219+
this.props.onChoose(
220+
position ? { uri: getGeoUri(position), timestamp: position.timestamp, timeout } : {
221+
timeout,
222+
});
213223
this.props.onFinished();
214224
};
215225

@@ -235,7 +245,12 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
235245
}
236246
<div className="mx_LocationPicker_footer">
237247
<form onSubmit={this.onOk}>
238-
248+
{ this.props.shareType === LocationShareType.Live &&
249+
<LiveDurationDropdown
250+
onChange={this.onTimeoutChange}
251+
timeout={this.state.timeout}
252+
/>
253+
}
239254
<AccessibleButton
240255
data-test-id="location-picker-submit-button"
241256
type="submit"
@@ -253,21 +268,32 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
253268
`mx_MLocationBody_marker-${this.props.shareType}`,
254269
userColorClass,
255270
)}
256-
id={this.getMarkerId()}>
257-
<div className="mx_MLocationBody_markerBorder">
258-
{ isSharingOwnLocation(this.props.shareType) ?
259-
<MemberAvatar
260-
member={this.props.sender}
261-
width={27}
262-
height={27}
263-
viewUserOnClick={false}
264-
/>
265-
: <LocationIcon className="mx_MLocationBody_markerIcon" />
266-
}
267-
</div>
268-
<div
269-
className="mx_MLocationBody_pointer"
270-
/>
271+
id={this.getMarkerId()}
272+
>
273+
{ /*
274+
maplibregl hijacks the div above to style the marker
275+
it must be in the dom when the map is initialised
276+
and keep a consistent class
277+
we want to hide the marker until it is set in the case of pin drop
278+
so hide the internal visible elements
279+
*/ }
280+
281+
{ !!this.marker && <>
282+
<div className="mx_MLocationBody_markerBorder">
283+
{ isSharingOwnLocation(this.props.shareType) ?
284+
<MemberAvatar
285+
member={this.props.sender}
286+
width={27}
287+
height={27}
288+
viewUserOnClick={false}
289+
/>
290+
: <LocationIcon className="mx_MLocationBody_markerIcon" />
291+
}
292+
</div>
293+
<div
294+
className="mx_MLocationBody_pointer"
295+
/>
296+
</> }
271297
</div>
272298
</div>
273299
);

src/i18n/strings/en_EN.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@
106106
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
107107
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
108108
"%(date)s at %(time)s": "%(date)s at %(time)s",
109+
"%(value)sd": "%(value)sd",
110+
"%(value)sh": "%(value)sh",
111+
"%(value)sm": "%(value)sm",
112+
"%(value)ss": "%(value)ss",
109113
"Who would you like to add to this community?": "Who would you like to add to this community?",
110114
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID",
111115
"Invite new community members": "Invite new community members",
@@ -2172,6 +2176,7 @@
21722176
"Submit logs": "Submit logs",
21732177
"Can't load this message": "Can't load this message",
21742178
"toggle event": "toggle event",
2179+
"Share for %(duration)s": "Share for %(duration)s",
21752180
"Location": "Location",
21762181
"Could not fetch location": "Could not fetch location",
21772182
"Click to move the pin": "Click to move the pin",
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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 from 'react';
18+
import { mount } from 'enzyme';
19+
import { act } from 'react-dom/test-utils';
20+
21+
import '../../../skinned-sdk';
22+
import LiveDurationDropdown, { DEFAULT_DURATION_MS }
23+
from '../../../../src/components/views/location/LiveDurationDropdown';
24+
import { findById, mockPlatformPeg } from '../../../test-utils';
25+
26+
mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) });
27+
28+
describe('<LiveDurationDropdown />', () => {
29+
const defaultProps = {
30+
timeout: DEFAULT_DURATION_MS,
31+
onChange: jest.fn(),
32+
};
33+
const getComponent = (props = {}) =>
34+
mount(<LiveDurationDropdown {...defaultProps} {...props} />);
35+
36+
const getOption = (wrapper, timeout) => findById(wrapper, `live-duration__${timeout}`).at(0);
37+
const getSelectedOption = (wrapper) => findById(wrapper, 'live-duration_value');
38+
const openDropdown = (wrapper) => act(() => {
39+
wrapper.find('[role="button"]').at(0).simulate('click');
40+
wrapper.setProps({});
41+
});
42+
43+
it('renders timeout as selected option', () => {
44+
const wrapper = getComponent();
45+
expect(getSelectedOption(wrapper).text()).toEqual('Share for 15m');
46+
});
47+
48+
it('renders non-default timeout as selected option', () => {
49+
const timeout = 1234567;
50+
const wrapper = getComponent({ timeout });
51+
expect(getSelectedOption(wrapper).text()).toEqual(`Share for 21m`);
52+
});
53+
54+
it('renders a dropdown option for a non-default timeout value', () => {
55+
const timeout = 1234567;
56+
const wrapper = getComponent({ timeout });
57+
openDropdown(wrapper);
58+
expect(getOption(wrapper, timeout).text()).toEqual(`Share for 21m`);
59+
});
60+
61+
it('updates value on option selection', () => {
62+
const onChange = jest.fn();
63+
const wrapper = getComponent({ onChange });
64+
65+
const ONE_HOUR = 3600000;
66+
67+
openDropdown(wrapper);
68+
69+
act(() => {
70+
getOption(wrapper, ONE_HOUR).simulate('click');
71+
});
72+
73+
expect(onChange).toHaveBeenCalledWith(ONE_HOUR);
74+
});
75+
});

0 commit comments

Comments
 (0)