diff --git a/ts/background.ts b/ts/background.ts index a03e5afb7e36..33132acd1927 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -32,6 +32,7 @@ import { filter } from './util/iterables'; import { isNotNil } from './util/isNotNil'; import { senderCertificateService } from './services/senderCertificate'; import { GROUP_CREDENTIALS_KEY } from './services/groupCredentialFetcher'; +import * as KeyboardLayout from './services/keyboardLayout'; import { routineProfileRefresh } from './routineProfileRefresh'; import { isMoreRecentThan, isOlderThan, toDayMillis } from './util/timestamp'; import { isValidReactionEmoji } from './reactions/isValidReactionEmoji'; @@ -142,6 +143,8 @@ export async function cleanupSessionResets(): Promise { } export async function startApp(): Promise { + await KeyboardLayout.initialize(); + window.Whisper.events = window._.clone(window.Backbone.Events); window.Signal.Util.MessageController.install(); window.Signal.conversationControllerStart(); @@ -1145,7 +1148,7 @@ export async function startApp(): Promise { }; document.addEventListener('keydown', event => { - const { ctrlKey, key, metaKey, shiftKey } = event; + const { ctrlKey, metaKey, shiftKey } = event; const commandKey = window.platform === 'darwin' && metaKey; const controlKey = window.platform !== 'darwin' && ctrlKey; @@ -1155,6 +1158,8 @@ export async function startApp(): Promise { const selectedId = state.conversations.selectedConversationId; const conversation = window.ConversationController.get(selectedId); + const key = KeyboardLayout.lookup(event); + // NAVIGATION // Show keyboard shortcuts - handled by Electron-managed keyboard shortcuts diff --git a/ts/components/CallScreen.tsx b/ts/components/CallScreen.tsx index 3cf5cd163ae2..8514917e1980 100644 --- a/ts/components/CallScreen.tsx +++ b/ts/components/CallScreen.tsx @@ -40,6 +40,7 @@ import { GroupCallRemoteParticipants } from './GroupCallRemoteParticipants'; import { LocalizerType } from '../types/Util'; import { NeedsScreenRecordingPermissionsModal } from './NeedsScreenRecordingPermissionsModal'; import { missingCaseError } from '../util/missingCaseError'; +import * as KeyboardLayout from '../services/keyboardLayout'; import { useActivateSpeakerViewOnPresenting } from '../hooks/useActivateSpeakerViewOnPresenting'; export type PropsType = { @@ -205,10 +206,12 @@ export const CallScreen: React.FC = ({ const handleKeyDown = (event: KeyboardEvent): void => { let eventHandled = false; - if (event.shiftKey && (event.key === 'V' || event.key === 'v')) { + const key = KeyboardLayout.lookup(event); + + if (event.shiftKey && (key === 'V' || key === 'v')) { toggleVideo(); eventHandled = true; - } else if (event.shiftKey && (event.key === 'M' || event.key === 'm')) { + } else if (event.shiftKey && (key === 'M' || key === 'm')) { toggleAudio(); eventHandled = true; } diff --git a/ts/components/CallingLobby.tsx b/ts/components/CallingLobby.tsx index 82577c01fc36..1147f920dfca 100644 --- a/ts/components/CallingLobby.tsx +++ b/ts/components/CallingLobby.tsx @@ -19,6 +19,7 @@ import { } from './CallingLobbyJoinButton'; import { AvatarColorType } from '../types/Colors'; import { LocalizerType } from '../types/Util'; +import * as KeyboardLayout from '../services/keyboardLayout'; import { ConversationType } from '../state/ducks/conversations'; import { isConversationTooBigToRing } from '../conversations/isConversationTooBigToRing'; @@ -116,10 +117,11 @@ export const CallingLobby = ({ function handleKeyDown(event: KeyboardEvent): void { let eventHandled = false; - if (event.shiftKey && (event.key === 'V' || event.key === 'v')) { + const key = KeyboardLayout.lookup(event); + if (event.shiftKey && (key === 'V' || key === 'v')) { toggleVideo(); eventHandled = true; - } else if (event.shiftKey && (event.key === 'M' || event.key === 'm')) { + } else if (event.shiftKey && (key === 'M' || key === 'm')) { toggleAudio(); eventHandled = true; } diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 90d607fb22cb..dcc009ab756c 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -67,6 +67,7 @@ import { emojiToData } from '../emoji/lib'; import type { SmartReactionPicker } from '../../state/smart/ReactionPicker'; import { getCustomColorStyle } from '../../util/getCustomColorStyle'; import { offsetDistanceModifier } from '../../util/popperUtil'; +import * as KeyboardLayout from '../../services/keyboardLayout'; type Trigger = { handleContextClick: (event: React.MouseEvent) => void; @@ -2283,8 +2284,10 @@ export class Message extends React.PureComponent { // Do not allow reactions to error messages const { canReply } = this.props; + const key = KeyboardLayout.lookup(event.nativeEvent); + if ( - (event.key === 'E' || event.key === 'e') && + (key === 'E' || key === 'e') && (event.metaKey || event.ctrlKey) && event.shiftKey && canReply diff --git a/ts/services/keyboardLayout.ts b/ts/services/keyboardLayout.ts new file mode 100644 index 000000000000..e89370f33e4e --- /dev/null +++ b/ts/services/keyboardLayout.ts @@ -0,0 +1,31 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { strictAssert } from '../util/assert'; + +type LayoutMapType = { get(code: string): string | undefined }; + +let layoutMap: LayoutMapType | undefined; + +export async function initialize(): Promise { + strictAssert(layoutMap === undefined, 'keyboardLayout already initialized'); + + const experimentalNavigator = (window.navigator as unknown) as { + keyboard: { getLayoutMap(): Promise }; + }; + + strictAssert( + typeof experimentalNavigator.keyboard?.getLayoutMap === 'function', + 'No support for getLayoutMap' + ); + + layoutMap = await experimentalNavigator.keyboard.getLayoutMap(); +} + +export function lookup({ + code, + key, +}: Pick): string | undefined { + strictAssert(layoutMap !== undefined, 'keyboardLayout not initialized'); + return layoutMap.get(code) ?? key; +}