diff --git a/res/css/_components.pcss b/res/css/_components.pcss index d6445f0143..f9b3ac059d 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.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/_DeviceDetails.pcss"; @import "./components/views/settings/devices/_DeviceTile.pcss"; @import "./components/views/settings/devices/_SelectableDeviceTile.pcss"; @import "./components/views/settings/shared/_SettingsSubsection.pcss"; diff --git a/res/css/components/views/settings/devices/_DeviceDetails.pcss b/res/css/components/views/settings/devices/_DeviceDetails.pcss new file mode 100644 index 0000000000..df1341dbb1 --- /dev/null +++ b/res/css/components/views/settings/devices/_DeviceDetails.pcss @@ -0,0 +1,70 @@ +/* +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_DeviceDetails { + display: flex; + flex-direction: column; + + width: 100%; + + padding: $spacing-16; + border-radius: 8px; + border: 1px solid $system; +} + +.mx_DeviceDetails_section { + padding-bottom: $spacing-16; + margin-bottom: $spacing-16; + border-bottom: 1px solid $system; + + &:last-child { + padding-bottom: 0; + border-bottom: 0; + margin-bottom: 0; + } +} + +.mx_DeviceDetails_sectionHeading { + margin: 0; +} + +.mxDeviceDetails_metadataTable { + font-size: $font-12px; + color: $secondary-content; + + width: 100%; + margin-top: $spacing-20; + + border-spacing: 0; + + th { + text-transform: uppercase; + font-weight: normal; + text-align: left; + } + + td { + padding-top: $spacing-8; + } + + .mxDeviceDetails_metadataLabel { + width: 160px; + } + + .mxDeviceDetails_metadataValue { + color: $primary-content; + } +} diff --git a/src/components/views/settings/DevicesPanelEntry.tsx b/src/components/views/settings/DevicesPanelEntry.tsx index beb7a8e86e..0109c37b9b 100644 --- a/src/components/views/settings/DevicesPanelEntry.tsx +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import { IMyDevice } from 'matrix-js-sdk/src/client'; import { logger } from "matrix-js-sdk/src/logger"; +import classNames from 'classnames'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; @@ -113,8 +114,6 @@ export default class DevicesPanelEntry extends React.Component { }; public render(): JSX.Element { - const myDeviceClass = this.props.isOwnDevice ? " mx_DevicesPanel_myDevice" : ''; - let iconClass = ''; let verifyButton: JSX.Element; if (this.props.verified !== null) { @@ -160,7 +159,7 @@ export default class DevicesPanelEntry extends React.Component { }; if (this.props.isOwnDevice) { - return
+ return
@@ -171,7 +170,7 @@ export default class DevicesPanelEntry extends React.Component { } return ( -
+
{ buttons } diff --git a/src/components/views/settings/devices/DeviceDetails.tsx b/src/components/views/settings/devices/DeviceDetails.tsx new file mode 100644 index 0000000000..6b5cd97928 --- /dev/null +++ b/src/components/views/settings/devices/DeviceDetails.tsx @@ -0,0 +1,79 @@ +/* +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 { IMyDevice } from 'matrix-js-sdk/src/matrix'; + +import { formatDate } from '../../../../DateUtils'; +import { _t } from '../../../../languageHandler'; +import Heading from '../../typography/Heading'; + +interface Props { + device: IMyDevice; +} + +interface MetadataTable { + heading?: string; + values: { label: string, value?: string | React.ReactNode }[]; +} + +const DeviceDetails: React.FC = ({ device }) => { + const metadata: MetadataTable[] = [ + { + values: [ + { label: _t('Session ID'), value: device.device_id }, + { + label: _t('Last activity'), + value: device.last_seen_ts && formatDate(new Date(device.last_seen_ts)), + }, + ], + }, + { + heading: _t('Device'), + values: [ + { label: _t('IP address'), value: device.last_seen_ip }, + ], + }, + ]; + return
+
+ { device.display_name ?? device.device_id } +
+
+

{ _t('Session details') }

+ { metadata.map(({ heading, values }, index) => + { heading && + + + + } + + + { values.map(({ label, value }) => + + + ) } + +
{ heading }
{ label }{ value }
, + ) } +
+
; +}; + +export default DeviceDetails; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4afa3fabe3..2fe0618900 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1692,7 +1692,11 @@ "Sign out devices|other": "Sign out devices", "Sign out devices|one": "Sign out device", "Authentication": "Authentication", + "Session ID": "Session ID", "Last activity": "Last activity", + "Device": "Device", + "IP address": "IP address", + "Session details": "Session details", "Verified": "Verified", "Unverified": "Unverified", "Unable to remove contact information": "Unable to remove contact information", @@ -2720,7 +2724,6 @@ "Confirm by comparing the following with the User Settings in your other session:": "Confirm by comparing the following with the User Settings in your other session:", "Confirm this user's session by comparing the following with their User Settings:": "Confirm this user's session by comparing the following with their User Settings:", "Session name": "Session name", - "Session ID": "Session ID", "Session key": "Session key", "If they don't match, the security of your communication may be compromised.": "If they don't match, the security of your communication may be compromised.", "Verify session": "Verify session", diff --git a/test/components/views/settings/devices/DeviceDetails-test.tsx b/test/components/views/settings/devices/DeviceDetails-test.tsx new file mode 100644 index 0000000000..49a44d67f5 --- /dev/null +++ b/test/components/views/settings/devices/DeviceDetails-test.tsx @@ -0,0 +1,53 @@ +/* +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 DeviceDetails from '../../../../../src/components/views/settings/devices/DeviceDetails'; + +describe('', () => { + const baseDevice = { + device_id: 'my-device', + }; + const defaultProps = { + device: baseDevice, + }; + const getComponent = (props = {}) => ; + // 14.03.2022 16:15 + const now = 1647270879403; + jest.useFakeTimers(); + + beforeEach(() => { + jest.setSystemTime(now); + }); + + it('renders device without metadata', () => { + const { container } = render(getComponent()); + expect(container).toMatchSnapshot(); + }); + + it('renders device with metadata', () => { + const device = { + ...baseDevice, + display_name: 'My Device', + last_seen_ip: '123.456.789', + last_seen_ts: now - 60000000, + }; + const { container } = render(getComponent({ device })); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap new file mode 100644 index 0000000000..b84e9f08f5 --- /dev/null +++ b/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap @@ -0,0 +1,161 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders device with metadata 1`] = ` +
+
+
+

+ My Device +

+
+
+

+ Session details +

+ + + + + + + + + + + +
+ Session ID + + my-device +
+ Last activity + + Sun 22:34 +
+ + + + + + + + + + + + +
+ Device +
+ IP address + + 123.456.789 +
+
+
+
+`; + +exports[` renders device without metadata 1`] = ` +
+
+
+

+ my-device +

+
+
+

+ Session details +

+ + + + + + + + + + +
+ Session ID + + my-device +
+ Last activity + +
+ + + + + + + + + + + +
+ Device +
+ IP address + +
+
+
+
+`;