From 5f95dc58018fc5c21e5c24f0372a29f13ebc0d72 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 29 Jul 2022 16:13:21 +0200 Subject: [PATCH 1/6] redesign device tile info --- res/css/_components.pcss | 3 +- .../views/settings/devices/_DeviceTile.pcss | 38 ++++++++ .../views/settings/DevicesPanelEntry.tsx | 33 +------ .../views/settings/devices/DeviceTile.tsx | 87 +++++++++++++++++++ src/i18n/strings/en_EN.json | 1 + 5 files changed, 131 insertions(+), 31 deletions(-) create mode 100644 res/css/components/views/settings/devices/_DeviceTile.pcss create mode 100644 src/components/views/settings/devices/DeviceTile.tsx diff --git a/res/css/_components.pcss b/res/css/_components.pcss index c4a5d6a434d..4b903d3de5d 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -1,4 +1,4 @@ -/* autogenerated by rethemendex.sh */ +// autogenerated by rethemendex.sh @import "./_animations.pcss"; @import "./_common.pcss"; @import "./_font-sizes.pcss"; @@ -27,6 +27,7 @@ @import "./components/views/location/_ZoomButtons.pcss"; @import "./components/views/messages/_MBeaconBody.pcss"; @import "./components/views/messages/shared/_MediaProcessingError.pcss"; +@import "./components/views/settings/devices/_DeviceTile.pcss"; @import "./components/views/spaces/_QuickThemeSwitcher.pcss"; @import "./structures/_AutoHideScrollbar.pcss"; @import "./structures/_BackdropPanel.pcss"; diff --git a/res/css/components/views/settings/devices/_DeviceTile.pcss b/res/css/components/views/settings/devices/_DeviceTile.pcss new file mode 100644 index 00000000000..5d69ba23829 --- /dev/null +++ b/res/css/components/views/settings/devices/_DeviceTile.pcss @@ -0,0 +1,38 @@ +/* +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_DeviceTile { + display: flex; + flex-direction: row; + align-items: center; + + width: 100%; +} + +.mx_DeviceTile_info { + flex: 1 1 100%; +} + +.mx_DeviceTile_metadata { + font-size: $font-12px; + color: $secondary-content; +} + +.mx_DeviceTile_actions { + display: grid; + grid-gap: $spacing-8; + grid-auto-flow: column; +} diff --git a/src/components/views/settings/DevicesPanelEntry.tsx b/src/components/views/settings/DevicesPanelEntry.tsx index 2e7094c7b35..169befba6eb 100644 --- a/src/components/views/settings/DevicesPanelEntry.tsx +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -29,6 +29,7 @@ import Modal from "../../../Modal"; import SetupEncryptionDialog from '../dialogs/security/SetupEncryptionDialog'; import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog'; import LogoutDialog from '../dialogs/LogoutDialog'; +import DeviceTile from './devices/DeviceTile'; interface IProps { device: IMyDevice; @@ -115,16 +116,6 @@ export default class DevicesPanelEntry extends React.Component { public render(): JSX.Element { const device = this.props.device; - - let lastSeen = ""; - if (device.last_seen_ts) { - const lastSeenDate = new Date(device.last_seen_ts); - lastSeen = _t("Last seen %(date)s at %(ip)s", { - date: formatDate(lastSeenDate), - ip: device.last_seen_ip, - }); - } - const myDeviceClass = this.props.isOwnDevice ? " mx_DevicesPanel_myDevice" : ''; let iconClass = ''; @@ -153,16 +144,6 @@ export default class DevicesPanelEntry extends React.Component { ; - const deviceName = device.display_name ? - - - { device.display_name } - - : - - { device.device_id } - ; - const buttons = this.state.renaming ?
{ return (
{ left } -
-
- { deviceName } -
-
- { lastSeen } -
-
-
+ { buttons } -
+
); } diff --git a/src/components/views/settings/devices/DeviceTile.tsx b/src/components/views/settings/devices/DeviceTile.tsx new file mode 100644 index 00000000000..d3462561d36 --- /dev/null +++ b/src/components/views/settings/devices/DeviceTile.tsx @@ -0,0 +1,87 @@ +/* +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, { Fragment } from "react"; +import { IMyDevice } from "matrix-js-sdk/src/matrix"; + +import { _t } from "../../../../languageHandler"; +import { formatDate, formatRelativeTime } from "../../../../DateUtils"; +import TooltipTarget from "../../elements/TooltipTarget"; +import { Alignment } from "../../elements/Tooltip"; +import Heading from "../../typography/Heading"; + +interface Props { + device: IMyDevice; + children?: React.ReactNode; +} + +const DeviceTileName: React.FC<{ device: IMyDevice }> = ({ device }) => { + if (device.display_name) { + return + + { device.display_name } + + ; + } + return + { device.device_id } + ; +}; + +const MS_6_DAYS = 6 * 24 * 60 * 60 * 1000; +const formatLastActivity = (timestamp: number, now = Date.now()): string => { + // less than a week ago + if (timestamp + MS_6_DAYS >= now) { + const date = new Date(timestamp); + // Tue 20:15 + return formatDate(date); + } + return formatRelativeTime(new Date(timestamp)); +}; + +const DeviceMetadata: React.FC<{ value: string, id: string }> = ({ value, id }) => ( + value ? { value } : null +); + +const DeviceTile: React.FC = ({ device, children }) => { + const lastActivity = device.last_seen_ts && `${_t('Last activity')} ${formatLastActivity(device.last_seen_ts)}`; + const metadata = [ + { id: 'lastActivity', value: lastActivity }, + { id: 'lastSeenIp', value: device.last_seen_ip }, + ]; + + return
+
+ +
+ { metadata.map(({ id, value }, index) => + + { !!index && ' · ' } + + , + ) } +
+
+
+ { children } +
+
; +}; + +export default DeviceTile; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 00017859015..2ade16b00be 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1685,6 +1685,7 @@ "Please enter verification code sent via text.": "Please enter verification code sent via text.", "Verification code": "Verification code", "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", + "Last activity": "Last activity", "Unable to remove contact information": "Unable to remove contact information", "Remove %(email)s?": "Remove %(email)s?", "Invalid Email Address": "Invalid Email Address", From 84c7d946b19c6894e950a6be970c04ce0903456e Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 29 Jul 2022 16:47:36 +0200 Subject: [PATCH 2/6] test DeviceTile except for broken date mocking --- .../views/settings/devices/_DeviceTile.pcss | 3 +- .../views/settings/DevicesPanelEntry.tsx | 3 - .../views/settings/devices/DeviceTile.tsx | 1 + .../settings/devices/DeviceTile-test.tsx | 101 ++++++++++++++++++ .../__snapshots__/DeviceTile-test.tsx.snap | 93 ++++++++++++++++ 5 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 test/components/views/settings/devices/DeviceTile-test.tsx create mode 100644 test/components/views/settings/devices/__snapshots__/DeviceTile-test.tsx.snap diff --git a/res/css/components/views/settings/devices/_DeviceTile.pcss b/res/css/components/views/settings/devices/_DeviceTile.pcss index 5d69ba23829..b30a2f2e0c6 100644 --- a/res/css/components/views/settings/devices/_DeviceTile.pcss +++ b/res/css/components/views/settings/devices/_DeviceTile.pcss @@ -23,7 +23,8 @@ limitations under the License. } .mx_DeviceTile_info { - flex: 1 1 100%; + margin-top: $spacing-4; + flex: 1 1 0; } .mx_DeviceTile_metadata { diff --git a/src/components/views/settings/DevicesPanelEntry.tsx b/src/components/views/settings/DevicesPanelEntry.tsx index 169befba6eb..5a5330fd3ee 100644 --- a/src/components/views/settings/DevicesPanelEntry.tsx +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -20,11 +20,9 @@ import { logger } from "matrix-js-sdk/src/logger"; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import { formatDate } from '../../../DateUtils'; import StyledCheckbox, { CheckboxStyle } from '../elements/StyledCheckbox'; import AccessibleButton from "../elements/AccessibleButton"; import Field from "../elements/Field"; -import TextWithTooltip from "../elements/TextWithTooltip"; import Modal from "../../../Modal"; import SetupEncryptionDialog from '../dialogs/security/SetupEncryptionDialog'; import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog'; @@ -115,7 +113,6 @@ export default class DevicesPanelEntry extends React.Component { }; public render(): JSX.Element { - const device = this.props.device; const myDeviceClass = this.props.isOwnDevice ? " mx_DevicesPanel_myDevice" : ''; let iconClass = ''; diff --git a/src/components/views/settings/devices/DeviceTile.tsx b/src/components/views/settings/devices/DeviceTile.tsx index d3462561d36..486724c4708 100644 --- a/src/components/views/settings/devices/DeviceTile.tsx +++ b/src/components/views/settings/devices/DeviceTile.tsx @@ -47,6 +47,7 @@ const DeviceTileName: React.FC<{ device: IMyDevice }> = ({ device }) => { const MS_6_DAYS = 6 * 24 * 60 * 60 * 1000; const formatLastActivity = (timestamp: number, now = Date.now()): string => { // less than a week ago + console.log(timestamp, now, timestamp + MS_6_DAYS >= now); if (timestamp + MS_6_DAYS >= now) { const date = new Date(timestamp); // Tue 20:15 diff --git a/test/components/views/settings/devices/DeviceTile-test.tsx b/test/components/views/settings/devices/DeviceTile-test.tsx new file mode 100644 index 00000000000..2c17306df90 --- /dev/null +++ b/test/components/views/settings/devices/DeviceTile-test.tsx @@ -0,0 +1,101 @@ +/* +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 { render } from '@testing-library/react'; +import { IMyDevice } from 'matrix-js-sdk/src/matrix'; + +import DeviceTile from '../../../../../src/components/views/settings/devices/DeviceTile'; + +describe('', () => { + const defaultProps = { + device: { + device_id: '123', + }, + }; + const getComponent = (props = {}) => ( + + ); + // 14.03.2022 16:15 + const now = 1647270879403; + const RealDate = global.Date; + class MockDate extends Date { + constructor(date?: any) { + super(date || now); + } + + now() { + return now; + } + } + + beforeEach(() => { + // @ts-ignore need Date constructor and now() + // to be equally mocked + global.Date = MockDate; + }); + + afterAll(() => { + global.Date = RealDate; + }); + + it('renders a device with no metadata', () => { + const { container } = render(getComponent()); + expect(container).toMatchSnapshot(); + }); + + it('renders display name with a tooltip', () => { + const device: IMyDevice = { + device_id: '123', + display_name: 'My device', + }; + const { container } = render(getComponent({ device })); + expect(container).toMatchSnapshot(); + }); + + it('renders last seen ip metadata', () => { + const device: IMyDevice = { + device_id: '123', + display_name: 'My device', + last_seen_ip: '1.2.3.4', + }; + const { getByTestId } = render(getComponent({ device })); + expect(getByTestId('device-metadata-lastSeenIp').textContent).toEqual(device.last_seen_ip); + }); + + it('separates metadata with a dot', () => { + const device: IMyDevice = { + device_id: '123', + last_seen_ip: '1.2.3.4', + last_seen_ts: now - 60000, + }; + const { container } = render(getComponent({ device })); + expect(container).toMatchSnapshot(); + }); + + describe('Last activity', () => { + const MS_DAY = 24 * 60 * 60 * 1000; + fit('renders with short date format when last activity is less than 6 days ago', () => { + const device: IMyDevice = { + device_id: '123', + last_seen_ip: '1.2.3.4', + last_seen_ts: now - (MS_DAY * 3), + }; + const { getByTestId } = render(getComponent({ device })); + expect(getByTestId('device-metadata-lastActivity').textContent).toEqual('Last activity Mar 11'); + }); + }); +}); diff --git a/test/components/views/settings/devices/__snapshots__/DeviceTile-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/DeviceTile-test.tsx.snap new file mode 100644 index 00000000000..c2429ad1546 --- /dev/null +++ b/test/components/views/settings/devices/__snapshots__/DeviceTile-test.tsx.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders a device with no metadata 1`] = ` +
+
+
+

+ 123 +

+ +
+
+
+
+`; + +exports[` renders display name with a tooltip 1`] = ` +
+
+
+
+

+ My device +

+
+ +
+
+
+
+`; + +exports[` separates metadata with a dot 1`] = ` +
+
+
+

+ 123 +

+ +
+
+
+
+`; From 0781ca38d31ad9c3358745d128706f3206302280 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 29 Jul 2022 17:10:25 +0200 Subject: [PATCH 3/6] mock dates the nice way, test lastactivity in device tile --- .../views/settings/devices/DeviceTile.tsx | 3 +- .../settings/devices/DeviceTile-test.tsx | 42 +++++++++++-------- .../__snapshots__/DeviceTile-test.tsx.snap | 2 +- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/components/views/settings/devices/DeviceTile.tsx b/src/components/views/settings/devices/DeviceTile.tsx index 486724c4708..03d952fbb1e 100644 --- a/src/components/views/settings/devices/DeviceTile.tsx +++ b/src/components/views/settings/devices/DeviceTile.tsx @@ -45,9 +45,8 @@ const DeviceTileName: React.FC<{ device: IMyDevice }> = ({ device }) => { }; const MS_6_DAYS = 6 * 24 * 60 * 60 * 1000; -const formatLastActivity = (timestamp: number, now = Date.now()): string => { +const formatLastActivity = (timestamp: number, now = new Date().getTime()): string => { // less than a week ago - console.log(timestamp, now, timestamp + MS_6_DAYS >= now); if (timestamp + MS_6_DAYS >= now) { const date = new Date(timestamp); // Tue 20:15 diff --git a/test/components/views/settings/devices/DeviceTile-test.tsx b/test/components/views/settings/devices/DeviceTile-test.tsx index 2c17306df90..d688eca9135 100644 --- a/test/components/views/settings/devices/DeviceTile-test.tsx +++ b/test/components/views/settings/devices/DeviceTile-test.tsx @@ -31,25 +31,11 @@ describe('', () => { ); // 14.03.2022 16:15 const now = 1647270879403; - const RealDate = global.Date; - class MockDate extends Date { - constructor(date?: any) { - super(date || now); - } - now() { - return now; - } - } + jest.useFakeTimers(); beforeEach(() => { - // @ts-ignore need Date constructor and now() - // to be equally mocked - global.Date = MockDate; - }); - - afterAll(() => { - global.Date = RealDate; + jest.setSystemTime(now); }); it('renders a device with no metadata', () => { @@ -88,14 +74,34 @@ describe('', () => { describe('Last activity', () => { const MS_DAY = 24 * 60 * 60 * 1000; - fit('renders with short date format when last activity is less than 6 days ago', () => { + it('renders with day of week and time when last activity is less than 6 days ago', () => { const device: IMyDevice = { device_id: '123', last_seen_ip: '1.2.3.4', last_seen_ts: now - (MS_DAY * 3), }; const { getByTestId } = render(getComponent({ device })); - expect(getByTestId('device-metadata-lastActivity').textContent).toEqual('Last activity Mar 11'); + expect(getByTestId('device-metadata-lastActivity').textContent).toEqual('Last activity Fri 15:14'); + }); + + it('renders with month and date when last activity is more than 6 days ago', () => { + const device: IMyDevice = { + device_id: '123', + last_seen_ip: '1.2.3.4', + last_seen_ts: now - (MS_DAY * 8), + }; + const { getByTestId } = render(getComponent({ device })); + expect(getByTestId('device-metadata-lastActivity').textContent).toEqual('Last activity Mar 6'); + }); + + it('renders with month, date, year when activity is in a different calendar year', () => { + const device: IMyDevice = { + device_id: '123', + last_seen_ip: '1.2.3.4', + last_seen_ts: new Date('2021-12-29').getTime(), + }; + const { getByTestId } = render(getComponent({ device })); + expect(getByTestId('device-metadata-lastActivity').textContent).toEqual('Last activity Dec 29, 2021'); }); }); }); diff --git a/test/components/views/settings/devices/__snapshots__/DeviceTile-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/DeviceTile-test.tsx.snap index c2429ad1546..299d72348c8 100644 --- a/test/components/views/settings/devices/__snapshots__/DeviceTile-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/DeviceTile-test.tsx.snap @@ -75,7 +75,7 @@ exports[` separates metadata with a dot 1`] = ` - Last activity Mon, Mar 14 15:13 + Last activity 15:13 · Date: Fri, 29 Jul 2022 17:25:55 +0200 Subject: [PATCH 4/6] tweak spacing style --- res/css/components/views/settings/devices/_DeviceTile.pcss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/res/css/components/views/settings/devices/_DeviceTile.pcss b/res/css/components/views/settings/devices/_DeviceTile.pcss index b30a2f2e0c6..159cace6ac0 100644 --- a/res/css/components/views/settings/devices/_DeviceTile.pcss +++ b/res/css/components/views/settings/devices/_DeviceTile.pcss @@ -23,11 +23,11 @@ limitations under the License. } .mx_DeviceTile_info { - margin-top: $spacing-4; flex: 1 1 0; } .mx_DeviceTile_metadata { + margin-top: 2px; font-size: $font-12px; color: $secondary-content; } @@ -36,4 +36,6 @@ limitations under the License. display: grid; grid-gap: $spacing-8; grid-auto-flow: column; + + margin-left: $spacing-8; } From 327b0ee2bf645c33e65bdbb9c28bad00811c5849 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 29 Jul 2022 17:27:24 +0200 Subject: [PATCH 5/6] update comment style in rethemendex --- res/css/_components.pcss | 2 +- res/css/rethemendex.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 4b903d3de5d..e4179635f93 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -1,4 +1,4 @@ -// autogenerated by rethemendex.sh +/* autogenerated by rethemendex.sh */ @import "./_animations.pcss"; @import "./_common.pcss"; @import "./_font-sizes.pcss"; diff --git a/res/css/rethemendex.sh b/res/css/rethemendex.sh index 1fc1bb84ccb..37090b96d8f 100755 --- a/res/css/rethemendex.sh +++ b/res/css/rethemendex.sh @@ -3,7 +3,7 @@ cd `dirname $0` { - echo "// autogenerated by rethemendex.sh" + echo "/* autogenerated by rethemendex.sh */" # we used to have exclude /themes from the find at this point. # as themes are no longer a spurious subdirectory of css/, we don't From da98c4a9b774ddf35c738c0d30d61d82bdf58d7c Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 1 Aug 2022 08:30:03 +0200 Subject: [PATCH 6/6] i18n --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2ade16b00be..1d1d79af48f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1300,7 +1300,6 @@ "You aren't signed into any other devices.": "You aren't signed into any other devices.", "This device": "This device", "Failed to set display name": "Failed to set display name", - "Last seen %(date)s at %(ip)s": "Last seen %(date)s at %(ip)s", "Sign Out": "Sign Out", "Display Name": "Display Name", "Rename": "Rename",