From be8e44e17e6102817bac5c2f2519ab44610525eb Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 27 Sep 2022 10:58:19 +0200 Subject: [PATCH 01/31] Add types for styleElements object --- src/theme.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/theme.ts b/src/theme.ts index 54bc42f807..06267b4a9e 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -237,8 +237,8 @@ export async function setTheme(theme?: string): Promise { // look for the stylesheet elements. // styleElements is a map from style name to HTMLLinkElement. - const styleElements = Object.create(null); - const themes = Array.from(document.querySelectorAll('[data-mx-theme]')); + const styleElements: Record = Object.create(null); + const themes = Array.from(document.querySelectorAll('[data-mx-theme]')); themes.forEach(theme => { styleElements[theme.attributes['data-mx-theme'].value.toLowerCase()] = theme; }); From f574247452f6e99fe33fc977eddff0e2e8ec1637 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Tue, 27 Sep 2022 11:02:57 +0200 Subject: [PATCH 02/31] Fix the white/black theme switch in Chrome Chrome doesn't fire twice the load event on a stylesheet when the disabled attribute is toggled (enabled => disabled => enabled) --- src/theme.ts | 56 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/src/theme.ts b/src/theme.ts index 06267b4a9e..f932cfe872 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -279,26 +279,48 @@ export async function setTheme(theme?: string): Promise { resolve(); }; - // turns out that Firefox preloads the CSS for link elements with - // the disabled attribute, but Chrome doesn't. + const isStyleSheetLoaded = () => Boolean( + [...document.styleSheets] + .find(styleSheet => styleSheet?.href === styleElements[stylesheetName].href), + ); - let cssLoaded = false; - - styleElements[stylesheetName].onload = () => { - switchTheme(); - }; - - for (let i = 0; i < document.styleSheets.length; i++) { - const ss = document.styleSheets[i]; - if (ss && ss.href === styleElements[stylesheetName].href) { - cssLoaded = true; - break; + function waitForStyleSheetLoading() { + // turns out that Firefox preloads the CSS for link elements with + // the disabled attribute, but Chrome doesn't. + if (isStyleSheetLoaded()) { + switchTheme(); + return; } + + let counter = 0; + + // In case of theme toggling (white => black => white) + // Chrome doesn't fire the `load` event when the white theme is selected the second times + const intervalId = setInterval(() => { + if (isStyleSheetLoaded()) { + clearInterval(intervalId); + styleElements[stylesheetName].onload = undefined; + styleElements[stylesheetName].onerror = undefined; + switchTheme(); + } + + // Avoid to be stuck in an endless loop if there is an issue in the stylesheet loading + counter++; + if (counter === 5) { + clearInterval(intervalId); + } + }, 100); + + styleElements[stylesheetName].onload = () => { + clearInterval(intervalId); + switchTheme(); + }; + + styleElements[stylesheetName].onerror = () => { + clearInterval(intervalId); + }; } - if (cssLoaded) { - styleElements[stylesheetName].onload = undefined; - switchTheme(); - } + waitForStyleSheetLoading(); }); } From 55450bc0a6b6f0a84ac550f8584c995fd6c6fcd4 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 28 Sep 2022 10:44:32 +0200 Subject: [PATCH 03/31] Use map instead of object for styleElements --- src/theme.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/theme.ts b/src/theme.ts index f932cfe872..a657807704 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -237,13 +237,13 @@ export async function setTheme(theme?: string): Promise { // look for the stylesheet elements. // styleElements is a map from style name to HTMLLinkElement. - const styleElements: Record = Object.create(null); + const styleElements = new Map(); const themes = Array.from(document.querySelectorAll('[data-mx-theme]')); themes.forEach(theme => { - styleElements[theme.attributes['data-mx-theme'].value.toLowerCase()] = theme; + styleElements.set(theme.attributes['data-mx-theme'].value.toLowerCase(), theme); }); - if (!(stylesheetName in styleElements)) { + if (!styleElements.has(stylesheetName)) { throw new Error("Unknown theme " + stylesheetName); } @@ -258,7 +258,8 @@ export async function setTheme(theme?: string): Promise { // having them interact badly... but this causes a flash of unstyled app // which is even uglier. So we don't. - styleElements[stylesheetName].disabled = false; + const styleSheet = styleElements.get(stylesheetName); + styleSheet.disabled = false; return new Promise((resolve) => { const switchTheme = function() { @@ -266,9 +267,9 @@ export async function setTheme(theme?: string): Promise { // theme set request as per https://github.com/vector-im/element-web/issues/5601. // We could alternatively lock or similar to stop the race, but // this is probably good enough for now. - styleElements[stylesheetName].disabled = false; - Object.values(styleElements).forEach((a: HTMLStyleElement) => { - if (a == styleElements[stylesheetName]) return; + styleSheet.disabled = false; + styleElements.forEach(a => { + if (a == styleSheet) return; a.disabled = true; }); const bodyStyles = global.getComputedStyle(document.body); @@ -281,7 +282,7 @@ export async function setTheme(theme?: string): Promise { const isStyleSheetLoaded = () => Boolean( [...document.styleSheets] - .find(styleSheet => styleSheet?.href === styleElements[stylesheetName].href), + .find(_styleSheet => _styleSheet?.href === styleSheet.href), ); function waitForStyleSheetLoading() { @@ -299,8 +300,8 @@ export async function setTheme(theme?: string): Promise { const intervalId = setInterval(() => { if (isStyleSheetLoaded()) { clearInterval(intervalId); - styleElements[stylesheetName].onload = undefined; - styleElements[stylesheetName].onerror = undefined; + styleSheet.onload = undefined; + styleSheet.onerror = undefined; switchTheme(); } @@ -311,12 +312,12 @@ export async function setTheme(theme?: string): Promise { } }, 100); - styleElements[stylesheetName].onload = () => { + styleSheet.onload = () => { clearInterval(intervalId); switchTheme(); }; - styleElements[stylesheetName].onerror = () => { + styleSheet.onerror = () => { clearInterval(intervalId); }; } From ad9cbe93994888e8f8ba88ce3614dc041a698930 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 28 Sep 2022 12:42:40 +0200 Subject: [PATCH 04/31] Add unit tests --- src/theme.ts | 12 +++-- test/theme-test.ts | 111 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 test/theme-test.ts diff --git a/src/theme.ts b/src/theme.ts index a657807704..9d2f836fb4 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -261,7 +261,7 @@ export async function setTheme(theme?: string): Promise { const styleSheet = styleElements.get(stylesheetName); styleSheet.disabled = false; - return new Promise((resolve) => { + return new Promise(((resolve, reject) => { const switchTheme = function() { // we re-enable our theme here just in case we raced with another // theme set request as per https://github.com/vector-im/element-web/issues/5601. @@ -307,21 +307,23 @@ export async function setTheme(theme?: string): Promise { // Avoid to be stuck in an endless loop if there is an issue in the stylesheet loading counter++; - if (counter === 5) { + if (counter === 10) { clearInterval(intervalId); + reject(); } - }, 100); + }, 200); styleSheet.onload = () => { clearInterval(intervalId); switchTheme(); }; - styleSheet.onerror = () => { + styleSheet.onerror = (e) => { clearInterval(intervalId); + reject(e); }; } waitForStyleSheetLoading(); - }); + })); } diff --git a/test/theme-test.ts b/test/theme-test.ts new file mode 100644 index 0000000000..47e42f5a70 --- /dev/null +++ b/test/theme-test.ts @@ -0,0 +1,111 @@ +/* +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 { setTheme } from "../src/theme"; + +describe('theme', () => { + describe('setTheme', () => { + let lightTheme; + let darkTheme; + + beforeEach(() => { + const styles = [ + { + attributes: { + 'data-mx-theme': { + value: 'light', + }, + }, + disabled: true, + href: 'urlLight', + onload: () => void 0, + }, + { + attributes: { + 'data-mx-theme': { + value: 'dark', + }, + }, + disabled: true, + href: 'urlDark', + onload: () => void 0, + }, + ]; + lightTheme = styles[0]; + darkTheme = styles[1]; + + jest.spyOn(document.body, 'style', 'get').mockReturnValue([] as any); + jest.spyOn(document, 'querySelectorAll').mockReturnValue(styles as any); + }); + + afterEach(() => { + jest.restoreAllMocks(); + jest.useRealTimers(); + }); + + it('should switch theme on onload call', async () => { + // When + await new Promise(resolve => { + setTheme('light').then(resolve); + lightTheme.onload(); + }); + + // Then + expect(lightTheme.disabled).toBe(false); + expect(darkTheme.disabled).toBe(true); + }); + + it('should reject promise on onerror call', () => { + return expect(new Promise(resolve => { + setTheme('light').catch(e => resolve(e)); + lightTheme.onerror('call onerror'); + })).resolves.toBe('call onerror'); + }); + + it('should switch theme if CSS are preloaded', async () => { + // When + jest.spyOn(document, 'styleSheets', 'get').mockReturnValue([lightTheme] as any); + + await setTheme('light'); + + // Then + expect(lightTheme.disabled).toBe(false); + expect(darkTheme.disabled).toBe(true); + }); + + it('should switch theme if CSS is loaded during pooling', async () => { + // When + jest.useFakeTimers(); + await new Promise(resolve => { + setTheme('light').then(resolve); + jest.spyOn(document, 'styleSheets', 'get').mockReturnValue([lightTheme] as any); + jest.advanceTimersByTime(200); + }); + + // Then + expect(lightTheme.disabled).toBe(false); + expect(darkTheme.disabled).toBe(true); + }); + + it('should reject promise if pooling maximum value is reached', () => { + jest.useFakeTimers(); + return new Promise(resolve => { + setTheme('light').catch(resolve); + jest.advanceTimersByTime(200 * 10); + }); + }); + }); +}); From b2c2ef2bd66230fdc39103293320983e73b32e90 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 28 Sep 2022 15:25:16 +0200 Subject: [PATCH 05/31] Test querySelectorAll call --- test/theme-test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/theme-test.ts b/test/theme-test.ts index 47e42f5a70..8e0e6c94e1 100644 --- a/test/theme-test.ts +++ b/test/theme-test.ts @@ -21,6 +21,8 @@ describe('theme', () => { let lightTheme; let darkTheme; + let spyQuerySelectorAll: jest.MockInstance, [selectors: string]>; + beforeEach(() => { const styles = [ { @@ -48,7 +50,7 @@ describe('theme', () => { darkTheme = styles[1]; jest.spyOn(document.body, 'style', 'get').mockReturnValue([] as any); - jest.spyOn(document, 'querySelectorAll').mockReturnValue(styles as any); + spyQuerySelectorAll = jest.spyOn(document, 'querySelectorAll').mockReturnValue(styles as any); }); afterEach(() => { @@ -64,6 +66,8 @@ describe('theme', () => { }); // Then + expect(spyQuerySelectorAll).toHaveBeenCalledWith('[data-mx-theme]'); + expect(spyQuerySelectorAll).toBeCalledTimes(1); expect(lightTheme.disabled).toBe(false); expect(darkTheme.disabled).toBe(true); }); From 99488b84ecb6cd9025561195d0414d11f3f385f9 Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Tue, 4 Oct 2022 19:54:26 +0200 Subject: [PATCH 06/31] Remove unspecced `original_event` field from the `readEventRelations` response (#9349) Signed-off-by: Dominik Henneke Signed-off-by: Dominik Henneke --- src/stores/widgets/StopGapWidgetDriver.ts | 2 -- .../widgets/StopGapWidgetDriver-test.ts | 21 ------------------- 2 files changed, 23 deletions(-) diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index f0b496f0b7..9de442a4b9 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -434,7 +434,6 @@ export class StopGapWidgetDriver extends WidgetDriver { } const { - originalEvent, events, nextBatch, prevBatch, @@ -451,7 +450,6 @@ export class StopGapWidgetDriver extends WidgetDriver { }); return { - originalEvent: originalEvent?.getEffectiveEvent(), chunk: events.map(e => e.getEffectiveEvent()), nextBatch, prevBatch, diff --git a/test/stores/widgets/StopGapWidgetDriver-test.ts b/test/stores/widgets/StopGapWidgetDriver-test.ts index 2d4fe90e1f..4e8764a854 100644 --- a/test/stores/widgets/StopGapWidgetDriver-test.ts +++ b/test/stores/widgets/StopGapWidgetDriver-test.ts @@ -209,7 +209,6 @@ describe("StopGapWidgetDriver", () => { }); await expect(driver.readEventRelations('$event')).resolves.toEqual({ - originalEvent: expect.objectContaining({ content: {} }), chunk: [], nextBatch: undefined, prevBatch: undefined, @@ -218,24 +217,6 @@ describe("StopGapWidgetDriver", () => { expect(client.relations).toBeCalledWith('!this-room-id', '$event', null, null, {}); }); - it('reads related events if the original event is missing', async () => { - client.relations.mockResolvedValue({ - // the relations function can return an undefined event, even - // though the typings don't permit an undefined value. - originalEvent: undefined as any, - events: [], - }); - - await expect(driver.readEventRelations('$event', '!room-id')).resolves.toEqual({ - originalEvent: undefined, - chunk: [], - nextBatch: undefined, - prevBatch: undefined, - }); - - expect(client.relations).toBeCalledWith('!room-id', '$event', null, null, {}); - }); - it('reads related events from a selected room', async () => { client.relations.mockResolvedValue({ originalEvent: new MatrixEvent(), @@ -244,7 +225,6 @@ describe("StopGapWidgetDriver", () => { }); await expect(driver.readEventRelations('$event', '!room-id')).resolves.toEqual({ - originalEvent: expect.objectContaining({ content: {} }), chunk: [ expect.objectContaining({ content: {} }), expect.objectContaining({ content: {} }), @@ -272,7 +252,6 @@ describe("StopGapWidgetDriver", () => { 25, 'f', )).resolves.toEqual({ - originalEvent: expect.objectContaining({ content: {} }), chunk: [], nextBatch: undefined, prevBatch: undefined, From 1032334b20c174a2df6eef5280405a81aca8811d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 Oct 2022 01:28:57 -0400 Subject: [PATCH 07/31] Convert inputs on Export/Import Room Key dialogs to be real Fields (#9350) * Convert inputs on Export/Import Room Key dialogs to be real Fields Fixes https://github.com/vector-im/element-web/issues/18517 * Correctly label the second field * Appease the linter --- src/@types/common.ts | 5 ++ .../dialogs/security/ExportE2eKeysDialog.tsx | 68 ++++++++++--------- .../dialogs/security/ImportE2eKeysDialog.tsx | 39 ++++++----- 3 files changed, 61 insertions(+), 51 deletions(-) diff --git a/src/@types/common.ts b/src/@types/common.ts index b4d01a75a5..b18fefc253 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -49,3 +49,8 @@ export type KeysWithObjectShape = { ? (Input[P] extends Array ? never : P) : never; }[keyof Input]; + +export type KeysStartingWith = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + [P in keyof Input]: P extends `${Str}${infer _X}` ? P : never; // we don't use _X +}[keyof Input]; diff --git a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx index 2f2bc36ec1..e020dbeea1 100644 --- a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx +++ b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx @@ -1,5 +1,6 @@ /* Copyright 2017 Vector Creations Ltd +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. @@ -15,7 +16,7 @@ limitations under the License. */ import FileSaver from 'file-saver'; -import React, { createRef } from 'react'; +import React from 'react'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import { logger } from "matrix-js-sdk/src/logger"; @@ -23,6 +24,8 @@ import { _t } from '../../../../languageHandler'; import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption'; import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; +import Field from "../../../../components/views/elements/Field"; +import { KeysStartingWith } from "../../../../@types/common"; enum Phase { Edit = "edit", @@ -36,12 +39,14 @@ interface IProps extends IDialogProps { interface IState { phase: Phase; errStr: string; + passphrase1: string; + passphrase2: string; } +type AnyPassphrase = KeysStartingWith; + export default class ExportE2eKeysDialog extends React.Component { private unmounted = false; - private passphrase1 = createRef(); - private passphrase2 = createRef(); constructor(props: IProps) { super(props); @@ -49,6 +54,8 @@ export default class ExportE2eKeysDialog extends React.Component this.state = { phase: Phase.Edit, errStr: null, + passphrase1: "", + passphrase2: "", }; } @@ -59,8 +66,8 @@ export default class ExportE2eKeysDialog extends React.Component private onPassphraseFormSubmit = (ev: React.FormEvent): boolean => { ev.preventDefault(); - const passphrase = this.passphrase1.current.value; - if (passphrase !== this.passphrase2.current.value) { + const passphrase = this.state.passphrase1; + if (passphrase !== this.state.passphrase2) { this.setState({ errStr: _t('Passphrases must match') }); return false; } @@ -112,6 +119,12 @@ export default class ExportE2eKeysDialog extends React.Component return false; }; + private onPassphraseChange = (ev: React.ChangeEvent, phrase: AnyPassphrase) => { + this.setState({ + [phrase]: ev.target.value, + } as Pick); + }; + public render(): JSX.Element { const disableForm = (this.state.phase === Phase.Exporting); @@ -146,36 +159,25 @@ export default class ExportE2eKeysDialog extends React.Component
-
- -
-
- -
+ this.onPassphraseChange(e, "passphrase1")} + autoFocus={true} + size={64} + type="password" + disabled={disableForm} + />
-
- -
-
- -
+ this.onPassphraseChange(e, "passphrase2")} + size={64} + type="password" + disabled={disableForm} + />
diff --git a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx index 65bbe0a70e..7c710cf676 100644 --- a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx +++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx @@ -1,5 +1,6 @@ /* Copyright 2017 Vector Creations Ltd +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. @@ -22,6 +23,7 @@ import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryptio import { _t } from '../../../../languageHandler'; import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; +import Field from "../../../../components/views/elements/Field"; function readFileAsArrayBuffer(file: File): Promise { return new Promise((resolve, reject) => { @@ -48,12 +50,12 @@ interface IState { enableSubmit: boolean; phase: Phase; errStr: string; + passphrase: string; } export default class ImportE2eKeysDialog extends React.Component { private unmounted = false; private file = createRef(); - private passphrase = createRef(); constructor(props: IProps) { super(props); @@ -62,6 +64,7 @@ export default class ImportE2eKeysDialog extends React.Component enableSubmit: false, phase: Phase.Edit, errStr: null, + passphrase: "", }; } @@ -69,16 +72,22 @@ export default class ImportE2eKeysDialog extends React.Component this.unmounted = true; } - private onFormChange = (ev: React.FormEvent): void => { + private onFormChange = (): void => { const files = this.file.current.files || []; this.setState({ - enableSubmit: (this.passphrase.current.value !== "" && files.length > 0), + enableSubmit: (this.state.passphrase !== "" && files.length > 0), }); }; + private onPassphraseChange = (ev: React.ChangeEvent): void => { + this.setState({ passphrase: ev.target.value }); + this.onFormChange(); // update general form state too + }; + private onFormSubmit = (ev: React.FormEvent): boolean => { ev.preventDefault(); - this.startImport(this.file.current.files[0], this.passphrase.current.value); + // noinspection JSIgnoredPromiseFromCall + this.startImport(this.file.current.files[0], this.state.passphrase); return false; }; @@ -161,20 +170,14 @@ export default class ImportE2eKeysDialog extends React.Component
-
- -
-
- -
+
From bd270b08dfc6bd604cd3dc47ed56e198d6bfdf6a Mon Sep 17 00:00:00 2001 From: Kerry Date: Wed, 5 Oct 2022 13:41:01 +0200 Subject: [PATCH 08/31] Device manager - add foundation for extended device info (#9344) * record device client inforamtion events on app start * matrix-client-information -> matrix_client_information * fix types * remove another unused export * add docs link * display device client information in device details * update snapshots * integration-ish test client information in metadata * tests * fix tests * export helper * DeviceClientInformation type * Device manager - select all devices (#9330) * add device selection that does nothing * multi select and sign out of sessions * test multiple selection * fix type after rebase * select all sessions * rename type * use ExtendedDevice type everywhere * rename clientName to appName for less collision with UA parser * fix bad find and replace * rename ExtendedDeviceInfo to ExtendedDeviceAppInfo * rename DeviceType comp to DeviceTypeIcon * update tests for new required property deviceType * add stubbed user agent parsing --- res/css/_components.pcss | 2 +- ...{_DeviceType.pcss => _DeviceTypeIcon.pcss} | 8 +-- res/css/views/settings/_DevicesPanel.pcss | 2 +- .../views/settings/DevicesPanelEntry.tsx | 8 ++- .../settings/devices/CurrentDeviceSection.tsx | 4 +- .../settings/devices/DeviceDetailHeading.tsx | 4 +- .../views/settings/devices/DeviceDetails.tsx | 4 +- .../views/settings/devices/DeviceTile.tsx | 16 +++-- .../{DeviceType.tsx => DeviceTypeIcon.tsx} | 22 ++++--- .../devices/DeviceVerificationStatusCard.tsx | 4 +- .../settings/devices/FilteredDeviceList.tsx | 28 ++++----- .../devices/SecurityRecommendations.tsx | 6 +- .../views/settings/devices/filter.ts | 6 +- .../views/settings/devices/types.ts | 13 +++-- .../views/settings/devices/useOwnDevices.ts | 23 ++++---- .../settings/tabs/user/SessionManagerTab.tsx | 18 +++--- src/utils/device/parseUserAgent.ts | 45 ++++++++++++++ .../__snapshots__/DevicesPanel-test.tsx.snap | 18 +++--- .../devices/CurrentDeviceSection-test.tsx | 3 + .../devices/DeviceDetailHeading-test.tsx | 2 + .../settings/devices/DeviceDetails-test.tsx | 4 +- .../settings/devices/DeviceTile-test.tsx | 2 + ...eType-test.tsx => DeviceTypeIcon-test.tsx} | 6 +- .../devices/FilteredDeviceList-test.tsx | 19 +++++- .../devices/SelectableDeviceTile-test.tsx | 2 + .../CurrentDeviceSection-test.tsx.snap | 12 ++-- .../__snapshots__/DeviceTile-test.tsx.snap | 24 ++++---- .../__snapshots__/DeviceType-test.tsx.snap | 58 ------------------- .../DeviceTypeIcon-test.tsx.snap | 58 +++++++++++++++++++ .../SelectableDeviceTile-test.tsx.snap | 6 +- .../views/settings/devices/filter-test.ts | 28 +++++++-- .../tabs/user/SessionManagerTab-test.tsx | 8 +-- .../SessionManagerTab-test.tsx.snap | 18 +++--- test/utils/device/parseUserAgent-test.ts | 25 ++++++++ 34 files changed, 319 insertions(+), 187 deletions(-) rename res/css/components/views/settings/devices/{_DeviceType.pcss => _DeviceTypeIcon.pcss} (91%) rename src/components/views/settings/devices/{DeviceType.tsx => DeviceTypeIcon.tsx} (70%) create mode 100644 src/utils/device/parseUserAgent.ts rename test/components/views/settings/devices/{DeviceType-test.tsx => DeviceTypeIcon-test.tsx} (86%) delete mode 100644 test/components/views/settings/devices/__snapshots__/DeviceType-test.tsx.snap create mode 100644 test/components/views/settings/devices/__snapshots__/DeviceTypeIcon-test.tsx.snap create mode 100644 test/utils/device/parseUserAgent-test.ts diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 9161942d87..17fb679f24 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -34,7 +34,7 @@ @import "./components/views/settings/devices/_DeviceExpandDetailsButton.pcss"; @import "./components/views/settings/devices/_DeviceSecurityCard.pcss"; @import "./components/views/settings/devices/_DeviceTile.pcss"; -@import "./components/views/settings/devices/_DeviceType.pcss"; +@import "./components/views/settings/devices/_DeviceTypeIcon.pcss"; @import "./components/views/settings/devices/_FilteredDeviceList.pcss"; @import "./components/views/settings/devices/_FilteredDeviceListHeader.pcss"; @import "./components/views/settings/devices/_SecurityRecommendations.pcss"; diff --git a/res/css/components/views/settings/devices/_DeviceType.pcss b/res/css/components/views/settings/devices/_DeviceTypeIcon.pcss similarity index 91% rename from res/css/components/views/settings/devices/_DeviceType.pcss rename to res/css/components/views/settings/devices/_DeviceTypeIcon.pcss index 66372bbdea..546d4f7ea1 100644 --- a/res/css/components/views/settings/devices/_DeviceType.pcss +++ b/res/css/components/views/settings/devices/_DeviceTypeIcon.pcss @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_DeviceType { +.mx_DeviceTypeIcon { flex: 0 0 auto; position: relative; margin-right: $spacing-8; @@ -22,7 +22,7 @@ limitations under the License. padding: 0 $spacing-8 $spacing-8 0; } -.mx_DeviceType_deviceIcon { +.mx_DeviceTypeIcon_deviceIcon { --background-color: $system; --icon-color: $secondary-content; @@ -36,12 +36,12 @@ limitations under the License. background-color: var(--background-color); } -.mx_DeviceType_selected .mx_DeviceType_deviceIcon { +.mx_DeviceTypeIcon_selected .mx_DeviceTypeIcon_deviceIcon { --background-color: $primary-content; --icon-color: $background; } -.mx_DeviceType_verificationIcon { +.mx_DeviceTypeIcon_verificationIcon { position: absolute; bottom: 0; right: 0; diff --git a/res/css/views/settings/_DevicesPanel.pcss b/res/css/views/settings/_DevicesPanel.pcss index 23a737c977..8a7842d4d0 100644 --- a/res/css/views/settings/_DevicesPanel.pcss +++ b/res/css/views/settings/_DevicesPanel.pcss @@ -58,7 +58,7 @@ limitations under the License. min-height: 35px; padding: 0 $spacing-8; - .mx_DeviceType { + .mx_DeviceTypeIcon { /* hide the new device type in legacy device list for backwards compat reasons */ display: none; diff --git a/src/components/views/settings/DevicesPanelEntry.tsx b/src/components/views/settings/DevicesPanelEntry.tsx index 0109c37b9b..aa152826bf 100644 --- a/src/components/views/settings/DevicesPanelEntry.tsx +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -29,6 +29,7 @@ import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDi import LogoutDialog from '../dialogs/LogoutDialog'; import DeviceTile from './devices/DeviceTile'; import SelectableDeviceTile from './devices/SelectableDeviceTile'; +import { DeviceType } from '../../../utils/device/parseUserAgent'; interface IProps { device: IMyDevice; @@ -153,9 +154,10 @@ export default class DevicesPanelEntry extends React.Component { ; - const deviceWithVerification = { + const extendedDevice = { ...this.props.device, isVerified: this.props.verified, + deviceType: DeviceType.Unknown, }; if (this.props.isOwnDevice) { @@ -163,7 +165,7 @@ export default class DevicesPanelEntry extends React.Component {
- + { buttons } ; @@ -171,7 +173,7 @@ export default class DevicesPanelEntry extends React.Component { return (
- + { buttons }
diff --git a/src/components/views/settings/devices/CurrentDeviceSection.tsx b/src/components/views/settings/devices/CurrentDeviceSection.tsx index 615c9c69f0..fc58617d31 100644 --- a/src/components/views/settings/devices/CurrentDeviceSection.tsx +++ b/src/components/views/settings/devices/CurrentDeviceSection.tsx @@ -24,10 +24,10 @@ import DeviceDetails from './DeviceDetails'; import DeviceExpandDetailsButton from './DeviceExpandDetailsButton'; import DeviceTile from './DeviceTile'; import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard'; -import { DeviceWithVerification } from './types'; +import { ExtendedDevice } from './types'; interface Props { - device?: DeviceWithVerification; + device?: ExtendedDevice; isLoading: boolean; isSigningOut: boolean; localNotificationSettings?: LocalNotificationSettings | undefined; diff --git a/src/components/views/settings/devices/DeviceDetailHeading.tsx b/src/components/views/settings/devices/DeviceDetailHeading.tsx index dea79d3b23..2673ef4e89 100644 --- a/src/components/views/settings/devices/DeviceDetailHeading.tsx +++ b/src/components/views/settings/devices/DeviceDetailHeading.tsx @@ -22,10 +22,10 @@ import Field from '../../elements/Field'; import Spinner from '../../elements/Spinner'; import { Caption } from '../../typography/Caption'; import Heading from '../../typography/Heading'; -import { DeviceWithVerification } from './types'; +import { ExtendedDevice } from './types'; interface Props { - device: DeviceWithVerification; + device: ExtendedDevice; saveDeviceName: (deviceName: string) => Promise; } diff --git a/src/components/views/settings/devices/DeviceDetails.tsx b/src/components/views/settings/devices/DeviceDetails.tsx index b87bfcef3c..4ed50c07b7 100644 --- a/src/components/views/settings/devices/DeviceDetails.tsx +++ b/src/components/views/settings/devices/DeviceDetails.tsx @@ -72,8 +72,8 @@ const DeviceDetails: React.FC = ({ id: 'application', heading: _t('Application'), values: [ - { label: _t('Name'), value: device.clientName }, - { label: _t('Version'), value: device.clientVersion }, + { label: _t('Name'), value: device.appName }, + { label: _t('Version'), value: device.appVersion }, { label: _t('URL'), value: device.url }, ], }, diff --git a/src/components/views/settings/devices/DeviceTile.tsx b/src/components/views/settings/devices/DeviceTile.tsx index bfeabfabb3..4c8e264751 100644 --- a/src/components/views/settings/devices/DeviceTile.tsx +++ b/src/components/views/settings/devices/DeviceTile.tsx @@ -21,16 +21,16 @@ import { _t } from "../../../../languageHandler"; import { formatDate, formatRelativeTime } from "../../../../DateUtils"; import Heading from "../../typography/Heading"; import { INACTIVE_DEVICE_AGE_DAYS, isDeviceInactive } from "./filter"; -import { DeviceWithVerification } from "./types"; -import { DeviceType } from "./DeviceType"; +import { ExtendedDevice } from "./types"; +import { DeviceTypeIcon } from "./DeviceTypeIcon"; export interface DeviceTileProps { - device: DeviceWithVerification; + device: ExtendedDevice; isSelected?: boolean; children?: React.ReactNode; onClick?: () => void; } -const DeviceTileName: React.FC<{ device: DeviceWithVerification }> = ({ device }) => { +const DeviceTileName: React.FC<{ device: ExtendedDevice }> = ({ device }) => { return { device.display_name || device.device_id } ; @@ -48,7 +48,7 @@ const formatLastActivity = (timestamp: number, now = new Date().getTime()): stri return formatRelativeTime(new Date(timestamp)); }; -const getInactiveMetadata = (device: DeviceWithVerification): { id: string, value: React.ReactNode } | undefined => { +const getInactiveMetadata = (device: ExtendedDevice): { id: string, value: React.ReactNode } | undefined => { const isInactive = isDeviceInactive(device); if (!isInactive) { @@ -89,7 +89,11 @@ const DeviceTile: React.FC = ({ ]; return
- +
diff --git a/src/components/views/settings/devices/DeviceType.tsx b/src/components/views/settings/devices/DeviceTypeIcon.tsx similarity index 70% rename from src/components/views/settings/devices/DeviceType.tsx rename to src/components/views/settings/devices/DeviceTypeIcon.tsx index a0fbe75c56..03b921f711 100644 --- a/src/components/views/settings/devices/DeviceType.tsx +++ b/src/components/views/settings/devices/DeviceTypeIcon.tsx @@ -21,33 +21,39 @@ import { Icon as UnknownDeviceIcon } from '../../../../../res/img/element-icons/ import { Icon as VerifiedIcon } from '../../../../../res/img/e2e/verified.svg'; import { Icon as UnverifiedIcon } from '../../../../../res/img/e2e/warning.svg'; import { _t } from '../../../../languageHandler'; -import { DeviceWithVerification } from './types'; +import { ExtendedDevice } from './types'; +import { DeviceType } from '../../../../utils/device/parseUserAgent'; interface Props { - isVerified?: DeviceWithVerification['isVerified']; + isVerified?: ExtendedDevice['isVerified']; isSelected?: boolean; + deviceType?: DeviceType; } -export const DeviceType: React.FC = ({ isVerified, isSelected }) => ( -
= ({ + isVerified, + isSelected, + deviceType, +}) => ( +
{ /* TODO(kerrya) all devices have an unknown type until PSG-650 */ } { isVerified ? : diff --git a/src/components/views/settings/devices/DeviceVerificationStatusCard.tsx b/src/components/views/settings/devices/DeviceVerificationStatusCard.tsx index 11e806e54e..127f5eedf6 100644 --- a/src/components/views/settings/devices/DeviceVerificationStatusCard.tsx +++ b/src/components/views/settings/devices/DeviceVerificationStatusCard.tsx @@ -21,11 +21,11 @@ import AccessibleButton from '../../elements/AccessibleButton'; import DeviceSecurityCard from './DeviceSecurityCard'; import { DeviceSecurityVariation, - DeviceWithVerification, + ExtendedDevice, } from './types'; interface Props { - device: DeviceWithVerification; + device: ExtendedDevice; onVerifyDevice?: () => void; } diff --git a/src/components/views/settings/devices/FilteredDeviceList.tsx b/src/components/views/settings/devices/FilteredDeviceList.tsx index 4cf7ac1a63..c2e8786052 100644 --- a/src/components/views/settings/devices/FilteredDeviceList.tsx +++ b/src/components/views/settings/devices/FilteredDeviceList.tsx @@ -33,7 +33,7 @@ import SelectableDeviceTile from './SelectableDeviceTile'; import { DevicesDictionary, DeviceSecurityVariation, - DeviceWithVerification, + ExtendedDevice, } from './types'; import { DevicesState } from './useOwnDevices'; import FilteredDeviceListHeader from './FilteredDeviceListHeader'; @@ -42,27 +42,27 @@ interface Props { devices: DevicesDictionary; pushers: IPusher[]; localNotificationSettings: Map; - expandedDeviceIds: DeviceWithVerification['device_id'][]; - signingOutDeviceIds: DeviceWithVerification['device_id'][]; - selectedDeviceIds: DeviceWithVerification['device_id'][]; + expandedDeviceIds: ExtendedDevice['device_id'][]; + signingOutDeviceIds: ExtendedDevice['device_id'][]; + selectedDeviceIds: ExtendedDevice['device_id'][]; filter?: DeviceSecurityVariation; onFilterChange: (filter: DeviceSecurityVariation | undefined) => void; - onDeviceExpandToggle: (deviceId: DeviceWithVerification['device_id']) => void; - onSignOutDevices: (deviceIds: DeviceWithVerification['device_id'][]) => void; + onDeviceExpandToggle: (deviceId: ExtendedDevice['device_id']) => void; + onSignOutDevices: (deviceIds: ExtendedDevice['device_id'][]) => void; saveDeviceName: DevicesState['saveDeviceName']; - onRequestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => void; + onRequestDeviceVerification?: (deviceId: ExtendedDevice['device_id']) => void; setPushNotifications: (deviceId: string, enabled: boolean) => Promise; - setSelectedDeviceIds: (deviceIds: DeviceWithVerification['device_id'][]) => void; + setSelectedDeviceIds: (deviceIds: ExtendedDevice['device_id'][]) => void; supportsMSC3881?: boolean | undefined; } const isDeviceSelected = ( - deviceId: DeviceWithVerification['device_id'], - selectedDeviceIds: DeviceWithVerification['device_id'][], + deviceId: ExtendedDevice['device_id'], + selectedDeviceIds: ExtendedDevice['device_id'][], ) => selectedDeviceIds.includes(deviceId); // devices without timestamp metadata should be sorted last -const sortDevicesByLatestActivity = (left: DeviceWithVerification, right: DeviceWithVerification) => +const sortDevicesByLatestActivity = (left: ExtendedDevice, right: ExtendedDevice) => (right.last_seen_ts || 0) - (left.last_seen_ts || 0); const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: DeviceSecurityVariation) => @@ -149,7 +149,7 @@ const NoResults: React.FC = ({ filter, clearFilter }) =>
; const DeviceListItem: React.FC<{ - device: DeviceWithVerification; + device: ExtendedDevice; pusher?: IPusher | undefined; localNotificationSettings?: LocalNotificationSettings | undefined; isExpanded: boolean; @@ -227,11 +227,11 @@ export const FilteredDeviceList = }: Props, ref: ForwardedRef) => { const sortedDevices = getFilteredSortedDevices(devices, filter); - function getPusherForDevice(device: DeviceWithVerification): IPusher | undefined { + function getPusherForDevice(device: ExtendedDevice): IPusher | undefined { return pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === device.device_id); } - const toggleSelection = (deviceId: DeviceWithVerification['device_id']): void => { + const toggleSelection = (deviceId: ExtendedDevice['device_id']): void => { if (isDeviceSelected(deviceId, selectedDeviceIds)) { // remove from selection setSelectedDeviceIds(selectedDeviceIds.filter(id => id !== deviceId)); diff --git a/src/components/views/settings/devices/SecurityRecommendations.tsx b/src/components/views/settings/devices/SecurityRecommendations.tsx index 3132eba38a..ddeb2f2e2e 100644 --- a/src/components/views/settings/devices/SecurityRecommendations.tsx +++ b/src/components/views/settings/devices/SecurityRecommendations.tsx @@ -23,13 +23,13 @@ import DeviceSecurityCard from './DeviceSecurityCard'; import { filterDevicesBySecurityRecommendation, INACTIVE_DEVICE_AGE_DAYS } from './filter'; import { DeviceSecurityVariation, - DeviceWithVerification, + ExtendedDevice, DevicesDictionary, } from './types'; interface Props { devices: DevicesDictionary; - currentDeviceId: DeviceWithVerification['device_id']; + currentDeviceId: ExtendedDevice['device_id']; goToFilteredList: (filter: DeviceSecurityVariation) => void; } @@ -38,7 +38,7 @@ const SecurityRecommendations: React.FC = ({ currentDeviceId, goToFilteredList, }) => { - const devicesArray = Object.values(devices); + const devicesArray = Object.values(devices); const unverifiedDevicesCount = filterDevicesBySecurityRecommendation( devicesArray, diff --git a/src/components/views/settings/devices/filter.ts b/src/components/views/settings/devices/filter.ts index ad2bc92152..05ceb9c697 100644 --- a/src/components/views/settings/devices/filter.ts +++ b/src/components/views/settings/devices/filter.ts @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { DeviceWithVerification, DeviceSecurityVariation } from "./types"; +import { ExtendedDevice, DeviceSecurityVariation } from "./types"; -type DeviceFilterCondition = (device: DeviceWithVerification) => boolean; +type DeviceFilterCondition = (device: ExtendedDevice) => boolean; const MS_DAY = 24 * 60 * 60 * 1000; export const INACTIVE_DEVICE_AGE_MS = 7.776e+9; // 90 days @@ -32,7 +32,7 @@ const filters: Record = { }; export const filterDevicesBySecurityRecommendation = ( - devices: DeviceWithVerification[], + devices: ExtendedDevice[], securityVariations: DeviceSecurityVariation[], ) => { const activeFilters = securityVariations.map(variation => filters[variation]); diff --git a/src/components/views/settings/devices/types.ts b/src/components/views/settings/devices/types.ts index 9543ac2b32..3fa125a09f 100644 --- a/src/components/views/settings/devices/types.ts +++ b/src/components/views/settings/devices/types.ts @@ -16,14 +16,17 @@ limitations under the License. import { IMyDevice } from "matrix-js-sdk/src/matrix"; +import { ExtendedDeviceInformation } from "../../../../utils/device/parseUserAgent"; + export type DeviceWithVerification = IMyDevice & { isVerified: boolean | null }; -export type ExtendedDeviceInfo = { - clientName?: string; - clientVersion?: string; +export type ExtendedDeviceAppInfo = { + // eg Element Web + appName?: string; + appVersion?: string; url?: string; }; -export type ExtendedDevice = DeviceWithVerification & ExtendedDeviceInfo; -export type DevicesDictionary = Record; +export type ExtendedDevice = DeviceWithVerification & ExtendedDeviceAppInfo & ExtendedDeviceInformation; +export type DevicesDictionary = Record; export enum DeviceSecurityVariation { Verified = 'Verified', diff --git a/src/components/views/settings/devices/useOwnDevices.ts b/src/components/views/settings/devices/useOwnDevices.ts index 2441a63a2b..c3b8cb0212 100644 --- a/src/components/views/settings/devices/useOwnDevices.ts +++ b/src/components/views/settings/devices/useOwnDevices.ts @@ -24,6 +24,7 @@ import { MatrixEvent, PUSHER_DEVICE_ID, PUSHER_ENABLED, + UNSTABLE_MSC3852_LAST_SEEN_UA, } from "matrix-js-sdk/src/matrix"; import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning"; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; @@ -34,8 +35,9 @@ import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifi import MatrixClientContext from "../../../../contexts/MatrixClientContext"; import { _t } from "../../../../languageHandler"; import { getDeviceClientInformation } from "../../../../utils/device/clientInformation"; -import { DevicesDictionary, DeviceWithVerification, ExtendedDeviceInfo } from "./types"; +import { DevicesDictionary, ExtendedDevice, ExtendedDeviceAppInfo } from "./types"; import { useEventEmitter } from "../../../../hooks/useEventEmitter"; +import { parseUserAgent } from "../../../../utils/device/parseUserAgent"; const isDeviceVerified = ( matrixClient: MatrixClient, @@ -63,12 +65,12 @@ const isDeviceVerified = ( } }; -const parseDeviceExtendedInformation = (matrixClient: MatrixClient, device: IMyDevice): ExtendedDeviceInfo => { +const parseDeviceExtendedInformation = (matrixClient: MatrixClient, device: IMyDevice): ExtendedDeviceAppInfo => { const { name, version, url } = getDeviceClientInformation(matrixClient, device.device_id); return { - clientName: name, - clientVersion: version, + appName: name, + appVersion: version, url, }; }; @@ -87,6 +89,7 @@ const fetchDevicesWithVerification = async ( ...device, isVerified: isDeviceVerified(matrixClient, crossSigningInfo, device), ...parseDeviceExtendedInformation(matrixClient, device), + ...parseUserAgent(device[UNSTABLE_MSC3852_LAST_SEEN_UA.name]), }, }), {}); @@ -104,10 +107,10 @@ export type DevicesState = { currentDeviceId: string; isLoadingDeviceList: boolean; // not provided when current session cannot request verification - requestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => Promise; + requestDeviceVerification?: (deviceId: ExtendedDevice['device_id']) => Promise; refreshDevices: () => Promise; - saveDeviceName: (deviceId: DeviceWithVerification['device_id'], deviceName: string) => Promise; - setPushNotifications: (deviceId: DeviceWithVerification['device_id'], enabled: boolean) => Promise; + saveDeviceName: (deviceId: ExtendedDevice['device_id'], deviceName: string) => Promise; + setPushNotifications: (deviceId: ExtendedDevice['device_id'], enabled: boolean) => Promise; error?: OwnDevicesError; supportsMSC3881?: boolean | undefined; }; @@ -189,7 +192,7 @@ export const useOwnDevices = (): DevicesState => { const isCurrentDeviceVerified = !!devices[currentDeviceId]?.isVerified; const requestDeviceVerification = isCurrentDeviceVerified && userId - ? async (deviceId: DeviceWithVerification['device_id']) => { + ? async (deviceId: ExtendedDevice['device_id']) => { return await matrixClient.requestVerification( userId, [deviceId], @@ -198,7 +201,7 @@ export const useOwnDevices = (): DevicesState => { : undefined; const saveDeviceName = useCallback( - async (deviceId: DeviceWithVerification['device_id'], deviceName: string): Promise => { + async (deviceId: ExtendedDevice['device_id'], deviceName: string): Promise => { const device = devices[deviceId]; // no change @@ -219,7 +222,7 @@ export const useOwnDevices = (): DevicesState => { }, [matrixClient, devices, refreshDevices]); const setPushNotifications = useCallback( - async (deviceId: DeviceWithVerification['device_id'], enabled: boolean): Promise => { + async (deviceId: ExtendedDevice['device_id'], enabled: boolean): Promise => { try { const pusher = pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === deviceId); if (pusher) { diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index ed1d04a754..2c94d5a5c2 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -29,7 +29,7 @@ import { useOwnDevices } from '../../devices/useOwnDevices'; import { FilteredDeviceList } from '../../devices/FilteredDeviceList'; import CurrentDeviceSection from '../../devices/CurrentDeviceSection'; import SecurityRecommendations from '../../devices/SecurityRecommendations'; -import { DeviceSecurityVariation, DeviceWithVerification } from '../../devices/types'; +import { DeviceSecurityVariation, ExtendedDevice } from '../../devices/types'; import { deleteDevicesWithInteractiveAuth } from '../../devices/deleteDevices'; import SettingsTab from '../SettingsTab'; @@ -38,10 +38,10 @@ const useSignOut = ( onSignoutResolvedCallback: () => Promise, ): { onSignOutCurrentDevice: () => void; - onSignOutOtherDevices: (deviceIds: DeviceWithVerification['device_id'][]) => Promise; - signingOutDeviceIds: DeviceWithVerification['device_id'][]; + onSignOutOtherDevices: (deviceIds: ExtendedDevice['device_id'][]) => Promise; + signingOutDeviceIds: ExtendedDevice['device_id'][]; } => { - const [signingOutDeviceIds, setSigningOutDeviceIds] = useState([]); + const [signingOutDeviceIds, setSigningOutDeviceIds] = useState([]); const onSignOutCurrentDevice = () => { Modal.createDialog( @@ -53,7 +53,7 @@ const useSignOut = ( ); }; - const onSignOutOtherDevices = async (deviceIds: DeviceWithVerification['device_id'][]) => { + const onSignOutOtherDevices = async (deviceIds: ExtendedDevice['device_id'][]) => { if (!deviceIds.length) { return; } @@ -96,8 +96,8 @@ const SessionManagerTab: React.FC = () => { supportsMSC3881, } = useOwnDevices(); const [filter, setFilter] = useState(); - const [expandedDeviceIds, setExpandedDeviceIds] = useState([]); - const [selectedDeviceIds, setSelectedDeviceIds] = useState([]); + const [expandedDeviceIds, setExpandedDeviceIds] = useState([]); + const [selectedDeviceIds, setSelectedDeviceIds] = useState([]); const filteredDeviceListRef = useRef(null); const scrollIntoViewTimeoutRef = useRef>(); @@ -105,7 +105,7 @@ const SessionManagerTab: React.FC = () => { const userId = matrixClient.getUserId(); const currentUserMember = userId && matrixClient.getUser(userId) || undefined; - const onDeviceExpandToggle = (deviceId: DeviceWithVerification['device_id']): void => { + const onDeviceExpandToggle = (deviceId: ExtendedDevice['device_id']): void => { if (expandedDeviceIds.includes(deviceId)) { setExpandedDeviceIds(expandedDeviceIds.filter(id => id !== deviceId)); } else { @@ -136,7 +136,7 @@ const SessionManagerTab: React.FC = () => { ); }; - const onTriggerDeviceVerification = useCallback((deviceId: DeviceWithVerification['device_id']) => { + const onTriggerDeviceVerification = useCallback((deviceId: ExtendedDevice['device_id']) => { if (!requestDeviceVerification) { return; } diff --git a/src/utils/device/parseUserAgent.ts b/src/utils/device/parseUserAgent.ts new file mode 100644 index 0000000000..32c57b7624 --- /dev/null +++ b/src/utils/device/parseUserAgent.ts @@ -0,0 +1,45 @@ +/* +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. +*/ + +export enum DeviceType { + Desktop = 'Desktop', + Mobile = 'Mobile', + Web = 'Web', + Unknown = 'Unknown', +} +export type ExtendedDeviceInformation = { + deviceType: DeviceType; + // eg Google Pixel 6 + deviceModel?: string; + // eg Android 11 + deviceOperatingSystem?: string; + // eg Firefox + clientName?: string; + // eg 1.1.0 + clientVersion?: string; +}; + +export const parseUserAgent = (userAgent?: string): ExtendedDeviceInformation => { + if (!userAgent) { + return { + deviceType: DeviceType.Unknown, + }; + } + // @TODO(kerrya) not yet implemented + return { + deviceType: DeviceType.Unknown, + }; +}; diff --git a/test/components/views/settings/__snapshots__/DevicesPanel-test.tsx.snap b/test/components/views/settings/__snapshots__/DevicesPanel-test.tsx.snap index df46340de3..05c0ca8c98 100644 --- a/test/components/views/settings/__snapshots__/DevicesPanel-test.tsx.snap +++ b/test/components/views/settings/__snapshots__/DevicesPanel-test.tsx.snap @@ -112,16 +112,16 @@ exports[` renders device panel with devices 1`] = ` data-testid="device-tile-device_1" >