Live location sharing - extract location markers into generic Marker (#8225)
* extract location markers into generic Marker Signed-off-by: Kerry Archibald <kerrya@element.io> * comments Signed-off-by: Kerry Archibald <kerrya@element.io> * remove skinned Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
parent
b9da2255c4
commit
b98739056e
10 changed files with 188 additions and 145 deletions
|
@ -10,6 +10,7 @@
|
||||||
@import "./components/views/location/_LiveDurationDropdown.scss";
|
@import "./components/views/location/_LiveDurationDropdown.scss";
|
||||||
@import "./components/views/location/_LocationShareMenu.scss";
|
@import "./components/views/location/_LocationShareMenu.scss";
|
||||||
@import "./components/views/location/_MapError.scss";
|
@import "./components/views/location/_MapError.scss";
|
||||||
|
@import "./components/views/location/_Marker.scss";
|
||||||
@import "./components/views/location/_ShareDialogButtons.scss";
|
@import "./components/views/location/_ShareDialogButtons.scss";
|
||||||
@import "./components/views/location/_ShareType.scss";
|
@import "./components/views/location/_ShareType.scss";
|
||||||
@import "./components/views/spaces/_QuickThemeSwitcher.scss";
|
@import "./components/views/spaces/_QuickThemeSwitcher.scss";
|
||||||
|
|
46
res/css/components/views/location/_Marker.scss
Normal file
46
res/css/components/views/location/_Marker.scss
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
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_Marker_defaultColor {
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Marker_border {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: drop-shadow(0px 3px 5px rgba(0, 0, 0, 0.2));
|
||||||
|
background-color: currentColor;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
// caret down
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
border-left: 5px solid transparent;
|
||||||
|
border-right: 5px solid transparent;
|
||||||
|
border-top: 5px solid currentColor;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Marker_icon {
|
||||||
|
color: white;
|
||||||
|
height: 20px;
|
||||||
|
}
|
|
@ -55,39 +55,6 @@ limitations under the License.
|
||||||
.maplibregl-user-location-dot {
|
.maplibregl-user-location-dot {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MLocationBody_markerBorder {
|
|
||||||
width: 31px;
|
|
||||||
height: 31px;
|
|
||||||
border-radius: 50%;
|
|
||||||
filter: drop-shadow(0px 3px 5px rgba(0, 0, 0, 0.2));
|
|
||||||
background-color: currentColor;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MLocationBody_pointer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: -3px;
|
|
||||||
left: 11px;
|
|
||||||
width: 9px;
|
|
||||||
height: 5px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
mask-image: url('$(res)/img/location/pointer.svg');
|
|
||||||
mask-position: center;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: 9px;
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
width: 9px;
|
|
||||||
height: 5px;
|
|
||||||
position: absolute;
|
|
||||||
background-color: currentColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LocationPicker_footer {
|
.mx_LocationPicker_footer {
|
||||||
|
@ -106,11 +73,6 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MLocationBody_markerIcon {
|
|
||||||
color: white;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_LocationPicker_pinText {
|
.mx_LocationPicker_pinText {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: $spacing-16;
|
top: $spacing-16;
|
||||||
|
@ -135,11 +97,3 @@ limitations under the License.
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// live marker color is set by user color class
|
|
||||||
// generated from userid
|
|
||||||
// others are $accent
|
|
||||||
.mx_MLocationBody_marker-Self,
|
|
||||||
.mx_MLocationBody_marker-Pin {
|
|
||||||
color: $accent;
|
|
||||||
}
|
|
||||||
|
|
|
@ -22,56 +22,6 @@ limitations under the License.
|
||||||
|
|
||||||
border-radius: $timeline-image-border-radius;
|
border-radius: $timeline-image-border-radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MLocationBody_markerBorder {
|
|
||||||
width: 31px;
|
|
||||||
height: 31px;
|
|
||||||
border-radius: 50%;
|
|
||||||
filter: drop-shadow(0px 3px 5px rgba(0, 0, 0, 0.2));
|
|
||||||
background-color: $accent;
|
|
||||||
|
|
||||||
// See _LocationPicker.scss
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.mx_BaseAvatar {
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MLocationBody_pointer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: -3px;
|
|
||||||
left: 11px;
|
|
||||||
width: 9px;
|
|
||||||
height: 5px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
mask-image: url('$(res)/img/location/pointer.svg');
|
|
||||||
mask-position: center;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: 9px;
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
width: 9px;
|
|
||||||
height: 5px;
|
|
||||||
position: absolute;
|
|
||||||
background-color: $accent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MLocationBody_markerContents {
|
|
||||||
background-color: $location-marker-color;
|
|
||||||
margin: 0;
|
|
||||||
width: 31px;
|
|
||||||
height: 31px;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: 16px;
|
|
||||||
mask-position: center;
|
|
||||||
mask-image: url('$(res)/img/element-icons/location.svg');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* In the timeline, we fit the width of the container */
|
/* In the timeline, we fit the width of the container */
|
||||||
|
|
|
@ -19,23 +19,20 @@ import maplibregl, { MapMouseEvent } from 'maplibre-gl';
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||||
import { ClientEvent, IClientWellKnown } from 'matrix-js-sdk/src/client';
|
import { ClientEvent, IClientWellKnown } from 'matrix-js-sdk/src/client';
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import { Icon as LocationIcon } from '../../../../res/img/element-icons/location.svg';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import { tileServerFromWellKnown } from '../../../utils/WellKnownUtils';
|
import { tileServerFromWellKnown } from '../../../utils/WellKnownUtils';
|
||||||
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
|
|
||||||
import { GenericPosition, genericPositionFromGeolocation, getGeoUri } from '../../../utils/beacon';
|
import { GenericPosition, genericPositionFromGeolocation, getGeoUri } from '../../../utils/beacon';
|
||||||
import { LocationShareError, findMapStyleUrl } from '../../../utils/location';
|
import { LocationShareError, findMapStyleUrl } from '../../../utils/location';
|
||||||
import MemberAvatar from '../avatars/MemberAvatar';
|
|
||||||
import ErrorDialog from '../dialogs/ErrorDialog';
|
import ErrorDialog from '../dialogs/ErrorDialog';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import { MapError } from './MapError';
|
import { MapError } from './MapError';
|
||||||
import LiveDurationDropdown, { DEFAULT_DURATION_MS } from './LiveDurationDropdown';
|
import LiveDurationDropdown, { DEFAULT_DURATION_MS } from './LiveDurationDropdown';
|
||||||
import { LocationShareType, ShareLocationFn } from './shareLocation';
|
import { LocationShareType, ShareLocationFn } from './shareLocation';
|
||||||
|
import Marker from './Marker';
|
||||||
|
|
||||||
export interface ILocationPickerProps {
|
export interface ILocationPickerProps {
|
||||||
sender: RoomMember;
|
sender: RoomMember;
|
||||||
|
@ -225,8 +222,6 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userColorClass = getUserNameColorClass(this.props.sender.userId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_LocationPicker">
|
<div className="mx_LocationPicker">
|
||||||
<div id="mx_LocationPicker_map" />
|
<div id="mx_LocationPicker_map" />
|
||||||
|
@ -256,13 +251,7 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div className={classNames(
|
<div id={this.getMarkerId()}>
|
||||||
"mx_MLocationBody_marker",
|
|
||||||
`mx_MLocationBody_marker-${this.props.shareType}`,
|
|
||||||
userColorClass,
|
|
||||||
)}
|
|
||||||
id={this.getMarkerId()}
|
|
||||||
>
|
|
||||||
{ /*
|
{ /*
|
||||||
maplibregl hijacks the div above to style the marker
|
maplibregl hijacks the div above to style the marker
|
||||||
it must be in the dom when the map is initialised
|
it must be in the dom when the map is initialised
|
||||||
|
@ -271,23 +260,12 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
|
||||||
so hide the internal visible elements
|
so hide the internal visible elements
|
||||||
*/ }
|
*/ }
|
||||||
|
|
||||||
{ !!this.marker && <>
|
{ !!this.marker && <Marker
|
||||||
<div className="mx_MLocationBody_markerBorder">
|
roomMember={isSharingOwnLocation(this.props.shareType) ? this.props.sender : undefined}
|
||||||
{ isSharingOwnLocation(this.props.shareType) ?
|
useMemberColor={this.props.shareType === LocationShareType.Live}
|
||||||
<MemberAvatar
|
|
||||||
member={this.props.sender}
|
|
||||||
width={27}
|
|
||||||
height={27}
|
|
||||||
viewUserOnClick={false}
|
|
||||||
/>
|
/>
|
||||||
: <LocationIcon className="mx_MLocationBody_markerIcon" />
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className="mx_MLocationBody_pointer"
|
|
||||||
/>
|
|
||||||
</> }
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
58
src/components/views/location/Marker.tsx
Normal file
58
src/components/views/location/Marker.tsx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
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 classNames from 'classnames';
|
||||||
|
import { RoomMember } from 'matrix-js-sdk/src/matrix';
|
||||||
|
|
||||||
|
import { Icon as LocationIcon } from '../../../../res/img/element-icons/location.svg';
|
||||||
|
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
|
||||||
|
import MemberAvatar from '../avatars/MemberAvatar';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id?: string;
|
||||||
|
// renders MemberAvatar when provided
|
||||||
|
roomMember?: RoomMember;
|
||||||
|
// use member text color as background
|
||||||
|
useMemberColor?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic location marker
|
||||||
|
*/
|
||||||
|
const Marker: React.FC<Props> = ({ id, roomMember, useMemberColor }) => {
|
||||||
|
const memberColorClass = useMemberColor && roomMember ? getUserNameColorClass(roomMember.userId) : '';
|
||||||
|
return <div
|
||||||
|
id={id}
|
||||||
|
className={classNames("mx_Marker", memberColorClass, {
|
||||||
|
"mx_Marker_defaultColor": !memberColorClass,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="mx_Marker_border">
|
||||||
|
{ roomMember ?
|
||||||
|
<MemberAvatar
|
||||||
|
member={roomMember}
|
||||||
|
width={36}
|
||||||
|
height={36}
|
||||||
|
viewUserOnClick={false}
|
||||||
|
/>
|
||||||
|
: <LocationIcon className="mx_Marker_icon" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Marker;
|
|
@ -26,7 +26,6 @@ import { ClientEvent, IClientWellKnown } from 'matrix-js-sdk/src/client';
|
||||||
|
|
||||||
import { IBodyProps } from "./IBodyProps";
|
import { IBodyProps } from "./IBodyProps";
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import MemberAvatar from '../avatars/MemberAvatar';
|
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import {
|
import {
|
||||||
parseGeoUri,
|
parseGeoUri,
|
||||||
|
@ -41,6 +40,7 @@ import { Alignment } from '../elements/Tooltip';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import { tileServerFromWellKnown } from '../../../utils/WellKnownUtils';
|
import { tileServerFromWellKnown } from '../../../utils/WellKnownUtils';
|
||||||
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
||||||
|
import Marker from '../location/Marker';
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
error: Error;
|
error: Error;
|
||||||
|
@ -175,16 +175,8 @@ export function LocationBodyContent(props: ILocationBodyContentProps):
|
||||||
className="mx_MLocationBody_map"
|
className="mx_MLocationBody_map"
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
const markerContents = (
|
// only pass member to marker when should render avatar marker
|
||||||
isSelfLocation(props.mxEvent.getContent())
|
const markerRoomMember = isSelfLocation(props.mxEvent.getContent()) ? props.mxEvent.sender : undefined;
|
||||||
? <MemberAvatar
|
|
||||||
member={props.mxEvent.sender}
|
|
||||||
width={27}
|
|
||||||
height={27}
|
|
||||||
viewUserOnClick={false}
|
|
||||||
/>
|
|
||||||
: <div className="mx_MLocationBody_markerContents" />
|
|
||||||
);
|
|
||||||
|
|
||||||
return <div className="mx_MLocationBody">
|
return <div className="mx_MLocationBody">
|
||||||
{
|
{
|
||||||
|
@ -198,14 +190,7 @@ export function LocationBodyContent(props: ILocationBodyContentProps):
|
||||||
</TooltipTarget>
|
</TooltipTarget>
|
||||||
: mapDiv
|
: mapDiv
|
||||||
}
|
}
|
||||||
<div className="mx_MLocationBody_marker" id={props.markerId}>
|
<Marker id={props.markerId} roomMember={markerRoomMember} />
|
||||||
<div className="mx_MLocationBody_markerBorder">
|
|
||||||
{ markerContents }
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="mx_MLocationBody_pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{
|
{
|
||||||
props.zoomButtons
|
props.zoomButtons
|
||||||
? <ZoomButtons
|
? <ZoomButtons
|
||||||
|
|
|
@ -301,7 +301,7 @@ describe("LocationPicker", () => {
|
||||||
));
|
));
|
||||||
|
|
||||||
// marker is set, icon not avatar
|
// marker is set, icon not avatar
|
||||||
expect(wrapper.find('.mx_MLocationBody_markerIcon').length).toBeTruthy();
|
expect(wrapper.find('.mx_Marker_icon').length).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('submits location', () => {
|
it('submits location', () => {
|
||||||
|
|
51
test/components/views/location/Marker-test.tsx
Normal file
51
test/components/views/location/Marker-test.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
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 { RoomMember } from 'matrix-js-sdk/src/matrix';
|
||||||
|
|
||||||
|
import Marker from '../../../../src/components/views/location/Marker';
|
||||||
|
|
||||||
|
describe('<Marker />', () => {
|
||||||
|
const defaultProps = {
|
||||||
|
id: 'abc123',
|
||||||
|
};
|
||||||
|
const getComponent = (props = {}) =>
|
||||||
|
mount(<Marker {...defaultProps} {...props} />);
|
||||||
|
|
||||||
|
it('renders with location icon when no room member', () => {
|
||||||
|
const component = getComponent();
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not try to use member color without room member', () => {
|
||||||
|
const component = getComponent({ useMemberColor: true });
|
||||||
|
expect(component.find('div').at(0).props().className).toEqual('mx_Marker mx_Marker_defaultColor');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses member color class', () => {
|
||||||
|
const member = new RoomMember('!room:server', '@user:server');
|
||||||
|
const component = getComponent({ useMemberColor: true, roomMember: member });
|
||||||
|
expect(component.find('div').at(0).props().className).toEqual('mx_Marker mx_Username_color3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders member avatar when roomMember is truthy', () => {
|
||||||
|
const member = new RoomMember('!room:server', '@user:server');
|
||||||
|
const component = getComponent({ roomMember: member });
|
||||||
|
expect(component.find('MemberAvatar').length).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,20 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<Marker /> renders with location icon when no room member 1`] = `
|
||||||
|
<Marker
|
||||||
|
id="abc123"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mx_Marker mx_Marker_defaultColor"
|
||||||
|
id="abc123"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mx_Marker_border"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mx_Marker_icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Marker>
|
||||||
|
`;
|
Loading…
Add table
Add a link
Reference in a new issue