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

Commit

Permalink
Live location share - set time limit (#8082)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
Kerry authored Mar 21, 2022
1 parent 8418b4f commit 14653d1
Show file tree
Hide file tree
Showing 13 changed files with 365 additions and 36 deletions.
1 change: 1 addition & 0 deletions res/css/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@import "./_font-weights.scss";
@import "./_spacing.scss";
@import "./components/views/beacon/_LeftPanelLiveShareWarning.scss";
@import "./components/views/location/_LiveDurationDropdown.scss";
@import "./components/views/location/_LocationShareMenu.scss";
@import "./components/views/location/_MapError.scss";
@import "./components/views/location/_ShareDialogButtons.scss";
Expand Down
19 changes: 19 additions & 0 deletions res/css/components/views/location/_LiveDurationDropdown.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
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_LiveDurationDropdown {
margin-bottom: $spacing-16;
}
18 changes: 9 additions & 9 deletions res/css/views/location/_LocationPicker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ limitations under the License.

height: 100%;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;

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

#mx_LocationPicker_map {
height: 100%;
border-radius: 8px;
border-top-left-radius: inherit;
border-top-right-radius: inherit;
flex: 1;

.maplibregl-ctrl.maplibregl-ctrl-group,
.maplibregl-ctrl.maplibregl-ctrl-attrib {
Expand All @@ -46,10 +48,6 @@ limitations under the License.
margin-top: 50px;
}

.maplibregl-ctrl-bottom-right {
bottom: 80px;
}

.maplibregl-user-location-accuracy-circle {
display: none;
}
Expand Down Expand Up @@ -93,15 +91,17 @@ limitations under the License.
}

.mx_LocationPicker_footer {
position: absolute;
bottom: 0px;
flex: 0;
width: 100%;
box-sizing: border-box;
padding: $spacing-16;
display: flex;
flex-direction: column;
justify-content: stretch;

border-bottom-left-radius: inherit;
border-bottom-right-radius: inherit;

background-color: $header-panel-bg-color;
}
}
Expand Down
22 changes: 22 additions & 0 deletions src/DateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,25 @@ export function formatRelativeTime(date: Date, showTwelveHour = false): string {
return relativeDate;
}
}

/**
* Formats duration in ms to human readable string
* Returns value in biggest possible unit (day, hour, min, second)
* Rounds values up until unit threshold
* ie. 23:13:57 -> 23h, 24:13:57 -> 1d, 44:56:56 -> 2d
*/
const MINUTE_MS = 60000;
const HOUR_MS = MINUTE_MS * 60;
const DAY_MS = HOUR_MS * 24;
export function formatDuration(durationMs: number): string {
if (durationMs >= DAY_MS) {
return _t('%(value)sd', { value: Math.round(durationMs / DAY_MS) });
}
if (durationMs >= HOUR_MS) {
return _t('%(value)sh', { value: Math.round(durationMs / HOUR_MS) });
}
if (durationMs >= MINUTE_MS) {
return _t('%(value)sm', { value: Math.round(durationMs / MINUTE_MS) });
}
return _t('%(value)ss', { value: Math.round(durationMs / 1000) });
}
72 changes: 72 additions & 0 deletions src/components/views/location/LiveDurationDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
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 { formatDuration } from '../../../DateUtils';
import { _t } from '../../../languageHandler';
import Dropdown from '../elements/Dropdown';

const DURATION_MS = {
fifteenMins: 900000,
oneHour: 3600000,
eightHours: 28800000,
};

export const DEFAULT_DURATION_MS = DURATION_MS.fifteenMins;

interface Props {
timeout: number;
onChange: (timeout: number) => void;
}

const getLabel = (durationMs: number) => {
return _t('Share for %(duration)s', { duration: formatDuration(durationMs) });
};

const LiveDurationDropdown: React.FC<Props> = ({ timeout, onChange }) => {
const options = Object.values(DURATION_MS).map((duration) =>
({ key: duration.toString(), duration, label: getLabel(duration) }),
);

// timeout is not one of our default values
// eg it was set by another client
if (!Object.values(DURATION_MS).includes(timeout)) {
options.push({
key: timeout.toString(), duration: timeout, label: getLabel(timeout),
});
}

const onOptionChange = (key: string) => {
// stringified value back to number
onChange(+key);
};

return <Dropdown
id='live-duration'
data-test-id='live-duration-dropdown'
label={getLabel(timeout)}
value={timeout.toString()}
onOptionChange={onOptionChange}
className='mx_LiveDurationDropdown'
>
{ options.map(({ key, label }) =>
<div data-test-id={`live-duration-option-${key}`} key={key}>{ label }</div>,
) }
</Dropdown>;
};

export default LiveDurationDropdown;
62 changes: 44 additions & 18 deletions src/components/views/location/LocationPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { LocationShareError } from './LocationShareErrors';
import AccessibleButton from '../elements/AccessibleButton';
import { MapError } from './MapError';
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
import LiveDurationDropdown, { DEFAULT_DURATION_MS } from './LiveDurationDropdown';
export interface ILocationPickerProps {
sender: RoomMember;
shareType: LocationShareType;
Expand All @@ -50,6 +51,7 @@ interface IPosition {
timestamp: number;
}
interface IState {
timeout: number;
position?: IPosition;
error?: LocationShareError;
}
Expand All @@ -70,6 +72,7 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {

this.state = {
position: undefined,
timeout: DEFAULT_DURATION_MS,
error: undefined,
};
}
Expand Down Expand Up @@ -206,10 +209,17 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
}
};

