Skip to content

Commit

Permalink
Live location sharing: beacon list view tiles (#8363)
Browse files Browse the repository at this point in the history
* add basic sidebar container

Signed-off-by: Kerry Archibald <kerrya@element.io>

* optionally show icon in beaconstatus

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add avatar and style list item

Signed-off-by: Kerry Archibald <kerrya@element.io>

* formatted last update time

Signed-off-by: Kerry Archibald <kerrya@element.io>

* test beacon list item

Signed-off-by: Kerry Archibald <kerrya@element.io>

* move makeRoomWithState events to test utils

Signed-off-by: Kerry Archibald <kerrya@element.io>

* move beacon test helpers into utils

Signed-off-by: Kerry Archibald <kerrya@element.io>

* newline

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add copyable text to beacon list item

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add copyable geo uri to list item

Signed-off-by: Kerry Archibald <kerrya@element.io>

* improve spacing

Signed-off-by: Kerry Archibald <kerrya@element.io>

* overflow scroll on list

Signed-off-by: Kerry Archibald <kerrya@element.io>
  • Loading branch information
Kerry committed Apr 20, 2022
1 parent 2f6b767 commit 4a38cbd
Show file tree
Hide file tree
Showing 18 changed files with 355 additions and 10 deletions.
1 change: 1 addition & 0 deletions res/css/_components.scss
Expand Up @@ -4,6 +4,7 @@
@import "./_font-sizes.scss";
@import "./_font-weights.scss";
@import "./_spacing.scss";
@import "./components/views/beacon/_BeaconListItem.scss";
@import "./components/views/beacon/_BeaconStatus.scss";
@import "./components/views/beacon/_BeaconViewDialog.scss";
@import "./components/views/beacon/_DialogSidebar.scss";
Expand Down
61 changes: 61 additions & 0 deletions res/css/components/views/beacon/_BeaconListItem.scss
@@ -0,0 +1,61 @@
/*
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_BeaconListItem {
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: flex-start;
padding: $spacing-12 0;

border-bottom: 1px solid $system;
}

.mx_BeaconListItem_avatarIcon {
flex: 0 0;
height: 32px;
width: 32px;
}

.mx_BeaconListItem_avatar {
flex: 0 0;
box-sizing: border-box;

margin-right: $spacing-8;
border: 2px solid $location-live-color;
}

.mx_BeaconListItem_info {
flex: 1 1 0;
display: flex;
flex-direction: column;
align-items: stretch;
}

.mx_BeaconListItem_status {
// override beacon status padding
padding: 0 !important;
margin-bottom: $spacing-8;

.mx_BeaconStatus_label {
font-weight: $font-semi-bold;
}
}

.mx_BeaconListItem_lastUpdated {
color: $tertiary-content;
font-size: $font-10px;
}
4 changes: 4 additions & 0 deletions res/css/components/views/beacon/_BeaconStatus.scss
Expand Up @@ -59,3 +59,7 @@ limitations under the License.
.mx_BeaconStatus_expiryTime {
color: $secondary-content;
}

.mx_BeaconStatus_label {
margin-bottom: 2px;
}
5 changes: 4 additions & 1 deletion res/css/components/views/beacon/_DialogSidebar.scss
Expand Up @@ -21,6 +21,9 @@ limitations under the License.
height: 100%;
width: 265px;

display: flex;
flex-direction: column;

box-sizing: border-box;
padding: $spacing-16;

Expand All @@ -34,7 +37,7 @@ limitations under the License.
align-items: center;
justify-content: space-between;

flex: 0;
flex: 0 0;
margin-bottom: $spacing-16;

color: $primary-content;
Expand Down
82 changes: 82 additions & 0 deletions src/components/views/beacon/BeaconListItem.tsx
@@ -0,0 +1,82 @@
/*
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, { 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 { _t } from '../../../languageHandler';
import MemberAvatar from '../avatars/MemberAvatar';
import CopyableText from '../elements/CopyableText';
import BeaconStatus from './BeaconStatus';
import { BeaconDisplayStatus } from './displayStatus';
import StyledLiveBeaconIcon from './StyledLiveBeaconIcon';

interface Props {
beacon: Beacon;
}

const BeaconListItem: React.FC<Props> = ({ beacon }) => {
const latestLocationState = useEventEmitterState(
beacon,
BeaconEvent.LocationUpdate,
() => beacon.latestLocationState,
);
const matrixClient = useContext(MatrixClientContext);
const room = matrixClient.getRoom(beacon.roomId);

if (!latestLocationState || !beacon.isLive) {
return null;
}

const isSelfLocation = beacon.beaconInfo.assetType === LocationAssetType.Self;
const beaconMember = isSelfLocation ?
room.getMember(beacon.beaconInfoOwner) :
undefined;

const humanizedUpdateTime = humanizeTime(latestLocationState.timestamp);

return <li className='mx_BeaconListItem'>
{ isSelfLocation ?
<MemberAvatar
className='mx_BeaconListItem_avatar'
member={beaconMember}
height={32}
width={32}
/> :
<StyledLiveBeaconIcon className='mx_BeaconListItem_avatarIcon' />
}
<div className='mx_BeaconListItem_info'>
<BeaconStatus
className='mx_BeaconListItem_status'
beacon={beacon}
label={beaconMember?.name || beacon.beaconInfo.description || beacon.beaconInfoOwner}
displayStatus={BeaconDisplayStatus.Active}
>
<CopyableText
border={false}
getTextToCopy={() => latestLocationState?.uri}
/>
</BeaconStatus>
<span className='mx_BeaconListItem_lastUpdated'>{ _t("Updated %(humanizedUpdateTime)s", { humanizedUpdateTime }) }</span>
</div>
</li>;
};

export default BeaconListItem;
8 changes: 5 additions & 3 deletions src/components/views/beacon/BeaconStatus.tsx
Expand Up @@ -28,6 +28,7 @@ import { formatTime } from '../../../DateUtils';
interface Props {
displayStatus: BeaconDisplayStatus;
displayLiveTimeRemaining?: boolean;
withIcon?: boolean;
beacon?: Beacon;
label?: string;
}
Expand All @@ -45,6 +46,7 @@ const BeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> =
label,
className,
children,
withIcon,
...rest
}) => {
const isIdle = displayStatus === BeaconDisplayStatus.Loading ||
Expand All @@ -54,11 +56,11 @@ const BeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> =
{...rest}
className={classNames('mx_BeaconStatus', `mx_BeaconStatus_${displayStatus}`, className)}
>
<StyledLiveBeaconIcon
{ withIcon && <StyledLiveBeaconIcon
className='mx_BeaconStatus_icon'
withError={displayStatus === BeaconDisplayStatus.Error}
isIdle={isIdle}
/>
/> }
<div className='mx_BeaconStatus_description'>

{ displayStatus === BeaconDisplayStatus.Loading && <span>{ _t('Loading live location...') }</span> }
Expand All @@ -68,7 +70,7 @@ const BeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> =

{ displayStatus === BeaconDisplayStatus.Active && beacon && <>
<>
{ label }
<span className='mx_BeaconStatus_label'>{ label }</span>
{ displayLiveTimeRemaining ?
<LiveTimeRemaining beacon={beacon} /> :
<BeaconExpiryTime beacon={beacon} />
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/beacon/DialogSidebar.tsx
Expand Up @@ -21,6 +21,7 @@ import { Icon as CloseIcon } from '../../../../res/img/image-view/close.svg';
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import Heading from '../typography/Heading';
import BeaconListItem from './BeaconListItem';

interface Props {
beacons: Beacon[];
Expand All @@ -41,8 +42,7 @@ const DialogSidebar: React.FC<Props> = ({ beacons, requestClose }) => {
</AccessibleButton>
</div>
<ol className='mx_DialogSidebar_list'>
{ /* TODO nice elements */ }
{ beacons.map((beacon, index) => <li key={beacon.identifier}>{ index }</li>) }
{ beacons.map((beacon) => <BeaconListItem key={beacon.identifier} beacon={beacon} />) }
</ol>
</div>;
};
Expand Down
1 change: 1 addition & 0 deletions src/components/views/beacon/OwnBeaconStatus.tsx
Expand Up @@ -54,6 +54,7 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
displayStatus={ownDisplayStatus}
label={_t('Live location enabled')}
displayLiveTimeRemaining
withIcon
{...rest}
>
{ ownDisplayStatus === BeaconDisplayStatus.Active && <AccessibleButton
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/elements/CopyableText.tsx
Expand Up @@ -24,7 +24,7 @@ import { ButtonEvent } from "./AccessibleButton";
import AccessibleTooltipButton from "./AccessibleTooltipButton";

interface IProps {
children: React.ReactNode;
children?: React.ReactNode;
getTextToCopy: () => string;
border?: boolean;
}
Expand Down
1 change: 1 addition & 0 deletions src/components/views/messages/MBeaconBody.tsx
Expand Up @@ -152,6 +152,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent }, ref) =>
beacon={beacon}
displayStatus={displayStatus}
label={_t('View live location')}
withIcon
/>
}
</div>
Expand Down
1 change: 1 addition & 0 deletions src/i18n/strings/en_EN.json
Expand Up @@ -2913,6 +2913,7 @@
"Click for more info": "Click for more info",
"Beta": "Beta",
"Join the beta": "Join the beta",
"Updated %(humanizedUpdateTime)s": "Updated %(humanizedUpdateTime)s",
"Live until %(expiryTime)s": "Live until %(expiryTime)s",
"Loading live location...": "Loading live location...",
"Live location ended": "Live location ended",
Expand Down
2 changes: 1 addition & 1 deletion src/utils/humanize.ts
Expand Up @@ -30,7 +30,7 @@ const HOURS_1_DAY = 26;
* @returns {string} The humanized time.
*/
export function humanizeTime(timeMillis: number): string {
const now = (new Date()).getTime();
const now = Date.now();
let msAgo = now - timeMillis;
const minutes = Math.abs(Math.ceil(msAgo / 60000));
const hours = Math.ceil(minutes / 60);
Expand Down

0 comments on commit 4a38cbd

Please sign in to comment.