diff --git a/.eslintrc.js b/.eslintrc.js index 2fb1cc943d8..45cd05506f6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -40,6 +40,36 @@ module.exports = { ), ], + // Ban matrix-js-sdk/src imports in favour of matrix-js-sdk/src/matrix imports to prevent unleashing hell. + "no-restricted-imports": ["error", { + "paths": [{ + "name": "matrix-js-sdk", + "message": "Please use matrix-js-sdk/src/matrix instead", + }, { + "name": "matrix-js-sdk/", + "message": "Please use matrix-js-sdk/src/matrix instead", + }, { + "name": "matrix-js-sdk/src", + "message": "Please use matrix-js-sdk/src/matrix instead", + }, { + "name": "matrix-js-sdk/src/", + "message": "Please use matrix-js-sdk/src/matrix instead", + }, { + "name": "matrix-js-sdk/src/index", + "message": "Please use matrix-js-sdk/src/matrix instead", + }, { + "name": "matrix-react-sdk", + "message": "Please use matrix-react-sdk/src/index instead", + }, { + "name": "matrix-react-sdk/", + "message": "Please use matrix-react-sdk/src/index instead", + }], + "patterns": [{ + "group": ["matrix-js-sdk/lib", "matrix-js-sdk/lib/", "matrix-js-sdk/lib/**"], + "message": "Please use matrix-js-sdk/src/* instead", + }], + }], + // There are too many a11y violations to fix at once // Turn violated rules off until they are fixed "jsx-a11y/alt-text": "off", @@ -57,32 +87,71 @@ module.exports = { "jsx-a11y/role-supports-aria-props": "off", "jsx-a11y/tabindex-no-positive": "off", }, - overrides: [{ - files: [ - "src/**/*.{ts,tsx}", - "test/**/*.{ts,tsx}", - ], - extends: [ - "plugin:matrix-org/typescript", - "plugin:matrix-org/react", - ], - rules: { - // Things we do that break the ideal style - "prefer-promise-reject-errors": "off", - "quotes": "off", - "no-extra-boolean-cast": "off", + overrides: [ + { + files: [ + "src/**/*.{ts,tsx}", + "test/**/*.{ts,tsx}", + ], + extends: [ + "plugin:matrix-org/typescript", + "plugin:matrix-org/react", + ], + rules: { + // Things we do that break the ideal style + "prefer-promise-reject-errors": "off", + "quotes": "off", + "no-extra-boolean-cast": "off", - // Remove Babel things manually due to override limitations - "@babel/no-invalid-this": ["off"], + // Remove Babel things manually due to override limitations + "@babel/no-invalid-this": ["off"], - // We're okay being explicit at the moment - "@typescript-eslint/no-empty-interface": "off", - // We disable this while we're transitioning - "@typescript-eslint/no-explicit-any": "off", - // We'd rather not do this but we do - "@typescript-eslint/ban-ts-comment": "off", + // We're okay being explicit at the moment + "@typescript-eslint/no-empty-interface": "off", + // We disable this while we're transitioning + "@typescript-eslint/no-explicit-any": "off", + // We'd rather not do this but we do + "@typescript-eslint/ban-ts-comment": "off", + }, }, - }], + // temporary override for offending icon require files + { + files: [ + "src/SdkConfig.ts", + "src/components/structures/FileDropTarget.tsx", + "src/components/structures/RoomStatusBar.tsx", + "src/components/structures/UserMenu.tsx", + "src/components/views/avatars/WidgetAvatar.tsx", + "src/components/views/dialogs/AddExistingToSpaceDialog.tsx", + "src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx", + "src/components/views/dialogs/ForwardDialog.tsx", + "src/components/views/dialogs/InviteDialog.tsx", + "src/components/views/dialogs/ModalWidgetDialog.tsx", + "src/components/views/dialogs/UploadConfirmDialog.tsx", + "src/components/views/dialogs/security/SetupEncryptionDialog.tsx", + "src/components/views/elements/AddressTile.tsx", + "src/components/views/elements/AppWarning.tsx", + "src/components/views/elements/SSOButtons.tsx", + "src/components/views/messages/MAudioBody.tsx", + "src/components/views/messages/MImageBody.tsx", + "src/components/views/messages/MFileBody.tsx", + "src/components/views/messages/MStickerBody.tsx", + "src/components/views/messages/MVideoBody.tsx", + "src/components/views/messages/MVoiceMessageBody.tsx", + "src/components/views/right_panel/EncryptionPanel.tsx", + "src/components/views/rooms/EntityTile.tsx", + "src/components/views/rooms/LinkPreviewGroup.tsx", + "src/components/views/rooms/MemberList.tsx", + "src/components/views/rooms/MessageComposer.tsx", + "src/components/views/rooms/ReplyPreview.tsx", + "src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx", + "src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx" + ], + rules: { + "@typescript-eslint/no-var-requires": "off", + }, + } + ], settings: { react: { version: "detect", diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aa6b18fc2a..a50e17b108c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,151 @@ +Changes in [3.42.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.42.0) (2022-03-15) +===================================================================================================== + +## 🔒 SECURITY FIXES + + * Fix a bug where URL previews could be enabled in the left-panel when they + should not have been. + +## ✨ Features + * Add unexposed account setting for hiding poll creation ([\#7972](https://github.com/matrix-org/matrix-react-sdk/pull/7972)). + * Allow pinning polls ([\#7922](https://github.com/matrix-org/matrix-react-sdk/pull/7922)). Fixes vector-im/element-web#20152. + * Make trailing `:` into a setting ([\#6711](https://github.com/matrix-org/matrix-react-sdk/pull/6711)). Fixes vector-im/element-web#16682. Contributed by @SimonBrandner. + * Location sharing > back button ([\#7958](https://github.com/matrix-org/matrix-react-sdk/pull/7958)). + * use LocationAssetType ([\#7965](https://github.com/matrix-org/matrix-react-sdk/pull/7965)). + * Location share type UI ([\#7924](https://github.com/matrix-org/matrix-react-sdk/pull/7924)). + * Add a few more UIComponent flags, and ensure they are used in existing code ([\#7937](https://github.com/matrix-org/matrix-react-sdk/pull/7937)). + * Add support for overriding strings in the app ([\#7886](https://github.com/matrix-org/matrix-react-sdk/pull/7886)). + * Add support for redirecting to external pages after logout ([\#7905](https://github.com/matrix-org/matrix-react-sdk/pull/7905)). + * Expose redaction power level in room settings ([\#7599](https://github.com/matrix-org/matrix-react-sdk/pull/7599)). Fixes vector-im/element-web#20590. Contributed by @SimonBrandner. + * Update and expand ways to access pinned messages ([\#7906](https://github.com/matrix-org/matrix-react-sdk/pull/7906)). Fixes vector-im/element-web#21209 and vector-im/element-web#21211. + * Add slash command to switch to a room's virtual room ([\#7839](https://github.com/matrix-org/matrix-react-sdk/pull/7839)). + +## 🐛 Bug Fixes + * Merge pull request from GHSA-qmf4-7w7j-vf23 ([\#8059](https://github.com/matrix-org/matrix-react-sdk/pull/8059)). + * Add another null guard for member ([\#7984](https://github.com/matrix-org/matrix-react-sdk/pull/7984)). Fixes vector-im/element-web#21319. + * Fix room account settings ([\#7999](https://github.com/matrix-org/matrix-react-sdk/pull/7999)). + * Fix missing summary text for pinned message changes ([\#7989](https://github.com/matrix-org/matrix-react-sdk/pull/7989)). Fixes vector-im/element-web#19823. + * Pass room to getRoomTombstone to avoid racing with setState ([\#7986](https://github.com/matrix-org/matrix-react-sdk/pull/7986)). + * Hide composer and call buttons when the room is tombstoned ([\#7975](https://github.com/matrix-org/matrix-react-sdk/pull/7975)). Fixes vector-im/element-web#21286. + * Fix bad ternary statement in autocomplete user pill insertions ([\#7977](https://github.com/matrix-org/matrix-react-sdk/pull/7977)). Fixes vector-im/element-web#21307. + * Fix sending locations into threads and fix i18n ([\#7943](https://github.com/matrix-org/matrix-react-sdk/pull/7943)). Fixes vector-im/element-web#21267. + * Fix location map attribution rendering over message action bar ([\#7974](https://github.com/matrix-org/matrix-react-sdk/pull/7974)). Fixes vector-im/element-web#21297. + * Fix wrongly asserting that PushRule::conditions is non-null ([\#7973](https://github.com/matrix-org/matrix-react-sdk/pull/7973)). Fixes vector-im/element-web#21305. + * Fix account & room settings race condition ([\#7953](https://github.com/matrix-org/matrix-react-sdk/pull/7953)). Fixes vector-im/element-web#21163. + * Fix bug with some space selections not being applied ([\#7971](https://github.com/matrix-org/matrix-react-sdk/pull/7971)). Fixes vector-im/element-web#21290. + * Revert "replace all require(.svg) with esm import" ([\#7969](https://github.com/matrix-org/matrix-react-sdk/pull/7969)). Fixes vector-im/element-web#21293. + * Hide unpinnable pinned messages in more cases ([\#7921](https://github.com/matrix-org/matrix-react-sdk/pull/7921)). + * Fix room list being laggy while scrolling 🐌 ([\#7939](https://github.com/matrix-org/matrix-react-sdk/pull/7939)). Fixes vector-im/element-web#21262. + * Make pinned messages more reliably reflect edits ([\#7920](https://github.com/matrix-org/matrix-react-sdk/pull/7920)). Fixes vector-im/element-web#17098. + * Improve accessibility of the BetaPill ([\#7949](https://github.com/matrix-org/matrix-react-sdk/pull/7949)). Fixes vector-im/element-web#21255. + * Autofocus correct composer after sending reaction ([\#7950](https://github.com/matrix-org/matrix-react-sdk/pull/7950)). Fixes vector-im/element-web#21273. + * Consider polls as message events for rendering redactions ([\#7944](https://github.com/matrix-org/matrix-react-sdk/pull/7944)). Fixes vector-im/element-web#21125. + * Prevent event tiles being shrunk/collapsed by flexbox ([\#7942](https://github.com/matrix-org/matrix-react-sdk/pull/7942)). Fixes vector-im/element-web#21269. + * Fix ExportDialog title on export cancellation ([\#7936](https://github.com/matrix-org/matrix-react-sdk/pull/7936)). Fixes vector-im/element-web#21260. Contributed by @luixxiul. + * Mandate use of js-sdk/src/matrix import over js-sdk/src ([\#7933](https://github.com/matrix-org/matrix-react-sdk/pull/7933)). Fixes vector-im/element-web#21253. + * Fix backspace not working in the invite dialog ([\#7931](https://github.com/matrix-org/matrix-react-sdk/pull/7931)). Fixes vector-im/element-web#21249. Contributed by @SimonBrandner. + * Fix right panel soft crashes due to missing room prop ([\#7923](https://github.com/matrix-org/matrix-react-sdk/pull/7923)). Fixes vector-im/element-web#21243. + * fix color of location share caret ([\#7917](https://github.com/matrix-org/matrix-react-sdk/pull/7917)). + * Wrap all EventTiles with a TileErrorBoundary and guard parsePermalink ([\#7916](https://github.com/matrix-org/matrix-react-sdk/pull/7916)). Fixes vector-im/element-web#21216. + * Fix changing space sometimes bouncing to the wrong space ([\#7910](https://github.com/matrix-org/matrix-react-sdk/pull/7910)). Fixes vector-im/element-web#20425. + * Ensure EventListSummary key does not change during backpagination ([\#7915](https://github.com/matrix-org/matrix-react-sdk/pull/7915)). Fixes vector-im/element-web#9192. + * Fix positioning of the thread context menu ([\#7918](https://github.com/matrix-org/matrix-react-sdk/pull/7918)). Fixes vector-im/element-web#21236. + * Inject sender into pinned messages ([\#7904](https://github.com/matrix-org/matrix-react-sdk/pull/7904)). Fixes vector-im/element-web#20314. + * Tweak info message padding in right panel timeline ([\#7901](https://github.com/matrix-org/matrix-react-sdk/pull/7901)). Fixes vector-im/element-web#21212. + * Fix another freeze on room switch ([\#7900](https://github.com/matrix-org/matrix-react-sdk/pull/7900)). Fixes vector-im/element-web#21127. + * Clean up error listener when location picker closes ([\#7902](https://github.com/matrix-org/matrix-react-sdk/pull/7902)). Fixes vector-im/element-web#21213. + * Fix edge case in context menu chevron positioning ([\#7899](https://github.com/matrix-org/matrix-react-sdk/pull/7899)). + * Fix composer format buttons on WebKit ([\#7898](https://github.com/matrix-org/matrix-react-sdk/pull/7898)). Fixes vector-im/element-web#20868. + * manage voicerecording state when deleting or sending a voice message ([\#7896](https://github.com/matrix-org/matrix-react-sdk/pull/7896)). Fixes vector-im/element-web#21151. + * Fix bug with useRoomHierarchy tight-looping loadMore on error ([\#7893](https://github.com/matrix-org/matrix-react-sdk/pull/7893)). + * Fix upload button & shortcut not working for narrow composer mode ([\#7894](https://github.com/matrix-org/matrix-react-sdk/pull/7894)). Fixes vector-im/element-web#21175 and vector-im/element-web#21142. + * Fix emoji insertion in thread composer going to the main composer ([\#7895](https://github.com/matrix-org/matrix-react-sdk/pull/7895)). Fixes vector-im/element-web#21202. + * Try harder to keep context menus inside the window ([\#7863](https://github.com/matrix-org/matrix-react-sdk/pull/7863)). Fixes vector-im/element-web#17527 and vector-im/element-web#18377. + * Fix edge case around event list summary layout ([\#7891](https://github.com/matrix-org/matrix-react-sdk/pull/7891)). Fixes vector-im/element-web#21180. + * Fix event list summary 1 hidden message pluralisation ([\#7890](https://github.com/matrix-org/matrix-react-sdk/pull/7890)). Fixes vector-im/element-web#21196. + * Fix vanishing recently viewed menu ([\#7887](https://github.com/matrix-org/matrix-react-sdk/pull/7887)). Fixes vector-im/element-web#20827. + * Fix freeze on room switch ([\#7884](https://github.com/matrix-org/matrix-react-sdk/pull/7884)). Fixes vector-im/element-web#21127. + * Check 'useSystemTheme' in quick settings theme switcher ([\#7809](https://github.com/matrix-org/matrix-react-sdk/pull/7809)). Fixes vector-im/element-web#21061. + * Fix 'my threads' filtering to include participated threads ([\#7882](https://github.com/matrix-org/matrix-react-sdk/pull/7882)). Fixes vector-im/element-web#20877. + * Remove log line to try to fix freeze on answering VoIP call ([\#7883](https://github.com/matrix-org/matrix-react-sdk/pull/7883)). + * Support social login & password on soft logout page ([\#7879](https://github.com/matrix-org/matrix-react-sdk/pull/7879)). Fixes vector-im/element-web#21099. + * Fix missing padding on server picker ([\#7864](https://github.com/matrix-org/matrix-react-sdk/pull/7864)). + * Throttle RoomState.members handlers ([\#7876](https://github.com/matrix-org/matrix-react-sdk/pull/7876)). Fixes vector-im/element-web#21127. + * Only show joined/invited in search dialog ([\#7875](https://github.com/matrix-org/matrix-react-sdk/pull/7875)). Fixes vector-im/element-web#21161. + * Don't pillify code blocks ([\#7861](https://github.com/matrix-org/matrix-react-sdk/pull/7861)). Fixes vector-im/element-web#20851 and vector-im/element-web#18687. + * Fix keyboard shortcut icons on macOS ([\#7869](https://github.com/matrix-org/matrix-react-sdk/pull/7869)). + +Changes in [3.42.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.42.0-rc.1) (2022-03-08) +=============================================================================================================== + +## ✨ Features + * Add unexposed account setting for hiding poll creation ([\#7972](https://github.com/matrix-org/matrix-react-sdk/pull/7972)). + * Allow pinning polls ([\#7922](https://github.com/matrix-org/matrix-react-sdk/pull/7922)). Fixes vector-im/element-web#20152. + * Make trailing `:` into a setting ([\#6711](https://github.com/matrix-org/matrix-react-sdk/pull/6711)). Fixes vector-im/element-web#16682. Contributed by @SimonBrandner. + * Location sharing > back button ([\#7958](https://github.com/matrix-org/matrix-react-sdk/pull/7958)). + * use LocationAssetType ([\#7965](https://github.com/matrix-org/matrix-react-sdk/pull/7965)). + * Location share type UI ([\#7924](https://github.com/matrix-org/matrix-react-sdk/pull/7924)). + * Add a few more UIComponent flags, and ensure they are used in existing code ([\#7937](https://github.com/matrix-org/matrix-react-sdk/pull/7937)). + * Add support for overriding strings in the app ([\#7886](https://github.com/matrix-org/matrix-react-sdk/pull/7886)). + * Add support for redirecting to external pages after logout ([\#7905](https://github.com/matrix-org/matrix-react-sdk/pull/7905)). + * Expose redaction power level in room settings ([\#7599](https://github.com/matrix-org/matrix-react-sdk/pull/7599)). Fixes vector-im/element-web#20590. Contributed by @SimonBrandner. + * Update and expand ways to access pinned messages ([\#7906](https://github.com/matrix-org/matrix-react-sdk/pull/7906)). Fixes vector-im/element-web#21209 and vector-im/element-web#21211. + * Add slash command to switch to a room's virtual room ([\#7839](https://github.com/matrix-org/matrix-react-sdk/pull/7839)). + +## 🐛 Bug Fixes + * Add another null guard for member ([\#7984](https://github.com/matrix-org/matrix-react-sdk/pull/7984)). Fixes vector-im/element-web#21319. + * Fix room account settings ([\#7999](https://github.com/matrix-org/matrix-react-sdk/pull/7999)). + * Fix missing summary text for pinned message changes ([\#7989](https://github.com/matrix-org/matrix-react-sdk/pull/7989)). Fixes vector-im/element-web#19823. + * Pass room to getRoomTombstone to avoid racing with setState ([\#7986](https://github.com/matrix-org/matrix-react-sdk/pull/7986)). + * Hide composer and call buttons when the room is tombstoned ([\#7975](https://github.com/matrix-org/matrix-react-sdk/pull/7975)). Fixes vector-im/element-web#21286. + * Fix bad ternary statement in autocomplete user pill insertions ([\#7977](https://github.com/matrix-org/matrix-react-sdk/pull/7977)). Fixes vector-im/element-web#21307. + * Fix sending locations into threads and fix i18n ([\#7943](https://github.com/matrix-org/matrix-react-sdk/pull/7943)). Fixes vector-im/element-web#21267. + * Fix location map attribution rendering over message action bar ([\#7974](https://github.com/matrix-org/matrix-react-sdk/pull/7974)). Fixes vector-im/element-web#21297. + * Fix wrongly asserting that PushRule::conditions is non-null ([\#7973](https://github.com/matrix-org/matrix-react-sdk/pull/7973)). Fixes vector-im/element-web#21305. + * Fix account & room settings race condition ([\#7953](https://github.com/matrix-org/matrix-react-sdk/pull/7953)). Fixes vector-im/element-web#21163. + * Fix bug with some space selections not being applied ([\#7971](https://github.com/matrix-org/matrix-react-sdk/pull/7971)). Fixes vector-im/element-web#21290. + * Revert "replace all require(.svg) with esm import" ([\#7969](https://github.com/matrix-org/matrix-react-sdk/pull/7969)). Fixes vector-im/element-web#21293. + * Hide unpinnable pinned messages in more cases ([\#7921](https://github.com/matrix-org/matrix-react-sdk/pull/7921)). + * Fix room list being laggy while scrolling 🐌 ([\#7939](https://github.com/matrix-org/matrix-react-sdk/pull/7939)). Fixes vector-im/element-web#21262. + * Make pinned messages more reliably reflect edits ([\#7920](https://github.com/matrix-org/matrix-react-sdk/pull/7920)). Fixes vector-im/element-web#17098. + * Improve accessibility of the BetaPill ([\#7949](https://github.com/matrix-org/matrix-react-sdk/pull/7949)). Fixes vector-im/element-web#21255. + * Autofocus correct composer after sending reaction ([\#7950](https://github.com/matrix-org/matrix-react-sdk/pull/7950)). Fixes vector-im/element-web#21273. + * Consider polls as message events for rendering redactions ([\#7944](https://github.com/matrix-org/matrix-react-sdk/pull/7944)). Fixes vector-im/element-web#21125. + * Prevent event tiles being shrunk/collapsed by flexbox ([\#7942](https://github.com/matrix-org/matrix-react-sdk/pull/7942)). Fixes vector-im/element-web#21269. + * Fix ExportDialog title on export cancellation ([\#7936](https://github.com/matrix-org/matrix-react-sdk/pull/7936)). Fixes vector-im/element-web#21260. Contributed by @luixxiul. + * Mandate use of js-sdk/src/matrix import over js-sdk/src ([\#7933](https://github.com/matrix-org/matrix-react-sdk/pull/7933)). Fixes vector-im/element-web#21253. + * Fix backspace not working in the invite dialog ([\#7931](https://github.com/matrix-org/matrix-react-sdk/pull/7931)). Fixes vector-im/element-web#21249. Contributed by @SimonBrandner. + * Fix right panel soft crashes due to missing room prop ([\#7923](https://github.com/matrix-org/matrix-react-sdk/pull/7923)). Fixes vector-im/element-web#21243. + * fix color of location share caret ([\#7917](https://github.com/matrix-org/matrix-react-sdk/pull/7917)). + * Wrap all EventTiles with a TileErrorBoundary and guard parsePermalink ([\#7916](https://github.com/matrix-org/matrix-react-sdk/pull/7916)). Fixes vector-im/element-web#21216. + * Fix changing space sometimes bouncing to the wrong space ([\#7910](https://github.com/matrix-org/matrix-react-sdk/pull/7910)). Fixes vector-im/element-web#20425. + * Ensure EventListSummary key does not change during backpagination ([\#7915](https://github.com/matrix-org/matrix-react-sdk/pull/7915)). Fixes vector-im/element-web#9192. + * Fix positioning of the thread context menu ([\#7918](https://github.com/matrix-org/matrix-react-sdk/pull/7918)). Fixes vector-im/element-web#21236. + * Inject sender into pinned messages ([\#7904](https://github.com/matrix-org/matrix-react-sdk/pull/7904)). Fixes vector-im/element-web#20314. + * Tweak info message padding in right panel timeline ([\#7901](https://github.com/matrix-org/matrix-react-sdk/pull/7901)). Fixes vector-im/element-web#21212. + * Fix another freeze on room switch ([\#7900](https://github.com/matrix-org/matrix-react-sdk/pull/7900)). Fixes vector-im/element-web#21127. + * Clean up error listener when location picker closes ([\#7902](https://github.com/matrix-org/matrix-react-sdk/pull/7902)). Fixes vector-im/element-web#21213. + * Fix edge case in context menu chevron positioning ([\#7899](https://github.com/matrix-org/matrix-react-sdk/pull/7899)). + * Fix composer format buttons on WebKit ([\#7898](https://github.com/matrix-org/matrix-react-sdk/pull/7898)). Fixes vector-im/element-web#20868. + * manage voicerecording state when deleting or sending a voice message ([\#7896](https://github.com/matrix-org/matrix-react-sdk/pull/7896)). Fixes vector-im/element-web#21151. + * Fix bug with useRoomHierarchy tight-looping loadMore on error ([\#7893](https://github.com/matrix-org/matrix-react-sdk/pull/7893)). + * Fix upload button & shortcut not working for narrow composer mode ([\#7894](https://github.com/matrix-org/matrix-react-sdk/pull/7894)). Fixes vector-im/element-web#21175 and vector-im/element-web#21142. + * Fix emoji insertion in thread composer going to the main composer ([\#7895](https://github.com/matrix-org/matrix-react-sdk/pull/7895)). Fixes vector-im/element-web#21202. + * Try harder to keep context menus inside the window ([\#7863](https://github.com/matrix-org/matrix-react-sdk/pull/7863)). Fixes vector-im/element-web#17527 and vector-im/element-web#18377. + * Fix edge case around event list summary layout ([\#7891](https://github.com/matrix-org/matrix-react-sdk/pull/7891)). Fixes vector-im/element-web#21180. + * Fix event list summary 1 hidden message pluralisation ([\#7890](https://github.com/matrix-org/matrix-react-sdk/pull/7890)). Fixes vector-im/element-web#21196. + * Fix vanishing recently viewed menu ([\#7887](https://github.com/matrix-org/matrix-react-sdk/pull/7887)). Fixes vector-im/element-web#20827. + * Fix freeze on room switch ([\#7884](https://github.com/matrix-org/matrix-react-sdk/pull/7884)). Fixes vector-im/element-web#21127. + * Check 'useSystemTheme' in quick settings theme switcher ([\#7809](https://github.com/matrix-org/matrix-react-sdk/pull/7809)). Fixes vector-im/element-web#21061. + * Fix 'my threads' filtering to include participated threads ([\#7882](https://github.com/matrix-org/matrix-react-sdk/pull/7882)). Fixes vector-im/element-web#20877. + * Remove log line to try to fix freeze on answering VoIP call ([\#7883](https://github.com/matrix-org/matrix-react-sdk/pull/7883)). + * Support social login & password on soft logout page ([\#7879](https://github.com/matrix-org/matrix-react-sdk/pull/7879)). Fixes vector-im/element-web#21099. + * Fix missing padding on server picker ([\#7864](https://github.com/matrix-org/matrix-react-sdk/pull/7864)). + * Throttle RoomState.members handlers ([\#7876](https://github.com/matrix-org/matrix-react-sdk/pull/7876)). Fixes vector-im/element-web#21127. + * Only show joined/invited in search dialog ([\#7875](https://github.com/matrix-org/matrix-react-sdk/pull/7875)). Fixes vector-im/element-web#21161. + * Don't pillify code blocks ([\#7861](https://github.com/matrix-org/matrix-react-sdk/pull/7861)). Fixes vector-im/element-web#20851 and vector-im/element-web#18687. + * Fix keyboard shortcut icons on macOS ([\#7869](https://github.com/matrix-org/matrix-react-sdk/pull/7869)). + Changes in [3.41.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.41.1) (2022-03-01) ===================================================================================================== diff --git a/__mocks__/maplibre-gl.js b/__mocks__/maplibre-gl.js new file mode 100644 index 00000000000..b1f114e8eff --- /dev/null +++ b/__mocks__/maplibre-gl.js @@ -0,0 +1,20 @@ +const EventEmitter = require("events"); +const { LngLat } = require('maplibre-gl'); + +class MockMap extends EventEmitter { + addControl = jest.fn(); + removeControl = jest.fn(); +} +class MockGeolocateControl extends EventEmitter { + +} +class MockMarker extends EventEmitter { + setLngLat = jest.fn().mockReturnValue(this); + addTo = jest.fn(); +} +module.exports = { + Map: MockMap, + GeolocateControl: MockGeolocateControl, + Marker: MockMarker, + LngLat, +}; diff --git a/__mocks__/svg.js b/__mocks__/svg.js new file mode 100644 index 00000000000..c2dc9d9aa73 --- /dev/null +++ b/__mocks__/svg.js @@ -0,0 +1,2 @@ +export const Icon = 'div'; +export default "image-file-stub"; diff --git a/docs/features/keyboardShortcuts.md b/docs/features/keyboardShortcuts.md new file mode 100644 index 00000000000..71814023543 --- /dev/null +++ b/docs/features/keyboardShortcuts.md @@ -0,0 +1,59 @@ +# Keyboard shortcuts + +## Using the `KeyBindingManger` + +The `KeyBindingManager` (accessible using `getKeyBindingManager()`) is a class +with several methods that allow you to get a `KeyBindingAction` based on a +`KeyboardEvent | React.KeyboardEvent`. + +The event passed to the `KeyBindingManager` gets compared to the list of +shortcuts that are retrieved from the `IKeyBindingsProvider`s. The +`IKeyBindingsProvider` is in `KeyBindingDefaults`. + +### Examples + +Let's say we want to close a menu when the correct keys were pressed: + +```ts +const onKeyDown = (ev: KeyboardEvent): void => { + let handled = true; + const action = getKeyBindingManager().getAccessibilityAction(ev) + switch (action) { + case KeyBindingAction.Escape: + closeMenu(); + break; + default: + handled = false; + break; + } + + if (handled) { + ev.preventDefault(); + ev.stopPropagation(); + } +} +``` + +## Managing keyboard shortcuts + +There are a few things at play when it comes to keyboard shortcuts. The +`KeyBindingManager` gets `IKeyBindingsProvider`s one of which is +`defaultBindingsProvider` defined in `KeyBindingDefaults`. In +`KeyBindingDefaults` a `getBindingsByCategory()` method is used to create +`KeyBinding`s based on `KeyboardShortcutSetting`s defined in +`KeyboardShortcuts`. + +### Adding keyboard shortcuts + +To add a keyboard shortcut there are two files we have to look at: +`KeyboardShortcuts.ts` and `KeyBindingDefaults.ts`. In most cases we only need +to edit `KeyboardShortcuts.ts`: add a `KeyBindingAction` and add the +`KeyBindingAction` to the `KEYBOARD_SHORTCUTS` object. + +Though, to make matters worse, sometimes we want to add a shortcut that has +multiple keybindings associated with. This keyboard shortcut won't be +customizable as it would be rather difficult to manage both from the point of +the settings and the UI. To do this, we have to add a `KeyBindingAction` and add +the UI representation of that keyboard shortcut to the `getUIOnlyShortcuts()` +method. Then, we also need to add the keybinding to the correct method in +`KeyBindingDefaults`. diff --git a/docs/icons.md b/docs/icons.md new file mode 100644 index 00000000000..d478b5407d0 --- /dev/null +++ b/docs/icons.md @@ -0,0 +1,44 @@ +# Icons + +Icons are loaded using [@svgr/webpack](https://www.npmjs.com/package/@svgr/webpack). This is configured in [element-web](https://github.com/vector-im/element-web/blob/develop/webpack.config.js#L458) + +Each .svg exports a `ReactComponent` at the named export `Icon`. +Icons have `role="presentation"` and `aria-hidden` automatically applied. These can be overriden by passing props to the icon component. + +eg +``` +import { Icon as FavoriteIcon } from 'res/img/element-icons/favorite.svg'; + +const MyComponent = () => { + return <> + + + ; +} +``` + +## Styling + +Icon components are svg elements and can be styled as usual. + +``` +// _MyComponents.scss +.mx_MyComponent-icon { + height: 20px; + width: 20px; + + * { + fill: $accent; + } +} + +// MyComponent.tsx +import { Icon as FavoriteIcon } from 'res/img/element-icons/favorite.svg'; + +const MyComponent = () => { + return <> + + + ; +} +``` \ No newline at end of file diff --git a/package.json b/package.json index 050fa0af8ef..9330f066a4e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.41.1", + "version": "3.42.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -91,9 +91,9 @@ "linkifyjs": "^4.0.0-beta.4", "lodash": "^4.17.20", "maplibre-gl": "^1.15.2", - "matrix-analytics-events": "github:matrix-org/matrix-analytics-events.git#8e75aaf0b3e045587daeaf97a7691dbfda2f20c0", + "matrix-analytics-events": "github:matrix-org/matrix-analytics-events.git#daad3faed54f0b1f1e026a7498b4653e4d01cd90", "matrix-events-sdk": "^0.0.1-beta.7", - "matrix-js-sdk": "15.6.0", + "matrix-js-sdk": "16.0.0", "matrix-widget-api": "^0.1.0-beta.18", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", @@ -169,13 +169,13 @@ "concurrently": "^5.3.0", "enzyme": "^3.11.0", "enzyme-to-json": "^3.6.2", - "eslint": "7.18.0", + "eslint": "8.9.0", "eslint-config-google": "^0.14.0", "eslint-plugin-import": "^2.25.4", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-matrix-org": "^0.4.0", - "eslint-plugin-react": "^7.22.0", - "eslint-plugin-react-hooks": "^4.2.0", + "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.3.0", "glob": "^7.1.6", "jest": "^27.4.0", "jest-canvas-mock": "^2.3.0", @@ -215,7 +215,8 @@ "/test/setupTests.js" ], "moduleNameMapper": { - "\\.(gif|png|svg|ttf|woff2)$": "/__mocks__/imageMock.js", + "\\.(gif|png|ttf|woff2)$": "/__mocks__/imageMock.js", + "\\.svg$": "/__mocks__/svg.js", "\\$webapp/i18n/languages.json": "/__mocks__/languages.json", "decoderWorker\\.min\\.js": "/__mocks__/empty.js", "decoderWorker\\.min\\.wasm": "/__mocks__/empty.js", diff --git a/res/css/_components.scss b/res/css/_components.scss index e9819fd89ea..c88fcbf52a1 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -4,12 +4,17 @@ @import "./_font-sizes.scss"; @import "./_font-weights.scss"; @import "./_spacing.scss"; +@import "./components/views/location/_LocationShareMenu.scss"; +@import "./components/views/location/_ShareDialogButtons.scss"; +@import "./components/views/location/_ShareType.scss"; +@import "./components/views/spaces/_QuickThemeSwitcher.scss"; @import "./structures/_AutoHideScrollbar.scss"; @import "./structures/_BackdropPanel.scss"; @import "./structures/_CompatibilityPage.scss"; @import "./structures/_ContextualMenu.scss"; @import "./structures/_CreateRoom.scss"; @import "./structures/_CustomRoomTagPanel.scss"; +@import "./structures/_FileDropTarget.scss"; @import "./structures/_FilePanel.scss"; @import "./structures/_GenericErrorPage.scss"; @import "./structures/_GroupFilterPanel.scss"; @@ -141,11 +146,11 @@ @import "./views/elements/_Dropdown.scss"; @import "./views/elements/_EditableItemList.scss"; @import "./views/elements/_ErrorBoundary.scss"; -@import "./views/elements/_GenericEventListSummary.scss"; @import "./views/elements/_EventTilePreview.scss"; @import "./views/elements/_ExternalLink.scss"; @import "./views/elements/_FacePile.scss"; @import "./views/elements/_Field.scss"; +@import "./views/elements/_GenericEventListSummary.scss"; @import "./views/elements/_ImageView.scss"; @import "./views/elements/_InfoTooltip.scss"; @import "./views/elements/_InlineSpinner.scss"; @@ -183,9 +188,9 @@ @import "./views/messages/_CallEvent.scss"; @import "./views/messages/_CreateEvent.scss"; @import "./views/messages/_DateSeparator.scss"; -@import "./views/messages/_JumpToDatePicker.scss"; @import "./views/messages/_EventTileBubble.scss"; @import "./views/messages/_HiddenBody.scss"; +@import "./views/messages/_JumpToDatePicker.scss"; @import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MFileBody.scss"; @import "./views/messages/_MImageBody.scss"; diff --git a/res/css/components/views/location/_LocationShareMenu.scss b/res/css/components/views/location/_LocationShareMenu.scss new file mode 100644 index 00000000000..f27935b6daf --- /dev/null +++ b/res/css/components/views/location/_LocationShareMenu.scss @@ -0,0 +1,22 @@ +/* +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_LocationShareMenu { + width: 375px; + height: 460px; + display: flex; + flex-direction: column; +} diff --git a/res/css/components/views/location/_ShareDialogButtons.scss b/res/css/components/views/location/_ShareDialogButtons.scss new file mode 100644 index 00000000000..c6d77d2da84 --- /dev/null +++ b/res/css/components/views/location/_ShareDialogButtons.scss @@ -0,0 +1,52 @@ +/* +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_ShareDialogButtons { + position: absolute; + width: 100%; + height: 0; + top: 0; +} + +.mx_ShareDialogButtons_button { + @mixin ButtonResetDefault; + height: 24px; + width: 24px; + border-radius: 50%; + background-color: $quinary-content; + opacity: 0.8; + text-align: center; + color: $secondary-content; + position: absolute; + top: $spacing-16; + + &:hover, &:focus { + opacity: 1; + } + + &.left { + left: $spacing-16; + } + + &.right { + right: $spacing-16; + } +} + +.mx_ShareDialogButtons_button-icon { + height: 10px; + margin-top: 3px; +} diff --git a/res/css/components/views/location/_ShareType.scss b/res/css/components/views/location/_ShareType.scss new file mode 100644 index 00000000000..ba21a7caa0d --- /dev/null +++ b/res/css/components/views/location/_ShareType.scss @@ -0,0 +1,108 @@ +/* +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_ShareType { + flex: 1; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + + padding: 60px $spacing-12 $spacing-32; + + color: $primary-content; +} + +.mx_ShareType_badge { + height: 60px; + width: 60px; + margin-bottom: $spacing-20; + background-color: $accent; + border-radius: 50%; + border: 14px solid $accent; + // colors icon + color: white; + box-sizing: border-box; +} + +.mx_ShareType_heading { + padding-bottom: $spacing-32; + text-align: center; +} + +.mx_ShareType_option { + @mixin ButtonResetDefault; + + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + padding: $spacing-8 $spacing-20; + margin-top: $spacing-12; + + color: $primary-content; + border: 1px solid $quinary-content; + border-radius: 8px; + + font-size: $font-15px; + + &:hover, &:focus { + border-color: $accent; + } + + // this style is only during active development + // when lab is enabled but feature not fully implemented + // pin drop option will be disabled + &.mx_AccessibleButton_disabled { + pointer-events: none; + opacity: 0.4; + } +} + +.mx_ShareType_option-icon { + height: 40px; + width: 40px; + box-sizing: border-box; + margin-right: $spacing-12; + flex: 0 0 40px; + border-width: 2px; + border-style: solid; + border-radius: 50%; + + &.Own { + // color is set by user color class + // generated from id + border-color: currentColor; + } + + &.Live { + background-color: $location-live-color; + // 20% brightness $location-live-color + border-color: #deddfd; + padding: 2px; + // colors icon + color: white; + } + + &.Pin { + border-color: $accent; + background-color: $accent; + padding: 7px; + // colors icon + color: white; + } +} diff --git a/res/css/components/views/spaces/_QuickThemeSwitcher.scss b/res/css/components/views/spaces/_QuickThemeSwitcher.scss new file mode 100644 index 00000000000..c0ca83eb177 --- /dev/null +++ b/res/css/components/views/spaces/_QuickThemeSwitcher.scss @@ -0,0 +1,40 @@ +/* +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_QuickThemeSwitcher { + display: flex; + align-items: center; + + .mx_Dropdown { + min-width: 100px; + margin-left: auto; + height: min-content; + } + + .mx_Dropdown_menu { + max-height: 70px; + } +} + +.mx_QuickThemeSwitcher_heading { + font-weight: $font-semi-bold; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-content; + text-transform: uppercase; + display: inline-block; + margin: 0; +} diff --git a/res/css/structures/_FileDropTarget.scss b/res/css/structures/_FileDropTarget.scss new file mode 100644 index 00000000000..b5963385bf2 --- /dev/null +++ b/res/css/structures/_FileDropTarget.scss @@ -0,0 +1,65 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +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. +*/ + +@keyframes mx_FileDropTarget_animation { + from { + opacity: 0; + } + to { + opacity: 0.95; + } +} + +.mx_FileDropTarget { + min-width: 0; + width: 100%; + height: 100%; + + font-size: $font-18px; + text-align: center; + + pointer-events: none; + + background-color: $background; + opacity: 0.95; + + position: absolute; + z-index: 3000; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + animation: mx_FileDropTarget_animation; + animation-duration: 0.5s; +} + +@keyframes mx_FileDropTarget_image_animation { + from { + transform: scaleX(0); + } + to { + transform: scaleX(1); + } +} + +.mx_FileDropTarget_image { + width: 32px; + animation: mx_FileDropTarget_image_animation; + animation-duration: 0.5s; + margin-bottom: 16px; +} diff --git a/res/css/structures/_QuickSettingsButton.scss b/res/css/structures/_QuickSettingsButton.scss index c490b05e89d..17417fa36f4 100644 --- a/res/css/structures/_QuickSettingsButton.scss +++ b/res/css/structures/_QuickSettingsButton.scss @@ -82,21 +82,6 @@ limitations under the License. .mx_QuickSettingsButton_pinToSidebarHeading { padding-left: 24px; position: relative; - - &::before { - background-color: $secondary-content; - content: ""; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - mask-image: url('$(res)/img/element-icons/room/pin-upright.svg'); - width: 16px; - height: 16px; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - } } .mx_Checkbox { @@ -112,31 +97,9 @@ limitations under the License. font-size: $font-15px; line-height: $font-24px; color: $secondary-content; - - &::before { - background-color: $secondary-content; - content: ""; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - width: 16px; - height: 16px; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - } } } - .mx_QuickSettingsButton_favouritesCheckbox .mx_Checkbox_background + div::before { - mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg'); - } - - .mx_QuickSettingsButton_peopleCheckbox .mx_Checkbox_background + div::before { - mask-image: url('$(res)/img/element-icons/room/members.svg'); - } - .mx_QuickSettingsButton_moreOptionsButton { padding-left: 22px; margin-left: 22px; @@ -145,45 +108,17 @@ limitations under the License. color: $secondary-content; position: relative; margin-bottom: 16px; - - &::before { - background-color: $secondary-content; - content: ""; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - width: 16px; - height: 16px; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); - } } +} - .mx_QuickSettingsButton_themePicker { - display: flex; - align-items: center; - - > h4 { - font-weight: $font-semi-bold; - font-size: $font-12px; - line-height: $font-15px; - color: $secondary-content; - text-transform: uppercase; - display: inline-block; - margin: 0; - } - - .mx_Dropdown { - min-width: 100px; - margin-left: auto; - height: min-content; - } - - .mx_Dropdown_menu { - max-height: 70px; - } +.mx_QuickSettingsButton_icon { + * { + fill: $secondary-content; } + width: 16px; + height: 16px; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); } diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index bb4622b7dae..a6b2970ddf6 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -32,56 +32,6 @@ limitations under the License. position: relative; } -@keyframes mx_RoomView_fileDropTarget_animation { - from { - opacity: 0; - } - to { - opacity: 0.95; - } -} - -.mx_RoomView_fileDropTarget { - min-width: 0px; - width: 100%; - height: 100%; - - font-size: $font-18px; - text-align: center; - - pointer-events: none; - - background-color: $background; - opacity: 0.95; - - position: absolute; - z-index: 3000; - - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - animation: mx_RoomView_fileDropTarget_animation; - animation-duration: 0.5s; -} - -@keyframes mx_RoomView_fileDropTarget_image_animation { - from { - transform: scaleX(0); - } - to { - transform: scaleX(1); - } -} - -.mx_RoomView_fileDropTarget_image { - width: 32px; - animation: mx_RoomView_fileDropTarget_image_animation; - animation-duration: 0.5s; - margin-bottom: 16px; -} - .mx_RoomView_auxPanel { min-width: 0px; width: 100%; diff --git a/res/css/structures/_TabbedView.scss b/res/css/structures/_TabbedView.scss index 96cad03bef8..a666bcee1bc 100644 --- a/res/css/structures/_TabbedView.scss +++ b/res/css/structures/_TabbedView.scss @@ -55,7 +55,6 @@ limitations under the License. .mx_TabbedView_maskedIcon { width: 16px; height: 16px; - margin-left: 8px; margin-right: 16px; } @@ -122,7 +121,7 @@ limitations under the License. align-items: center; vertical-align: text-top; cursor: pointer; - padding: 8px 0; + padding: 8px; border-radius: $border-radius-8px; font-size: $font-13px; position: relative; diff --git a/res/css/views/beta/_BetaCard.scss b/res/css/views/beta/_BetaCard.scss index dbbc008a069..80a70ad0d62 100644 --- a/res/css/views/beta/_BetaCard.scss +++ b/res/css/views/beta/_BetaCard.scss @@ -105,7 +105,7 @@ limitations under the License. display: inline-block; vertical-align: text-bottom; - &.mx_BetaCard_betaPill_clickable { + &.mx_AccessibleButton { cursor: pointer; } } diff --git a/res/css/views/elements/_ServerPicker.scss b/res/css/views/elements/_ServerPicker.scss index 484a777a97d..865867c94fb 100644 --- a/res/css/views/elements/_ServerPicker.scss +++ b/res/css/views/elements/_ServerPicker.scss @@ -16,6 +16,7 @@ limitations under the License. .mx_ServerPicker { margin-bottom: 14px; + padding-bottom: $spacing-16; border-bottom: 1px solid rgba(141, 151, 165, 0.2); display: grid; grid-template-columns: auto min-content; @@ -79,7 +80,6 @@ limitations under the License. color: $tertiary-content; grid-column: 1 / 2; grid-row: 3; - margin-bottom: 16px; } } diff --git a/res/css/views/location/_LocationPicker.scss b/res/css/views/location/_LocationPicker.scss index 125b33994cc..76e56eedd9f 100644 --- a/res/css/views/location/_LocationPicker.scss +++ b/res/css/views/location/_LocationPicker.scss @@ -15,11 +15,9 @@ limitations under the License. */ .mx_LocationPicker { - width: 375px; - height: 460px; - border-radius: 8px; + height: 100%; position: relative; #mx_LocationPicker_map { @@ -27,11 +25,15 @@ limitations under the License. border-radius: 8px; .maplibregl-ctrl.maplibregl-ctrl-group { + // place below the close button + // padding-16 + 24px close button + padding-10 margin-top: 50px; + margin-right: $spacing-16; } .maplibregl-ctrl-bottom-right { bottom: 68px; + margin-right: $spacing-16; } .maplibregl-user-location-accuracy-circle { @@ -58,7 +60,22 @@ limitations under the License. .mx_MLocationBody_pointer { position: absolute; bottom: -3px; - left: 12px; + left: 11px; + width: 9px; + height: 5px; + + &::before { + mask-image: url('$(res)/img/location/pointer.svg'); + mask-position: center; + mask-repeat: no-repeat; + mask-size: 9px; + content: ''; + display: inline-block; + width: 9px; + height: 5px; + position: absolute; + background-color: $accent; + } } } @@ -78,34 +95,6 @@ limitations under the License. min-width: 328px; min-height: 48px; } - - button.mx_LocationPicker_cancelButton { - border: none; - border-radius: 12px; - position: absolute; - top: -360px; - right: 5px; - background-color: $quinary-content; - width: 24px; - height: 24px; - padding: 0px; - color: rgba(0, 0, 0, 0); - } - - button.mx_LocationPicker_cancelButton::before { - content: ''; - background-color: $primary-content; - min-width: 8px; - min-height: 8px; - width: 8px; - height: 8px; - position: absolute; - margin: 4px 8px; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - mask-image: url('$(res)/img/cancel-small.svg'); - } } } diff --git a/res/css/views/messages/_MLocationBody.scss b/res/css/views/messages/_MLocationBody.scss index 071c953424f..e4135d46d44 100644 --- a/res/css/views/messages/_MLocationBody.scss +++ b/res/css/views/messages/_MLocationBody.scss @@ -18,6 +18,7 @@ limitations under the License. .mx_MLocationBody_map { width: 450px; height: 300px; + z-index: 0; // keeps the entire map under the message action bar border-radius: $timeline-image-border-radius; } @@ -38,17 +39,31 @@ limitations under the License. .mx_MLocationBody_pointer { position: absolute; bottom: -3px; - left: 12px; + left: 11px; + width: 9px; + height: 5px; + + &::before { + mask-image: url('$(res)/img/location/pointer.svg'); + mask-position: center; + mask-repeat: no-repeat; + mask-size: 9px; + content: ''; + display: inline-block; + width: 9px; + height: 5px; + position: absolute; + background-color: $accent; + } } .mx_MLocationBody_markerContents { background-color: $location-marker-color; - margin: 4px; - width: 24px; - height: 24px; - padding-top: 8px; + margin: 0; + width: 31px; + height: 31px; mask-repeat: no-repeat; - mask-size: contain; + mask-size: 16px; mask-position: center; mask-image: url('$(res)/img/element-icons/location.svg'); } diff --git a/res/css/views/right_panel/_RoomSummaryCard.scss b/res/css/views/right_panel/_RoomSummaryCard.scss index 08d8502150f..a3bee47b5b3 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.scss +++ b/res/css/views/right_panel/_RoomSummaryCard.scss @@ -259,6 +259,10 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/files.svg'); } +.mx_RoomSummaryCard_icon_pins::before { + mask-image: url('$(res)/img/element-icons/room/pin-upright.svg'); +} + .mx_RoomSummaryCard_icon_threads::before { mask-image: url('$(res)/img/element-icons/message/thread.svg'); } diff --git a/res/css/views/right_panel/_ThreadPanel.scss b/res/css/views/right_panel/_ThreadPanel.scss index 31ddbcafdac..cb438c47068 100644 --- a/res/css/views/right_panel/_ThreadPanel.scss +++ b/res/css/views/right_panel/_ThreadPanel.scss @@ -106,11 +106,18 @@ limitations under the License. padding-right: 16px; } - &.mx_ThreadView .mx_AutoHideScrollbar { + &.mx_ThreadView .mx_ThreadView_timelinePanelWrapper { /* the scrollbar is 8px wide, and we want a 12px gap with the side of the panel. Hence the magic number, 8+4=12 */ width: calc(100% - 4px); padding-right: 4px; + position: relative; + min-height: 0; // don't displace the composer + flex-grow: 1; + + .mx_FileDropTarget { + border-radius: 8px; + } } .mx_RoomView_MessageList { @@ -191,6 +198,28 @@ limitations under the License. font-size: $font-12px; color: $secondary-content; } + + // handling for hidden events (e.g reactions) in the thread view + &.mx_ThreadView .mx_GenericEventListSummary_unstyledList .mx_EventTile_info { + .mx_EventTile_line { + padding-left: 0 !important; // override main timeline padding + + .mx_EventTile_content { + margin-left: 54px; // align with text + width: calc(100% - 54px - 8px); // match width of parent + } + } + + .mx_EventTile_avatar { + position: absolute; + left: 36px !important; // override main timeline positioning + z-index: 9; // position above the hover styling + } + + .mx_ViewSourceEvent_toggle { + display: none; // hide the hidden event expand button, not enough space, view source can still be used + } + } } .mx_ThreadPanel_replies { diff --git a/res/css/views/right_panel/_TimelineCard.scss b/res/css/views/right_panel/_TimelineCard.scss index a2431dec01c..07227c1b9af 100644 --- a/res/css/views/right_panel/_TimelineCard.scss +++ b/res/css/views/right_panel/_TimelineCard.scss @@ -58,6 +58,12 @@ limitations under the License. padding-right: 36px; } + .mx_EventTile:not([data-layout="bubble"]) .mx_ThreadInfo { + margin-left: 36px; + margin-right: 0; + max-width: min(calc(100% - 36px), 600px); + } + .mx_GroupLayout .mx_EventTile > .mx_SenderProfile { margin-left: 36px; } diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index a9d2cd8b098..3472b656bf3 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -36,7 +36,6 @@ limitations under the License. margin-top: var(--gutterSize); margin-left: 49px; font-size: $font-14px; - flex-shrink: 0; .mx_ThreadInfo { clear: both; diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index a08fe8a268d..57052de159e 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -18,6 +18,8 @@ limitations under the License. $left-gutter: 64px; .mx_EventTile { + flex-shrink: 0; + .mx_EventTile_receiptSent, .mx_EventTile_receiptSending { // Give it some dimensions so the tooltip can position properly @@ -734,7 +736,7 @@ $left-gutter: 64px; .mx_ThreadInfo { min-width: 267px; max-width: min(calc(100% - 64px), 600px); - width: auto; + width: fit-content; height: 40px; position: relative; background-color: $system; @@ -788,6 +790,12 @@ $left-gutter: 64px; } } +.mx_MessagePanel_narrow .mx_ThreadInfo { + min-width: initial; + max-width: initial; + width: initial; +} + .mx_ThreadInfo_content { text-overflow: ellipsis; overflow: hidden; diff --git a/res/css/views/rooms/_MessageComposerFormatBar.scss b/res/css/views/rooms/_MessageComposerFormatBar.scss index f92a3cfbcdb..97886cb3589 100644 --- a/res/css/views/rooms/_MessageComposerFormatBar.scss +++ b/res/css/views/rooms/_MessageComposerFormatBar.scss @@ -37,12 +37,6 @@ limitations under the License. display: inline-block; position: relative; margin: 2px; - - &:hover { - background: $panel-actions; - border-radius: 6px; - z-index: 1; - } } .mx_MessageComposerFormatBar_button { @@ -50,6 +44,14 @@ limitations under the License. height: 28px; box-sizing: border-box; vertical-align: middle; + background: none; + border: none; + + &:hover { + background: $panel-actions; + border-radius: 6px; + z-index: 1; + } } .mx_MessageComposerFormatBar_button::after { diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index bc01fdf32ff..b91d417879f 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -357,6 +357,10 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/files.svg'); } + .mx_RoomTile_iconPins::before { + mask-image: url('$(res)/img/element-icons/room/pin-upright.svg'); + } + .mx_RoomTile_iconWidgets::before { mask-image: url('$(res)/img/element-icons/room/apps.svg'); } diff --git a/res/img/element-icons/cancel-rounded.svg b/res/img/element-icons/cancel-rounded.svg new file mode 100644 index 00000000000..7439aaeabac --- /dev/null +++ b/res/img/element-icons/cancel-rounded.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/caret-left.svg b/res/img/element-icons/caret-left.svg new file mode 100644 index 00000000000..14c28dc3b1e --- /dev/null +++ b/res/img/element-icons/caret-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/location.svg b/res/img/element-icons/location.svg index be83b121046..fc8337a43ba 100644 --- a/res/img/element-icons/location.svg +++ b/res/img/element-icons/location.svg @@ -1,3 +1,3 @@ - + diff --git a/res/img/location/live-location.svg b/res/img/location/live-location.svg new file mode 100644 index 00000000000..09628709a71 --- /dev/null +++ b/res/img/location/live-location.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 7f7b92d4d58..e9d8ad639cb 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -179,6 +179,11 @@ $call-view-button-off-background: $primary-content; $video-feed-secondary-background: $system; // ******************** +// Location sharing +// ******************** +$location-live-color: #5c56f5; +// ******************** + // Location sharing // ******************** .maplibregl-ctrl-attrib-button { diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 26fe3365b88..9c570c44f7a 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -188,6 +188,12 @@ $message-bubble-background: #424242; $message-bubble-background-self: #303030; $message-bubble-background-selected: #3F4931; +// Location sharing +// ******************** +$location-marker-color: #ffffff; +$location-live-color: #5c56f5; +// ******************** + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 0d7ab603903..8cfd6d90aec 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -38,7 +38,6 @@ $accent: #8BC34A; $selection-fg-color: $primary-bg-color; $focus-brightness: 105%; -$location-marker-color: #ffffff; $other-user-pill-bg-color: rgba(0, 0, 0, 0.1); @@ -293,6 +292,12 @@ $pinned-color: $tertiary-content; $groupFilterPanel-divider-color: $tertiary-content; +// Location sharing +// ******************** +$location-marker-color: #ffffff; +$location-live-color: #5c56f5; +// ******************** + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index fef21553f03..7d6533cecc8 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -290,7 +290,6 @@ $pinned-color: $tertiary-content; $avatar-initial-color: $background; $primary-hairline-color: transparent; $focus-brightness: 105%; -$location-marker-color: #ffffff; // ******************** // blur amounts for left left panel (only for element theme) @@ -305,6 +304,12 @@ $location-marker-color: #ffffff; $copy-button-url: "$(res)/img/feather-customised/clipboard.svg"; // ******************** +// Location sharing +// ******************** +$location-marker-color: #ffffff; +$location-live-color: #5c56f5; +// ******************** + // Mixins // ******************** @define-mixin mx_DialogButton { diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 4a1d5877d17..6ac72a2dd61 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -52,6 +52,7 @@ import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake"; import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import { Skinner } from "../Skinner"; import AutoRageshakeStore from "../stores/AutoRageshakeStore"; +import { ConfigOptions } from "../SdkConfig"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -62,6 +63,7 @@ declare global { Olm: { init: () => Promise; }; + mxReactSdkConfig: ConfigOptions; // Needed for Safari, unknown to TypeScript webkitAudioContext: typeof AudioContext; diff --git a/src/@types/groups.ts b/src/@types/groups.ts new file mode 100644 index 00000000000..d3a7455e2fc --- /dev/null +++ b/src/@types/groups.ts @@ -0,0 +1,59 @@ +/* +Copyright 2021 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 const CreateEventField = "io.element.migrated_from_community"; + +export interface IGroupRoom { + displayname: string; + name?: string; + roomId: string; + canonicalAlias?: string; + avatarUrl?: string; + topic?: string; + numJoinedMembers?: number; + worldReadable?: boolean; + guestCanJoin?: boolean; + isPublic?: boolean; +} + +/* eslint-disable camelcase */ +export interface IGroupSummary { + profile: { + avatar_url?: string; + is_openly_joinable?: boolean; + is_public?: boolean; + long_description: string; + name: string; + short_description: string; + }; + rooms_section: { + rooms: unknown[]; + categories: Record; + total_room_count_estimate: number; + }; + user: { + is_privileged: boolean; + is_public: boolean; + is_publicised: boolean; + membership: string; + }; + users_section: { + users: unknown[]; + roles: Record; + total_user_count_estimate: number; + }; +} +/* eslint-enable camelcase */ diff --git a/src/@types/svg.d.ts b/src/@types/svg.d.ts index 96f671c52f4..f4b99be05b4 100644 --- a/src/@types/svg.d.ts +++ b/src/@types/svg.d.ts @@ -1,5 +1,6 @@ /* Copyright 2021 Šimon Brandner +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. @@ -16,5 +17,6 @@ limitations under the License. declare module "*.svg" { const path: string; + export const Icon: React.FC>; export default path; } diff --git a/src/AddThreepid.ts b/src/AddThreepid.ts index ab4c04c7256..4c84294f7a6 100644 --- a/src/AddThreepid.ts +++ b/src/AddThreepid.ts @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IRequestMsisdnTokenResponse, IRequestTokenResponse } from "matrix-js-sdk/src"; +import { IRequestMsisdnTokenResponse, IRequestTokenResponse } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from './MatrixClientPeg'; import Modal from './Modal'; diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index 54fc6081c9e..40ac9d11996 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -309,6 +309,12 @@ export default abstract class BasePlatform { return false; } + public overrideBrowserShortcuts(): boolean { + return false; + } + + public navigateForwardBack(back: boolean): void {} + getAvailableSpellCheckLanguages(): Promise | null { return null; } diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index d1458ef39c6..60094262edb 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -19,14 +19,22 @@ limitations under the License. import React from 'react'; import { base32 } from "rfc4648"; -import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType } from "matrix-js-sdk/src/webrtc/call"; -import { CallError } from "matrix-js-sdk/src/webrtc/call"; +import { + CallError, + CallErrorCode, + CallEvent, + CallParty, + CallState, + CallType, + MatrixCall, +} from "matrix-js-sdk/src/webrtc/call"; import { logger } from 'matrix-js-sdk/src/logger'; -import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring"; +import { randomLowercaseString, randomUppercaseString } from "matrix-js-sdk/src/randomstring"; import EventEmitter from 'events'; import { RuleId, TweakName, Tweaks } from "matrix-js-sdk/src/@types/PushRules"; import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor'; import { SyncState } from "matrix-js-sdk/src/sync"; +import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler"; import { MatrixClientPeg } from './MatrixClientPeg'; import Modal from './Modal'; @@ -50,10 +58,9 @@ import VoipUserMapper from './VoipUserMapper'; import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid'; import SdkConfig from './SdkConfig'; import { ensureDMExists, findDMForUser } from './createRoom'; -import { WidgetLayoutStore, Container } from './stores/widgets/WidgetLayoutStore'; -import { getIncomingCallToastKey } from './toasts/IncomingCallToast'; +import { Container, WidgetLayoutStore } from './stores/widgets/WidgetLayoutStore'; +import IncomingCallToast, { getIncomingCallToastKey } from './toasts/IncomingCallToast'; import ToastStore from './stores/ToastStore'; -import IncomingCallToast from "./toasts/IncomingCallToast"; import Resend from './Resend'; import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload"; @@ -145,9 +152,9 @@ export default class CallHandler extends EventEmitter { public roomIdForCall(call: MatrixCall): string { if (!call) return null; - const voipConfig = SdkConfig.get()['voip']; - - if (voipConfig && voipConfig.obeyAssertedIdentity) { + // check asserted identity: if we're not obeying asserted identity, + // this map will never be populated, but we check anyway for sanity + if (this.shouldObeyAssertedfIdentity()) { const nativeUser = this.assertedIdentityNativeUsers[call.callId]; if (nativeUser) { const room = findDMForUser(MatrixClientPeg.get(), nativeUser); @@ -172,7 +179,7 @@ export default class CallHandler extends EventEmitter { } if (SettingsStore.getValue(UIFeature.Voip)) { - MatrixClientPeg.get().on('Call.incoming', this.onCallIncoming); + MatrixClientPeg.get().on(CallEventHandlerEvent.Incoming, this.onCallIncoming); } this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS); @@ -181,7 +188,7 @@ export default class CallHandler extends EventEmitter { public stop(): void { const cli = MatrixClientPeg.get(); if (cli) { - cli.removeListener('Call.incoming', this.onCallIncoming); + cli.removeListener(CallEventHandlerEvent.Incoming, this.onCallIncoming); } } @@ -255,6 +262,10 @@ export default class CallHandler extends EventEmitter { } } + private shouldObeyAssertedfIdentity(): boolean { + return SdkConfig.get()['voip']?.obeyAssertedIdentity; + } + public getSupportsPstnProtocol(): boolean { return this.supportsPstnProtocol; } @@ -482,6 +493,11 @@ export default class CallHandler extends EventEmitter { logger.log(`Call ID ${call.callId} got new asserted identity:`, call.getRemoteAssertedIdentity()); + if (!this.shouldObeyAssertedfIdentity()) { + logger.log("asserted identity not enabled in config: ignoring"); + return; + } + const newAssertedIdentity = call.getRemoteAssertedIdentity().id; let newNativeAssertedIdentity = newAssertedIdentity; if (newAssertedIdentity) { diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index caaaf7e9a1a..8b0ddc83688 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -24,7 +24,7 @@ import encrypt from "browser-encrypt-attachment"; import extractPngChunks from "png-chunks-extract"; import { IAbortablePromise, IImageInfo } from "matrix-js-sdk/src/@types/partials"; import { logger } from "matrix-js-sdk/src/logger"; -import { IEventRelation, ISendEventResponse } from "matrix-js-sdk/src"; +import { IEventRelation, ISendEventResponse } from "matrix-js-sdk/src/matrix"; import { IEncryptedFile, IMediaEventInfo } from "./customisations/models/IMediaEventContent"; import dis from './dispatcher/dispatcher'; @@ -447,7 +447,7 @@ export default class ContentMessages { public async sendContentListToRoom( files: File[], roomId: string, - relation: IEventRelation | null, + relation: IEventRelation | undefined, matrixClient: MatrixClient, context = TimelineRenderingType.Room, ): Promise { @@ -566,7 +566,7 @@ export default class ContentMessages { private sendContentToRoom( file: File, roomId: string, - relation: IEventRelation, + relation: IEventRelation | undefined, matrixClient: MatrixClient, promBefore: Promise, ) { diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index 470491ecd54..05f86453f1f 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -16,6 +16,8 @@ limitations under the License. import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { logger } from "matrix-js-sdk/src/logger"; +import { CryptoEvent } from "matrix-js-sdk/src/crypto"; +import { ClientEvent, EventType, RoomStateEvent } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from './MatrixClientPeg'; import dis from "./dispatcher/dispatcher"; @@ -32,7 +34,7 @@ import { hideToast as hideUnverifiedSessionsToast, showToast as showUnverifiedSessionsToast, } from "./toasts/UnverifiedSessionToast"; -import { isSecretStorageBeingAccessed, accessSecretStorage } from "./SecurityManager"; +import { accessSecretStorage, isSecretStorageBeingAccessed } from "./SecurityManager"; import { isSecureBackupRequired } from './utils/WellKnownUtils'; import { isLoggedIn } from './components/structures/MatrixChat'; import { ActionPayload } from "./dispatcher/payloads"; @@ -63,28 +65,31 @@ export default class DeviceListener { } start() { - MatrixClientPeg.get().on('crypto.willUpdateDevices', this.onWillUpdateDevices); - MatrixClientPeg.get().on('crypto.devicesUpdated', this.onDevicesUpdated); - MatrixClientPeg.get().on('deviceVerificationChanged', this.onDeviceVerificationChanged); - MatrixClientPeg.get().on('userTrustStatusChanged', this.onUserTrustStatusChanged); - MatrixClientPeg.get().on('crossSigning.keysChanged', this.onCrossSingingKeysChanged); - MatrixClientPeg.get().on('accountData', this.onAccountData); - MatrixClientPeg.get().on('sync', this.onSync); - MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents); + MatrixClientPeg.get().on(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices); + MatrixClientPeg.get().on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated); + MatrixClientPeg.get().on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged); + MatrixClientPeg.get().on(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged); + MatrixClientPeg.get().on(CryptoEvent.KeysChanged, this.onCrossSingingKeysChanged); + MatrixClientPeg.get().on(ClientEvent.AccountData, this.onAccountData); + MatrixClientPeg.get().on(ClientEvent.Sync, this.onSync); + MatrixClientPeg.get().on(RoomStateEvent.Events, this.onRoomStateEvents); this.dispatcherRef = dis.register(this.onAction); this.recheck(); } stop() { if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener('crypto.willUpdateDevices', this.onWillUpdateDevices); - MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this.onDevicesUpdated); - MatrixClientPeg.get().removeListener('deviceVerificationChanged', this.onDeviceVerificationChanged); - MatrixClientPeg.get().removeListener('userTrustStatusChanged', this.onUserTrustStatusChanged); - MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this.onCrossSingingKeysChanged); - MatrixClientPeg.get().removeListener('accountData', this.onAccountData); - MatrixClientPeg.get().removeListener('sync', this.onSync); - MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents); + MatrixClientPeg.get().removeListener(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices); + MatrixClientPeg.get().removeListener(CryptoEvent.DevicesUpdated, this.onDevicesUpdated); + MatrixClientPeg.get().removeListener( + CryptoEvent.DeviceVerificationChanged, + this.onDeviceVerificationChanged, + ); + MatrixClientPeg.get().removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged); + MatrixClientPeg.get().removeListener(CryptoEvent.KeysChanged, this.onCrossSingingKeysChanged); + MatrixClientPeg.get().removeListener(ClientEvent.AccountData, this.onAccountData); + MatrixClientPeg.get().removeListener(ClientEvent.Sync, this.onSync); + MatrixClientPeg.get().removeListener(RoomStateEvent.Events, this.onRoomStateEvents); } if (this.dispatcherRef) { dis.unregister(this.dispatcherRef); @@ -179,9 +184,7 @@ export default class DeviceListener { }; private onRoomStateEvents = (ev: MatrixEvent) => { - if (ev.getType() !== "m.room.encryption") { - return; - } + if (ev.getType() !== EventType.RoomEncryption) return; // If a room changes to encrypted, re-check as it may be our first // encrypted room. This also catches encrypted room creation as well. @@ -294,8 +297,9 @@ export default class DeviceListener { } } - logger.log("Old unverified sessions: " + Array.from(oldUnverifiedDeviceIds).join(',')); - logger.log("New unverified sessions: " + Array.from(newUnverifiedDeviceIds).join(',')); + logger.debug("Old unverified sessions: " + Array.from(oldUnverifiedDeviceIds).join(',')); + logger.debug("New unverified sessions: " + Array.from(newUnverifiedDeviceIds).join(',')); + logger.debug("Currently showing toasts for: " + Array.from(this.displayingToastsForDeviceIds).join(',')); // Display or hide the batch toast for old unverified sessions if (oldUnverifiedDeviceIds.size > 0) { @@ -312,6 +316,7 @@ export default class DeviceListener { // ...and hide any we don't need any more for (const deviceId of this.displayingToastsForDeviceIds) { if (!newUnverifiedDeviceIds.has(deviceId)) { + logger.debug("Hiding unverified session toast for " + deviceId); hideUnverifiedSessionsToast(deviceId); } } diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 37c085c1d28..b6f69de0a05 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -32,9 +32,9 @@ import { IExtendedSanitizeOptions } from './@types/sanitize-html'; import SettingsStore from './settings/SettingsStore'; import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks"; import { getEmojiFromUnicode } from "./emoji"; -import ReplyChain from "./components/views/elements/ReplyChain"; import { mediaFromMxc } from "./customisations/Media"; import { ELEMENT_URL_PATTERN, options as linkifyMatrixOptions } from './linkify-matrix'; +import { stripHTMLReply, stripPlainReply } from './utils/Reply'; // Anything outside the basic multilingual plane will be a surrogate pair const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/; @@ -501,8 +501,8 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts let formattedBody = typeof content.formatted_body === 'string' ? content.formatted_body : null; const plainBody = typeof content.body === 'string' ? content.body : ""; - if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyChain.stripHTMLReply(formattedBody); - strippedBody = opts.stripReplyFallback ? ReplyChain.stripPlainReply(plainBody) : plainBody; + if (opts.stripReplyFallback && formattedBody) formattedBody = stripHTMLReply(formattedBody); + strippedBody = opts.stripReplyFallback ? stripPlainReply(plainBody) : plainBody; bodyHasEmoji = mightContainEmoji(isFormattedBody ? formattedBody : plainBody); diff --git a/src/KeyBindingsDefaults.ts b/src/KeyBindingsDefaults.ts index e122b2dff83..5870f6451ea 100644 --- a/src/KeyBindingsDefaults.ts +++ b/src/KeyBindingsDefaults.ts @@ -26,13 +26,13 @@ import { import { CATEGORIES, CategoryName, - getCustomizableShortcuts, + getKeyboardShortcuts, KeyBindingAction, } from "./accessibility/KeyboardShortcuts"; export const getBindingsByCategory = (category: CategoryName): KeyBinding[] => { return CATEGORIES[category].settingNames.reduce((bindings, name) => { - const value = getCustomizableShortcuts()[name]?.default; + const value = getKeyboardShortcuts()[name]?.default; if (value) { bindings.push({ action: name as KeyBindingAction, @@ -149,16 +149,15 @@ const roomBindings = (): KeyBinding[] => { }; const navigationBindings = (): KeyBinding[] => { - const bindings = getBindingsByCategory(CategoryName.NAVIGATION); + return getBindingsByCategory(CategoryName.NAVIGATION); +}; - bindings.push({ - action: "KeyBinding.closeDialogOrContextMenu" as KeyBindingAction, - keyCombo: { - key: Key.ESCAPE, - }, - }); +const accessibilityBindings = (): KeyBinding[] => { + return getBindingsByCategory(CategoryName.ACCESSIBILITY); +}; - return bindings; +const callBindings = (): KeyBinding[] => { + return getBindingsByCategory(CategoryName.CALLS); }; const labsBindings = (): KeyBinding[] => { @@ -173,5 +172,7 @@ export const defaultBindingsProvider: IKeyBindingsProvider = { getRoomListBindings: roomListBindings, getRoomBindings: roomBindings, getNavigationBindings: navigationBindings, + getAccessibilityBindings: accessibilityBindings, + getCallBindings: callBindings, getLabsBindings: labsBindings, }; diff --git a/src/KeyBindingsManager.ts b/src/KeyBindingsManager.ts index 995ca3bf323..7a79a69ce87 100644 --- a/src/KeyBindingsManager.ts +++ b/src/KeyBindingsManager.ts @@ -155,6 +155,14 @@ export class KeyBindingsManager { return this.getAction(this.bindingsProviders.map(it => it.getNavigationBindings), ev); } + getAccessibilityAction(ev: KeyboardEvent | React.KeyboardEvent): KeyBindingAction | undefined { + return this.getAction(this.bindingsProviders.map(it => it.getAccessibilityBindings), ev); + } + + getCallAction(ev: KeyboardEvent | React.KeyboardEvent): KeyBindingAction | undefined { + return this.getAction(this.bindingsProviders.map(it => it.getCallBindings), ev); + } + getLabsAction(ev: KeyboardEvent | React.KeyboardEvent): KeyBindingAction | undefined { return this.getAction(this.bindingsProviders.map(it => it.getLabsBindings), ev); } diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index de73fcc0514..85f05358516 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -58,6 +58,7 @@ import LazyLoadingDisabledDialog from "./components/views/dialogs/LazyLoadingDis import SessionRestoreErrorDialog from "./components/views/dialogs/SessionRestoreErrorDialog"; import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog"; import { setSentryUser } from "./sentry"; +import SdkConfig from "./SdkConfig"; const HOMESERVER_URL_KEY = "mx_hs_url"; const ID_SERVER_URL_KEY = "mx_is_url"; @@ -845,6 +846,13 @@ export async function onLoggedOut(): Promise { stopMatrixClient(); await clearStorage({ deleteEverything: true }); LifecycleCustomisations.onLoggedOutAndStorageCleared?.(); + + // Do this last so we can make sure all storage has been cleared and all + // customisations got the memo. + if (SdkConfig.get().logout_redirect_url) { + logger.log("Redirecting to external provider to finish logout"); + window.location.href = SdkConfig.get().logout_redirect_url; + } } /** diff --git a/src/Notifier.ts b/src/Notifier.ts index f5eafba407c..c5383488f2f 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -17,8 +17,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event"; +import { Room, RoomEvent } from "matrix-js-sdk/src/models/room"; +import { ClientEvent } from "matrix-js-sdk/src/client"; import { logger } from "matrix-js-sdk/src/logger"; import { MsgType } from "matrix-js-sdk/src/@types/event"; import { LOCATION_EVENT_TYPE } from "matrix-js-sdk/src/@types/location"; @@ -201,20 +202,20 @@ export const Notifier = { this.boundOnRoomReceipt = this.boundOnRoomReceipt || this.onRoomReceipt.bind(this); this.boundOnEventDecrypted = this.boundOnEventDecrypted || this.onEventDecrypted.bind(this); - MatrixClientPeg.get().on('event', this.boundOnEvent); - MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt); - MatrixClientPeg.get().on('Event.decrypted', this.boundOnEventDecrypted); - MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange); + MatrixClientPeg.get().on(ClientEvent.Event, this.boundOnEvent); + MatrixClientPeg.get().on(RoomEvent.Receipt, this.boundOnRoomReceipt); + MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.boundOnEventDecrypted); + MatrixClientPeg.get().on(ClientEvent.Sync, this.boundOnSyncStateChange); this.toolbarHidden = false; this.isSyncing = false; }, stop: function() { if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener('Event', this.boundOnEvent); - MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt); - MatrixClientPeg.get().removeListener('Event.decrypted', this.boundOnEventDecrypted); - MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange); + MatrixClientPeg.get().removeListener(ClientEvent.Event, this.boundOnEvent); + MatrixClientPeg.get().removeListener(RoomEvent.Receipt, this.boundOnRoomReceipt); + MatrixClientPeg.get().removeListener(MatrixEventEvent.Decrypted, this.boundOnEventDecrypted); + MatrixClientPeg.get().removeListener(ClientEvent.Sync, this.boundOnSyncStateChange); } this.isSyncing = false; }, diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index 343aea62358..3b2ae009aec 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -152,13 +152,13 @@ export class PosthogAnalytics { // we persist the last `$screen_name` and send it for all events until it is replaced private lastScreen: ScreenName = "Loading"; - private sanitizeProperties = (properties: posthog.Properties): posthog.Properties => { + private sanitizeProperties = (properties: posthog.Properties, eventName: string): posthog.Properties => { // Callback from posthog to sanitize properties before sending them to the server. // // Here we sanitize posthog's built in properties which leak PII e.g. url reporting. // See utils.js _.info.properties in posthog-js. - if (properties["eventName"] === "$pageview") { + if (eventName === "$pageview") { this.lastScreen = properties["$current_url"]; } // We inject a screen identifier in $current_url as per https://posthog.com/tutorials/spa diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index 1f39eddc17c..39d1d5d4aa7 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -61,17 +61,6 @@ export function aggregateNotificationCount(rooms: Room[]): {count: number, highl }, { count: 0, highlight: false }); } -export function getRoomHasBadge(room: Room): boolean { - const roomNotifState = getRoomNotifsState(room.roomId); - const highlight = room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0; - const notificationCount = room.getUnreadNotificationCount(); - - const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState); - const mentionBadges = highlight && shouldShowMentionBadge(roomNotifState); - - return notifBadges || mentionBadges; -} - export function getRoomNotifsState(roomId: string): RoomNotifState { if (MatrixClientPeg.get().isGuest()) return RoomNotifState.AllMessages; @@ -88,14 +77,14 @@ export function getRoomNotifsState(roomId: string): RoomNotifState { roomRule = MatrixClientPeg.get().getRoomPushRule('global', roomId); } catch (err) { // Possible that the client doesn't have pushRules yet. If so, it - // hasn't started eiher, so indicate that this room is not notifying. + // hasn't started either, so indicate that this room is not notifying. return null; } // XXX: We have to assume the default is to notify for all messages // (in particular this will be 'wrong' for one to one rooms because // they will notify loudly for all messages) - if (!roomRule || !roomRule.enabled) return RoomNotifState.AllMessages; + if (!roomRule?.enabled) return RoomNotifState.AllMessages; // a mute at the room level will still allow mentions // to notify @@ -213,17 +202,15 @@ function findOverrideMuteRule(roomId: string): IPushRule { return null; } for (const rule of cli.pushRules.global.override) { - if (isRuleForRoom(roomId, rule)) { - if (isMuteRule(rule) && rule.enabled) { - return rule; - } + if (rule.enabled && isRuleForRoom(roomId, rule) && isMuteRule(rule)) { + return rule; } } return null; } function isRuleForRoom(roomId: string, rule: IPushRule): boolean { - if (rule.conditions.length !== 1) { + if (rule.conditions?.length !== 1) { return false; } const cond = rule.conditions[0]; diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index b97ab51c9f4..1d6863d3ce5 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -20,13 +20,19 @@ export interface ISsoRedirectOptions { on_welcome_page?: boolean; // eslint-disable-line camelcase } +/* eslint-disable camelcase */ export interface ConfigOptions { [key: string]: any; + logout_redirect_url?: string; + // sso_immediate_redirect is deprecated in favour of sso_redirect_options.immediate - sso_immediate_redirect?: boolean; // eslint-disable-line camelcase - sso_redirect_options?: ISsoRedirectOptions; // eslint-disable-line camelcase + sso_immediate_redirect?: boolean; + sso_redirect_options?: ISsoRedirectOptions; + + custom_translations_url?: string; } +/* eslint-enable camelcase*/ export const DEFAULTS: ConfigOptions = { // Brand name of the app @@ -44,7 +50,7 @@ export const DEFAULTS: ConfigOptions = { }, desktopBuilds: { available: true, - logo: require("../res/img/element-desktop-logo.svg"), + logo: require("../res/img/element-desktop-logo.svg").default, url: "https://schildi.chat/desktop", }, }; @@ -56,14 +62,14 @@ export default class SdkConfig { SdkConfig.instance = i; // For debugging purposes - (window).mxReactSdkConfig = i; + window.mxReactSdkConfig = i; } - static get() { + public static get() { return SdkConfig.instance || {}; } - static put(cfg: ConfigOptions) { + public static put(cfg: ConfigOptions) { const defaultKeys = Object.keys(DEFAULTS); for (let i = 0; i < defaultKeys.length; ++i) { if (cfg[defaultKeys[i]] === undefined) { @@ -73,11 +79,11 @@ export default class SdkConfig { SdkConfig.setInstance(cfg); } - static unset() { + public static unset() { SdkConfig.setInstance({}); } - static add(cfg: ConfigOptions) { + public static add(cfg: ConfigOptions) { const liveConfig = SdkConfig.get(); const newConfig = Object.assign({}, liveConfig, cfg); SdkConfig.put(newConfig); diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 637f324743f..9bb90e607a5 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -22,14 +22,14 @@ import { User } from "matrix-js-sdk/src/models/user"; import { Direction } from 'matrix-js-sdk/src/models/event-timeline'; import { EventType } from "matrix-js-sdk/src/@types/event"; import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers'; -import { parseFragment as parseHtml, Element as ChildElement } from "parse5"; +import { Element as ChildElement, parseFragment as parseHtml } from "parse5"; import { logger } from "matrix-js-sdk/src/logger"; import { IContent } from 'matrix-js-sdk/src/models/event'; import { SlashCommand as SlashCommandEvent } from "matrix-analytics-events/types/typescript/SlashCommand"; import { MatrixClientPeg } from './MatrixClientPeg'; import dis from './dispatcher/dispatcher'; -import { _t, _td, newTranslatableError, ITranslatableError } from './languageHandler'; +import { _t, _td, ITranslatableError, newTranslatableError } from './languageHandler'; import Modal from './Modal'; import MultiInviter from './utils/MultiInviter'; import { linkifyAndSanitizeHtml } from './HtmlUtils'; @@ -65,6 +65,7 @@ import RoomViewStore from "./stores/RoomViewStore"; import { XOR } from "./@types/common"; import { PosthogAnalytics } from "./PosthogAnalytics"; import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload"; +import VoipUserMapper from './VoipUserMapper'; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -932,7 +933,7 @@ export const Commands = [ command: 'addwidget', args: '', description: _td('Adds a custom widget by URL to the room'), - isEnabled: () => SettingsStore.getValue(UIFeature.Widgets), + isEnabled: () => SettingsStore.getValue(UIFeature.Widgets) && shouldShowComponent(UIComponent.AddIntegrations), runFn: function(roomId, widgetUrl) { if (!widgetUrl) { return reject(newTranslatableError("Please supply a widget URL or embed code")); @@ -1129,6 +1130,26 @@ export const Commands = [ }, category: CommandCategories.advanced, }), + new Command({ + command: "tovirtual", + description: _td("Switches to this room's virtual room, if it has one"), + category: CommandCategories.advanced, + isEnabled(): boolean { + return CallHandler.instance.getSupportsVirtualRooms(); + }, + runFn: (roomId) => { + return success((async () => { + const room = await VoipUserMapper.sharedInstance().getVirtualRoomForRoom(roomId); + if (!room) throw newTranslatableError("No virtual room for this room"); + dis.dispatch({ + action: Action.ViewRoom, + room_id: room.roomId, + metricsTrigger: "SlashCommand", + metricsViaKeyboard: true, + }); + })()); + }, + }), new Command({ command: "query", description: _td("Opens chat with the given user"), diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index c98c39a88d4..cb39d9affb4 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -42,13 +42,20 @@ export default class VoipUserMapper { return results[0].userid; } - public async getOrCreateVirtualRoomForRoom(roomId: string): Promise { + private async getVirtualUserForRoom(roomId: string): Promise { const userId = DMRoomMap.shared().getUserIdForRoomId(roomId); if (!userId) return null; const virtualUser = await this.userToVirtualUser(userId); if (!virtualUser) return null; + return virtualUser; + } + + public async getOrCreateVirtualRoomForRoom(roomId: string): Promise { + const virtualUser = await this.getVirtualUserForRoom(roomId); + if (!virtualUser) return null; + const virtualRoomId = await ensureVirtualRoomExists(MatrixClientPeg.get(), virtualUser, roomId); MatrixClientPeg.get().setRoomAccountData(virtualRoomId, VIRTUAL_ROOM_EVENT_TYPE, { native_room: roomId, @@ -59,6 +66,17 @@ export default class VoipUserMapper { return virtualRoomId; } + /** + * Gets the ID of the virtual room for a room, or null if the room has no + * virtual room + */ + public async getVirtualRoomForRoom(roomId: string): Promise { + const virtualUser = await this.getVirtualUserForRoom(roomId); + if (!virtualUser) return null; + + return findDMForUser(MatrixClientPeg.get(), virtualUser); + } + public nativeRoomForVirtualRoom(roomId: string): string { const cachedNativeRoomId = this.virtualToNativeRoomIdCache.get(roomId); if (cachedNativeRoomId) { diff --git a/src/accessibility/KeyboardShortcuts.ts b/src/accessibility/KeyboardShortcuts.ts index 0bd3cffcad4..c7f4ef68bbd 100644 --- a/src/accessibility/KeyboardShortcuts.ts +++ b/src/accessibility/KeyboardShortcuts.ts @@ -20,6 +20,7 @@ import { isMac, Key } from "../Keyboard"; import { IBaseSetting } from "../settings/Settings"; import SettingsStore from "../settings/SettingsStore"; import IncompatibleController from "../settings/controllers/IncompatibleController"; +import PlatformPeg from "../PlatformPeg"; export enum KeyBindingAction { /** Send a message */ @@ -115,6 +116,35 @@ export enum KeyBindingAction { /** Select next room with unread messages */ SelectNextUnreadRoom = 'KeyBinding.nextUnreadRoom', + /** Switches to a space by number */ + SwitchToSpaceByNumber = "KeyBinding.switchToSpaceByNumber", + /** Opens user settings */ + OpenUserSettings = "KeyBinding.openUserSettings", + /** Navigates backward */ + PreviousVisitedRoomOrCommunity = "KeyBinding.previousVisitedRoomOrCommunity", + /** Navigates forward */ + NextVisitedRoomOrCommunity = "KeyBinding.nextVisitedRoomOrCommunity", + + /** Toggles microphone while on a call */ + ToggleMicInCall = "KeyBinding.toggleMicInCall", + /** Toggles webcam while on a call */ + ToggleWebcamInCall = "KeyBinding.toggleWebcamInCall", + + /** Accessibility actions */ + Escape = "KeyBinding.escape", + Enter = "KeyBinding.enter", + Space = "KeyBinding.space", + Backspace = "KeyBinding.backspace", + Delete = "KeyBinding.delete", + Home = "KeyBinding.home", + End = "KeyBinding.end", + ArrowLeft = "KeyBinding.arrowLeft", + ArrowUp = "KeyBinding.arrowUp", + ArrowRight = "KeyBinding.arrowRight", + ArrowDown = "KeyBinding.arrowDown", + Tab = "KeyBinding.tab", + Comma = "KeyBinding.comma", + /** Toggle visibility of hidden events */ ToggleHiddenEventVisibility = 'KeyBinding.toggleHiddenEventVisibility', } @@ -132,17 +162,18 @@ type KeyboardShortcutSetting = IBaseSetting; type IKeyboardShortcuts = { // TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager - [k in (KeyBindingAction | string)]: KeyboardShortcutSetting; + [k in (KeyBindingAction)]?: KeyboardShortcutSetting; }; export interface ICategory { - categoryLabel: string; + categoryLabel?: string; // TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager - settingNames: (KeyBindingAction | string)[]; + settingNames: (KeyBindingAction)[]; } export enum CategoryName { NAVIGATION = "Navigation", + ACCESSIBILITY = "Accessibility", CALLS = "Calls", COMPOSER = "Composer", ROOM_LIST = "Room List", @@ -175,7 +206,7 @@ export const KEY_ICON: Record = { }; if (isMac) { KEY_ICON[Key.META] = "⌘"; - KEY_ICON[Key.SHIFT] = "⌥"; + KEY_ICON[Key.ALT] = "⌥"; } export const CATEGORIES: Record = { @@ -200,8 +231,8 @@ export const CATEGORIES: Record = { }, [CategoryName.CALLS]: { categoryLabel: _td("Calls"), settingNames: [ - "KeyBinding.toggleMicInCall", - "KeyBinding.toggleWebcamInCall", + KeyBindingAction.ToggleMicInCall, + KeyBindingAction.ToggleWebcamInCall, ], }, [CategoryName.ROOM]: { categoryLabel: _td("Room"), @@ -225,12 +256,26 @@ export const CATEGORIES: Record = { KeyBindingAction.NextRoom, KeyBindingAction.PrevRoom, ], + }, [CategoryName.ACCESSIBILITY]: { + categoryLabel: _td("Accessibility"), + settingNames: [ + KeyBindingAction.Escape, + KeyBindingAction.Enter, + KeyBindingAction.Space, + KeyBindingAction.Backspace, + KeyBindingAction.Delete, + KeyBindingAction.Home, + KeyBindingAction.End, + KeyBindingAction.ArrowLeft, + KeyBindingAction.ArrowUp, + KeyBindingAction.ArrowRight, + KeyBindingAction.ArrowDown, + KeyBindingAction.Comma, + ], }, [CategoryName.NAVIGATION]: { categoryLabel: _td("Navigation"), settingNames: [ KeyBindingAction.ToggleUserMenu, - "KeyBinding.closeDialogOrContextMenu", - "KeyBinding.activateSelectedButton", KeyBindingAction.ToggleRoomSidePanel, KeyBindingAction.ToggleSpacePanel, KeyBindingAction.ShowKeyboardSettings, @@ -240,6 +285,10 @@ export const CATEGORIES: Record = { KeyBindingAction.SelectPrevUnreadRoom, KeyBindingAction.SelectNextRoom, KeyBindingAction.SelectPrevRoom, + KeyBindingAction.OpenUserSettings, + KeyBindingAction.SwitchToSpaceByNumber, + KeyBindingAction.PreviousVisitedRoomOrCommunity, + KeyBindingAction.NextVisitedRoomOrCommunity, ], }, [CategoryName.AUTOCOMPLETE]: { categoryLabel: _td("Autocomplete"), @@ -258,6 +307,17 @@ export const CATEGORIES: Record = { }, }; +const DESKTOP_SHORTCUTS = [ + KeyBindingAction.OpenUserSettings, + KeyBindingAction.SwitchToSpaceByNumber, + KeyBindingAction.PreviousVisitedRoomOrCommunity, + KeyBindingAction.NextVisitedRoomOrCommunity, +]; + +const MAC_ONLY_SHORTCUTS = [ + KeyBindingAction.OpenUserSettings, +]; + // This is very intentionally modelled after SETTINGS as it will make it easier // to implement customizable keyboard shortcuts // TODO: TravisR will fix this nightmare when the new version of the SettingsStore becomes a thing @@ -332,14 +392,14 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = { }, displayName: _td("Navigate to previous message in composer history"), }, - "KeyBinding.toggleMicInCall": { + [KeyBindingAction.ToggleMicInCall]: { default: { ctrlOrCmdKey: true, key: Key.D, }, displayName: _td("Toggle microphone mute"), }, - "KeyBinding.toggleWebcamInCall": { + [KeyBindingAction.ToggleWebcamInCall]: { default: { ctrlOrCmdKey: true, key: Key.E, @@ -538,13 +598,117 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = { }, displayName: _td("Undo edit"), }, + [KeyBindingAction.EditRedo]: { + default: { + key: isMac ? Key.Z : Key.Y, + ctrlOrCmdKey: true, + shiftKey: isMac, + }, + displayName: _td("Redo edit"), + }, + [KeyBindingAction.PreviousVisitedRoomOrCommunity]: { + default: { + metaKey: isMac, + altKey: !isMac, + key: isMac ? Key.SQUARE_BRACKET_LEFT : Key.ARROW_LEFT, + }, + displayName: _td("Previous recently visited room or community"), + }, + [KeyBindingAction.NextVisitedRoomOrCommunity]: { + default: { + metaKey: isMac, + altKey: !isMac, + key: isMac ? Key.SQUARE_BRACKET_RIGHT : Key.ARROW_RIGHT, + }, + displayName: _td("Next recently visited room or community"), + }, + [KeyBindingAction.SwitchToSpaceByNumber]: { + default: { + ctrlOrCmdKey: true, + key: DIGITS, + }, + displayName: _td("Switch to space by number"), + }, + [KeyBindingAction.OpenUserSettings]: { + default: { + metaKey: true, + key: Key.COMMA, + }, + displayName: _td("Open user settings"), + }, + [KeyBindingAction.Escape]: { + default: { + key: Key.ESCAPE, + }, + displayName: _td("Close dialog or context menu"), + }, + [KeyBindingAction.Enter]: { + default: { + key: Key.ENTER, + }, + displayName: _td("Activate selected button"), + }, + [KeyBindingAction.Space]: { + default: { + key: Key.SPACE, + }, + }, + [KeyBindingAction.Backspace]: { + default: { + key: Key.BACKSPACE, + }, + }, + [KeyBindingAction.Delete]: { + default: { + key: Key.DELETE, + }, + }, + [KeyBindingAction.Home]: { + default: { + key: Key.HOME, + }, + }, + [KeyBindingAction.End]: { + default: { + key: Key.END, + }, + }, + [KeyBindingAction.ArrowLeft]: { + default: { + key: Key.ARROW_LEFT, + }, + }, + [KeyBindingAction.ArrowUp]: { + default: { + key: Key.ARROW_UP, + }, + }, + [KeyBindingAction.ArrowRight]: { + default: { + key: Key.ARROW_RIGHT, + }, + }, + [KeyBindingAction.ArrowDown]: { + default: { + key: Key.ARROW_DOWN, + }, + }, + [KeyBindingAction.Comma]: { + default: { + key: Key.COMMA, + }, + }, }; -// XXX: These have to be manually mirrored in KeyBindingDefaults -const getNonCustomizableShortcuts = (): IKeyboardShortcuts => { +/** + * This function gets the keyboard shortcuts that should be presented in the UI + * but they shouldn't be consumed by KeyBindingDefaults. That means that these + * have to be manually mirrored in KeyBindingDefaults. + */ +const getUIOnlyShortcuts = (): IKeyboardShortcuts => { const ctrlEnterToSend = SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend'); - return { + const keyboardShortcuts: IKeyboardShortcuts = { [KeyBindingAction.SendMessage]: { default: { key: Key.ENTER, @@ -578,58 +742,63 @@ const getNonCustomizableShortcuts = (): IKeyboardShortcuts => { }, displayName: _td("Search (must be enabled)"), }, - "KeyBinding.closeDialogOrContextMenu": { - default: { - key: Key.ESCAPE, - }, - displayName: _td("Close dialog or context menu"), - }, - "KeyBinding.activateSelectedButton": { + }; + + if (PlatformPeg.get().overrideBrowserShortcuts()) { + // XXX: This keyboard shortcut isn't manually added to + // KeyBindingDefaults as it can't be easily handled by the + // KeyBindingManager + keyboardShortcuts[KeyBindingAction.SwitchToSpaceByNumber] = { default: { - key: Key.ENTER, + ctrlOrCmdKey: true, + key: DIGITS, }, - displayName: _td("Activate selected button"), - }, - }; + displayName: _td("Switch to space by number"), + }; + } + + return keyboardShortcuts; }; -export const getCustomizableShortcuts = (): IKeyboardShortcuts => { - const keyboardShortcuts = Object.assign({}, KEYBOARD_SHORTCUTS); +/** + * This function gets keyboard shortcuts that can be consumed by the KeyBindingDefaults. + */ +export const getKeyboardShortcuts = (): IKeyboardShortcuts => { + const overrideBrowserShortcuts = PlatformPeg.get().overrideBrowserShortcuts(); - keyboardShortcuts[KeyBindingAction.EditRedo] = { - default: { - key: isMac ? Key.Z : Key.Y, - ctrlOrCmdKey: true, - shiftKey: isMac, - }, - displayName: _td("Redo edit"), - }; + return Object.keys(KEYBOARD_SHORTCUTS).filter((k: KeyBindingAction) => { + if (KEYBOARD_SHORTCUTS[k]?.controller?.settingDisabled) return false; + if (MAC_ONLY_SHORTCUTS.includes(k) && !isMac) return false; + if (DESKTOP_SHORTCUTS.includes(k) && !overrideBrowserShortcuts) return false; - return Object.keys(keyboardShortcuts).filter(k => { - return !keyboardShortcuts[k].controller?.settingDisabled; + return true; }).reduce((o, key) => { - o[key] = keyboardShortcuts[key]; + o[key] = KEYBOARD_SHORTCUTS[key]; return o; - }, {}); + }, {} as IKeyboardShortcuts); }; -export const getKeyboardShortcuts = (): IKeyboardShortcuts => { +/** + * Gets keyboard shortcuts that should be presented to the user in the UI. + */ +export const getKeyboardShortcutsForUI = (): IKeyboardShortcuts => { const entries = [ - ...Object.entries(getNonCustomizableShortcuts()), - ...Object.entries(getCustomizableShortcuts()), + ...Object.entries(getUIOnlyShortcuts()), + ...Object.entries(getKeyboardShortcuts()), ]; return entries.reduce((acc, [key, value]) => { acc[key] = value; return acc; - }, {}); + }, {} as IKeyboardShortcuts); }; -export const registerShortcut = ( - shortcutName: string, - categoryName: CategoryName, - shortcut: KeyboardShortcutSetting, -): void => { - KEYBOARD_SHORTCUTS[shortcutName] = shortcut; - CATEGORIES[categoryName].settingNames.push(shortcutName); -}; +// For tests +export function mock({ keyboardShortcuts, macOnlyShortcuts, desktopShortcuts }): void { + Object.keys(KEYBOARD_SHORTCUTS).forEach((k) => delete KEYBOARD_SHORTCUTS[k]); + if (keyboardShortcuts) Object.assign(KEYBOARD_SHORTCUTS, keyboardShortcuts); + MAC_ONLY_SHORTCUTS.splice(0, MAC_ONLY_SHORTCUTS.length); + if (macOnlyShortcuts) macOnlyShortcuts.forEach((e) => MAC_ONLY_SHORTCUTS.push(e)); + DESKTOP_SHORTCUTS.splice(0, DESKTOP_SHORTCUTS.length); + if (desktopShortcuts) desktopShortcuts.forEach((e) => DESKTOP_SHORTCUTS.push(e)); +} diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 842b4edce03..ea6699d3676 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -27,7 +27,8 @@ import React, { RefObject, } from "react"; -import { Key } from "../Keyboard"; +import { getKeyBindingsManager } from "../KeyBindingsManager"; +import { KeyBindingAction } from "./KeyboardShortcuts"; import { FocusHandler, Ref } from "./roving/types"; /** @@ -207,12 +208,13 @@ export const RovingTabIndexProvider: React.FC = ({ } let handled = false; + const action = getKeyBindingsManager().getAccessibilityAction(ev); let focusRef: RefObject; // Don't interfere with input default keydown behaviour // but allow people to move focus from it with Tab. if (checkInputableElement(ev.target as HTMLElement)) { - switch (ev.key) { - case Key.TAB: + switch (action) { + case KeyBindingAction.Tab: handled = true; if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef); @@ -222,8 +224,8 @@ export const RovingTabIndexProvider: React.FC = ({ } } else { // check if we actually have any items - switch (ev.key) { - case Key.HOME: + switch (action) { + case KeyBindingAction.Home: if (handleHomeEnd) { handled = true; // move focus to first (visible) item @@ -231,7 +233,7 @@ export const RovingTabIndexProvider: React.FC = ({ } break; - case Key.END: + case KeyBindingAction.End: if (handleHomeEnd) { handled = true; // move focus to last (visible) item @@ -239,10 +241,10 @@ export const RovingTabIndexProvider: React.FC = ({ } break; - case Key.ARROW_DOWN: - case Key.ARROW_RIGHT: - if ((ev.key === Key.ARROW_DOWN && handleUpDown) || - (ev.key === Key.ARROW_RIGHT && handleLeftRight) + case KeyBindingAction.ArrowDown: + case KeyBindingAction.ArrowRight: + if ((action === KeyBindingAction.ArrowDown && handleUpDown) || + (action === KeyBindingAction.ArrowRight && handleLeftRight) ) { handled = true; if (context.state.refs.length > 0) { @@ -252,9 +254,11 @@ export const RovingTabIndexProvider: React.FC = ({ } break; - case Key.ARROW_UP: - case Key.ARROW_LEFT: - if ((ev.key === Key.ARROW_UP && handleUpDown) || (ev.key === Key.ARROW_LEFT && handleLeftRight)) { + case KeyBindingAction.ArrowUp: + case KeyBindingAction.ArrowLeft: + if ((action === KeyBindingAction.ArrowUp && handleUpDown) || + (action === KeyBindingAction.ArrowLeft && handleLeftRight) + ) { handled = true; if (context.state.refs.length > 0) { const idx = context.state.refs.indexOf(context.state.activeRef); diff --git a/src/accessibility/Toolbar.tsx b/src/accessibility/Toolbar.tsx index c0f2b567484..73d44e22a41 100644 --- a/src/accessibility/Toolbar.tsx +++ b/src/accessibility/Toolbar.tsx @@ -17,7 +17,8 @@ limitations under the License. import React from "react"; import { RovingTabIndexProvider } from "./RovingTabIndex"; -import { Key } from "../Keyboard"; +import { getKeyBindingsManager } from "../KeyBindingsManager"; +import { KeyBindingAction } from "./KeyboardShortcuts"; interface IProps extends Omit, "onKeyDown"> { } @@ -34,9 +35,10 @@ const Toolbar: React.FC = ({ children, ...props }) => { let handled = true; // HOME and END are handled by RovingTabIndexProvider - switch (ev.key) { - case Key.ARROW_UP: - case Key.ARROW_DOWN: + const action = getKeyBindingsManager().getAccessibilityAction(ev); + switch (action) { + case KeyBindingAction.ArrowUp: + case KeyBindingAction.ArrowDown: if (target.hasAttribute('aria-haspopup')) { target.click(); } diff --git a/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx index 7349646f2ba..182428df009 100644 --- a/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx +++ b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx @@ -18,14 +18,15 @@ limitations under the License. import React from "react"; -import { Key } from "../../Keyboard"; import { useRovingTabIndex } from "../RovingTabIndex"; import StyledCheckbox from "../../components/views/elements/StyledCheckbox"; +import { KeyBindingAction } from "../KeyboardShortcuts"; +import { getKeyBindingsManager } from "../../KeyBindingsManager"; interface IProps extends React.ComponentProps { label?: string; onChange(); // we handle keyup/down ourselves so lose the ChangeEvent - onClose(): void; // gets called after onChange on Key.ENTER + onClose(): void; // gets called after onChange on KeyBindingAction.ActivateSelectedButton } // Semantic component for representing a styled role=menuitemcheckbox @@ -33,22 +34,37 @@ export const StyledMenuItemCheckbox: React.FC = ({ children, label, onCh const [onFocus, isActive, ref] = useRovingTabIndex(); const onKeyDown = (e: React.KeyboardEvent) => { - if (e.key === Key.ENTER || e.key === Key.SPACE) { + let handled = true; + const action = getKeyBindingsManager().getAccessibilityAction(e); + + switch (action) { + case KeyBindingAction.Space: + onChange(); + break; + case KeyBindingAction.Enter: + onChange(); + // Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12 + onClose(); + break; + default: + handled = false; + } + + if (handled) { e.stopPropagation(); e.preventDefault(); - onChange(); - // Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12 - if (e.key === Key.ENTER) { - onClose(); - } } }; const onKeyUp = (e: React.KeyboardEvent) => { - // prevent the input default handler as we handle it on keydown to match - // https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html - if (e.key === Key.SPACE || e.key === Key.ENTER) { - e.stopPropagation(); - e.preventDefault(); + const action = getKeyBindingsManager().getAccessibilityAction(e); + switch (action) { + case KeyBindingAction.Space: + case KeyBindingAction.Enter: + // prevent the input default handler as we handle it on keydown to match + // https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html + e.stopPropagation(); + e.preventDefault(); + break; } }; return ( diff --git a/src/accessibility/context_menu/StyledMenuItemRadio.tsx b/src/accessibility/context_menu/StyledMenuItemRadio.tsx index 0ce7f3d6f6f..99440977f1f 100644 --- a/src/accessibility/context_menu/StyledMenuItemRadio.tsx +++ b/src/accessibility/context_menu/StyledMenuItemRadio.tsx @@ -18,14 +18,15 @@ limitations under the License. import React from "react"; -import { Key } from "../../Keyboard"; import { useRovingTabIndex } from "../RovingTabIndex"; import StyledRadioButton from "../../components/views/elements/StyledRadioButton"; +import { KeyBindingAction } from "../KeyboardShortcuts"; +import { getKeyBindingsManager } from "../../KeyBindingsManager"; interface IProps extends React.ComponentProps { label?: string; onChange(); // we handle keyup/down ourselves so lose the ChangeEvent - onClose(): void; // gets called after onChange on Key.ENTER + onClose(): void; // gets called after onChange on KeyBindingAction.Enter } // Semantic component for representing a styled role=menuitemradio @@ -33,22 +34,37 @@ export const StyledMenuItemRadio: React.FC = ({ children, label, onChang const [onFocus, isActive, ref] = useRovingTabIndex(); const onKeyDown = (e: React.KeyboardEvent) => { - if (e.key === Key.ENTER || e.key === Key.SPACE) { + let handled = true; + const action = getKeyBindingsManager().getAccessibilityAction(e); + + switch (action) { + case KeyBindingAction.Space: + onChange(); + break; + case KeyBindingAction.Enter: + onChange(); + // Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12 + onClose(); + break; + default: + handled = false; + } + + if (handled) { e.stopPropagation(); e.preventDefault(); - onChange(); - // Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12 - if (e.key === Key.ENTER) { - onClose(); - } } }; const onKeyUp = (e: React.KeyboardEvent) => { - // prevent the input default handler as we handle it on keydown to match - // https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html - if (e.key === Key.SPACE || e.key === Key.ENTER) { - e.stopPropagation(); - e.preventDefault(); + const action = getKeyBindingsManager().getAccessibilityAction(e); + switch (action) { + case KeyBindingAction.Enter: + case KeyBindingAction.Space: + // prevent the input default handler as we handle it on keydown to match + // https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html + e.stopPropagation(); + e.preventDefault(); + break; } }; return ( diff --git a/src/actions/MatrixActionCreators.ts b/src/actions/MatrixActionCreators.ts index c17ae3b5967..c4d75cc854a 100644 --- a/src/actions/MatrixActionCreators.ts +++ b/src/actions/MatrixActionCreators.ts @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient } from "matrix-js-sdk/src/client"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { Room } from "matrix-js-sdk/src/models/room"; +import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client"; +import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event"; +import { Room, RoomEvent } from "matrix-js-sdk/src/models/room"; import { IRoomTimelineData } from "matrix-js-sdk/src/models/event-timeline-set"; import dis from "../dispatcher/dispatcher"; @@ -274,7 +274,11 @@ let matrixClientListenersStop: Listener[] = []; * when given the MatrixClient as an argument as well as * arguments emitted in the MatrixClient event. */ -function addMatrixClientListener(matrixClient: MatrixClient, eventName: string, actionCreator: ActionCreator): void { +function addMatrixClientListener( + matrixClient: MatrixClient, + eventName: Parameters[0], + actionCreator: ActionCreator, +): void { const listener: Listener = (...args) => { const payload = actionCreator(matrixClient, ...args); if (payload) { @@ -298,15 +302,15 @@ export default { * @param {MatrixClient} matrixClient the MatrixClient to listen to events from */ start(matrixClient: MatrixClient) { - addMatrixClientListener(matrixClient, 'sync', createSyncAction); - addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction); - addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction); - addMatrixClientListener(matrixClient, 'Room', createRoomAction); - addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction); - addMatrixClientListener(matrixClient, 'Room.receipt', createRoomReceiptAction); - addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction); - addMatrixClientListener(matrixClient, 'Room.myMembership', createSelfMembershipAction); - addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction); + addMatrixClientListener(matrixClient, ClientEvent.Sync, createSyncAction); + addMatrixClientListener(matrixClient, ClientEvent.AccountData, createAccountDataAction); + addMatrixClientListener(matrixClient, RoomEvent.AccountData, createRoomAccountDataAction); + addMatrixClientListener(matrixClient, ClientEvent.Room, createRoomAction); + addMatrixClientListener(matrixClient, RoomEvent.Tags, createRoomTagsAction); + addMatrixClientListener(matrixClient, RoomEvent.Receipt, createRoomReceiptAction); + addMatrixClientListener(matrixClient, RoomEvent.Timeline, createRoomTimelineAction); + addMatrixClientListener(matrixClient, RoomEvent.MyMembership, createSelfMembershipAction); + addMatrixClientListener(matrixClient, MatrixEventEvent.Decrypted, createEventDecryptedAction); }, /** diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 3253d7ba330..53df137f6d6 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -20,8 +20,9 @@ import FileSaver from 'file-saver'; import { logger } from "matrix-js-sdk/src/logger"; import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; import { TrustInfo } from "matrix-js-sdk/src/crypto/backup"; -import { CrossSigningKeys } from "matrix-js-sdk/src"; +import { CrossSigningKeys } from "matrix-js-sdk/src/matrix"; import { IRecoveryKey } from "matrix-js-sdk/src/crypto/api"; +import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { MatrixClientPeg } from '../../../../MatrixClientPeg'; import { _t, _td } from '../../../../languageHandler'; @@ -145,13 +146,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent { - // ignore members in other rooms - if (member.roomId !== this.room.roomId) { - return; - } + private onRoomStateUpdate = (state: RoomState) => { + // ignore updates in other rooms + if (state.roomId !== this.room.roomId) return; // blow away the users cache this.users = null; diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index fa164bca17a..22316189940 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -21,16 +21,18 @@ import ReactDOM from "react-dom"; import classNames from "classnames"; import FocusLock from "react-focus-lock"; -import { Key } from "../../Keyboard"; import { Writeable } from "../../@types/common"; import { replaceableComponent } from "../../utils/replaceableComponent"; import UIStore from "../../stores/UIStore"; import { checkInputableElement, RovingTabIndexProvider } from "../../accessibility/RovingTabIndex"; +import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts"; +import { getKeyBindingsManager } from "../../KeyBindingsManager"; // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and // pass in a custom control as the actual body. +const WINDOW_PADDING = 10; const ContextualMenuContainerId = "mx_ContextualMenu_Container"; function getOrCreateContainer(): HTMLDivElement { @@ -190,30 +192,32 @@ export default class ContextMenu extends React.PureComponent { private onKeyDown = (ev: React.KeyboardEvent) => { ev.stopPropagation(); // prevent keyboard propagating out of the context menu, we're focus-locked + const action = getKeyBindingsManager().getAccessibilityAction(ev); + // If someone is managing their own focus, we will only exit for them with Escape. // They are probably using props.focusLock along with this option as well. if (!this.props.managed) { - if (ev.key === Key.ESCAPE) { + if (action === KeyBindingAction.Escape) { this.props.onFinished(); } return; } // When an is focused, only handle the Escape key - if (checkInputableElement(ev.target as HTMLElement) && ev.key !== Key.ESCAPE) { + if (checkInputableElement(ev.target as HTMLElement) && action !== KeyBindingAction.Escape) { return; } - if ( - ev.key === Key.ESCAPE || + if ([ + KeyBindingAction.Escape, // You can only navigate the ContextMenu by arrow keys and Home/End (see RovingTabIndex). // Tabbing to the next section of the page, will close the ContextMenu. - ev.key === Key.TAB || + KeyBindingAction.Tab, // When someone moves left or right along a (like the // MessageActionBar), we should close any ContextMenu that is open. - ev.key === Key.ARROW_LEFT || - ev.key === Key.ARROW_RIGHT - ) { + KeyBindingAction.ArrowLeft, + KeyBindingAction.ArrowRight, + ].includes(action)) { this.props.onFinished(); } }; @@ -247,21 +251,51 @@ export default class ContextMenu extends React.PureComponent { if (chevronFace === ChevronFace.Top || chevronFace === ChevronFace.Bottom) { chevronOffset.left = props.chevronOffset; - } else if (position.top !== undefined) { - const target = position.top; - - // By default, no adjustment is made - let adjusted = target; + } else { + chevronOffset.top = props.chevronOffset; + } - // If we know the dimensions of the context menu, adjust its position - // such that it does not leave the (padded) window. - if (contextMenuRect) { - const padding = 10; - adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding); + // If we know the dimensions of the context menu, adjust its position to + // keep it within the bounds of the (padded) window + const { windowWidth, windowHeight } = UIStore.instance; + if (contextMenuRect) { + if (position.top !== undefined) { + let maxTop = windowHeight - WINDOW_PADDING; + if (!this.props.bottomAligned) { + maxTop -= contextMenuRect.height; + } + position.top = Math.min(position.top, maxTop); + // Adjust the chevron if necessary + if (chevronOffset.top !== undefined) { + chevronOffset.top = props.chevronOffset + props.top - position.top; + } + } else if (position.bottom !== undefined) { + position.bottom = Math.min( + position.bottom, + windowHeight - contextMenuRect.height - WINDOW_PADDING, + ); + if (chevronOffset.top !== undefined) { + chevronOffset.top = props.chevronOffset + position.bottom - props.bottom; + } + } + if (position.left !== undefined) { + let maxLeft = windowWidth - WINDOW_PADDING; + if (!this.props.rightAligned) { + maxLeft -= contextMenuRect.width; + } + position.left = Math.min(position.left, maxLeft); + if (chevronOffset.left !== undefined) { + chevronOffset.left = props.chevronOffset + props.left - position.left; + } + } else if (position.right !== undefined) { + position.right = Math.min( + position.right, + windowWidth - contextMenuRect.width - WINDOW_PADDING, + ); + if (chevronOffset.left !== undefined) { + chevronOffset.left = props.chevronOffset + position.right - props.right; + } } - - position.top = adjusted; - chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted); } let chevron; diff --git a/src/components/structures/FileDropTarget.tsx b/src/components/structures/FileDropTarget.tsx new file mode 100644 index 00000000000..f6572a05e85 --- /dev/null +++ b/src/components/structures/FileDropTarget.tsx @@ -0,0 +1,120 @@ +/* +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, { useEffect, useState } from "react"; + +import { _t } from "../../languageHandler"; + +interface IProps { + parent: HTMLElement; + onFileDrop(dataTransfer: DataTransfer): void; +} + +interface IState { + dragging: boolean; + counter: number; +} + +const FileDropTarget: React.FC = ({ parent, onFileDrop }) => { + const [state, setState] = useState({ + dragging: false, + counter: 0, + }); + + useEffect(() => { + if (!parent || parent.ondrop) return; + + const onDragEnter = (ev: DragEvent) => { + ev.stopPropagation(); + ev.preventDefault(); + + setState(state => ({ + // We always increment the counter no matter the types, because dragging is + // still happening. If we didn't, the drag counter would get out of sync. + counter: state.counter + 1, + // See: + // https://docs.w3cub.com/dom/datatransfer/types + // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#file + dragging: ( + ev.dataTransfer.types.includes("Files") || + ev.dataTransfer.types.includes("application/x-moz-file") + ) ? true : state.dragging, + })); + }; + + const onDragLeave = (ev: DragEvent) => { + ev.stopPropagation(); + ev.preventDefault(); + + setState(state => ({ + counter: state.counter - 1, + dragging: state.counter <= 1 ? false : state.dragging, + })); + }; + + const onDragOver = (ev: DragEvent) => { + ev.stopPropagation(); + ev.preventDefault(); + + ev.dataTransfer.dropEffect = "none"; + + // See: + // https://docs.w3cub.com/dom/datatransfer/types + // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#file + if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) { + ev.dataTransfer.dropEffect = "copy"; + } + }; + + const onDrop = (ev: DragEvent) => { + ev.stopPropagation(); + ev.preventDefault(); + onFileDrop(ev.dataTransfer); + + setState(state => ({ + dragging: false, + counter: state.counter - 1, + })); + }; + + parent.addEventListener("drop", onDrop); + parent.addEventListener("dragover", onDragOver); + parent.addEventListener("dragenter", onDragEnter); + parent.addEventListener("dragleave", onDragLeave); + + return () => { + // disconnect the D&D event listeners from the room view. This + // is really just for hygiene - we're going to be + // deleted anyway, so it doesn't matter if the event listeners + // don't get cleaned up. + parent.removeEventListener("drop", onDrop); + parent.removeEventListener("dragover", onDragOver); + parent.removeEventListener("dragenter", onDragEnter); + parent.removeEventListener("dragleave", onDragLeave); + }; + }, [parent, onFileDrop]); + + if (state.dragging) { + return
+ + { _t("Drop file here to upload") } +
; + } + + return null; +}; + +export default FileDropTarget; diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index bae9f74a07e..43358978777 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -1,6 +1,6 @@ /* Copyright 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 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,12 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { createRef } from 'react'; import { Filter } from 'matrix-js-sdk/src/filter'; import { EventTimelineSet, IRoomTimelineData } from "matrix-js-sdk/src/models/event-timeline-set"; import { Direction } from "matrix-js-sdk/src/models/event-timeline"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { Room } from 'matrix-js-sdk/src/models/room'; +import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event"; +import { Room, RoomEvent } from 'matrix-js-sdk/src/models/room'; import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window'; import { logger } from "matrix-js-sdk/src/logger"; @@ -36,6 +36,7 @@ import Spinner from "../views/elements/Spinner"; import { Layout } from "../../settings/enums/Layout"; import { UserNameColorMode } from '../../settings/enums/UserNameColorMode'; import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext'; +import Measured from '../views/elements/Measured'; interface IProps { roomId: string; @@ -46,6 +47,7 @@ interface IProps { interface IState { timelineSet: EventTimelineSet; + narrow: boolean; } /* @@ -53,14 +55,17 @@ interface IState { */ @replaceableComponent("structures.FilePanel") class FilePanel extends React.Component { + static contextType = RoomContext; + // This is used to track if a decrypted event was a live event and should be // added to the timeline. private decryptingEvents = new Set(); public noRoom: boolean; - static contextType = RoomContext; + private card = createRef(); state = { timelineSet: null, + narrow: false, }; private onRoomTimeline = ( @@ -123,8 +128,8 @@ class FilePanel extends React.Component { // this could be made more general in the future or the filter logic // could be fixed. if (EventIndexPeg.get() !== null) { - client.on('Room.timeline', this.onRoomTimeline); - client.on('Event.decrypted', this.onEventDecrypted); + client.on(RoomEvent.Timeline, this.onRoomTimeline); + client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted); } } @@ -135,8 +140,8 @@ class FilePanel extends React.Component { if (!MatrixClientPeg.get().isRoomEncrypted(this.props.roomId)) return; if (EventIndexPeg.get() !== null) { - client.removeListener('Room.timeline', this.onRoomTimeline); - client.removeListener('Event.decrypted', this.onEventDecrypted); + client.removeListener(RoomEvent.Timeline, this.onRoomTimeline); + client.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted); } } @@ -186,6 +191,10 @@ class FilePanel extends React.Component { } }; + private onMeasurement = (narrow: boolean): void => { + this.setState({ narrow }); + }; + public async updateTimelineSet(roomId: string): Promise { const client = MatrixClientPeg.get(); const room = client.getRoom(roomId); @@ -258,12 +267,18 @@ class FilePanel extends React.Component { + { public static contextType = MatrixClientContext; + public context!: React.ContextType; public state = { orderedTags: [], @@ -63,8 +65,8 @@ class GroupFilterPanel extends React.Component { if (this.unmounted) { @@ -82,8 +84,8 @@ class GroupFilterPanel extends React.Component - +
{ _t('Add a Room') }
@@ -235,7 +235,7 @@ class FeaturedRoom extends React.Component { const deleteButton = this.props.editing ? Delete - +
{ _t('Add a User') }
@@ -386,7 +386,7 @@ class FeaturedUser extends React.Component { const deleteButton = this.props.editing ? Delete { localStorage.setItem(UPGRADE_NOTICE_LS_KEY, "true"); this.setState({ showUpgradeNotice: false }); - } + }; _onCreateSpaceClick = () => { createSpaceFromCommunity(this._matrixClient, this.props.groupId); @@ -844,7 +844,7 @@ export default class GroupView extends React.Component { }, ) } - + ; } @@ -925,7 +925,7 @@ export default class GroupView extends React.Component { onClick={this._onAddRoomsClick} >
- +
{ _t('Add rooms to this community') } @@ -1257,7 +1257,7 @@ export default class GroupView extends React.Component {