private onTimeoutChange = (timeout: number): void => {
this.setState({ timeout });
};

private onOk = () => {
const position = this.state.position;
const { timeout, position } = this.state;

this.props.onChoose(position ? { uri: getGeoUri(position), timestamp: position.timestamp } : {});
this.props.onChoose(
position ? { uri: getGeoUri(position), timestamp: position.timestamp, timeout } : {
timeout,
});
this.props.onFinished();
};

Expand All @@ -235,7 +245,12 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
}
<div className="mx_LocationPicker_footer">
<form onSubmit={this.onOk}>

{ this.props.shareType === LocationShareType.Live &&
<LiveDurationDropdown
onChange={this.onTimeoutChange}
timeout={this.state.timeout}
/>
}
<AccessibleButton
data-test-id="location-picker-submit-button"
type="submit"
Expand All @@ -253,21 +268,32 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
`mx_MLocationBody_marker-${this.props.shareType}`,
userColorClass,
)}
id={this.getMarkerId()}>
<div className="mx_MLocationBody_markerBorder">
{ isSharingOwnLocation(this.props.shareType) ?
<MemberAvatar
member={this.props.sender}
width={27}
height={27}
viewUserOnClick={false}
/>
: <LocationIcon className="mx_MLocationBody_markerIcon" />
}
</div>
<div
className="mx_MLocationBody_pointer"
/>
id={this.getMarkerId()}
>
{ /*
maplibregl hijacks the div above to style the marker
it must be in the dom when the map is initialised
and keep a consistent class
we want to hide the marker until it is set in the case of pin drop
so hide the internal visible elements
*/ }

{ !!this.marker && <>
<div className="mx_MLocationBody_markerBorder">
{ isSharingOwnLocation(this.props.shareType) ?
<MemberAvatar
member={this.props.sender}
width={27}
height={27}
viewUserOnClick={false}
/>
: <LocationIcon className="mx_MLocationBody_markerIcon" />
}
</div>
<div
className="mx_MLocationBody_pointer"
/>
</> }
</div>
</div>
);
Expand Down
5 changes: 5 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
"%(date)s at %(time)s": "%(date)s at %(time)s",
"%(value)sd": "%(value)sd",
"%(value)sh": "%(value)sh",
"%(value)sm": "%(value)sm",
"%(value)ss": "%(value)ss",
"Who would you like to add to this community?": "Who would you like to add to this community?",
"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",
"Invite new community members": "Invite new community members",
Expand Down Expand Up @@ -2172,6 +2176,7 @@
"Submit logs": "Submit logs",
"Can't load this message": "Can't load this message",
"toggle event": "toggle event",
"Share for %(duration)s": "Share for %(duration)s",
"Location": "Location",
"Could not fetch location": "Could not fetch location",
"Click to move the pin": "Click to move the pin",
Expand Down
75 changes: 75 additions & 0 deletions test/components/views/location/LiveDurationDropdown-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
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 { act } from 'react-dom/test-utils';

import '../../../skinned-sdk';
import LiveDurationDropdown, { DEFAULT_DURATION_MS }
from '../../../../src/components/views/location/LiveDurationDropdown';
import { findById, mockPlatformPeg } from '../../../test-utils';

mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) });

describe('<LiveDurationDropdown />', () => {
const defaultProps = {
timeout: DEFAULT_DURATION_MS,
onChange: jest.fn(),
};
const getComponent = (props = {}) =>
mount(<LiveDurationDropdown {...defaultProps} {...props} />);

const getOption = (wrapper, timeout) => findById(wrapper, `live-duration__${timeout}`).at(0);
const getSelectedOption = (wrapper) => findById(wrapper, 'live-duration_value');
const openDropdown = (wrapper) => act(() => {
wrapper.find('[role="button"]').at(0).simulate('click');
wrapper.setProps({});
});

it('renders timeout as selected option', () => {
const wrapper = getComponent();
expect(getSelectedOption(wrapper).text()).toEqual('Share for 15m');
});

it('renders non-default timeout as selected option', () => {
const timeout = 1234567;
const wrapper = getComponent({ timeout });
expect(getSelectedOption(wrapper).text()).toEqual(`Share for 21m`);
});

it('renders a dropdown option for a non-default timeout value', () => {
const timeout = 1234567;
const wrapper = getComponent({ timeout });
openDropdown(wrapper);
expect(getOption(wrapper, timeout).text()).toEqual(`Share for 21m`);
});

it('updates value on option selection', () => {
const onChange = jest.fn();
const wrapper = getComponent({ onChange });

const ONE_HOUR = 3600000;

openDropdown(wrapper);

act(() => {
getOption(wrapper, ONE_HOUR).simulate('click');
});

expect(onChange).toHaveBeenCalledWith(ONE_HOUR);
});
});
Loading

0 comments on commit 14653d1

Please sign in to comment.