Live location share - tiles without tile server (PSG-591) (#8962)

* live location without map POC

* styles

* force map tiles to show no map for test build

* check latestlocationstate exists

* just use loading style map fallback when cant display map

* style map error for tile view

* set pointer cursor when map error is clickable

* test mbeaconbody with map display error, lint

* lint more good

* remove changes for first attempt tile

* make maperror test id more accurate

* fussy import ordering

* PR tweaks
pull/28788/head^2
Kerry 2022-07-06 16:34:33 +02:00 committed by GitHub
parent e65409861a
commit 60faf6d025
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 430 additions and 211 deletions

View File

@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_OwnBeaconStatus_button {
margin-left: $spacing-8;
}
.mx_EventTile[data-layout="bubble"] .mx_OwnBeaconStatus_button {
// align to top to make room for timestamp
// in bubble view

View File

@ -18,9 +18,41 @@ limitations under the License.
padding: 100px $spacing-32 0;
text-align: center;
p {
margin: $spacing-16 0 $spacing-32;
--mx-map-error-icon-color: $secondary-content;
--mx-map-error-icon-size: 58px;
}
.mx_MapError.mx_MapError_isMinimised {
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: $spacing-24;
background-color: $panels;
font-size: $font-12px;
line-height: $font-16px;
--mx-map-error-icon-color: $alert;
--mx-map-error-icon-size: 26px;
.mx_MapError_message {
margin: 0;
max-width: 275px;
}
.mx_MapError_heading {
padding-top: $spacing-8;
// override h3 heading size
font-size: inherit !important;
font-weight: normal !important;
}
}
.mx_MapError_message {
margin: $spacing-16 0 $spacing-32;
}
.mx_MapError_heading {
@ -28,9 +60,9 @@ limitations under the License.
}
.mx_MapError_icon {
height: 58px;
height: var(--mx-map-error-icon-size);
path {
fill: $secondary-content;
fill: var(--mx-map-error-icon-color);
}
}

View File

@ -24,6 +24,29 @@ limitations under the License.
overflow: hidden;
}
.mx_MBeaconBody.mx_MBeaconBody_withoutMap {
height: auto;
.mx_MBeaconBody_chin {
position: relative;
background-color: transparent;
}
}
.mx_MBeaconBody_withoutMapContent {
background-color: $panels;
border-radius: 4px;
}
.mx_MBeaconBody_withoutMapInfoLastUpdated {
// 48px lines up with icon in BeaconStatus
margin-top: -$spacing-8;
padding: 0 $spacing-8 $spacing-8 48px;
color: $tertiary-content;
font-size: $font-10px;
}
.mx_MBeaconBody_map {
height: 100%;
width: 100%;
@ -32,11 +55,18 @@ limitations under the License.
cursor: pointer;
}
.mx_MBeaconBody_mapFallback {
.mx_MBeaconBody_mapFallback,
.mx_MBeaconBody_mapError {
// pushes spinner/icon up
// to appear more centered with the footer
padding-bottom: 50px;
padding-bottom: 50px !important;
}
.mx_MBeaconBody_mapErrorInteractive {
cursor: pointer;
}
.mx_MBeaconBody_mapFallback {
cursor: default;
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useState, useRef } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import {
Beacon,
@ -38,6 +38,8 @@ import DialogSidebar from './DialogSidebar';
import DialogOwnBeaconStatus from './DialogOwnBeaconStatus';
import BeaconStatusTooltip from './BeaconStatusTooltip';
import MapFallback from '../location/MapFallback';
import { MapError } from '../location/MapError';
import { LocationShareError } from '../../../utils/location';
interface IProps extends IDialogProps {
roomId: Room['roomId'];
@ -83,6 +85,15 @@ const BeaconViewDialog: React.FC<IProps> = ({
const { bounds, centerGeoUri } = useInitialMapPosition(liveBeacons, focusBeacon);
const [mapDisplayError, setMapDisplayError] = useState<Error>();
// automatically open the sidebar if there is no map to see
useEffect(() => {
if (mapDisplayError) {
setSidebarOpen(true);
}
}, [mapDisplayError]);
return (
<BaseDialog
className='mx_BeaconViewDialog'
@ -90,11 +101,12 @@ const BeaconViewDialog: React.FC<IProps> = ({
fixedWidth={false}
>
<MatrixClientContext.Provider value={matrixClient}>
{ !!liveBeacons?.length ? <Map
{ (!!liveBeacons?.length && !mapDisplayError) && <Map
id='mx_BeaconViewDialog'
bounds={bounds}
centerGeoUri={centerGeoUri}
interactive
onError={setMapDisplayError}
className="mx_BeaconViewDialog_map"
>
{
@ -109,7 +121,14 @@ const BeaconViewDialog: React.FC<IProps> = ({
<ZoomButtons map={map} />
</>
}
</Map> :
</Map> }
{ mapDisplayError &&
<MapError
error={mapDisplayError.message as LocationShareError}
isMinimised
/>
}
{ !liveBeacons?.length && !mapDisplayError &&
<MapFallback
data-test-id='beacon-view-dialog-map-fallback'
className='mx_BeaconViewDialog_map'

View File

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import { Icon as WarningBadge } from '../../../../res/img/element-icons/warning-badge.svg';
import { _t } from '../../../languageHandler';
@ -22,18 +23,38 @@ import { getLocationShareErrorMessage, LocationShareError } from '../../../utils
import AccessibleButton from '../elements/AccessibleButton';
import Heading from '../typography/Heading';
interface Props {
onFinished: () => void;
export interface MapErrorProps {
error: LocationShareError;
onFinished?: () => void;
isMinimised?: boolean;
className?: string;
onClick?: () => void;
}
export const MapError: React.FC<Props> = ({
onFinished, error,
}) => (<div data-test-id='location-picker-error' className="mx_MapError">
<WarningBadge className="mx_MapError_icon" />
<Heading className="mx_MapError_heading" size='h3'>{ _t("Unable to load map") }</Heading>
<p>
export const MapError: React.FC<MapErrorProps> = ({
error,
isMinimised,
className,
onFinished,
onClick,
}) => (
<div data-test-id='map-rendering-error'
className={classNames('mx_MapError', className, { 'mx_MapError_isMinimised': isMinimised })}
onClick={onClick}
>
<WarningBadge className='mx_MapError_icon' />
<Heading className='mx_MapError_heading' size='h3'>{ _t('Unable to load map') }</Heading>
<p className='mx_MapError_message'>
{ getLocationShareErrorMessage(error) }
</p>
<AccessibleButton element='button' kind="primary" onClick={onFinished}>{ _t("OK") }</AccessibleButton>
</div>);
{ onFinished &&
<AccessibleButton
element='button'
kind='primary'
onClick={onFinished}
>
{ _t('OK') }
</AccessibleButton>
}
</div>
);

View File

@ -30,7 +30,6 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
const MapFallback: React.FC<Props> = ({ className, isLoading, children, ...rest }) => {
return <div className={classNames('mx_MapFallback', className)} {...rest}>
<MapFallbackImage className='mx_MapFallback_bg' />
{ /* <div className='mx_MapFallback_bg'/> */ }
{ isLoading ? <Spinner h={32} w={32} /> : <LocationMarkerIcon className='mx_MapFallback_icon' /> }
{ children }
</div>;

View File

@ -26,17 +26,19 @@ import {
import { BeaconLocationState } from 'matrix-js-sdk/src/content-helpers';
import { randomString } from 'matrix-js-sdk/src/randomstring';
import { M_BEACON } from 'matrix-js-sdk/src/@types/beacon';
import classNames from 'classnames';
import MatrixClientContext from '../../../contexts/MatrixClientContext';
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { isBeaconWaitingToStart, useBeacon } from '../../../utils/beacon';
import { isSelfLocation } from '../../../utils/location';
import { isSelfLocation, LocationShareError } from '../../../utils/location';
import { BeaconDisplayStatus, getBeaconDisplayStatus } from '../beacon/displayStatus';
import BeaconStatus from '../beacon/BeaconStatus';
import OwnBeaconStatus from '../beacon/OwnBeaconStatus';
import Map from '../location/Map';
import { MapError } from '../location/MapError';
import MapFallback from '../location/MapFallback';
import SmartMarker from '../location/SmartMarker';
import { GetRelationsForEvent } from '../rooms/EventTile';
@ -136,7 +138,16 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
const matrixClient = useContext(MatrixClientContext);
const [error, setError] = useState<Error>();
const displayStatus = getBeaconDisplayStatus(isLive, latestLocationState, error, waitingToStart);
const isMapDisplayError = error?.message === LocationShareError.MapStyleUrlNotConfigured ||
error?.message === LocationShareError.MapStyleUrlNotReachable;
const displayStatus = getBeaconDisplayStatus(
isLive,
latestLocationState,
// if we are unable to display maps because it is not configured for the server
// don't display an error
isMapDisplayError ? undefined : error,
waitingToStart,
);
const markerRoomMember = isSelfLocation(mxEvent.getContent()) ? mxEvent.sender : undefined;
const isOwnBeacon = beacon?.beaconInfoOwner === matrixClient.getUserId();
@ -152,6 +163,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
roomId: mxEvent.getRoomId(),
matrixClient,
focusBeacon: beacon,
isMapDisplayError,
},
"mx_BeaconViewDialog_wrapper",
false, // isPriority
@ -160,8 +172,11 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
};
return (
<div className='mx_MBeaconBody' ref={ref}>
{ displayStatus === BeaconDisplayStatus.Active ?
<div
className='mx_MBeaconBody'
ref={ref}
>
{ (displayStatus === BeaconDisplayStatus.Active && !isMapDisplayError) ?
<Map
id={mapId}
centerGeoUri={latestLocationState.uri}
@ -180,7 +195,20 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
/>
}
</Map>
: <MapFallback
: isMapDisplayError ?
<MapError
error={error.message as LocationShareError}
onClick={onClick}
className={classNames(
'mx_MBeaconBody_mapError',
// set interactive class when maximised map can be opened
{ 'mx_MBeaconBody_mapErrorInteractive':
displayStatus === BeaconDisplayStatus.Active,
},
)}
isMinimised
/> :
<MapFallback
isLoading={displayStatus === BeaconDisplayStatus.Loading}
className='mx_MBeaconBody_map mx_MBeaconBody_mapFallback'
/>

View File

@ -99,7 +99,7 @@ describe("LocationPicker", () => {
wrapper.setProps({});
});
expect(findByTestId(wrapper, 'location-picker-error').find('p').text()).toEqual(
expect(findByTestId(wrapper, 'map-rendering-error').find('p').text()).toEqual(
"This homeserver is not configured correctly to display maps, "
+ "or the configured map server may be unreachable.",
);
@ -115,7 +115,7 @@ describe("LocationPicker", () => {
const wrapper = getComponent();
wrapper.setProps({});
expect(findByTestId(wrapper, 'location-picker-error').find('p').text()).toEqual(
expect(findByTestId(wrapper, 'map-rendering-error').find('p').text()).toEqual(
"This homeserver is not configured to display maps.",
);
});
@ -130,7 +130,7 @@ describe("LocationPicker", () => {
const wrapper = getComponent();
wrapper.setProps({});
expect(findByTestId(wrapper, 'location-picker-error').find('p').text()).toEqual(
expect(findByTestId(wrapper, 'map-rendering-error').find('p').text()).toEqual(
"This homeserver is not configured correctly to display maps, "
+ "or the configured map server may be unreachable.",
);

View File

@ -15,28 +15,45 @@ limitations under the License.
*/
import React from 'react';
import { mount } from 'enzyme';
import { render, RenderResult } from '@testing-library/react';
import { MapError } from '../../../../src/components/views/location/MapError';
import { MapError, MapErrorProps } from '../../../../src/components/views/location/MapError';
import { LocationShareError } from '../../../../src/utils/location';
describe('<MapError />', () => {
const defaultProps = {
onFinished: jest.fn(),
error: LocationShareError.MapStyleUrlNotConfigured,
className: 'test',
};
const getComponent = (props = {}) =>
mount(<MapError {...defaultProps} {...props} />);
const getComponent = (props: Partial<MapErrorProps> = {}): RenderResult =>
render(<MapError {...defaultProps} {...props} />);
it('renders correctly for MapStyleUrlNotConfigured', () => {
const component = getComponent();
expect(component).toMatchSnapshot();
const { container } = getComponent();
expect(container).toMatchSnapshot();
});
it('renders correctly for MapStyleUrlNotReachable', () => {
const component = getComponent({
const { container } = getComponent({
error: LocationShareError.MapStyleUrlNotReachable,
});
expect(component).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
it('does not render button when onFinished falsy', () => {
const { queryByText } = getComponent({
error: LocationShareError.MapStyleUrlNotReachable,
onFinished: undefined,
});
// no button
expect(queryByText('OK')).toBeFalsy();
});
it('applies class when isMinimised is truthy', () => {
const { container } = getComponent({
isMinimised: true,
});
expect(container).toMatchSnapshot();
});
});

View File

@ -1,95 +1,91 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<MapError /> renders correctly for MapStyleUrlNotConfigured 1`] = `
<MapError
error="MapStyleUrlNotConfigured"
onFinished={[MockFunction]}
exports[`<MapError /> applies class when isMinimised is truthy 1`] = `
<div>
<div
class="mx_MapError test mx_MapError_isMinimised"
data-test-id="map-rendering-error"
>
<div
className="mx_MapError"
data-test-id="location-picker-error"
>
<div
className="mx_MapError_icon"
class="mx_MapError_icon"
/>
<Heading
className="mx_MapError_heading"
size="h3"
>
<h3
className="mx_Heading_h3 mx_MapError_heading"
class="mx_Heading_h3 mx_MapError_heading"
>
Unable to load map
</h3>
</Heading>
<p>
<p
class="mx_MapError_message"
>
This homeserver is not configured to display maps.
</p>
<AccessibleButton
element="button"
kind="primary"
onClick={[MockFunction]}
role="button"
tabIndex={0}
>
<button
className="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
onClick={[MockFunction]}
onKeyDown={[Function]}
onKeyUp={[Function]}
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabIndex={0}
tabindex="0"
>
OK
</button>
</AccessibleButton>
</div>
</MapError>
</div>
`;
exports[`<MapError /> renders correctly for MapStyleUrlNotConfigured 1`] = `
<div>
<div
class="mx_MapError test"
data-test-id="map-rendering-error"
>
<div
class="mx_MapError_icon"
/>
<h3
class="mx_Heading_h3 mx_MapError_heading"
>
Unable to load map
</h3>
<p
class="mx_MapError_message"
>
This homeserver is not configured to display maps.
</p>
<button
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
OK
</button>
</div>
</div>
`;
exports[`<MapError /> renders correctly for MapStyleUrlNotReachable 1`] = `
<MapError
error="MapStyleUrlNotReachable"
onFinished={[MockFunction]}
<div>
<div
class="mx_MapError test"
data-test-id="map-rendering-error"
>
<div
className="mx_MapError"
data-test-id="location-picker-error"
>
<div
className="mx_MapError_icon"
class="mx_MapError_icon"
/>
<Heading
className="mx_MapError_heading"
size="h3"
>
<h3
className="mx_Heading_h3 mx_MapError_heading"
class="mx_Heading_h3 mx_MapError_heading"
>
Unable to load map
</h3>
</Heading>
<p>
<p
class="mx_MapError_message"
>
This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.
</p>
<AccessibleButton
element="button"
kind="primary"
onClick={[MockFunction]}
role="button"
tabIndex={0}
>
<button
className="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
onClick={[MockFunction]}
onKeyDown={[Function]}
onKeyUp={[Function]}
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabIndex={0}
tabindex="0"
>
OK
</button>
</AccessibleButton>
</div>
</MapError>
</div>
`;

View File

@ -33,6 +33,7 @@ import {
getMockClientWithEventEmitter,
makeBeaconEvent,
makeBeaconInfoEvent,
makeRoomWithBeacons,
makeRoomWithStateEvents,
} from '../../../test-utils';
import { RoomPermalinkCreator } from '../../../../src/utils/permalinks/Permalinks';
@ -40,6 +41,9 @@ import { MediaEventHelper } from '../../../../src/utils/MediaEventHelper';
import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
import Modal from '../../../../src/Modal';
import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils';
import { MapError } from '../../../../src/components/views/location/MapError';
import * as mapUtilHooks from '../../../../src/utils/location/useMap';
import { LocationShareError } from '../../../../src/utils/location';
describe('<MBeaconBody />', () => {
// 14.03.2022 16:15
@ -94,6 +98,7 @@ describe('<MBeaconBody />', () => {
jest.clearAllMocks();
});
const testBeaconStatuses = () => {
it('renders stopped beacon UI for an explicitly stopped beacon', () => {
const beaconInfoEvent = makeBeaconInfoEvent(aliceId,
roomId,
@ -200,6 +205,9 @@ describe('<MBeaconBody />', () => {
// beacon1 has been superceded by beacon2
expect(component.text()).toEqual("Live location ended");
});
};
testBeaconStatuses();
describe('on liveness change', () => {
it('renders stopped UI when a beacon stops being live', () => {
@ -458,4 +466,34 @@ describe('<MBeaconBody />', () => {
);
});
});
describe('when map display is not configured', () => {
beforeEach(() => {
// mock map utils to raise MapStyleUrlNotConfigured error
jest.spyOn(mapUtilHooks, 'useMap').mockImplementation(
({ onError }) => {
onError(new Error(LocationShareError.MapStyleUrlNotConfigured));
return mockMap;
});
});
it('renders maps unavailable error for a live beacon with location', () => {
const beaconInfoEvent = makeBeaconInfoEvent(aliceId,
roomId,
{ isLive: true },
'$alice-room1-1',
);
const location1 = makeBeaconEvent(
aliceId, { beaconInfoId: beaconInfoEvent.getId(), geoUri: 'geo:51,41', timestamp: now + 1 },
);
makeRoomWithBeacons(roomId, mockClient, [beaconInfoEvent], [location1]);
const component = getComponent({ mxEvent: beaconInfoEvent });
expect(component.find(MapError)).toMatchSnapshot();
});
// test that statuses display as expected with a map display error
testBeaconStatuses();
});
});

View File

@ -0,0 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<MBeaconBody /> when map display is not configured renders maps unavailable error for a live beacon with location 1`] = `
<MapError
className="mx_MBeaconBody_mapError mx_MBeaconBody_mapErrorInteractive"
error="MapStyleUrlNotConfigured"
isMinimised={true}
onClick={[Function]}
>
<div
className="mx_MapError mx_MBeaconBody_mapError mx_MBeaconBody_mapErrorInteractive mx_MapError_isMinimised"
data-test-id="map-rendering-error"
onClick={[Function]}
>
<div
className="mx_MapError_icon"
/>
<Heading
className="mx_MapError_heading"
size="h3"
>
<h3
className="mx_Heading_h3 mx_MapError_heading"
>
Unable to load map
</h3>
</Heading>
<p
className="mx_MapError_message"
>
This homeserver is not configured to display maps.
</p>
</div>
</MapError>
`;