Live location share - focus on user location on list item click (PSG-609) (#9051)
* extract preventDefaultWrapper into utils * add click handling to beacon list item * add click handling to dialog sidebar * focus in on beacons when clicked in list * stylelint * fussy import ordering * test beacon focusing in beaocnviewdialog
This commit is contained in:
parent
38a913488f
commit
dc6ceb1d1c
16 changed files with 473 additions and 89 deletions
|
@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import React, { HTMLProps, useContext } from 'react';
|
||||
import { Beacon, BeaconEvent } from 'matrix-js-sdk/src/matrix';
|
||||
import { LocationAssetType } from 'matrix-js-sdk/src/@types/location';
|
||||
|
||||
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
||||
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
|
||||
import { humanizeTime } from '../../../utils/humanize';
|
||||
import { preventDefaultWrapper } from '../../../utils/NativeEventUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
import BeaconStatus from './BeaconStatus';
|
||||
|
@ -32,7 +33,7 @@ interface Props {
|
|||
beacon: Beacon;
|
||||
}
|
||||
|
||||
const BeaconListItem: React.FC<Props> = ({ beacon }) => {
|
||||
const BeaconListItem: React.FC<Props & HTMLProps<HTMLLIElement>> = ({ beacon, ...rest }) => {
|
||||
const latestLocationState = useEventEmitterState(
|
||||
beacon,
|
||||
BeaconEvent.LocationUpdate,
|
||||
|
@ -52,7 +53,7 @@ const BeaconListItem: React.FC<Props> = ({ beacon }) => {
|
|||
|
||||
const humanizedUpdateTime = humanizeTime(latestLocationState.timestamp);
|
||||
|
||||
return <li className='mx_BeaconListItem'>
|
||||
return <li className='mx_BeaconListItem' {...rest}>
|
||||
{ isSelfLocation ?
|
||||
<MemberAvatar
|
||||
className='mx_BeaconListItem_avatar'
|
||||
|
@ -69,7 +70,11 @@ const BeaconListItem: React.FC<Props> = ({ beacon }) => {
|
|||
label={beaconMember?.name || beacon.beaconInfo.description || beacon.beaconInfoOwner}
|
||||
displayStatus={BeaconDisplayStatus.Active}
|
||||
>
|
||||
<ShareLatestLocation latestLocationState={latestLocationState} />
|
||||
{ /* eat events from interactive share buttons
|
||||
so parent click handlers are not triggered */ }
|
||||
<div className='mx_BeaconListItem_interactions' onClick={preventDefaultWrapper(() => {})}>
|
||||
<ShareLatestLocation latestLocationState={latestLocationState} />
|
||||
</div>
|
||||
</BeaconStatus>
|
||||
<span className='mx_BeaconListItem_lastUpdated'>{ _t("Updated %(humanizedUpdateTime)s", { humanizedUpdateTime }) }</span>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import {
|
||||
Beacon,
|
||||
|
@ -45,7 +45,16 @@ interface IProps extends IDialogProps {
|
|||
roomId: Room['roomId'];
|
||||
matrixClient: MatrixClient;
|
||||
// open the map centered on this beacon's location
|
||||
focusBeacon?: Beacon;
|
||||
initialFocusedBeacon?: Beacon;
|
||||
}
|
||||
|
||||
// track the 'focused time' as ts
|
||||
// to make it possible to refocus the same beacon
|
||||
// as the beacon location may change
|
||||
// or the map may move around
|
||||
interface FocusedBeaconState {
|
||||
ts: number;
|
||||
beacon?: Beacon;
|
||||
}
|
||||
|
||||
const getBoundsCenter = (bounds: Bounds): string | undefined => {
|
||||
|
@ -59,31 +68,52 @@ const getBoundsCenter = (bounds: Bounds): string | undefined => {
|
|||
});
|
||||
};
|
||||
|
||||
const useInitialMapPosition = (liveBeacons: Beacon[], focusBeacon?: Beacon): {
|
||||
const useInitialMapPosition = (liveBeacons: Beacon[], { beacon, ts }: FocusedBeaconState): {
|
||||
bounds?: Bounds; centerGeoUri: string;
|
||||
} => {
|
||||
const bounds = useRef<Bounds | undefined>(getBeaconBounds(liveBeacons));
|
||||
const centerGeoUri = useRef<string>(
|
||||
focusBeacon?.latestLocationState?.uri ||
|
||||
getBoundsCenter(bounds.current),
|
||||
const [bounds, setBounds] = useState<Bounds | undefined>(getBeaconBounds(liveBeacons));
|
||||
const [centerGeoUri, setCenterGeoUri] = useState<string>(
|
||||
beacon?.latestLocationState?.uri ||
|
||||
getBoundsCenter(bounds),
|
||||
);
|
||||
return { bounds: bounds.current, centerGeoUri: centerGeoUri.current };
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
// this check ignores the first initial focused beacon state
|
||||
// as centering logic on map zooms to show everything
|
||||
// instead of focusing down
|
||||
ts !== 0 &&
|
||||
// only set focus to a known location
|
||||
beacon?.latestLocationState?.uri
|
||||
) {
|
||||
// append custom `mxTs` parameter to geoUri
|
||||
// so map is triggered to refocus on this uri
|
||||
// event if it was previously the center geouri
|
||||
// but the map have moved/zoomed
|
||||
setCenterGeoUri(`${beacon?.latestLocationState?.uri};mxTs=${Date.now()}`);
|
||||
setBounds(getBeaconBounds([beacon]));
|
||||
}
|
||||
}, [beacon, ts]);
|
||||
|
||||
return { bounds, centerGeoUri };
|
||||
};
|
||||
|
||||
/**
|
||||
* Dialog to view live beacons maximised
|
||||
*/
|
||||
const BeaconViewDialog: React.FC<IProps> = ({
|
||||
focusBeacon,
|
||||
initialFocusedBeacon,
|
||||
roomId,
|
||||
matrixClient,
|
||||
onFinished,
|
||||
}) => {
|
||||
const liveBeacons = useLiveBeacons(roomId, matrixClient);
|
||||
const [focusedBeaconState, setFocusedBeaconState] =
|
||||
useState<FocusedBeaconState>({ beacon: initialFocusedBeacon, ts: 0 });
|
||||
|
||||
const [isSidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
const { bounds, centerGeoUri } = useInitialMapPosition(liveBeacons, focusBeacon);
|
||||
const { bounds, centerGeoUri } = useInitialMapPosition(liveBeacons, focusedBeaconState);
|
||||
|
||||
const [mapDisplayError, setMapDisplayError] = useState<Error>();
|
||||
|
||||
|
@ -94,6 +124,10 @@ const BeaconViewDialog: React.FC<IProps> = ({
|
|||
}
|
||||
}, [mapDisplayError]);
|
||||
|
||||
const onBeaconListItemClick = (beacon: Beacon) => {
|
||||
setFocusedBeaconState({ beacon, ts: Date.now() });
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_BeaconViewDialog'
|
||||
|
@ -144,7 +178,7 @@ const BeaconViewDialog: React.FC<IProps> = ({
|
|||
</MapFallback>
|
||||
}
|
||||
{ isSidebarOpen ?
|
||||
<DialogSidebar beacons={liveBeacons} requestClose={() => setSidebarOpen(false)} /> :
|
||||
<DialogSidebar beacons={liveBeacons} onBeaconClick={onBeaconListItemClick} requestClose={() => setSidebarOpen(false)} /> :
|
||||
<AccessibleButton
|
||||
kind='primary'
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
|
|
|
@ -26,9 +26,14 @@ import BeaconListItem from './BeaconListItem';
|
|||
interface Props {
|
||||
beacons: Beacon[];
|
||||
requestClose: () => void;
|
||||
onBeaconClick: (beacon: Beacon) => void;
|
||||
}
|
||||
|
||||
const DialogSidebar: React.FC<Props> = ({ beacons, requestClose }) => {
|
||||
const DialogSidebar: React.FC<Props> = ({
|
||||
beacons,
|
||||
onBeaconClick,
|
||||
requestClose,
|
||||
}) => {
|
||||
return <div className='mx_DialogSidebar'>
|
||||
<div className='mx_DialogSidebar_header'>
|
||||
<Heading size='h4'>{ _t('View List') }</Heading>
|
||||
|
@ -36,13 +41,17 @@ const DialogSidebar: React.FC<Props> = ({ beacons, requestClose }) => {
|
|||
className='mx_DialogSidebar_closeButton'
|
||||
onClick={requestClose}
|
||||
title={_t('Close sidebar')}
|
||||
data-test-id='dialog-sidebar-close'
|
||||
data-testid='dialog-sidebar-close'
|
||||
>
|
||||
<CloseIcon className='mx_DialogSidebar_closeButtonIcon' />
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<ol className='mx_DialogSidebar_list'>
|
||||
{ beacons.map((beacon) => <BeaconListItem key={beacon.identifier} beacon={beacon} />) }
|
||||
{ beacons.map((beacon) => <BeaconListItem
|
||||
key={beacon.identifier}
|
||||
beacon={beacon}
|
||||
onClick={() => onBeaconClick(beacon)}
|
||||
/>) }
|
||||
</ol>
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@ import React, { HTMLProps } from 'react';
|
|||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { useOwnLiveBeacons } from '../../../utils/beacon';
|
||||
import { preventDefaultWrapper } from '../../../utils/NativeEventUtils';
|
||||
import BeaconStatus from './BeaconStatus';
|
||||
import { BeaconDisplayStatus } from './displayStatus';
|
||||
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
||||
|
@ -45,14 +46,6 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
|
|||
onResetLocationPublishError,
|
||||
} = useOwnLiveBeacons([beacon?.identifier]);
|
||||
|
||||
// eat events here to avoid 1) the map and 2) reply or thread tiles
|
||||
// moving under the beacon status on stop/retry click
|
||||
const preventDefaultWrapper = (callback: () => void) => (e?: ButtonEvent) => {
|
||||
e?.stopPropagation();
|
||||
e?.preventDefault();
|
||||
callback();
|
||||
};
|
||||
|
||||
// combine display status with errors that only occur for user's own beacons
|
||||
const ownDisplayStatus = hasLocationPublishError || hasStopSharingError ?
|
||||
BeaconDisplayStatus.Error :
|
||||
|
@ -68,7 +61,9 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
|
|||
{ ownDisplayStatus === BeaconDisplayStatus.Active && <AccessibleButton
|
||||
data-test-id='beacon-status-stop-beacon'
|
||||
kind='link'
|
||||
onClick={preventDefaultWrapper(onStopSharing)}
|
||||
// eat events here to avoid 1) the map and 2) reply or thread tiles
|
||||
// moving under the beacon status on stop/retry click
|
||||
onClick={preventDefaultWrapper<ButtonEvent>(onStopSharing)}
|
||||
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
||||
disabled={stoppingInProgress}
|
||||
>
|
||||
|
@ -78,6 +73,8 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
|
|||
{ hasLocationPublishError && <AccessibleButton
|
||||
data-test-id='beacon-status-reset-wire-error'
|
||||
kind='link'
|
||||
// eat events here to avoid 1) the map and 2) reply or thread tiles
|
||||
// moving under the beacon status on stop/retry click
|
||||
onClick={preventDefaultWrapper(onResetLocationPublishError)}
|
||||
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
||||
>
|
||||
|
@ -87,6 +84,8 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
|
|||
{ hasStopSharingError && <AccessibleButton
|
||||
data-test-id='beacon-status-stop-beacon-retry'
|
||||
kind='link'
|
||||
// eat events here to avoid 1) the map and 2) reply or thread tiles
|
||||
// moving under the beacon status on stop/retry click
|
||||
onClick={preventDefaultWrapper(onStopSharing)}
|
||||
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
||||
>
|
||||
|
|
|
@ -80,6 +80,13 @@ const useMapWithStyle = ({ id, centerGeoUri, onError, interactive, bounds }) =>
|
|||
interface MapProps {
|
||||
id: string;
|
||||
interactive?: boolean;
|
||||
/**
|
||||
* set map center to geoUri coords
|
||||
* Center will only be set to valid geoUri
|
||||
* this prop is only simply diffed by useEffect, so to trigger *recentering* of the same geoUri
|
||||
* append the uri with a var not used by the geoUri spec
|
||||
* eg a timestamp: `geo:54,42;mxTs=123`
|
||||
*/
|
||||
centerGeoUri?: string;
|
||||
bounds?: Bounds;
|
||||
className?: string;
|
||||
|
|
|
@ -162,7 +162,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
|
|||
{
|
||||
roomId: mxEvent.getRoomId(),
|
||||
matrixClient,
|
||||
focusBeacon: beacon,
|
||||
initialFocusedBeacon: beacon,
|
||||
isMapDisplayError,
|
||||
},
|
||||
"mx_BeaconViewDialog_wrapper",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue