Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 94f3168

Browse files
author
Kerry
authored
Device manager - New device tile info design (#9122)(PSG-637)
* redesign device tile info * test DeviceTile except for broken date mocking * mock dates the nice way, test lastactivity in device tile * tweak spacing style * update comment style in rethemendex * i18n
1 parent 7eaed1a commit 94f3168

File tree

8 files changed

+334
-35
lines changed

8 files changed

+334
-35
lines changed

res/css/_components.pcss

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
@import "./components/views/location/_ZoomButtons.pcss";
2828
@import "./components/views/messages/_MBeaconBody.pcss";
2929
@import "./components/views/messages/shared/_MediaProcessingError.pcss";
30+
@import "./components/views/settings/devices/_DeviceTile.pcss";
3031
@import "./components/views/spaces/_QuickThemeSwitcher.pcss";
3132
@import "./structures/_AutoHideScrollbar.pcss";
3233
@import "./structures/_BackdropPanel.pcss";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_DeviceTile {
18+
display: flex;
19+
flex-direction: row;
20+
align-items: center;
21+
22+
width: 100%;
23+
}
24+
25+
.mx_DeviceTile_info {
26+
flex: 1 1 0;
27+
}
28+
29+
.mx_DeviceTile_metadata {
30+
margin-top: 2px;
31+
font-size: $font-12px;
32+
color: $secondary-content;
33+
}
34+
35+
.mx_DeviceTile_actions {
36+
display: grid;
37+
grid-gap: $spacing-8;
38+
grid-auto-flow: column;
39+
40+
margin-left: $spacing-8;
41+
}

res/css/rethemendex.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
cd `dirname $0`
44

55
{
6-
echo "// autogenerated by rethemendex.sh"
6+
echo "/* autogenerated by rethemendex.sh */"
77

88
# we used to have exclude /themes from the find at this point.
99
# as themes are no longer a spurious subdirectory of css/, we don't

src/components/views/settings/DevicesPanelEntry.tsx

+3-33
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,14 @@ import { logger } from "matrix-js-sdk/src/logger";
2020

2121
import { _t } from '../../../languageHandler';
2222
import { MatrixClientPeg } from '../../../MatrixClientPeg';
23-
import { formatDate } from '../../../DateUtils';
2423
import StyledCheckbox, { CheckboxStyle } from '../elements/StyledCheckbox';
2524
import AccessibleButton from "../elements/AccessibleButton";
2625
import Field from "../elements/Field";
27-
import TextWithTooltip from "../elements/TextWithTooltip";
2826
import Modal from "../../../Modal";
2927
import SetupEncryptionDialog from '../dialogs/security/SetupEncryptionDialog';
3028
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
3129
import LogoutDialog from '../dialogs/LogoutDialog';
30+
import DeviceTile from './devices/DeviceTile';
3231

3332
interface IProps {
3433
device: IMyDevice;
@@ -114,17 +113,6 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
114113
};
115114

116115
public render(): JSX.Element {
117-
const device = this.props.device;
118-
119-
let lastSeen = "";
120-
if (device.last_seen_ts) {
121-
const lastSeenDate = new Date(device.last_seen_ts);
122-
lastSeen = _t("Last seen %(date)s at %(ip)s", {
123-
date: formatDate(lastSeenDate),
124-
ip: device.last_seen_ip,
125-
});
126-
}
127-
128116
const myDeviceClass = this.props.isOwnDevice ? " mx_DevicesPanel_myDevice" : '';
129117

130118
let iconClass = '';
@@ -153,16 +141,6 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
153141
<StyledCheckbox kind={CheckboxStyle.Outline} onChange={this.onDeviceToggled} checked={this.props.selected} />
154142
</div>;
155143

156-
const deviceName = device.display_name ?
157-
<React.Fragment>
158-
<TextWithTooltip tooltip={device.display_name + " (" + device.device_id + ")"}>
159-
{ device.display_name }
160-
</TextWithTooltip>
161-
</React.Fragment> :
162-
<React.Fragment>
163-
{ device.device_id }
164-
</React.Fragment>;
165-
166144
const buttons = this.state.renaming ?
167145
<form className="mx_DevicesPanel_renameForm" onSubmit={this.onRenameSubmit}>
168146
<Field
@@ -187,17 +165,9 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
187165
return (
188166
<div className={"mx_DevicesPanel_device" + myDeviceClass}>
189167
{ left }
190-
<div className="mx_DevicesPanel_deviceInfo">
191-
<div className="mx_DevicesPanel_deviceName">
192-
{ deviceName }
193-
</div>
194-
<div className="mx_DevicesPanel_lastSeen">
195-
{ lastSeen }
196-
</div>
197-
</div>
198-
<div className="mx_DevicesPanel_deviceButtons">
168+
<DeviceTile device={this.props.device}>
199169
{ buttons }
200-
</div>
170+
</DeviceTile>
201171
</div>
202172
);
203173
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React, { Fragment } from "react";
18+
import { IMyDevice } from "matrix-js-sdk/src/matrix";
19+
20+
import { _t } from "../../../../languageHandler";
21+
import { formatDate, formatRelativeTime } from "../../../../DateUtils";
22+
import TooltipTarget from "../../elements/TooltipTarget";
23+
import { Alignment } from "../../elements/Tooltip";
24+
import Heading from "../../typography/Heading";
25+
26+
interface Props {
27+
device: IMyDevice;
28+
children?: React.ReactNode;
29+
}
30+
31+
const DeviceTileName: React.FC<{ device: IMyDevice }> = ({ device }) => {
32+
if (device.display_name) {
33+
return <TooltipTarget
34+
alignment={Alignment.Left}
35+
label={`${device.display_name} (${device.device_id})`}
36+
>
37+
<Heading size='h4'>
38+
{ device.display_name }
39+
</Heading>
40+
</TooltipTarget>;
41+
}
42+
return <Heading size='h4'>
43+
{ device.device_id }
44+
</Heading>;
45+
};
46+
47+
const MS_6_DAYS = 6 * 24 * 60 * 60 * 1000;
48+
const formatLastActivity = (timestamp: number, now = new Date().getTime()): string => {
49+
// less than a week ago
50+
if (timestamp + MS_6_DAYS >= now) {
51+
const date = new Date(timestamp);
52+
// Tue 20:15
53+
return formatDate(date);
54+
}
55+
return formatRelativeTime(new Date(timestamp));
56+
};
57+
58+
const DeviceMetadata: React.FC<{ value: string, id: string }> = ({ value, id }) => (
59+
value ? <span data-testid={`device-metadata-${id}`}>{ value }</span> : null
60+
);
61+
62+
const DeviceTile: React.FC<Props> = ({ device, children }) => {
63+
const lastActivity = device.last_seen_ts && `${_t('Last activity')} ${formatLastActivity(device.last_seen_ts)}`;
64+
const metadata = [
65+
{ id: 'lastActivity', value: lastActivity },
66+
{ id: 'lastSeenIp', value: device.last_seen_ip },
67+
];
68+
69+
return <div className="mx_DeviceTile">
70+
<div className="mx_DeviceTile_info">
71+
<DeviceTileName device={device} />
72+
<div className="mx_DeviceTile_metadata">
73+
{ metadata.map(({ id, value }, index) =>
74+
<Fragment key={id}>
75+
{ !!index && ' · ' }
76+
<DeviceMetadata id={id} value={value} />
77+
</Fragment>,
78+
) }
79+
</div>
80+
</div>
81+
<div className="mx_DeviceTile_actions">
82+
{ children }
83+
</div>
84+
</div>;
85+
};
86+
87+
export default DeviceTile;

src/i18n/strings/en_EN.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1303,7 +1303,6 @@
13031303
"You aren't signed into any other devices.": "You aren't signed into any other devices.",
13041304
"This device": "This device",
13051305
"Failed to set display name": "Failed to set display name",
1306-
"Last seen %(date)s at %(ip)s": "Last seen %(date)s at %(ip)s",
13071306
"Sign Out": "Sign Out",
13081307
"Display Name": "Display Name",
13091308
"Rename": "Rename",
@@ -1691,6 +1690,7 @@
16911690
"Please enter verification code sent via text.": "Please enter verification code sent via text.",
16921691
"Verification code": "Verification code",
16931692
"Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.",
1693+
"Last activity": "Last activity",
16941694
"Unable to remove contact information": "Unable to remove contact information",
16951695
"Remove %(email)s?": "Remove %(email)s?",
16961696
"Invalid Email Address": "Invalid Email Address",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from 'react';
18+
import { render } from '@testing-library/react';
19+
import { IMyDevice } from 'matrix-js-sdk/src/matrix';
20+
21+
import DeviceTile from '../../../../../src/components/views/settings/devices/DeviceTile';
22+
23+
describe('<DeviceTile />', () => {
24+
const defaultProps = {
25+
device: {
26+
device_id: '123',
27+
},
28+
};
29+
const getComponent = (props = {}) => (
30+
<DeviceTile {...defaultProps} {...props} />
31+
);
32+
// 14.03.2022 16:15
33+
const now = 1647270879403;
34+
35+
jest.useFakeTimers();
36+
37+
beforeEach(() => {
38+
jest.setSystemTime(now);
39+
});
40+
41+
it('renders a device with no metadata', () => {
42+
const { container } = render(getComponent());
43+
expect(container).toMatchSnapshot();
44+
});
45+
46+
it('renders display name with a tooltip', () => {
47+
const device: IMyDevice = {
48+
device_id: '123',
49+
display_name: 'My device',
50+
};
51+
const { container } = render(getComponent({ device }));
52+
expect(container).toMatchSnapshot();
53+
});
54+
55+
it('renders last seen ip metadata', () => {
56+
const device: IMyDevice = {
57+
device_id: '123',
58+
display_name: 'My device',
59+
last_seen_ip: '1.2.3.4',
60+
};
61+
const { getByTestId } = render(getComponent({ device }));
62+
expect(getByTestId('device-metadata-lastSeenIp').textContent).toEqual(device.last_seen_ip);
63+
});
64+
65+
it('separates metadata with a dot', () => {
66+
const device: IMyDevice = {
67+
device_id: '123',
68+
last_seen_ip: '1.2.3.4',
69+
last_seen_ts: now - 60000,
70+
};
71+
const { container } = render(getComponent({ device }));
72+
expect(container).toMatchSnapshot();
73+
});
74+
75+
describe('Last activity', () => {
76+
const MS_DAY = 24 * 60 * 60 * 1000;
77+
it('renders with day of week and time when last activity is less than 6 days ago', () => {
78+
const device: IMyDevice = {
79+
device_id: '123',
80+
last_seen_ip: '1.2.3.4',
81+
last_seen_ts: now - (MS_DAY * 3),
82+
};
83+
const { getByTestId } = render(getComponent({ device }));
84+
expect(getByTestId('device-metadata-lastActivity').textContent).toEqual('Last activity Fri 15:14');
85+
});
86+
87+
it('renders with month and date when last activity is more than 6 days ago', () => {
88+
const device: IMyDevice = {
89+
device_id: '123',
90+
last_seen_ip: '1.2.3.4',
91+
last_seen_ts: now - (MS_DAY * 8),
92+
};
93+
const { getByTestId } = render(getComponent({ device }));
94+
expect(getByTestId('device-metadata-lastActivity').textContent).toEqual('Last activity Mar 6');
95+
});
96+
97+
it('renders with month, date, year when activity is in a different calendar year', () => {
98+
const device: IMyDevice = {
99+
device_id: '123',
100+
last_seen_ip: '1.2.3.4',
101+
last_seen_ts: new Date('2021-12-29').getTime(),
102+
};
103+
const { getByTestId } = render(getComponent({ device }));
104+
expect(getByTestId('device-metadata-lastActivity').textContent).toEqual('Last activity Dec 29, 2021');
105+
});
106+
});
107+
});

0 commit comments

Comments
 (0)