Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.
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
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