From cbc1f18fa9175e25cd3b65f06ab51565525c2a74 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 26 Jul 2023 12:53:05 +0100 Subject: [PATCH 1/4] Add getUserOperatingSystem util --- platforms/web/lib/utils.test.ts | 81 +++++++++++++++++++++++++++++++++ platforms/web/lib/utils.ts | 42 +++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 platforms/web/lib/utils.test.ts create mode 100644 platforms/web/lib/utils.ts diff --git a/platforms/web/lib/utils.test.ts b/platforms/web/lib/utils.test.ts new file mode 100644 index 000000000..92490ba0b --- /dev/null +++ b/platforms/web/lib/utils.test.ts @@ -0,0 +1,81 @@ +/* +Copyright 2023 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 { afterAll, beforeAll } from 'vitest'; + +import { getUserOperatingSystem } from './utils'; + +const mockUserAgent = (ua: string) => { + Object.defineProperty(window.navigator, 'userAgent', { + value: ua, + writable: true, + }); +}; + +describe('utils', () => { + describe('getUserOperatingSystem', () => { + let originalUserAgent = ''; + + beforeAll(() => { + originalUserAgent = navigator.userAgent; + }); + + afterAll(() => { + mockUserAgent(originalUserAgent); + }); + + test('returns null for unknown operating systems', () => { + mockUserAgent('wut?!'); + const os = getUserOperatingSystem(); + expect(os).toBeNull(); + }); + + test.each([ + [ + 'Windows', + // eslint-disable-next-line max-len + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246', + ], + [ + 'macOS', + // eslint-disable-next-line max-len + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9', + ], + [ + 'Linux', + // eslint-disable-next-line max-len + 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1', + ], + [ + 'iOS', + // eslint-disable-next-line max-len + 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/69.0.3497.105 Mobile/15E148 Safari/605.1', + ], + [ + 'Android', + // eslint-disable-next-line max-len + 'Mozilla/5.0 (Linux; Android 13; SM-S901B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36', + ], + ])( + 'should correctly detect %s', + (expectedOperatingSystem, userAgent) => { + mockUserAgent(userAgent); + const os = getUserOperatingSystem(); + expect(os).toBe(expectedOperatingSystem); + }, + ); + }); +}); diff --git a/platforms/web/lib/utils.ts b/platforms/web/lib/utils.ts new file mode 100644 index 000000000..e03462bad --- /dev/null +++ b/platforms/web/lib/utils.ts @@ -0,0 +1,42 @@ +/* +Copyright 2023 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. +*/ + +/** + * Findss the operating system of the user + * @returns {string|null} the operating system, `null` if the operating system is unknown + */ +export function getUserOperatingSystem(): + | 'Windows' + | 'macOS' + | 'Linux' + | 'iOS' + | 'Android' + | null { + const userAgent = navigator.userAgent.toLowerCase(); + if (userAgent.includes('iphone') || userAgent.includes('ipad')) { + return 'iOS'; + } else if (userAgent.includes('android')) { + return 'Android'; + } else if (userAgent.includes('win')) { + return 'Windows'; + } else if (userAgent.includes('mac')) { + return 'macOS'; + } else if (userAgent.includes('linux')) { + return 'Linux'; + } else { + return null; + } +} From 49a9ccdf1b2e3dbdc65d77c1fa7e4b8777dbc31a Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 26 Jul 2023 13:09:49 +0100 Subject: [PATCH 2/4] Fix ctrl+backspace shortcut on linux/windows --- platforms/web/lib/useListeners/event.test.ts | 51 +++++++++++++------ platforms/web/lib/useListeners/event.ts | 11 +++++ platforms/web/lib/utils.test.ts | 52 ++++++++++---------- 3 files changed, 73 insertions(+), 41 deletions(-) diff --git a/platforms/web/lib/useListeners/event.test.ts b/platforms/web/lib/useListeners/event.test.ts index 6c9b1299a..61d865035 100644 --- a/platforms/web/lib/useListeners/event.test.ts +++ b/platforms/web/lib/useListeners/event.test.ts @@ -19,6 +19,7 @@ import init, { new_composer_model } from '../../generated/wysiwyg'; import { extractActionStates, handleKeyDown } from './event'; import { FormatBlockEvent } from './types'; import { FormattingFunctions } from '../types'; +import { WINDOWS_UA, mockUserAgent } from '../utils.test'; beforeAll(async () => { await init(); @@ -44,6 +45,16 @@ describe('getFormattingState', () => { }); describe('handleKeyDown', () => { + let originalUserAgent = ''; + + beforeAll(() => { + originalUserAgent = navigator.userAgent; + }); + + afterAll(() => { + mockUserAgent(originalUserAgent); + }); + it.each([ ['formatBold', { ctrlKey: true, key: 'b' }], ['formatBold', { metaKey: true, key: 'b' }], @@ -60,21 +71,31 @@ describe('handleKeyDown', () => { ['sendMessage', { ctrlKey: true, key: 'Enter' }], ['sendMessage', { metaKey: true, key: 'Enter' }], ['formatStrikeThrough', { shiftKey: true, altKey: true, key: '5' }], - ])('Should dispatch %s when %o', async (expected, input) => { - const elem = document.createElement('input'); - const event = new KeyboardEvent('keydown', input); - - const result = new Promise((resolve) => { - elem.addEventListener('wysiwygInput', (({ - detail: { blockType }, - }: FormatBlockEvent) => { - resolve(blockType); - }) as EventListener); - }); + ['deleteWordBackward', { ctrlKey: true, key: 'Backspace' }, WINDOWS_UA], + ])( + 'Should dispatch %s when %o', + async (expected, input, userAgent?: string) => { + if (userAgent) { + mockUserAgent(userAgent); + } else { + mockUserAgent(originalUserAgent); + } - const model = new_composer_model(); + const elem = document.createElement('input'); + const event = new KeyboardEvent('keydown', input); - handleKeyDown(event, elem, model, {} as FormattingFunctions); - expect(await result).toBe(expected); - }); + const result = new Promise((resolve) => { + elem.addEventListener('wysiwygInput', (({ + detail: { blockType }, + }: FormatBlockEvent) => { + resolve(blockType); + }) as EventListener); + }); + + const model = new_composer_model(); + + handleKeyDown(event, elem, model, {} as FormattingFunctions); + expect(await result).toBe(expected); + }, + ); }); diff --git a/platforms/web/lib/useListeners/event.ts b/platforms/web/lib/useListeners/event.ts index 30e8b2107..03c53b885 100644 --- a/platforms/web/lib/useListeners/event.ts +++ b/platforms/web/lib/useListeners/event.ts @@ -37,6 +37,7 @@ import { TestUtilities } from '../useTestCases/types'; import { AllActionStates } from '../types'; import { mapToAllActionStates } from './utils'; import { AtRoomSuggestionEvent, LinkEvent, SuggestionEvent } from './types'; +import { getUserOperatingSystem } from '../utils'; /** * Send a custom event named wysiwygInput @@ -82,6 +83,16 @@ function getInputFromKeyDown( } } + const operatingSystem = getUserOperatingSystem(); + if (operatingSystem === 'Windows' || operatingSystem === 'Linux') { + if (e.ctrlKey) { + switch (e.key) { + case 'Backspace': + return 'deleteWordBackward'; + } + } + } + if (e.ctrlKey || e.metaKey) { switch (e.key) { case 'b': diff --git a/platforms/web/lib/utils.test.ts b/platforms/web/lib/utils.test.ts index 92490ba0b..5c5a24c4a 100644 --- a/platforms/web/lib/utils.test.ts +++ b/platforms/web/lib/utils.test.ts @@ -18,13 +18,33 @@ import { afterAll, beforeAll } from 'vitest'; import { getUserOperatingSystem } from './utils'; -const mockUserAgent = (ua: string) => { +export const mockUserAgent = (ua: string) => { Object.defineProperty(window.navigator, 'userAgent', { value: ua, writable: true, }); }; +export const WINDOWS_UA = + // eslint-disable-next-line max-len + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246'; + +export const MAC_OS_UA = + // eslint-disable-next-line max-len + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9'; + +export const LINUX_UA = + // eslint-disable-next-line max-len + 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1'; + +export const IOS_UA = + // eslint-disable-next-line max-len + 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/69.0.3497.105 Mobile/15E148 Safari/605.1'; + +export const ANDROID_UA = + // eslint-disable-next-line max-len + 'Mozilla/5.0 (Linux; Android 13; SM-S901B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36'; + describe('utils', () => { describe('getUserOperatingSystem', () => { let originalUserAgent = ''; @@ -44,31 +64,11 @@ describe('utils', () => { }); test.each([ - [ - 'Windows', - // eslint-disable-next-line max-len - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246', - ], - [ - 'macOS', - // eslint-disable-next-line max-len - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9', - ], - [ - 'Linux', - // eslint-disable-next-line max-len - 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1', - ], - [ - 'iOS', - // eslint-disable-next-line max-len - 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/69.0.3497.105 Mobile/15E148 Safari/605.1', - ], - [ - 'Android', - // eslint-disable-next-line max-len - 'Mozilla/5.0 (Linux; Android 13; SM-S901B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36', - ], + ['Windows', WINDOWS_UA], + ['macOS', MAC_OS_UA], + ['Linux', LINUX_UA], + ['iOS', IOS_UA], + ['Android', ANDROID_UA], ])( 'should correctly detect %s', (expectedOperatingSystem, userAgent) => { From 25d0095b0af1ab4d51f55c90253f62730e961440 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 26 Jul 2023 15:28:50 +0100 Subject: [PATCH 3/4] Fix typo Co-authored-by: Florian Duros --- platforms/web/lib/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platforms/web/lib/utils.ts b/platforms/web/lib/utils.ts index e03462bad..665060fee 100644 --- a/platforms/web/lib/utils.ts +++ b/platforms/web/lib/utils.ts @@ -15,7 +15,7 @@ limitations under the License. */ /** - * Findss the operating system of the user + * Finds the operating system of the user * @returns {string|null} the operating system, `null` if the operating system is unknown */ export function getUserOperatingSystem(): From d0aa064cc9e9b350c7d3860c0cc3d5b4430a6b4b Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 26 Jul 2023 15:29:50 +0100 Subject: [PATCH 4/4] Only mock when needed --- platforms/web/lib/useListeners/event.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/platforms/web/lib/useListeners/event.test.ts b/platforms/web/lib/useListeners/event.test.ts index 61d865035..643ddc008 100644 --- a/platforms/web/lib/useListeners/event.test.ts +++ b/platforms/web/lib/useListeners/event.test.ts @@ -77,8 +77,6 @@ describe('handleKeyDown', () => { async (expected, input, userAgent?: string) => { if (userAgent) { mockUserAgent(userAgent); - } else { - mockUserAgent(originalUserAgent); } const elem = document.createElement('input');