diff --git a/.eslintrc.js b/.eslintrc.js index 4658fb9792b..37379f13047 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,8 +1,11 @@ module.exports = { - plugins: ["matrix-org"], + plugins: [ + "matrix-org", + ], extends: [ "plugin:matrix-org/babel", "plugin:matrix-org/react", + "plugin:matrix-org/a11y", ], env: { browser: true, @@ -36,6 +39,24 @@ module.exports = { "Use Media helper instead to centralise access for customisation.", ), ], + + // There are too many a11y violations to fix at once + // Turn violated rules off until they are fixed + "jsx-a11y/alt-text": "off", + "jsx-a11y/aria-activedescendant-has-tabindex": "off", + "jsx-a11y/click-events-have-key-events": "off", + "jsx-a11y/iframe-has-title": "off", + "jsx-a11y/interactive-supports-focus": "off", + "jsx-a11y/label-has-associated-control": "off", + "jsx-a11y/media-has-caption": "off", + "jsx-a11y/mouse-events-have-key-events": "off", + "jsx-a11y/no-autofocus": "off", + "jsx-a11y/no-noninteractive-element-interactions": "off", + "jsx-a11y/no-noninteractive-element-to-interactive-role": "off", + "jsx-a11y/no-noninteractive-tabindex": "off", + "jsx-a11y/no-static-element-interactions": "off", + "jsx-a11y/role-supports-aria-props": "off", + "jsx-a11y/tabindex-no-positive": "off", }, overrides: [{ files: [ diff --git a/CHANGELOG.md b/CHANGELOG.md index 416ddc21c51..dbfa7210365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,199 @@ +Changes in [3.38.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.38.0) (2022-01-17) +===================================================================================================== + +## ✨ Features + * Add permission dropdown for sending reactions ([\#7492](https://github.com/matrix-org/matrix-react-sdk/pull/7492)). Fixes vector-im/element-web#20450. + * Ship maximised widgets and remove feature flag ([\#7509](https://github.com/matrix-org/matrix-react-sdk/pull/7509)). + * Properly maintain aspect ratio of inline images ([\#7503](https://github.com/matrix-org/matrix-react-sdk/pull/7503)). + * Add zoom buttons to the location view ([\#7482](https://github.com/matrix-org/matrix-react-sdk/pull/7482)). + * Remove bubble from around location events ([\#7459](https://github.com/matrix-org/matrix-react-sdk/pull/7459)). Fixes vector-im/element-web#20323. + * Disable "Publish this room" option in invite only rooms ([\#7441](https://github.com/matrix-org/matrix-react-sdk/pull/7441)). Fixes vector-im/element-web#6596. Contributed by @aaronraimist. + * Give secret key field an `id` ([\#7489](https://github.com/matrix-org/matrix-react-sdk/pull/7489)). Fixes vector-im/element-web#20390. Contributed by @SimonBrandner. + * Display a tooltip when you hover over a location ([\#7472](https://github.com/matrix-org/matrix-react-sdk/pull/7472)). + * Open map in a dialog when it is clicked ([\#7465](https://github.com/matrix-org/matrix-react-sdk/pull/7465)). + * a11y - wrap notification level radios in fieldsets ([\#7471](https://github.com/matrix-org/matrix-react-sdk/pull/7471)). + * Wrap inputs in fieldsets in Space visibility settings ([\#7350](https://github.com/matrix-org/matrix-react-sdk/pull/7350)). + * History based navigation with new right panel store ([\#7398](https://github.com/matrix-org/matrix-react-sdk/pull/7398)). Fixes vector-im/element-web#19686 vector-im/element-web#19660 and vector-im/element-web#19634. + * Associate room alias warning with public option in settings ([\#7430](https://github.com/matrix-org/matrix-react-sdk/pull/7430)). + * Disable quick reactions button when no permissions ([\#7412](https://github.com/matrix-org/matrix-react-sdk/pull/7412)). Fixes vector-im/element-web#20270. + * Allow opening a map view in OpenStreetMap ([\#7428](https://github.com/matrix-org/matrix-react-sdk/pull/7428)). + * Display the user's avatar when they shared their location ([\#7424](https://github.com/matrix-org/matrix-react-sdk/pull/7424)). + * Remove the Forward and Share buttons for location messages only ([\#7423](https://github.com/matrix-org/matrix-react-sdk/pull/7423)). + * Add configuration to disable relative date markers in timeline ([\#7405](https://github.com/matrix-org/matrix-react-sdk/pull/7405)). + * Space preferences for whether or not you see DMs in a Space ([\#7250](https://github.com/matrix-org/matrix-react-sdk/pull/7250)). Fixes vector-im/element-web#19529 and vector-im/element-web#19955. + * Have LocalEchoWrapper emit updates so the app can react faster ([\#7358](https://github.com/matrix-org/matrix-react-sdk/pull/7358)). Fixes vector-im/element-web#19749. + * Use semantic heading on dialog component ([\#7383](https://github.com/matrix-org/matrix-react-sdk/pull/7383)). + * Add `/jumptodate` slash command ([\#7372](https://github.com/matrix-org/matrix-react-sdk/pull/7372)). Fixes vector-im/element-web#7677. + * Update room context menu copy ([\#7361](https://github.com/matrix-org/matrix-react-sdk/pull/7361)). Fixes vector-im/element-web#20133. + * Use lazy rendering in the AddExistingToSpaceDialog ([\#7369](https://github.com/matrix-org/matrix-react-sdk/pull/7369)). Fixes vector-im/element-web#18784. + * Tweak FacePile tooltip to include whether or not you are included ([\#7367](https://github.com/matrix-org/matrix-react-sdk/pull/7367)). Fixes vector-im/element-web#17278. + +## 🐛 Bug Fixes + * Fix wrongly wrapping code blocks, breaking line numbers ([\#7507](https://github.com/matrix-org/matrix-react-sdk/pull/7507)). Fixes vector-im/element-web#20316. + * Set header buttons to no phase when right panel is closed ([\#7506](https://github.com/matrix-org/matrix-react-sdk/pull/7506)). + * Fix active Jitsi calls (and other active widgets) not being visible on screen, by showing them in PiP if they are not visible in any other container ([\#7435](https://github.com/matrix-org/matrix-react-sdk/pull/7435)). Fixes vector-im/element-web#15169 and vector-im/element-web#20275. + * Fix layout of message bubble preview in settings ([\#7497](https://github.com/matrix-org/matrix-react-sdk/pull/7497)). + * Prevent mutations of js-sdk owned objects as it breaks accountData ([\#7504](https://github.com/matrix-org/matrix-react-sdk/pull/7504)). Fixes matrix-org/element-web-rageshakes#7822. + * fallback properly with pluralized strings ([\#7495](https://github.com/matrix-org/matrix-react-sdk/pull/7495)). Fixes vector-im/element-web#20455. + * Consider continuations when resolving whether a tile is last in section ([\#7461](https://github.com/matrix-org/matrix-react-sdk/pull/7461)). Fixes vector-im/element-web#20368 and vector-im/element-web#20369. + * Fix read receipts and sent indicators for bubble layout ([\#7460](https://github.com/matrix-org/matrix-react-sdk/pull/7460)). Fixes vector-im/element-web#18298 and vector-im/element-web#20345. + * null-guard dataset mxTheme to prevent html exports from exploding ([\#7493](https://github.com/matrix-org/matrix-react-sdk/pull/7493)). Fixes vector-im/element-web#20453. + * Fix avatar container overlapping give feedback cta ([\#7491](https://github.com/matrix-org/matrix-react-sdk/pull/7491)). Fixes matrix-org/element-web-rageshakes#7987. + * Fix jump to bottom button working when on a permalink ([\#7494](https://github.com/matrix-org/matrix-react-sdk/pull/7494)). Fixes vector-im/element-web#19813. + * Remove the Description from the location picker ([\#7485](https://github.com/matrix-org/matrix-react-sdk/pull/7485)). + * Fix look of the untrusted device dialog ([\#7487](https://github.com/matrix-org/matrix-react-sdk/pull/7487)). Fixes vector-im/element-web#20447. Contributed by @SimonBrandner. + * Hide maximise button in the sticker picker ([\#7488](https://github.com/matrix-org/matrix-react-sdk/pull/7488)). Fixes vector-im/element-web#20443. Contributed by @SimonBrandner. + * Fix space ordering to match newer spec ([\#7481](https://github.com/matrix-org/matrix-react-sdk/pull/7481)). + * Fix typing notification colors ([\#7490](https://github.com/matrix-org/matrix-react-sdk/pull/7490)). Fixes vector-im/element-web#20144. Contributed by @SimonBrandner. + * fix fallback for pluralized strings ([\#7480](https://github.com/matrix-org/matrix-react-sdk/pull/7480)). Fixes vector-im/element-web#20426. + * Fix right panel soft crashes chat rooms ([\#7479](https://github.com/matrix-org/matrix-react-sdk/pull/7479)). Fixes vector-im/element-web#20433. + * update yarn.lock and i18n ([\#7476](https://github.com/matrix-org/matrix-react-sdk/pull/7476)). Fixes vector-im/element-web#20426 and vector-im/element-web#20423. + * Don't send typing notification when restoring composer draft ([\#7477](https://github.com/matrix-org/matrix-react-sdk/pull/7477)). Fixes vector-im/element-web#20424. + * Fix room joining spinner being incorrect if you change room mid-join ([\#7473](https://github.com/matrix-org/matrix-react-sdk/pull/7473)). + * Only return the approved widget capabilities instead of accepting all requested capabilities ([\#7454](https://github.com/matrix-org/matrix-react-sdk/pull/7454)). Contributed by @dhenneke. + * Fix quoting messages from the search view ([\#7466](https://github.com/matrix-org/matrix-react-sdk/pull/7466)). Fixes vector-im/element-web#20353. + * Attribute fallback i18n strings with lang attribute ([\#7323](https://github.com/matrix-org/matrix-react-sdk/pull/7323)). + * Fix spotlight cmd-k wrongly expanding left panel ([\#7463](https://github.com/matrix-org/matrix-react-sdk/pull/7463)). Fixes vector-im/element-web#20399. + * Fix room_id check when adding user widgets ([\#7448](https://github.com/matrix-org/matrix-react-sdk/pull/7448)). Fixes vector-im/element-web#19382. Contributed by @bink. + * Add new line in settings label ([\#7451](https://github.com/matrix-org/matrix-react-sdk/pull/7451)). Fixes vector-im/element-web#20365. + * Fix handling incoming redactions in EventIndex ([\#7443](https://github.com/matrix-org/matrix-react-sdk/pull/7443)). Fixes vector-im/element-web#19326. + * Fix room alias address isn't checked for validity before being shown as added ([\#7107](https://github.com/matrix-org/matrix-react-sdk/pull/7107)). Fixes vector-im/element-web#19609. Contributed by @Palid. + * Call view accessibility fixes ([\#7439](https://github.com/matrix-org/matrix-react-sdk/pull/7439)). Fixes vector-im/element-web#18516. + * Fix offscreen canvas breaking with split-brained firefox support ([\#7440](https://github.com/matrix-org/matrix-react-sdk/pull/7440)). + * Removed red shield in forwarding preview. ([\#7447](https://github.com/matrix-org/matrix-react-sdk/pull/7447)). Contributed by @ankur12-1610. + * Wrap status message ([\#7325](https://github.com/matrix-org/matrix-react-sdk/pull/7325)). Fixes vector-im/element-web#20092. Contributed by @SimonBrandner. + * Move hideSender logic into state so it causes re-render ([\#7413](https://github.com/matrix-org/matrix-react-sdk/pull/7413)). Fixes vector-im/element-web#18448. + * Fix dialpad positioning ([\#7446](https://github.com/matrix-org/matrix-react-sdk/pull/7446)). Fixes vector-im/element-web#20175. Contributed by @SimonBrandner. + * Hide non-functional list options on Suggested sublist ([\#7410](https://github.com/matrix-org/matrix-react-sdk/pull/7410)). Fixes vector-im/element-web#20252. + * Fix width overflow in mini composer overflow menu ([\#7411](https://github.com/matrix-org/matrix-react-sdk/pull/7411)). Fixes vector-im/element-web#20263. + * Fix being wrongly sent to Home space when creating/joining/leaving rooms ([\#7418](https://github.com/matrix-org/matrix-react-sdk/pull/7418)). Fixes matrix-org/element-web-rageshakes#7331 vector-im/element-web#20246 and vector-im/element-web#20240. + * Fix HTML Export where the data-mx-theme is `Light` not `light` ([\#7415](https://github.com/matrix-org/matrix-react-sdk/pull/7415)). + * Don't disable username/password fields whilst doing wk-lookup ([\#7438](https://github.com/matrix-org/matrix-react-sdk/pull/7438)). Fixes vector-im/element-web#20121. + * Prevent keyboard propagation out of context menus ([\#7437](https://github.com/matrix-org/matrix-react-sdk/pull/7437)). Fixes vector-im/element-web#20317. + * Fix nulls leaking into geo urls ([\#7433](https://github.com/matrix-org/matrix-react-sdk/pull/7433)). + * Fix zIndex of peristent apps in miniMode ([\#7429](https://github.com/matrix-org/matrix-react-sdk/pull/7429)). + * Space panel should watch spaces for space name changes ([\#7432](https://github.com/matrix-org/matrix-react-sdk/pull/7432)). + * Fix list formatting alternating on edit ([\#7422](https://github.com/matrix-org/matrix-react-sdk/pull/7422)). Fixes vector-im/element-web#20073. Contributed by @renancleyson-dev. + * Don't show `Testing small changes` without UIFeature.Feedback ([\#7427](https://github.com/matrix-org/matrix-react-sdk/pull/7427)). Fixes vector-im/element-web#20298. + * Fix invisible toggle space panel button ([\#7426](https://github.com/matrix-org/matrix-react-sdk/pull/7426)). Fixes vector-im/element-web#20279. + * Fix legacy breadcrumbs wrongly showing up ([\#7425](https://github.com/matrix-org/matrix-react-sdk/pull/7425)). + * Space Panel use SettingsStore instead of SpaceStore as source of truth ([\#7404](https://github.com/matrix-org/matrix-react-sdk/pull/7404)). Fixes vector-im/element-web#20250. + * Fix inline code block nowrap issue ([\#7406](https://github.com/matrix-org/matrix-react-sdk/pull/7406)). + * Fix notification badge for All Rooms space ([\#7401](https://github.com/matrix-org/matrix-react-sdk/pull/7401)). Fixes vector-im/element-web#20229. + * Show error if could not load space hierarchy ([\#7399](https://github.com/matrix-org/matrix-react-sdk/pull/7399)). Fixes vector-im/element-web#20221. + * Increase gap between ELS and the subsequent event to prevent overlap ([\#7391](https://github.com/matrix-org/matrix-react-sdk/pull/7391)). Fixes vector-im/element-web#18319. + * Fix list of members in space preview ([\#7356](https://github.com/matrix-org/matrix-react-sdk/pull/7356)). Fixes vector-im/element-web#19781. + * Fix sizing of e2e shield in bubble layout ([\#7394](https://github.com/matrix-org/matrix-react-sdk/pull/7394)). Fixes vector-im/element-web#19090. + * Fix bubble radius wrong when followed by a state event from same user ([\#7393](https://github.com/matrix-org/matrix-react-sdk/pull/7393)). Fixes vector-im/element-web#18982. + * Fix alignment between ELS and Events in bubble layout ([\#7392](https://github.com/matrix-org/matrix-react-sdk/pull/7392)). Fixes vector-im/element-web#19652 and vector-im/element-web#19057. + * Don't include the accuracy parameter in location events if accuracy could not be determined. ([\#7375](https://github.com/matrix-org/matrix-react-sdk/pull/7375)). + * Make compact layout only apply to Modern layout ([\#7382](https://github.com/matrix-org/matrix-react-sdk/pull/7382)). Fixes vector-im/element-web#18412. + * Pin qrcode to fix e2e verification bug ([\#7378](https://github.com/matrix-org/matrix-react-sdk/pull/7378)). Fixes vector-im/element-web#20188. + * Add internationalisation to progress strings in room export dialog ([\#7385](https://github.com/matrix-org/matrix-react-sdk/pull/7385)). Fixes vector-im/element-web#20208. + * Prevent escape to cancel edit from also scrolling to bottom ([\#7380](https://github.com/matrix-org/matrix-react-sdk/pull/7380)). Fixes vector-im/element-web#20182. + * Fix narrow mode composer buttons for polls labs ([\#7386](https://github.com/matrix-org/matrix-react-sdk/pull/7386)). Fixes vector-im/element-web#20067. + * Fix useUserStatusMessage exploding on unknown user ([\#7365](https://github.com/matrix-org/matrix-react-sdk/pull/7365)). + * Fix room join spinner in room list header ([\#7364](https://github.com/matrix-org/matrix-react-sdk/pull/7364)). Fixes vector-im/element-web#20139. + * Fix room search sometimes not opening spotlight ([\#7363](https://github.com/matrix-org/matrix-react-sdk/pull/7363)). Fixes matrix-org/element-web-rageshakes#7288. + +Changes in [3.38.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.38.0-rc.1) (2022-01-11) +=============================================================================================================== + +## ✨ Features + * Ship maximised widgets and remove feature flag ([\#7509](https://github.com/matrix-org/matrix-react-sdk/pull/7509)). + * Properly maintain aspect ratio of inline images ([\#7503](https://github.com/matrix-org/matrix-react-sdk/pull/7503)). + * Add zoom buttons to the location view ([\#7482](https://github.com/matrix-org/matrix-react-sdk/pull/7482)). + * Remove bubble from around location events ([\#7459](https://github.com/matrix-org/matrix-react-sdk/pull/7459)). Fixes vector-im/element-web#20323. + * Disable "Publish this room" option in invite only rooms ([\#7441](https://github.com/matrix-org/matrix-react-sdk/pull/7441)). Fixes vector-im/element-web#6596. Contributed by @aaronraimist. + * Add permission dropdown for sending reactions ([\#7492](https://github.com/matrix-org/matrix-react-sdk/pull/7492)). Fixes vector-im/element-web#20450. + * Give secret key field an `id` ([\#7489](https://github.com/matrix-org/matrix-react-sdk/pull/7489)). Fixes vector-im/element-web#20390. Contributed by @SimonBrandner. + * Display a tooltip when you hover over a location ([\#7472](https://github.com/matrix-org/matrix-react-sdk/pull/7472)). + * Open map in a dialog when it is clicked ([\#7465](https://github.com/matrix-org/matrix-react-sdk/pull/7465)). + * a11y - wrap notification level radios in fieldsets ([\#7471](https://github.com/matrix-org/matrix-react-sdk/pull/7471)). + * Wrap inputs in fieldsets in Space visibility settings ([\#7350](https://github.com/matrix-org/matrix-react-sdk/pull/7350)). + * History based navigation with new right panel store ([\#7398](https://github.com/matrix-org/matrix-react-sdk/pull/7398)). Fixes vector-im/element-web#19686 vector-im/element-web#19660 and vector-im/element-web#19634. + * Associate room alias warning with public option in settings ([\#7430](https://github.com/matrix-org/matrix-react-sdk/pull/7430)). + * Disable quick reactions button when no permissions ([\#7412](https://github.com/matrix-org/matrix-react-sdk/pull/7412)). Fixes vector-im/element-web#20270. + * Allow opening a map view in OpenStreetMap ([\#7428](https://github.com/matrix-org/matrix-react-sdk/pull/7428)). + * Display the user's avatar when they shared their location ([\#7424](https://github.com/matrix-org/matrix-react-sdk/pull/7424)). + * Remove the Forward and Share buttons for location messages only ([\#7423](https://github.com/matrix-org/matrix-react-sdk/pull/7423)). + * Add configuration to disable relative date markers in timeline ([\#7405](https://github.com/matrix-org/matrix-react-sdk/pull/7405)). + * Space preferences for whether or not you see DMs in a Space ([\#7250](https://github.com/matrix-org/matrix-react-sdk/pull/7250)). Fixes vector-im/element-web#19529 and vector-im/element-web#19955. + * Have LocalEchoWrapper emit updates so the app can react faster ([\#7358](https://github.com/matrix-org/matrix-react-sdk/pull/7358)). Fixes vector-im/element-web#19749. + * Use semantic heading on dialog component ([\#7383](https://github.com/matrix-org/matrix-react-sdk/pull/7383)). + * Add `/jumptodate` slash command ([\#7372](https://github.com/matrix-org/matrix-react-sdk/pull/7372)). Fixes vector-im/element-web#7677. + * Update room context menu copy ([\#7361](https://github.com/matrix-org/matrix-react-sdk/pull/7361)). Fixes vector-im/element-web#20133. + * Use lazy rendering in the AddExistingToSpaceDialog ([\#7369](https://github.com/matrix-org/matrix-react-sdk/pull/7369)). Fixes vector-im/element-web#18784. + * Tweak FacePile tooltip to include whether or not you are included ([\#7367](https://github.com/matrix-org/matrix-react-sdk/pull/7367)). Fixes vector-im/element-web#17278. + +## 🐛 Bug Fixes + * Fix wrongly wrapping code blocks, breaking line numbers ([\#7507](https://github.com/matrix-org/matrix-react-sdk/pull/7507)). Fixes vector-im/element-web#20316. + * Set header buttons to no phase when right panel is closed ([\#7506](https://github.com/matrix-org/matrix-react-sdk/pull/7506)). + * Fix active Jitsi calls (and other active widgets) not being visible on screen, by showing them in PiP if they are not visible in any other container ([\#7435](https://github.com/matrix-org/matrix-react-sdk/pull/7435)). Fixes vector-im/element-web#15169 and vector-im/element-web#20275. + * Fix layout of message bubble preview in settings ([\#7497](https://github.com/matrix-org/matrix-react-sdk/pull/7497)). + * Prevent mutations of js-sdk owned objects as it breaks accountData ([\#7504](https://github.com/matrix-org/matrix-react-sdk/pull/7504)). Fixes matrix-org/element-web-rageshakes#7822. + * fallback properly with pluralized strings ([\#7495](https://github.com/matrix-org/matrix-react-sdk/pull/7495)). Fixes vector-im/element-web#20455. + * Consider continuations when resolving whether a tile is last in section ([\#7461](https://github.com/matrix-org/matrix-react-sdk/pull/7461)). Fixes vector-im/element-web#20368 and vector-im/element-web#20369. + * Fix read receipts and sent indicators for bubble layout ([\#7460](https://github.com/matrix-org/matrix-react-sdk/pull/7460)). Fixes vector-im/element-web#18298 and vector-im/element-web#20345. + * null-guard dataset mxTheme to prevent html exports from exploding ([\#7493](https://github.com/matrix-org/matrix-react-sdk/pull/7493)). Fixes vector-im/element-web#20453. + * Fix avatar container overlapping give feedback cta ([\#7491](https://github.com/matrix-org/matrix-react-sdk/pull/7491)). Fixes matrix-org/element-web-rageshakes#7987. + * Fix jump to bottom button working when on a permalink ([\#7494](https://github.com/matrix-org/matrix-react-sdk/pull/7494)). Fixes vector-im/element-web#19813. + * Remove the Description from the location picker ([\#7485](https://github.com/matrix-org/matrix-react-sdk/pull/7485)). + * Fix look of the untrusted device dialog ([\#7487](https://github.com/matrix-org/matrix-react-sdk/pull/7487)). Fixes vector-im/element-web#20447. Contributed by @SimonBrandner. + * Hide maximise button in the sticker picker ([\#7488](https://github.com/matrix-org/matrix-react-sdk/pull/7488)). Fixes vector-im/element-web#20443. Contributed by @SimonBrandner. + * Fix space ordering to match newer spec ([\#7481](https://github.com/matrix-org/matrix-react-sdk/pull/7481)). + * Fix typing notification colors ([\#7490](https://github.com/matrix-org/matrix-react-sdk/pull/7490)). Fixes vector-im/element-web#20144. Contributed by @SimonBrandner. + * fix fallback for pluralized strings ([\#7480](https://github.com/matrix-org/matrix-react-sdk/pull/7480)). Fixes vector-im/element-web#20426. + * Fix right panel soft crashes chat rooms ([\#7479](https://github.com/matrix-org/matrix-react-sdk/pull/7479)). Fixes vector-im/element-web#20433. + * update yarn.lock and i18n ([\#7476](https://github.com/matrix-org/matrix-react-sdk/pull/7476)). Fixes vector-im/element-web#20426 and vector-im/element-web#20423. + * Don't send typing notification when restoring composer draft ([\#7477](https://github.com/matrix-org/matrix-react-sdk/pull/7477)). Fixes vector-im/element-web#20424. + * Fix room joining spinner being incorrect if you change room mid-join ([\#7473](https://github.com/matrix-org/matrix-react-sdk/pull/7473)). + * Only return the approved widget capabilities instead of accepting all requested capabilities ([\#7454](https://github.com/matrix-org/matrix-react-sdk/pull/7454)). Contributed by @dhenneke. + * Fix quoting messages from the search view ([\#7466](https://github.com/matrix-org/matrix-react-sdk/pull/7466)). Fixes vector-im/element-web#20353. + * Attribute fallback i18n strings with lang attribute ([\#7323](https://github.com/matrix-org/matrix-react-sdk/pull/7323)). + * Fix spotlight cmd-k wrongly expanding left panel ([\#7463](https://github.com/matrix-org/matrix-react-sdk/pull/7463)). Fixes vector-im/element-web#20399. + * Fix room_id check when adding user widgets ([\#7448](https://github.com/matrix-org/matrix-react-sdk/pull/7448)). Fixes vector-im/element-web#19382. Contributed by @bink. + * Add new line in settings label ([\#7451](https://github.com/matrix-org/matrix-react-sdk/pull/7451)). Fixes vector-im/element-web#20365. + * Fix handling incoming redactions in EventIndex ([\#7443](https://github.com/matrix-org/matrix-react-sdk/pull/7443)). Fixes vector-im/element-web#19326. + * Fix room alias address isn't checked for validity before being shown as added ([\#7107](https://github.com/matrix-org/matrix-react-sdk/pull/7107)). Fixes vector-im/element-web#19609. Contributed by @Palid. + * Call view accessibility fixes ([\#7439](https://github.com/matrix-org/matrix-react-sdk/pull/7439)). Fixes vector-im/element-web#18516. + * Fix offscreen canvas breaking with split-brained firefox support ([\#7440](https://github.com/matrix-org/matrix-react-sdk/pull/7440)). + * Removed red shield in forwarding preview. ([\#7447](https://github.com/matrix-org/matrix-react-sdk/pull/7447)). Contributed by @ankur12-1610. + * Wrap status message ([\#7325](https://github.com/matrix-org/matrix-react-sdk/pull/7325)). Fixes vector-im/element-web#20092. Contributed by @SimonBrandner. + * Move hideSender logic into state so it causes re-render ([\#7413](https://github.com/matrix-org/matrix-react-sdk/pull/7413)). Fixes vector-im/element-web#18448. + * Fix dialpad positioning ([\#7446](https://github.com/matrix-org/matrix-react-sdk/pull/7446)). Fixes vector-im/element-web#20175. Contributed by @SimonBrandner. + * Hide non-functional list options on Suggested sublist ([\#7410](https://github.com/matrix-org/matrix-react-sdk/pull/7410)). Fixes vector-im/element-web#20252. + * Fix width overflow in mini composer overflow menu ([\#7411](https://github.com/matrix-org/matrix-react-sdk/pull/7411)). Fixes vector-im/element-web#20263. + * Fix being wrongly sent to Home space when creating/joining/leaving rooms ([\#7418](https://github.com/matrix-org/matrix-react-sdk/pull/7418)). Fixes matrix-org/element-web-rageshakes#7331 vector-im/element-web#20246 and vector-im/element-web#20240. + * Fix HTML Export where the data-mx-theme is `Light` not `light` ([\#7415](https://github.com/matrix-org/matrix-react-sdk/pull/7415)). + * Don't disable username/password fields whilst doing wk-lookup ([\#7438](https://github.com/matrix-org/matrix-react-sdk/pull/7438)). Fixes vector-im/element-web#20121. + * Prevent keyboard propagation out of context menus ([\#7437](https://github.com/matrix-org/matrix-react-sdk/pull/7437)). Fixes vector-im/element-web#20317. + * Fix nulls leaking into geo urls ([\#7433](https://github.com/matrix-org/matrix-react-sdk/pull/7433)). + * Fix zIndex of peristent apps in miniMode ([\#7429](https://github.com/matrix-org/matrix-react-sdk/pull/7429)). + * Space panel should watch spaces for space name changes ([\#7432](https://github.com/matrix-org/matrix-react-sdk/pull/7432)). + * Fix list formatting alternating on edit ([\#7422](https://github.com/matrix-org/matrix-react-sdk/pull/7422)). Fixes vector-im/element-web#20073. Contributed by @renancleyson-dev. + * Don't show `Testing small changes` without UIFeature.Feedback ([\#7427](https://github.com/matrix-org/matrix-react-sdk/pull/7427)). Fixes vector-im/element-web#20298. + * Fix invisible toggle space panel button ([\#7426](https://github.com/matrix-org/matrix-react-sdk/pull/7426)). Fixes vector-im/element-web#20279. + * Fix legacy breadcrumbs wrongly showing up ([\#7425](https://github.com/matrix-org/matrix-react-sdk/pull/7425)). + * Space Panel use SettingsStore instead of SpaceStore as source of truth ([\#7404](https://github.com/matrix-org/matrix-react-sdk/pull/7404)). Fixes vector-im/element-web#20250. + * Fix inline code block nowrap issue ([\#7406](https://github.com/matrix-org/matrix-react-sdk/pull/7406)). + * Fix notification badge for All Rooms space ([\#7401](https://github.com/matrix-org/matrix-react-sdk/pull/7401)). Fixes vector-im/element-web#20229. + * Show error if could not load space hierarchy ([\#7399](https://github.com/matrix-org/matrix-react-sdk/pull/7399)). Fixes vector-im/element-web#20221. + * Increase gap between ELS and the subsequent event to prevent overlap ([\#7391](https://github.com/matrix-org/matrix-react-sdk/pull/7391)). Fixes vector-im/element-web#18319. + * Fix list of members in space preview ([\#7356](https://github.com/matrix-org/matrix-react-sdk/pull/7356)). Fixes vector-im/element-web#19781. + * Fix sizing of e2e shield in bubble layout ([\#7394](https://github.com/matrix-org/matrix-react-sdk/pull/7394)). Fixes vector-im/element-web#19090. + * Fix bubble radius wrong when followed by a state event from same user ([\#7393](https://github.com/matrix-org/matrix-react-sdk/pull/7393)). Fixes vector-im/element-web#18982. + * Fix alignment between ELS and Events in bubble layout ([\#7392](https://github.com/matrix-org/matrix-react-sdk/pull/7392)). Fixes vector-im/element-web#19652 and vector-im/element-web#19057. + * Don't include the accuracy parameter in location events if accuracy could not be determined. ([\#7375](https://github.com/matrix-org/matrix-react-sdk/pull/7375)). + * Make compact layout only apply to Modern layout ([\#7382](https://github.com/matrix-org/matrix-react-sdk/pull/7382)). Fixes vector-im/element-web#18412. + * Pin qrcode to fix e2e verification bug ([\#7378](https://github.com/matrix-org/matrix-react-sdk/pull/7378)). Fixes vector-im/element-web#20188. + * Add internationalisation to progress strings in room export dialog ([\#7385](https://github.com/matrix-org/matrix-react-sdk/pull/7385)). Fixes vector-im/element-web#20208. + * Prevent escape to cancel edit from also scrolling to bottom ([\#7380](https://github.com/matrix-org/matrix-react-sdk/pull/7380)). Fixes vector-im/element-web#20182. + * Fix narrow mode composer buttons for polls labs ([\#7386](https://github.com/matrix-org/matrix-react-sdk/pull/7386)). Fixes vector-im/element-web#20067. + * Fix useUserStatusMessage exploding on unknown user ([\#7365](https://github.com/matrix-org/matrix-react-sdk/pull/7365)). + * Fix room join spinner in room list header ([\#7364](https://github.com/matrix-org/matrix-react-sdk/pull/7364)). Fixes vector-im/element-web#20139. + * Fix room search sometimes not opening spotlight ([\#7363](https://github.com/matrix-org/matrix-react-sdk/pull/7363)). Fixes matrix-org/element-web-rageshakes#7288. + Changes in [3.37.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.37.0) (2021-12-20) ===================================================================================================== diff --git a/__mocks__/browser-request.js b/__mocks__/browser-request.js index 4c59e8a43ae..aa9c7102998 100644 --- a/__mocks__/browser-request.js +++ b/__mocks__/browser-request.js @@ -1,10 +1,15 @@ const en = require("../src/i18n/strings/en_EN"); const de = require("../src/i18n/strings/de_DE"); +const lv = { + "Save": "Saglabāt", + "Uploading %(filename)s and %(count)s others|one": "Качване на %(filename)s и %(count)s друг", +}; // Mock the browser-request for the languageHandler tests to return -// Fake languages.json containing references to en_EN and de_DE +// Fake languages.json containing references to en_EN, de_DE and lv // en_EN.json // de_DE.json +// lv.json - mock version with few translations, used to test fallback translation module.exports = jest.fn((opts, cb) => { const url = opts.url || opts.uri; if (url && url.endsWith("languages.json")) { @@ -17,11 +22,17 @@ module.exports = jest.fn((opts, cb) => { "fileName": "de_DE.json", "label": "German", }, + "lv": { + "fileName": "lv.json", + "label": "Latvian" + } })); } else if (url && url.endsWith("en_EN.json")) { cb(undefined, {status: 200}, JSON.stringify(en)); } else if (url && url.endsWith("de_DE.json")) { cb(undefined, {status: 200}, JSON.stringify(de)); + } else if (url && url.endsWith("lv.json")) { + cb(undefined, {status: 200}, JSON.stringify(lv)); } else { cb(true, {status: 404}, ""); } diff --git a/package.json b/package.json index a206d01da0d..8d8b354c4b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.37.0", + "version": "3.38.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -71,7 +71,6 @@ "emojibase-data": "^6.2.0", "emojibase-regex": "^5.1.3", "escape-html": "^1.0.3", - "eslint-plugin-import": "^2.25.2", "file-saver": "^2.0.5", "filesize": "6.1.0", "flux": "2.1.1", @@ -87,7 +86,7 @@ "lodash": "^4.17.20", "maplibre-gl": "^1.15.2", "matrix-analytics-events": "https://github.com/matrix-org/matrix-analytics-events.git#1eab4356548c97722a183912fda1ceabbe8cc7c1", - "matrix-js-sdk": "15.3.0", + "matrix-js-sdk": "15.4.0", "matrix-widget-api": "^0.1.0-beta.18", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", @@ -166,7 +165,9 @@ "enzyme-to-json": "^3.6.2", "eslint": "7.18.0", "eslint-config-google": "^0.14.0", - "eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#48ec1e6af2cfb8310b9a6e23edf2dc7a26ddd580", + "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", "glob": "^7.1.6", diff --git a/res/css/_common.scss b/res/css/_common.scss index a664b49c3df..d97034100ce 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -356,6 +356,7 @@ legend { .mx_Dialog_header { position: relative; + padding: 3px 0; margin-bottom: 10px; } @@ -365,20 +366,21 @@ legend { height: 25px; margin-left: -2px; margin-right: 4px; + margin-bottom: 2px; } .mx_Dialog_title { - font-size: $font-22px; - font-weight: $font-semi-bold; - line-height: $font-36px; color: $dialog-title-fg-color; + display: inline-block; + width: 100%; + box-sizing: border-box; } .mx_Dialog_header.mx_Dialog_headerWithButton > .mx_Dialog_title { text-align: center; } -.mx_Dialog_header.mx_Dialog_headerWithCancel > .mx_Dialog_title { - margin-right: 20px; // leave space for the 'X' cancel button +.mx_Dialog_header.mx_Dialog_headerWithCancel { + padding-right: 20px; // leave space for the 'X' cancel button } .mx_Dialog_title.danger { @@ -421,10 +423,13 @@ legend { * in the app look the same by being AccessibleButtons, or possibly by having explict button classes. * We should go through and have one consistent set of styles for buttons throughout the app. * For now, I am duplicating the selectors here for mx_Dialog and mx_DialogButtons. + * + * Elements that should not be styled like a dialog button are mentioned in a :not() pseudo-class. + * For the widest browser support, we use multiple :not pseudo-classes instead of :not(.a, .b). */ -.mx_Dialog button:not(.mx_Dialog_nonDialogButton), +.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.maplibregl-ctrl-attrib-button):not(.mx_AccessibleButton), .mx_Dialog input[type="submit"], -.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton), +.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton), .mx_Dialog_buttons input[type="submit"] { @mixin mx_DialogButton; margin-left: 0px; @@ -439,25 +444,25 @@ legend { font-family: inherit; } -.mx_Dialog button:not(.mx_Dialog_nonDialogButton):last-child { +.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.maplibregl-ctrl-attrib-button):not(.mx_AccessibleButton):last-child { margin-right: 0px; } -.mx_Dialog button:not(.mx_Dialog_nonDialogButton):hover, +.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.maplibregl-ctrl-attrib-button):not(.mx_AccessibleButton):hover, .mx_Dialog input[type="submit"]:hover, -.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):hover, +.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):hover, .mx_Dialog_buttons input[type="submit"]:hover { @mixin mx_DialogButton_hover; } -.mx_Dialog button:not(.mx_Dialog_nonDialogButton):focus, +.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.maplibregl-ctrl-attrib-button):not(.mx_AccessibleButton):focus, .mx_Dialog input[type="submit"]:focus, -.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):focus, +.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus, .mx_Dialog_buttons input[type="submit"]:focus { filter: brightness($focus-brightness); } -.mx_Dialog button.mx_Dialog_primary, +.mx_Dialog button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.maplibregl-ctrl-attrib-button), .mx_Dialog input[type="submit"].mx_Dialog_primary, .mx_Dialog_buttons button.mx_Dialog_primary, .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary { @@ -466,7 +471,7 @@ legend { min-width: 156px; } -.mx_Dialog button.danger, +.mx_Dialog button.danger:not(.mx_Dialog_nonDialogButton):not(.maplibregl-ctrl-attrib-button), .mx_Dialog input[type="submit"].danger, .mx_Dialog_buttons button.danger, .mx_Dialog_buttons input[type="submit"].danger { @@ -475,15 +480,15 @@ legend { color: $accent-fg-color; } -.mx_Dialog button.warning, +.mx_Dialog button.warning:not(.mx_Dialog_nonDialogButton):not(.maplibregl-ctrl-attrib-button), .mx_Dialog input[type="submit"].warning { border: solid 1px $alert; color: $alert; } -.mx_Dialog button:not(.mx_Dialog_nonDialogButton):disabled, +.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.maplibregl-ctrl-attrib-button):not(.mx_AccessibleButton):disabled, .mx_Dialog input[type="submit"]:disabled, -.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):disabled, +.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled, .mx_Dialog_buttons input[type="submit"]:disabled { background-color: $light-fg-color; border: solid 1px $light-fg-color; @@ -494,7 +499,7 @@ legend { .mx_Dialog_wrapper.mx_Dialog_spinner .mx_Dialog { width: auto; border-radius: $border-radius-8px; - padding: 0px; + padding: 8px; box-shadow: none; /* Don't show scroll-bars on spinner dialogs */ @@ -686,3 +691,15 @@ legend { outline-style: auto; } } + +@define-mixin ButtonResetDefault { + appearance: none; + background: none; + border: none; + padding: 0; + margin: 0; + font-size: inherit; + font-family: inherit; + line-height: inherit; + cursor: pointer; +} diff --git a/res/css/_components.scss b/res/css/_components.scss index 7dcf2f90e7b..2a22a4b72d2 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -3,6 +3,7 @@ @import "./_common.scss"; @import "./_font-sizes.scss"; @import "./_font-weights.scss"; +@import "./_spacing.scss"; @import "./structures/_AutoHideScrollbar.scss"; @import "./structures/_BackdropPanel.scss"; @import "./structures/_CompatibilityPage.scss"; @@ -96,6 +97,7 @@ @import "./views/dialogs/_JoinRuleDropdown.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_LeaveSpaceDialog.scss"; +@import "./views/dialogs/_LocationViewDialog.scss"; @import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_ModalWidgetDialog.scss"; @@ -112,6 +114,7 @@ @import "./views/dialogs/_SettingsDialog.scss"; @import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_SlashCommandHelpDialog.scss"; +@import "./views/dialogs/_SpacePreferencesDialog.scss"; @import "./views/dialogs/_SpaceSettingsDialog.scss"; @import "./views/dialogs/_SpotlightDialog.scss"; @import "./views/dialogs/_TabbedIntegrationManagerDialog.scss"; @@ -299,6 +302,7 @@ @import "./views/toasts/_AnalyticsToast.scss"; @import "./views/toasts/_IncomingCallToast.scss"; @import "./views/toasts/_NonUrgentEchoFailureToast.scss"; +@import "./views/typography/_Heading.scss"; @import "./views/verification/_VerificationShowSas.scss"; @import "./views/voip/CallView/_CallViewButtons.scss"; @import "./views/voip/_CallContainer.scss"; diff --git a/src/dispatcher/payloads/ToggleRightPanelPayload.ts b/res/css/_spacing.scss similarity index 61% rename from src/dispatcher/payloads/ToggleRightPanelPayload.ts rename to res/css/_spacing.scss index 06351948901..4022cb1f47b 100644 --- a/src/dispatcher/payloads/ToggleRightPanelPayload.ts +++ b/res/css/_spacing.scss @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +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. @@ -14,14 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ActionPayload } from "../payloads"; -import { Action } from "../actions"; +// 1rem :: 10px -export interface ToggleRightPanelPayload extends ActionPayload { - action: Action.ToggleRightPanel; - - /** - * The type of room that the panel is toggled in. - */ - type: "group" | "room"; -} +$spacing-4: 4px; +$spacing-8: 8px; +$spacing-12: 12px; +$spacing-16: 16px; +$spacing-20: 20px; +$spacing-24: 24px; +$spacing-28: 28px; +$spacing-32: 32px; +$spacing-40: 40px; diff --git a/res/css/structures/_SpaceHierarchy.scss b/res/css/structures/_SpaceHierarchy.scss index 6d865d039de..03e554b8422 100644 --- a/res/css/structures/_SpaceHierarchy.scss +++ b/res/css/structures/_SpaceHierarchy.scss @@ -15,35 +15,6 @@ limitations under the License. */ .mx_SpaceRoomView_landing { - .mx_Dialog_title { - display: flex; - - .mx_BaseAvatar { - margin-right: 12px; - align-self: center; - } - - .mx_BaseAvatar_image { - border-radius: 8px; - } - - > div { - > h1 { - font-weight: $font-semi-bold; - font-size: $font-18px; - line-height: $font-22px; - margin: 0; - } - - > div { - font-weight: 400; - color: $secondary-content; - font-size: $font-15px; - line-height: $font-24px; - } - } - } - .mx_AccessibleButton_kind_link { padding: 0; font-size: inherit; @@ -249,7 +220,7 @@ limitations under the License. line-height: $font-18px; color: $secondary-content; grid-row: 2; - grid-column: 1/3; + grid-column: 2/3; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index d973156973c..874c5e8e4a4 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -71,7 +71,7 @@ $activeBorderColor: $primary-content; } &:hover .mx_SpacePanel_toggleCollapse { - opacity: 100%; + opacity: 1; } ul { @@ -411,6 +411,10 @@ $activeBorderColor: $primary-content; mask-image: url('$(res)/img/element-icons/roomlist/search.svg'); } + .mx_SpacePanel_iconPreferences::before { + mask-image: url('$(res)/img/element-icons/settings/preference.svg'); + } + .mx_SpacePanel_noIcon { display: none; diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 0f3470b0b45..8fbd6fbb703 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -254,6 +254,10 @@ $SpaceRoomViewInnerWidth: 428px; flex-direction: column; min-width: 0; + > .mx_BaseAvatar { + width: 80px; + } + > .mx_BaseAvatar_image, > .mx_BaseAvatar > .mx_BaseAvatar_image { border-radius: 12px; diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index ab5d1d5a03d..4ebc4508489 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -215,14 +215,51 @@ limitations under the License. .mx_UserMenu_CustomStatusSection { margin: 0 12px 8px; - .mx_UserMenu_CustomStatusSection_input { + .mx_UserMenu_CustomStatusSection_field { position: relative; display: flex; - > input { + &.mx_UserMenu_CustomStatusSection_field_hasQuery { + .mx_UserMenu_CustomStatusSection_clear { + display: block; + } + } + + > .mx_UserMenu_CustomStatusSection_input { border: 1px solid $accent; border-radius: 8px; width: 100%; + + &:focus + .mx_UserMenu_CustomStatusSection_clear { + display: block; + } + } + + > .mx_UserMenu_CustomStatusSection_clear { + display: none; + + position: absolute; + top: 50%; + right: 0; + transform: translateY(-50%); + + width: 16px; + height: 16px; + margin-right: 8px; + background-color: $quinary-content; + border-radius: 50%; + + &::before { + content: ""; + position: absolute; + width: inherit; + height: inherit; + mask-image: url('$(res)/img/feather-customised/x.svg'); + mask-position: center; + mask-size: 12px; + mask-repeat: no-repeat; + background-color: $secondary-content; + } } } diff --git a/res/css/structures/auth/_SetupEncryptionBody.scss b/res/css/structures/auth/_SetupEncryptionBody.scss index f40e31280b0..1999fb581db 100644 --- a/res/css/structures/auth/_SetupEncryptionBody.scss +++ b/res/css/structures/auth/_SetupEncryptionBody.scss @@ -17,8 +17,9 @@ limitations under the License. .mx_SetupEncryptionBody_reset { color: $light-fg-color; margin-top: $font-14px; +} - a.mx_SetupEncryptionBody_reset_link:is(:link, :hover, :visited) { - color: $alert; - } +.mx_SetupEncryptionBody_reset_link { + @mixin ButtonResetDefault; + color: $alert; } diff --git a/res/css/views/context_menus/_MessageContextMenu.scss b/res/css/views/context_menus/_MessageContextMenu.scss index 0189384dd9a..e743619f8fd 100644 --- a/res/css/views/context_menus/_MessageContextMenu.scss +++ b/res/css/views/context_menus/_MessageContextMenu.scss @@ -54,6 +54,10 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/settings/appearance.svg'); } + .mx_MessageContextMenu_iconOpenInMapSite::before { + mask-image: url('$(res)/img/external-link.svg'); + } + .mx_MessageContextMenu_iconEndPoll::before { mask-image: url('$(res)/img/element-icons/check-white.svg'); } diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss index 925a2277c36..fe8947ecff6 100644 --- a/res/css/views/dialogs/_DevtoolsDialog.scss +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -257,3 +257,10 @@ limitations under the License. margin-bottom: 8px; } } + +.mx_DevTools_SettingsExplorer_setting { + // override default link button color + // as it is the same as the background highlight + // used on focus + color: $links !important; +} diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index d36875093d3..205aaaa2026 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -44,17 +44,12 @@ limitations under the License. pointer-events: none; } - .mx_EventTile_msgOption { - display: none; - } - // When forwarding messages from encrypted rooms, EventTile will complain // that our preview is unencrypted, which doesn't actually matter - .mx_EventTile_e2eIcon_unencrypted { - display: none; - } - // We also hide download links to not encourage users to try interacting + .mx_EventTile_msgOption, + .mx_EventTile_e2eIcon_unencrypted, + .mx_EventTile_e2eIcon_warning, .mx_MFileBody_download { display: none; } diff --git a/res/css/views/dialogs/_LocationViewDialog.scss b/res/css/views/dialogs/_LocationViewDialog.scss new file mode 100644 index 00000000000..e7cdaf88007 --- /dev/null +++ b/res/css/views/dialogs/_LocationViewDialog.scss @@ -0,0 +1,96 @@ +/* +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_LocationViewDialog_wrapper .mx_Dialog { + padding: 0px; + + /* Unset contain and position to allow the close button + to appear outside the dialog */ + contain: unset; + position: unset; +} + +.mx_LocationViewDialog { + /* subtract 0.5px to prevent single-pixel margin due to rounding */ + width: calc(80vw - 0.5px); + height: calc(80vh - 0.5px); + overflow: hidden; + + .mx_Dialog_header { + margin: 0px; + padding: 0px; + position: unset; + + .mx_Dialog_title { + display: none; + } + + .mx_Dialog_cancelButton { + z-index: 4010; + position: absolute; + right: 5vw; + top: 5vh; + width: 20px; + height: 20px; + background-color: $dialog-close-external-color; + } + } + + .mx_MLocationBody { + position: absolute; + + .mx_MLocationBody_map { + width: 80vw; + height: 80vh; + } + + .mx_MLocationBody_zoomButtons { + position: absolute; + display: grid; + grid-template-columns: auto; + grid-row-gap: 8px; + + right: 24px; + bottom: 48px; + + .mx_AccessibleButton { + background-color: $background; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.25); + border-radius: 4px; + width: 24px; + height: 24px; + + .mx_MLocationBody_zoomButton { + background-color: $primary-content; + margin: 4px; + width: 16px; + height: 16px; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } + + .mx_MLocationBody_plusButton { + mask-image: url('$(res)/img/element-icons/plus-button.svg'); + } + + .mx_MLocationBody_minusButton { + mask-image: url('$(res)/img/element-icons/minus-button.svg'); + } + } + } + } +} diff --git a/res/css/views/dialogs/_SettingsDialog.scss b/res/css/views/dialogs/_SettingsDialog.scss index f9b373c30a1..e982b6245fa 100644 --- a/res/css/views/dialogs/_SettingsDialog.scss +++ b/res/css/views/dialogs/_SettingsDialog.scss @@ -15,7 +15,10 @@ limitations under the License. */ // Not actually a component but things shared by settings components -.mx_UserSettingsDialog, .mx_RoomSettingsDialog, .mx_SpaceSettingsDialog { +.mx_UserSettingsDialog, +.mx_RoomSettingsDialog, +.mx_SpaceSettingsDialog, +.mx_SpacePreferencesDialog { width: 90vw; max-width: 1000px; // set the height too since tabbed view scrolls itself. @@ -36,8 +39,4 @@ limitations under the License. // colliding harshly with the dialog when scrolled down. padding-bottom: 100px; } - - .mx_Dialog_title { - margin-bottom: 24px; - } } diff --git a/res/css/views/dialogs/_SpacePreferencesDialog.scss b/res/css/views/dialogs/_SpacePreferencesDialog.scss new file mode 100644 index 00000000000..3b6bacca84a --- /dev/null +++ b/res/css/views/dialogs/_SpacePreferencesDialog.scss @@ -0,0 +1,46 @@ +/* +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. +*/ + +.mx_SpacePreferencesDialog { + width: 700px; + height: 400px; + + > h4 { + margin: -12px 0 0; + font-weight: normal; + font-size: $font-15px; + line-height: $font-24px; + color: $secondary-content; + } + + .mx_TabbedView { + top: 80px; + + .mx_SettingsTab { + min-width: unset; + + .mx_SettingsTab_section { + font-size: $font-15px; + line-height: $font-24px; + + .mx_Checkbox + p { + color: $secondary-content; + margin: 0 20px 0 24px; + } + } + } + } +} diff --git a/res/css/views/dialogs/_SpaceSettingsDialog.scss b/res/css/views/dialogs/_SpaceSettingsDialog.scss index 78fbd573bd2..8f064f6b380 100644 --- a/res/css/views/dialogs/_SpaceSettingsDialog.scss +++ b/res/css/views/dialogs/_SpaceSettingsDialog.scss @@ -38,7 +38,7 @@ limitations under the License. } & + .mx_SettingsTab_subheading { - border-top: 1px solid $quinary-content; + border-top: 1px solid $menu-border-color; margin-top: 0; padding-top: 24px; } @@ -60,15 +60,6 @@ limitations under the License. margin-left: 26px; } } - - .mx_SettingsTab_showAdvanced { - margin: 16px 0; - padding: 0; - } - - .mx_SettingsFlag { - margin-top: 24px; - } } .mx_SpaceSettingsDialog_buttons { @@ -86,6 +77,11 @@ limitations under the License. .mx_AccessibleButton_hasKind { padding: 8px 22px; + + &.mx_SettingsTab_showAdvanced { + margin: 16px 0; + padding: 0; + } } .mx_TabbedView_tabLabel { diff --git a/res/css/views/dialogs/_SpotlightDialog.scss b/res/css/views/dialogs/_SpotlightDialog.scss index 4a6e7d62887..8f03fdb2f80 100644 --- a/res/css/views/dialogs/_SpotlightDialog.scss +++ b/res/css/views/dialogs/_SpotlightDialog.scss @@ -52,7 +52,7 @@ limitations under the License. flex-direction: column; .mx_Dialog_header { - margin-bottom: 0; + display: none; } .mx_SpotlightDialog_searchBox { diff --git a/res/css/views/dialogs/_UntrustedDeviceDialog.scss b/res/css/views/dialogs/_UntrustedDeviceDialog.scss index 0ecd9d4f717..30b01c3f408 100644 --- a/res/css/views/dialogs/_UntrustedDeviceDialog.scss +++ b/res/css/views/dialogs/_UntrustedDeviceDialog.scss @@ -23,4 +23,10 @@ limitations under the License. margin-left: 0; } } + + .mx_Dialog_buttons { + display: flex; + justify-content: flex-end; + gap: 8px; + } } diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 121593dd9cb..d695f296cb2 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -107,6 +107,16 @@ limitations under the License. opacity: 0.4; } +.mx_AccessibleButton_kind_link_inline { + color: $accent; + font-size: inherit; + padding: 0 2px; +} + +.mx_AccessibleButton_kind_link_inline.mx_AccessibleButton_disabled { + opacity: 0.4; +} + .mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_link_sm { padding: 5px 12px; color: $accent; diff --git a/res/css/views/elements/_ReplyChain.scss b/res/css/views/elements/_ReplyChain.scss index 80007343cc6..fc5833c6be1 100644 --- a/res/css/views/elements/_ReplyChain.scss +++ b/res/css/views/elements/_ReplyChain.scss @@ -23,7 +23,12 @@ limitations under the License. border-left: 2px solid $accent; .mx_ReplyChain_show { - cursor: pointer; + @mixin ButtonResetDefault; + color: inherit; + + &:hover { + color: $links; + } } &.mx_ReplyChain_color1 { diff --git a/res/css/views/elements/_SSOButtons.scss b/res/css/views/elements/_SSOButtons.scss index cb92a6d17d6..e8b086c3981 100644 --- a/res/css/views/elements/_SSOButtons.scss +++ b/res/css/views/elements/_SSOButtons.scss @@ -45,6 +45,10 @@ limitations under the License. } } + .mx_SSOButton:hover { + background-color: $panel-hover; + } + .mx_SSOButton_default { color: $accent; background-color: $button-secondary-bg-color; diff --git a/res/css/views/elements/_SettingsFlag.scss b/res/css/views/elements/_SettingsFlag.scss index 68e324fe812..533487d98cf 100644 --- a/res/css/views/elements/_SettingsFlag.scss +++ b/res/css/views/elements/_SettingsFlag.scss @@ -17,7 +17,7 @@ limitations under the License. .mx_SettingsFlag { display: flex; flex-direction: row; - align-items: center; + align-items: flex-start; justify-content: space-between; margin-bottom: 4px; @@ -33,6 +33,7 @@ limitations under the License. font-size: $font-14px; color: $primary-content; padding-right: 10px; + padding-top: 4px; } .mx_SettingsFlag_microcopy { diff --git a/res/css/views/location/_LocationPicker.scss b/res/css/views/location/_LocationPicker.scss index b386268cec2..ce1d11cafef 100644 --- a/res/css/views/location/_LocationPicker.scss +++ b/res/css/views/location/_LocationPicker.scss @@ -26,7 +26,7 @@ limitations under the License. } #mx_LocationPicker_map { - height: 324px; + height: 408px; border-radius: 8px 8px 0px 0px; } diff --git a/res/css/views/messages/_MLocationBody.scss b/res/css/views/messages/_MLocationBody.scss index 5bb90fbf7b0..948822bea8a 100644 --- a/res/css/views/messages/_MLocationBody.scss +++ b/res/css/views/messages/_MLocationBody.scss @@ -14,9 +14,30 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_MLocationBody_map { - width: 450px; - height: 300px; +.mx_MLocationBody { + .mx_MLocationBody_map { + width: 450px; + height: 300px; - border-radius: $timeline-image-border-radius; + border-radius: $timeline-image-border-radius; + } + + .mx_MLocationBody_markerBorder { + width: 31px; + height: 31px; + border-radius: 50%; + background-color: $accent; + filter: drop-shadow(0px 3px 5px rgba(0, 0, 0, 0.2)); + + .mx_BaseAvatar { + margin-top: 2px; + margin-left: 2px; + } + } + + .mx_MLocationBody_pointer { + position: absolute; + bottom: -3px; + left: 12px; + } } diff --git a/res/css/views/messages/_ReactionsRow.scss b/res/css/views/messages/_ReactionsRow.scss index 1b0b8479328..d4695888dfc 100644 --- a/res/css/views/messages/_ReactionsRow.scss +++ b/res/css/views/messages/_ReactionsRow.scss @@ -57,15 +57,13 @@ limitations under the License. } .mx_ReactionsRow_showAll { + @mixin ButtonResetDefault; text-decoration: none; font-size: $font-12px; line-height: $font-20px; margin-left: 4px; vertical-align: middle; - - &:link, &:visited { - color: $tertiary-content; - } + color: $tertiary-content; &:hover { color: $primary-content; diff --git a/res/css/views/messages/_ViewSourceEvent.scss b/res/css/views/messages/_ViewSourceEvent.scss index f076c774737..5e288eb19ac 100644 --- a/res/css/views/messages/_ViewSourceEvent.scss +++ b/res/css/views/messages/_ViewSourceEvent.scss @@ -18,6 +18,7 @@ limitations under the License. display: flex; opacity: 0.6; font-size: $font-12px; + width: 100%; pre, code { flex: 1; @@ -29,11 +30,15 @@ limitations under the License. } .mx_ViewSourceEvent_toggle { + visibility: hidden; + // override styles from AccessibleButton + border-radius: 0; + padding: 0; + // icon mask-repeat: no-repeat; mask-position: 0 center; mask-size: auto 12px; width: 12px; - visibility: hidden; background-color: $accent; mask-image: url("$(res)/img/element-icons/maximise-expand.svg"); } diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 0c13f0e8988..8373558fa96 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -204,11 +204,19 @@ limitations under the License. } .mx_UserInfo_statusMessage { + $statusLineHeight: 16px; + $statusNumberOfLines: 3; + font-size: $font-11px; + line-height: $statusLineHeight; opacity: 0.5; overflow: hidden; - white-space: nowrap; - text-overflow: clip; + word-break: break-word; + text-overflow: ellipsis; + display: -webkit-box; + max-height: calc($statusLineHeight * $statusNumberOfLines); + -webkit-line-clamp: $statusNumberOfLines; + -webkit-box-orient: vertical; } .mx_AutoHideScrollbar { diff --git a/res/css/views/rooms/_Autocomplete.scss b/res/css/views/rooms/_Autocomplete.scss index 95e3f973e9b..932b76d9f6e 100644 --- a/res/css/views/rooms/_Autocomplete.scss +++ b/res/css/views/rooms/_Autocomplete.scss @@ -22,6 +22,7 @@ .mx_Autocomplete_Completion_block { min-height: 34px; display: flex; + flex-wrap: wrap; padding: 0 12px; user-select: none; cursor: pointer; @@ -57,6 +58,7 @@ .mx_Autocomplete_Completion_description { color: gray; + min-width: 150px; } .mx_Autocomplete_Completion_container_pill { diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 7e2a1ec45e7..e7ed2b42a56 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -20,13 +20,13 @@ limitations under the License. --gutterSize: 11px; --cornerRadius: 12px; --maxWidth: 70%; + margin-right: 60px; } .mx_EventTile[data-layout=bubble] { position: relative; margin-top: var(--gutterSize); margin-left: 49px; - margin-right: 100px; font-size: $font-14px; .mx_ThreadInfo { @@ -89,6 +89,12 @@ limitations under the License. font-size: $font-15px; } + .mx_MessageActionBar { + top: -28px; + right: 0; + z-index: 9; // above the avatar + } + &[data-self=false] { .mx_EventTile_line { border-bottom-right-radius: var(--cornerRadius); @@ -101,21 +107,12 @@ limitations under the License. left: -34px; } - .mx_MessageActionBar { - right: 0; - transform: translate3d(90%, 50%, 0); - } - --backgroundColor: $eventbubble-others-bg; } &[data-self=true] { .mx_EventTile_line { float: right; border-bottom-left-radius: var(--cornerRadius); - > a { - left: auto; - right: -68px; - } .mx_MImageBody .mx_MImageBody_thumbnail { border-bottom-left-radius: var(--cornerRadius); @@ -160,11 +157,13 @@ limitations under the License. margin: 0 -12px 0 -9px; border-top-left-radius: var(--cornerRadius); border-top-right-radius: var(--cornerRadius); + > a { position: absolute; - padding: 10px 20px; - top: 0; - left: -68px; + padding: 4px 8px; + bottom: 0; + right: 0; + z-index: 3; // above media and location share maps } //noinspection CssReplaceWithShorthandSafely @@ -176,24 +175,29 @@ limitations under the License. border-top-left-radius: var(--cornerRadius); border-top-right-radius: var(--cornerRadius); } + + .mx_EventTile_e2eIcon { + flex-shrink: 0; // keep it at full size + } } - .mx_EventTile_line:not(.mx_EventTile_mediaLine) { + &:not(.mx_EventTile_noBubble) .mx_EventTile_line:not(.mx_EventTile_mediaLine) { padding: var(--gutterSize); + padding-right: 60px; // space for the timestamp background: var(--backgroundColor); } &.mx_EventTile_continuation[data-self=false] .mx_EventTile_line { border-top-left-radius: 0; - .mx_MImageBody .mx_MImageBody_thumbnail { + .mx_MImageBody .mx_MImageBody_thumbnail, .mx_MLocationBody_map { border-top-left-radius: 0; } } &.mx_EventTile_lastInSection[data-self=false] .mx_EventTile_line { border-bottom-left-radius: var(--cornerRadius); - .mx_MImageBody .mx_MImageBody_thumbnail { + .mx_MImageBody .mx_MImageBody_thumbnail, .mx_MLocationBody_map { border-bottom-left-radius: var(--cornerRadius); } } @@ -201,14 +205,14 @@ limitations under the License. &.mx_EventTile_continuation[data-self=true] .mx_EventTile_line { border-top-right-radius: 0; - .mx_MImageBody .mx_MImageBody_thumbnail { + .mx_MImageBody .mx_MImageBody_thumbnail, .mx_MLocationBody_map { border-top-right-radius: 0; } } &.mx_EventTile_lastInSection[data-self=true] .mx_EventTile_line { border-bottom-right-radius: var(--cornerRadius); - .mx_MImageBody .mx_MImageBody_thumbnail { + .mx_MImageBody .mx_MImageBody_thumbnail, .mx_MLocationBody_map { border-bottom-right-radius: var(--cornerRadius); } } @@ -302,7 +306,7 @@ limitations under the License. .mx_EventTile_readAvatars { position: absolute; - right: -110px; + right: -78px; // as close to right gutter without clipping as possible bottom: 0; top: auto; } @@ -312,6 +316,10 @@ limitations under the License. } } +.mx_EventTile.mx_EventTile_noBubble[data-layout=bubble] { + --backgroundColor: transparent; +} + .mx_EventTile.mx_EventTile_bubbleContainer[data-layout=bubble], .mx_EventTile.mx_EventTile_leftAlignedBubble[data-layout=bubble], .mx_EventTile.mx_EventTile_info[data-layout=bubble], @@ -352,14 +360,24 @@ limitations under the License. .mx_EventListSummary[data-layout=bubble] { --maxWidth: 70%; margin-left: calc(var(--avatarSize) + var(--gutterSize)); - margin-right: 94px; + .mx_EventListSummary_toggle { + margin: 0 55px 0 5px; float: none; - margin: 0; - order: 9; - margin-left: 5px; - margin-right: 55px; + + &[aria-expanded=false] { + order: 9; + } + &[aria-expanded=true] { + text-align: right; + margin-right: 100px; + } + } + + .mx_EventListSummary_line { + display: none; } + .mx_EventListSummary_avatars { padding-top: 0; } @@ -368,28 +386,24 @@ limitations under the License. content: ""; clear: both; } +} - .mx_EventTile { - margin: 0 6px; - padding: 2px 0; - } +.mx_EventListSummary[data-expanded=false][data-layout=bubble] { + // Align with left edge of bubble tiles + padding: 0 49px; +} - .mx_EventTile_line { - margin: 0; - > a { - // Align timestamps with those of normal bubble tiles - left: -76px; - } - } +.mx_EventListSummary[data-expanded=true][data-layout=bubble] { + display: contents; - .mx_MessageActionBar { - transform: translate3d(90%, 0, 0); + .mx_EventTile { + padding: 2px 0; } } -.mx_EventListSummary[data-expanded=false][data-layout=bubble] { - // Align with left edge of bubble tiles - padding: 0 49px; +// increase margin between ELS and the next Event to not have our user avatar overlap the expand/collapse button +.mx_EventListSummary[data-layout=bubble][data-expanded=false] + .mx_EventTile[data-layout=bubble][data-self=true] { + margin-top: 20px; } /* events that do not require bubble layout */ diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index e7905e848db..c4720e29181 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -17,6 +17,34 @@ limitations under the License. $left-gutter: 64px; +.mx_EventTile { + .mx_EventTile_receiptSent, + .mx_EventTile_receiptSending { + // We don't use `position: relative` on the element because then it won't line + // up with the other read receipts + + &::before { + background-color: $tertiary-content; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 14px; + width: 14px; + height: 14px; + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + } + } + .mx_EventTile_receiptSent::before { + mask-image: url('$(res)/img/element-icons/circle-sent.svg'); + } + .mx_EventTile_receiptSending::before { + mask-image: url('$(res)/img/element-icons/circle-sending.svg'); + } +} + .mx_EventTile:not([data-layout=bubble]) { max-width: 100%; clear: both; @@ -178,32 +206,6 @@ $left-gutter: 64px; color: $accent-fg-color; } - .mx_EventTile_receiptSent, - .mx_EventTile_receiptSending { - // We don't use `position: relative` on the element because then it won't line - // up with the other read receipts - - &::before { - background-color: $tertiary-content; - mask-repeat: no-repeat; - mask-position: center; - mask-size: 14px; - width: 14px; - height: 14px; - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - } - } - .mx_EventTile_receiptSent::before { - mask-image: url('$(res)/img/element-icons/circle-sent.svg'); - } - .mx_EventTile_receiptSending::before { - mask-image: url('$(res)/img/element-icons/circle-sending.svg'); - } - &.mx_EventTile_contextual { opacity: 0.4; } @@ -454,6 +456,15 @@ $left-gutter: 64px; pre { border: 1px solid transparent; } + + // selector wrongly applies to pill avatars but those have explicit width/height passed at a higher specificity + &.markdown-body img { + // the image will have max-width and max-height applied during sanitization + width: 100%; + height: 100%; + object-fit: contain; + object-position: left top; + } } .mx_EventTile_clamp { @@ -478,8 +489,17 @@ $left-gutter: 64px; background-color: $codeblock-background-color; } - pre code > * { - display: inline; + // this selector wrongly applies to code blocks too but we will unset it in the next one + code { + white-space: pre-wrap; // don't collapse spaces in inline code blocks + } + + pre code { + white-space: pre; // we want code blocks to be scrollable and not wrap + + > * { + display: inline; + } } pre { @@ -497,10 +517,6 @@ $left-gutter: 64px; background: transparent; } } - - code { - white-space: pre-wrap; // don't collapse spaces in inline code blocks - } } .mx_EventTile_lineNumbers { @@ -619,7 +635,8 @@ $left-gutter: 64px; opacity: 0.5; } -.mx_EventTile_keyRequestInfo_text a { +.mx_EventTile_keyRequestInfo_text .mx_AccessibleButton { + @mixin ButtonResetDefault; color: $primary-content; text-decoration: underline; cursor: pointer; @@ -788,6 +805,17 @@ $left-gutter: 64px; background-color: $quinary-content; } + &::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + /* enough to cover all sibling elements */ + z-index: 10; + } + &:last-child { &::after { content: unset; diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 829f9f989bb..4d113341c8f 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -431,4 +431,12 @@ limitations under the License. .mx_MessageComposer_Menu .mx_CallContextMenu_item { display: flex; align-items: center; + max-width: unset; + width: 100%; +} + +.mx_MessageComposer_Menu .mx_ContextualMenu { + min-width: 150px; + width: max-content; + padding: 5px 10px 5px 0; } diff --git a/res/css/views/rooms/_WhoIsTypingTile.scss b/res/css/views/rooms/_WhoIsTypingTile.scss index 49655742bb1..6e223ed0e0b 100644 --- a/res/css/views/rooms/_WhoIsTypingTile.scss +++ b/res/css/views/rooms/_WhoIsTypingTile.scss @@ -43,8 +43,8 @@ limitations under the License. .mx_WhoIsTypingTile_remainingAvatarPlaceholder { position: relative; display: inline-block; - color: #acacac; - background-color: #ddd; + color: $primary-content; + background-color: $quinary-content; border: 1px solid $background; border-radius: 40px; width: 24px; diff --git a/res/css/views/settings/_JoinRuleSettings.scss b/res/css/views/settings/_JoinRuleSettings.scss index 76b1b6333af..30d6b6b678c 100644 --- a/res/css/views/settings/_JoinRuleSettings.scss +++ b/res/css/views/settings/_JoinRuleSettings.scss @@ -77,7 +77,7 @@ limitations under the License. color: $secondary-content; & + .mx_StyledRadioButton { - border-top: 1px solid $menu-border-color; + border-top: 1px solid $quinary-content; } } } diff --git a/res/css/views/settings/_LayoutSwitcher.scss b/res/css/views/settings/_LayoutSwitcher.scss index 53b0fac7933..44506aa1b10 100644 --- a/res/css/views/settings/_LayoutSwitcher.scss +++ b/res/css/views/settings/_LayoutSwitcher.scss @@ -46,6 +46,10 @@ limitations under the License. align-items: center; padding: 10px; pointer-events: none; + + .mx_EventTile[data-layout=bubble] .mx_EventTile_line { + padding-right: 11px; + } } .mx_StyledRadioButton { diff --git a/res/css/views/settings/_Notifications.scss b/res/css/views/settings/_Notifications.scss index b950256ced1..c6c83853689 100644 --- a/res/css/views/settings/_Notifications.scss +++ b/res/css/views/settings/_Notifications.scss @@ -14,53 +14,56 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_UserNotifSettings { - color: $primary-content; // override from default settings page styles - - .mx_UserNotifSettings_pushRulesTable { - width: calc(100% + 12px); // +12px to line up center of 'Noisy' column with toggle switches - table-layout: fixed; - border-collapse: collapse; - border-spacing: 0; - margin-top: 40px; - - tr > th { - font-weight: $font-semi-bold; +.mx_UserNotifSettings_grid { + width: calc(100% + 12px); // +12px to line up center of 'Noisy' column with toggle switches + display: grid; + grid-template-columns: auto repeat(3, 62px); + place-items: center center; + grid-gap: 8px; + margin-top: $spacing-40; + + // Override StyledRadioButton default styles + .mx_StyledRadioButton { + justify-content: center; + + .mx_StyledRadioButton_content { + display: none; } - tr > th:first-child { - text-align: left; - font-size: $font-18px; - } - - tr > th:nth-child(n + 2) { - color: $secondary-content; - font-size: $font-12px; - vertical-align: middle; - width: 66px; + .mx_StyledRadioButton_spacer { + display: none; } + } +} - tr > td:nth-child(n + 2) { - text-align: center; - } +.mx_UserNotifSettings_gridRowContainer { + display: contents; +} - tr > td { - padding-top: 8px; - } +.mx_UserNotifSettings_gridRow { + display: contents; +} - // Override StyledRadioButton default styles - .mx_StyledRadioButton { - justify-content: center; +.mx_UserNotifSettings_gridRowLabel { + justify-self: start; + // does not accept + // display: inline | inline-block + // force it inline using float + float: left; +} +.mx_UserNotifSettings_gridRowHeading { + font-size: $font-18px; + font-weight: $font-semi-bold; +} - .mx_StyledRadioButton_content { - display: none; - } +.mx_UserNotifSettings_gridColumnLabel { + color: $secondary-content; + font-size: $font-12px; + font-weight: $font-semi-bold; +} - .mx_StyledRadioButton_spacer { - display: none; - } - } - } +.mx_UserNotifSettings { + color: $primary-content; // override from default settings page styles .mx_UserNotifSettings_floatingSection { margin-top: 40px; diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss index ce27b5dd13f..46c89347a86 100644 --- a/res/css/views/settings/_ProfileSettings.scss +++ b/res/css/views/settings/_ProfileSettings.scss @@ -54,7 +54,6 @@ limitations under the License. .mx_ProfileSettings_profileForm { @mixin mx_Settings_fullWidthField; - border-bottom: 1px solid $menu-border-color; } .mx_ProfileSettings_buttons { diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 926e8d04598..5f6109cea42 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -159,3 +159,7 @@ limitations under the License. .mx_SettingsTab a { color: $accent-alt; } + +.mx_SettingsTab_toggleWithDescription { + margin-top: 24px; +} diff --git a/src/components/views/location/LocationShareType.tsx b/res/css/views/typography/_Heading.scss similarity index 54% rename from src/components/views/location/LocationShareType.tsx rename to res/css/views/typography/_Heading.scss index 957860514ef..9b7ddeaef3f 100644 --- a/src/components/views/location/LocationShareType.tsx +++ b/res/css/views/typography/_Heading.scss @@ -14,17 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ -enum LocationShareType { - Custom = -1, - OnceOff = 0, - OneMine = 60, - FiveMins = 5 * 60, - ThirtyMins = 30 * 60, - OneHour = 60 * 60, - ThreeHours = 3 * 60 * 60, - SixHours = 6 * 60 * 60, - OneDay = 24 * 60 * 60, - Forever = Number.MAX_SAFE_INTEGER, +.mx_Heading_h1 { + font-size: $font-32px; + font-weight: $font-semi-bold; + line-height: $font-39px; + margin-inline: unset; + margin-block: unset; } -export default LocationShareType; +.mx_Heading_h2 { + font-size: $font-24px; + font-weight: $font-semi-bold; + line-height: $font-29px; + margin-inline: unset; + margin-block: unset; +} + +.mx_Heading_h3 { + font-size: $font-18px; + font-weight: $font-semi-bold; + line-height: $font-22px; + margin-inline: unset; + margin-block: unset; +} diff --git a/res/css/views/voip/CallView/_CallViewButtons.scss b/res/css/views/voip/CallView/_CallViewButtons.scss index b046ddf40aa..9305d07f3b7 100644 --- a/res/css/views/voip/CallView/_CallViewButtons.scss +++ b/res/css/views/voip/CallView/_CallViewButtons.scss @@ -16,6 +16,13 @@ See the License for the specific language governing permissions and limitations under the License. */ +// data-whatintent makes more sense here semantically but then the tooltip would stay visible without the button +// which looks broken, so we match the behaviour of tooltips which is fine too. +[data-whatinput="mouse"] .mx_CallViewButtons.mx_CallViewButtons_hidden { + opacity: 0.001; // opacity 0 can cause a re-layout + pointer-events: none; +} + .mx_CallViewButtons { position: absolute; display: flex; @@ -26,11 +33,6 @@ limitations under the License. z-index: 200; // To be above _all_ feeds gap: 18px; - &.mx_CallViewButtons_hidden { - opacity: 0.001; // opacity 0 can cause a re-layout - pointer-events: none; - } - .mx_CallViewButtons_button { cursor: pointer; diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index d67b75e47f5..cba68eccf83 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -211,9 +211,6 @@ border-radius: $border-radius-8px; } .mx_CallView_presenting { - opacity: 1; - transition: opacity 0.5s; - position: absolute; margin-top: 18px; padding: 4px 8px; @@ -223,8 +220,3 @@ border-radius: $border-radius-8px; color: white; background-color: #17191c; } - -.mx_CallView_presenting_hidden { - opacity: 0.001; // opacity 0 can cause a re-layout - pointer-events: none; -} diff --git a/res/img/element-icons/minus-button.svg b/res/img/element-icons/minus-button.svg new file mode 100644 index 00000000000..ca61c23b76b --- /dev/null +++ b/res/img/element-icons/minus-button.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/plus-button.svg b/res/img/element-icons/plus-button.svg new file mode 100644 index 00000000000..cbc25c4553e --- /dev/null +++ b/res/img/element-icons/plus-button.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/location/pointer.svg b/res/img/location/pointer.svg new file mode 100644 index 00000000000..8a7c5edf712 --- /dev/null +++ b/res/img/location/pointer.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index c33ad9d1cba..d4e23966c27 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -103,7 +103,8 @@ $input-lighter-bg-color: #f5f5f5; // ******************** $dialog-title-fg-color: $primary-content; $dialog-backdrop-color: $menu-border-color; -$dialog-close-fg-color: #aaaaaa; +$dialog-close-fg-color: #9fa9ba; +$dialog-close-external-color: $primary-content; // ******************** // RoomList diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index c84bc6e1a05..112d0b1611a 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -69,7 +69,8 @@ $h3-color: $primary-fg-color; $dialog-title-fg-color: $base-text-color; $dialog-backdrop-color: #000; $dialog-shadow-color: rgba(0, 0, 0, 0.48); -$dialog-close-fg-color: #aaaaaa; +$dialog-close-fg-color: #9fa9ba; +$dialog-close-external-color: $text-primary-color; $lightbox-background-bg-color: #000; $lightbox-background-bg-opacity: 0.85; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 3371d78028f..6c7fc402fde 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -95,6 +95,7 @@ $dialog-title-fg-color: #424242; $dialog-backdrop-color: rgba(48, 48, 48, 0.38); $dialog-shadow-color: rgba(0, 0, 0, 0.48); $dialog-close-fg-color: #c1c1c1; +$dialog-close-external-color: $primary-bg-color; $lightbox-background-bg-color: #000; $lightbox-background-bg-opacity: 0.95; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index fc4a4a242ac..85069bac010 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -162,6 +162,7 @@ $input-fg-color: rgba(74, 74, 74, 0.9); $dialog-title-fg-color: #424242; $dialog-backdrop-color: rgba(48, 48, 48, 0.38); $dialog-close-fg-color: #c1c1c1; +$dialog-close-external-color: $background; $dialog-shadow-color: rgba(0, 0, 0, 0.48); // ******************** diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index e22dd89d137..7b1a95ac559 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -32,7 +32,7 @@ import SettingsStore from "../settings/SettingsStore"; import { ActiveRoomObserver } from "../ActiveRoomObserver"; import { Notifier } from "../Notifier"; import type { Renderer } from "react-dom"; -import RightPanelStore from "../stores/RightPanelStore"; +import RightPanelStore from "../stores/right-panel/RightPanelStore"; import WidgetStore from "../stores/WidgetStore"; import CallHandler from "../CallHandler"; import { Analytics } from "../Analytics"; @@ -163,6 +163,16 @@ declare global { interface HTMLAudioElement { type?: string; + // sinkId & setSinkId are experimental and typescript doesn't know about them + sinkId: string; + setSinkId(outputId: string): void; + } + + interface HTMLVideoElement { + type?: string; + // sinkId & setSinkId are experimental and typescript doesn't know about them + sinkId: string; + setSinkId(outputId: string): void; } interface HTMLStyleElement { diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index a4f91fc71bf..d9062f8ce43 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -170,7 +170,36 @@ export default abstract class BasePlatform { */ abstract requestNotificationPermission(): Promise; - abstract displayNotification(title: string, msg: string, avatarUrl: string, room: Room); + public displayNotification( + title: string, + msg: string, + avatarUrl: string, + room: Room, + ev?: MatrixEvent, + ): Notification { + const notifBody = { + body: msg, + silent: true, // we play our own sounds + }; + if (avatarUrl) notifBody['icon'] = avatarUrl; + const notification = new window.Notification(title, notifBody); + + notification.onclick = () => { + const payload: ActionPayload = { + action: Action.ViewRoom, + room_id: room.roomId, + }; + + if (ev.getThread()) { + payload.event_id = ev.getId(); + } + + dis.dispatch(payload); + window.focus(); + }; + + return notification; + } loudNotification(ev: MatrixEvent, room: Room) { } diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index e7d6e991297..5d4973798d6 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -26,7 +26,7 @@ import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/ 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.api"; +import { SyncState } from "matrix-js-sdk/src/sync"; import { MatrixClientPeg } from './MatrixClientPeg'; import Modal from './Modal'; diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 1405362c831..3e37f292d1a 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -127,15 +127,18 @@ async function createThumbnail( } let canvas: HTMLCanvasElement | OffscreenCanvas; - if (window.OffscreenCanvas) { + let context: CanvasRenderingContext2D; + try { canvas = new window.OffscreenCanvas(targetWidth, targetHeight); - } else { + context = canvas.getContext("2d"); + } catch (e) { + // Fallback support for other browsers (Safari and Firefox for now) canvas = document.createElement("canvas"); (canvas as HTMLCanvasElement).width = targetWidth; (canvas as HTMLCanvasElement).height = targetHeight; + context = canvas.getContext("2d"); } - const context = canvas.getContext("2d"); context.drawImage(element, 0, 0, targetWidth, targetHeight); let thumbnailPromise: Promise; diff --git a/src/CountlyAnalytics.ts b/src/CountlyAnalytics.ts index da0021c2cfd..bb4db04cc24 100644 --- a/src/CountlyAnalytics.ts +++ b/src/CountlyAnalytics.ts @@ -439,7 +439,7 @@ export default class CountlyAnalytics { public async disable() { if (this.disabled) return; - await this.track("Opt-Out" ); + await this.track("Opt-Out"); this.endSession(); window.clearInterval(this.heartbeatIntervalId); window.clearTimeout(this.activityIntervalId); diff --git a/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts index dc8fdc5739f..5db75fe0f37 100644 --- a/src/DecryptionFailureTracker.ts +++ b/src/DecryptionFailureTracker.ts @@ -94,9 +94,9 @@ export class DecryptionFailureTracker { // localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap)); // } - public eventDecrypted(e: MatrixEvent, err: MatrixError | Error): void { + public eventDecrypted(e: MatrixEvent, err: MatrixError): void { if (err) { - this.addDecryptionFailure(new DecryptionFailure(e.getId(), err.code)); + this.addDecryptionFailure(new DecryptionFailure(e.getId(), err.errcode)); } else { // Could be an event in the failures, remove it this.removeDecryptionFailuresForEvent(e); diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 924f2be5a97..2e7c5e2dd51 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -207,8 +207,12 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to return { tagName, attribs: {} }; } - const width = Number(attribs.width) || 800; - const height = Number(attribs.height) || 600; + const width = Math.min(Number(attribs.width) || 800, 800); + const height = Math.min(Number(attribs.height) || 600, 600); + // specify width/height as max values instead of absolute ones to allow object-fit to do its thing + // we only allow our own styles for this tag so overwrite the attribute + attribs.style = `max-width: ${width}px; max-height: ${height}px;`; + attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height); return { tagName, attribs }; }, @@ -223,9 +227,12 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to return { tagName, attribs }; }, '*': function(tagName: string, attribs: sanitizeHtml.Attributes) { - // Delete any style previously assigned, style is an allowedTag for font and span - // because attributes are stripped after transforming - delete attribs.style; + // Delete any style previously assigned, style is an allowedTag for font, span & img, + // because attributes are stripped after transforming. + // For img this is trusted as it is generated wholly within the img transformation method. + if (tagName !== "img") { + delete attribs.style; + } // Sanitise and transform data-mx-color and data-mx-bg-color to their CSS // equivalents @@ -249,7 +256,7 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to }); if (style) { - attribs.style = style; + attribs.style = style + (attribs.style || ""); } return { tagName, attribs }; @@ -266,12 +273,15 @@ const sanitizeHtmlParams: IExtendedSanitizeOptions = { 'details', 'summary', ], allowedAttributes: { + // attribute sanitization happens after transformations, so we have to accept `style` for font, span & img + // but strip during the transformation. // custom ones first: font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix span: ['data-mx-maths', 'data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix div: ['data-mx-maths'], a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix - img: ['src', 'width', 'height', 'alt', 'title'], + // img tags also accept width/height, we just map those to max-width & max-height during transformation + img: ['src', 'alt', 'title', 'style'], ol: ['start'], code: ['class'], // We don't actually allow all classes, we filter them in transformTags }, diff --git a/src/Markdown.ts b/src/Markdown.ts index a086e9516ef..e24c9b72870 100644 --- a/src/Markdown.ts +++ b/src/Markdown.ts @@ -212,7 +212,7 @@ export default class Markdown { const walker = this.parsed.walker(); let ev; - while ( (ev = walker.next()) ) { + while (ev = walker.next()) { const node = ev.node; if (TEXT_NODES.indexOf(node.type) > -1) { // definitely text diff --git a/src/Notifier.ts b/src/Notifier.ts index 2fc2ee36437..35b3aee1652 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -122,7 +122,7 @@ export const Notifier = { avatarUrl = Avatar.avatarUrlForMember(ev.sender, 40, 40, 'crop'); } - const notif = plaf.displayNotification(title, msg, avatarUrl, room); + const notif = plaf.displayNotification(title, msg, avatarUrl, room, ev); // if displayNotification returns non-null, the platform supports // clearing notifications later, so keep track of this. @@ -381,7 +381,7 @@ export const Notifier = { _evaluateEvent: function(ev: MatrixEvent) { const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); - if (actions && actions.notify) { + if (actions?.notify) { if (RoomViewStore.getRoomId() === room.roomId && UserActivity.sharedInstance().userActiveRecently() && !Modal.hasDialogs() diff --git a/src/Rooms.ts b/src/Rooms.ts index 74774112ebc..f657b156e5f 100644 --- a/src/Rooms.ts +++ b/src/Rooms.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { Room } from "matrix-js-sdk/src/models/room"; +import { EventType } from "matrix-js-sdk/src/@types/event"; import { MatrixClientPeg } from './MatrixClientPeg'; import AliasCustomisations from './customisations/Alias'; @@ -91,10 +92,10 @@ export function guessAndSetDMRoom(room: Room, isDirect: boolean): Promise export async function setDMRoom(roomId: string, userId: string): Promise { if (MatrixClientPeg.get().isGuest()) return; - const mDirectEvent = MatrixClientPeg.get().getAccountData('m.direct'); + const mDirectEvent = MatrixClientPeg.get().getAccountData(EventType.Direct); let dmRoomMap = {}; - if (mDirectEvent !== undefined) dmRoomMap = mDirectEvent.getContent(); + if (mDirectEvent !== undefined) dmRoomMap = { ...mDirectEvent.getContent() }; // copy as we will mutate // remove it from the lists of any others users // (it can only be a DM room for one person) @@ -118,7 +119,7 @@ export async function setDMRoom(roomId: string, userId: string): Promise { dmRoomMap[userId] = roomList; } - await MatrixClientPeg.get().setAccountData('m.direct', dmRoomMap); + await MatrixClientPeg.get().setAccountData(EventType.Direct, dmRoomMap); } /** diff --git a/src/ScalarMessaging.ts b/src/ScalarMessaging.ts index 486ef5b2996..b0b8b3a0d4e 100644 --- a/src/ScalarMessaging.ts +++ b/src/ScalarMessaging.ts @@ -250,7 +250,6 @@ import { objectClone } from "./utils/objects"; enum Action { CloseScalar = "close_scalar", GetWidgets = "get_widgets", - SetWidgets = "set_widgets", SetWidget = "set_widget", JoinRulesState = "join_rules_state", SetPlumbingState = "set_plumbing_state", @@ -630,7 +629,7 @@ const onMessage = function(event: MessageEvent): void { if (event.data.action === Action.GetWidgets) { getWidgets(event, null); return; - } else if (event.data.action === Action.SetWidgets) { + } else if (event.data.action === Action.SetWidget) { setWidget(event, null); return; } else { diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index f7435ddda81..0004f786280 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -19,6 +19,7 @@ limitations under the License. import * as React from 'react'; 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"; @@ -286,6 +287,50 @@ export const Commands = [ category: CommandCategories.admin, renderingTypes: [TimelineRenderingType.Room], }), + new Command({ + command: 'jumptodate', + args: '', + description: _td('Jump to the given date in the timeline (YYYY-MM-DD)'), + isEnabled: () => SettingsStore.getValue("feature_jump_to_date"), + runFn: function(roomId, args) { + if (args) { + return success((async () => { + const unixTimestamp = Date.parse(args); + if (!unixTimestamp) { + throw new Error( + // FIXME: Use newTranslatableError here instead + // otherwise the rageshake error messages will be + // translated too + _t( + // eslint-disable-next-line max-len + 'We were unable to understand the given date (%(inputDate)s). Try using the format YYYY-MM-DD.', + { inputDate: args }, + ), + ); + } + + const cli = MatrixClientPeg.get(); + const { event_id: eventId, origin_server_ts: originServerTs } = await cli.timestampToEvent( + roomId, + unixTimestamp, + Direction.Forward, + ); + logger.log( + `/timestamp_to_event: found ${eventId} (${originServerTs}) for timestamp=${unixTimestamp}`, + ); + dis.dispatch({ + action: Action.ViewRoom, + eventId, + highlighted: true, + room_id: roomId, + }); + })()); + } + + return reject(this.getUsage()); + }, + category: CommandCategories.actions, + }), new Command({ command: 'nick', args: '', diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 639bec8a3d3..53fa8e3e566 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -27,12 +27,13 @@ import { isValid3pidInvite } from "./RoomInvite"; import SettingsStore from "./settings/SettingsStore"; import { ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES } from "./mjolnir/BanList"; import { WIDGET_LAYOUT_EVENT_TYPE } from "./stores/widgets/WidgetLayoutStore"; -import { RightPanelPhases } from './stores/RightPanelStorePhases'; +import { RightPanelPhases } from './stores/right-panel/RightPanelStorePhases'; import { Action } from './dispatcher/actions'; import defaultDispatcher from './dispatcher/dispatcher'; -import { SetRightPanelPhasePayload } from './dispatcher/payloads/SetRightPanelPhasePayload'; import { MatrixClientPeg } from "./MatrixClientPeg"; import { ROOM_SECURITY_TAB } from "./components/views/dialogs/RoomSettingsDialog"; +import AccessibleButton from './components/views/elements/AccessibleButton'; +import RightPanelStore from './stores/right-panel/RightPanelStore'; // These functions are frequently used just to check whether an event has // any text to display at all. For this reason they return deferred values @@ -229,9 +230,9 @@ function textForJoinRulesEvent(ev: MatrixEvent, allowJSX: boolean): () => string { _t('%(senderDisplayName)s changed who can join this room. View settings.', { senderDisplayName, }, { - "a": (sub) => + "a": (sub) => { sub } - , + , }) } ; } @@ -503,11 +504,7 @@ const onPinnedOrUnpinnedMessageClick = (messageId: string, roomId: string): void }; const onPinnedMessagesClick = (): void => { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.PinnedMessages, - allowClose: false, - }); + RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false); }; function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string | JSX.Element | null { @@ -532,13 +529,13 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string { senderName }, { "a": (sub) => - onPinnedOrUnpinnedMessageClick(messageId, roomId)}> + onPinnedOrUnpinnedMessageClick(messageId, roomId)}> { sub } - , + , "b": (sub) => - + { sub } - , + , }, ) } @@ -560,13 +557,13 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string { senderName }, { "a": (sub) => - onPinnedOrUnpinnedMessageClick(messageId, roomId)}> + onPinnedOrUnpinnedMessageClick(messageId, roomId)}> { sub } - , + , "b": (sub) => - + { sub } - , + , }, ) } @@ -582,7 +579,12 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string { _t( "%(senderName)s changed the pinned messages for the room.", { senderName }, - { "a": (sub) => { sub } }, + { + "a": (sub) => + + { sub } + , + }, ) } ); diff --git a/src/Unread.ts b/src/Unread.ts index da5b883f925..905798eb038 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -67,6 +67,15 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean { return false; } + // if the read receipt relates to an event is that part of a thread + // we consider that there are no unread messages + // This might be a false negative, but probably the best we can do until + // the read receipts have evolved to cater for threads + const event = room.findEventById(readUpToId); + if (event?.getThread()) { + return false; + } + // this just looks at whatever history we have, which if we've only just started // up probably won't be very much, so if the last couple of events are ones that // don't count, we don't know if there are any events that do count between where diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 65494a210d8..842b4edce03 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -43,6 +43,17 @@ import { FocusHandler, Ref } from "./roving/types"; * https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#Technique_1_Roving_tabindex */ +// Check for form elements which utilize the arrow keys for native functions +// like many of the text input varieties. +// +// i.e. it's ok to press the down arrow on a radio button to move to the next +// radio. But it's not ok to press the down arrow on a to +// move away because the down arrow should move the cursor to the end of the +// input. +export function checkInputableElement(el: HTMLElement): boolean { + return el.matches('input:not([type="radio"]):not([type="checkbox"]), textarea, select, [contenteditable=true]'); +} + export interface IState { activeRef: Ref; refs: Ref[]; @@ -187,7 +198,7 @@ export const RovingTabIndexProvider: React.FC = ({ const context = useMemo(() => ({ state, dispatch }), [state]); - const onKeyDownHandler = useCallback((ev) => { + const onKeyDownHandler = useCallback((ev: React.KeyboardEvent) => { if (onKeyDown) { onKeyDown(ev, context.state); if (ev.defaultPrevented) { @@ -198,7 +209,18 @@ export const RovingTabIndexProvider: React.FC = ({ let handled = false; let focusRef: RefObject; // Don't interfere with input default keydown behaviour - if (ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") { + // but allow people to move focus from it with Tab. + if (checkInputableElement(ev.target as HTMLElement)) { + switch (ev.key) { + case Key.TAB: + handled = true; + if (context.state.refs.length > 0) { + const idx = context.state.refs.indexOf(context.state.activeRef); + focusRef = findSiblingElement(context.state.refs, idx + (ev.shiftKey ? -1 : 1), ev.shiftKey); + } + break; + } + } else { // check if we actually have any items switch (ev.key) { case Key.HOME: @@ -270,9 +292,11 @@ export const RovingTabIndexProvider: React.FC = ({ // onFocus should be called when the index gained focus in any manner // isActive should be used to set tabIndex in a manner such as `tabIndex={isActive ? 0 : -1}` // ref should be passed to a DOM node which will be used for DOM compareDocumentPosition -export const useRovingTabIndex = (inputRef?: Ref): [FocusHandler, boolean, Ref] => { +export const useRovingTabIndex = ( + inputRef?: RefObject, +): [FocusHandler, boolean, RefObject] => { const context = useContext(RovingTabIndexContext); - let ref = useRef(null); + let ref = useRef(null); if (inputRef) { // if we are given a ref, use it instead of ours diff --git a/src/accessibility/context_menu/MenuItem.tsx b/src/accessibility/context_menu/MenuItem.tsx index 9c0b2482740..7f231c7bc0f 100644 --- a/src/accessibility/context_menu/MenuItem.tsx +++ b/src/accessibility/context_menu/MenuItem.tsx @@ -18,10 +18,9 @@ limitations under the License. import React from "react"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; -import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; +import { RovingAccessibleButton, RovingAccessibleTooltipButton } from "../RovingTabIndex"; -interface IProps extends React.ComponentProps { +interface IProps extends React.ComponentProps { label?: string; tooltip?: string; } @@ -31,15 +30,14 @@ export const MenuItem: React.FC = ({ children, label, tooltip, ...props const ariaLabel = props["aria-label"] || label; if (tooltip) { - return + return { children } - ; + ; } return ( - + { children } - + ); }; - diff --git a/src/accessibility/context_menu/MenuItemCheckbox.tsx b/src/accessibility/context_menu/MenuItemCheckbox.tsx index 67da4cc85a3..6eb66102f3a 100644 --- a/src/accessibility/context_menu/MenuItemCheckbox.tsx +++ b/src/accessibility/context_menu/MenuItemCheckbox.tsx @@ -18,9 +18,9 @@ limitations under the License. import React from "react"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; +import { RovingAccessibleButton } from "../RovingTabIndex"; -interface IProps extends React.ComponentProps { +interface IProps extends React.ComponentProps { label?: string; active: boolean; } @@ -28,16 +28,15 @@ interface IProps extends React.ComponentProps { // Semantic component for representing a role=menuitemcheckbox export const MenuItemCheckbox: React.FC = ({ children, label, active, disabled, ...props }) => { return ( - { children } - + ); }; diff --git a/src/accessibility/context_menu/MenuItemRadio.tsx b/src/accessibility/context_menu/MenuItemRadio.tsx index eb50d458365..f8c85dd8f9a 100644 --- a/src/accessibility/context_menu/MenuItemRadio.tsx +++ b/src/accessibility/context_menu/MenuItemRadio.tsx @@ -18,9 +18,9 @@ limitations under the License. import React from "react"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; +import { RovingAccessibleButton } from "../RovingTabIndex"; -interface IProps extends React.ComponentProps { +interface IProps extends React.ComponentProps { label?: string; active: boolean; } @@ -28,16 +28,15 @@ interface IProps extends React.ComponentProps { // Semantic component for representing a role=menuitemradio export const MenuItemRadio: React.FC = ({ children, label, active, disabled, ...props }) => { return ( - { children } - + ); }; diff --git a/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx index 66846cc4849..7349646f2ba 100644 --- a/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx +++ b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx @@ -19,6 +19,7 @@ limitations under the License. import React from "react"; import { Key } from "../../Keyboard"; +import { useRovingTabIndex } from "../RovingTabIndex"; import StyledCheckbox from "../../components/views/elements/StyledCheckbox"; interface IProps extends React.ComponentProps { @@ -29,6 +30,8 @@ interface IProps extends React.ComponentProps { // Semantic component for representing a styled role=menuitemcheckbox export const StyledMenuItemCheckbox: React.FC = ({ children, label, onChange, onClose, ...props }) => { + const [onFocus, isActive, ref] = useRovingTabIndex(); + const onKeyDown = (e: React.KeyboardEvent) => { if (e.key === Key.ENTER || e.key === Key.SPACE) { e.stopPropagation(); @@ -52,11 +55,13 @@ export const StyledMenuItemCheckbox: React.FC = ({ children, label, onCh { children } diff --git a/src/accessibility/context_menu/StyledMenuItemRadio.tsx b/src/accessibility/context_menu/StyledMenuItemRadio.tsx index e3d340ef3e8..0ce7f3d6f6f 100644 --- a/src/accessibility/context_menu/StyledMenuItemRadio.tsx +++ b/src/accessibility/context_menu/StyledMenuItemRadio.tsx @@ -19,6 +19,7 @@ limitations under the License. import React from "react"; import { Key } from "../../Keyboard"; +import { useRovingTabIndex } from "../RovingTabIndex"; import StyledRadioButton from "../../components/views/elements/StyledRadioButton"; interface IProps extends React.ComponentProps { @@ -29,6 +30,8 @@ interface IProps extends React.ComponentProps { // Semantic component for representing a styled role=menuitemradio export const StyledMenuItemRadio: React.FC = ({ children, label, onChange, onClose, ...props }) => { + const [onFocus, isActive, ref] = useRovingTabIndex(); + const onKeyDown = (e: React.KeyboardEvent) => { if (e.key === Key.ENTER || e.key === Key.SPACE) { e.stopPropagation(); @@ -52,11 +55,13 @@ export const StyledMenuItemRadio: React.FC = ({ children, label, onChang { children } diff --git a/src/autocomplete/Components.tsx b/src/autocomplete/Components.tsx index 8f155f7f55b..079e407c5e2 100644 --- a/src/autocomplete/Components.tsx +++ b/src/autocomplete/Components.tsx @@ -31,11 +31,19 @@ interface ITextualCompletionProps { } export const TextualCompletion = forwardRef((props, ref) => { - const { title, subtitle, description, className, ...restProps } = props; + const { + title, + subtitle, + description, + className, + 'aria-selected': ariaSelectedAttribute, + ...restProps + } = props; return (
{ title } @@ -50,11 +58,20 @@ interface IPillCompletionProps extends ITextualCompletionProps { } export const PillCompletion = forwardRef((props, ref) => { - const { title, subtitle, description, className, children, ...restProps } = props; + const { + title, + subtitle, + description, + className, + children, + 'aria-selected': ariaSelectedAttribute, + ...restProps + } = props; return (
{ children } diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index a60df457704..31719a2cf14 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -27,7 +27,7 @@ interface IProps extends Omit, "onScroll"> { } export default class AutoHideScrollbar extends React.Component { - private containerRef: React.RefObject = React.createRef(); + public readonly containerRef: React.RefObject = React.createRef(); public componentDidMount() { if (this.containerRef.current && this.props.onScroll) { diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index e1aa014bdcd..c2ebbdc784d 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -25,7 +25,7 @@ import { Key } from "../../Keyboard"; import { Writeable } from "../../@types/common"; import { replaceableComponent } from "../../utils/replaceableComponent"; import UIStore from "../../stores/UIStore"; -import { getInputableElement } from "./LoggedInView"; +import { checkInputableElement, RovingTabIndexProvider } from "../../accessibility/RovingTabIndex"; // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and @@ -180,108 +180,41 @@ export default class ContextMenu extends React.PureComponent { if (this.props.onFinished) this.props.onFinished(); }; - private onMoveFocus = (element: Element, up: boolean) => { - let descending = false; // are we currently descending or ascending through the DOM tree? - - do { - const child = up ? element.lastElementChild : element.firstElementChild; - const sibling = up ? element.previousElementSibling : element.nextElementSibling; - - if (descending) { - if (child) { - element = child; - } else if (sibling) { - element = sibling; - } else { - descending = false; - element = element.parentElement; - } - } else { - if (sibling) { - element = sibling; - descending = true; - } else { - element = element.parentElement; - } - } - - if (element) { - if (element.classList.contains("mx_ContextualMenu")) { // we hit the top - element = up ? element.lastElementChild : element.firstElementChild; - descending = true; - } - } - } while (element && !element.getAttribute("role")?.startsWith("menuitem")); - - if (element) { - (element as HTMLElement).focus(); - } - }; - - private onMoveFocusHomeEnd = (element: Element, up: boolean) => { - let results = element.querySelectorAll('[role^="menuitem"]'); - if (!results) { - results = element.querySelectorAll('[tab-index]'); - } - if (results && results.length) { - if (up) { - (results[0] as HTMLElement).focus(); - } else { - (results[results.length - 1] as HTMLElement).focus(); - } - } - }; - private onClick = (ev: React.MouseEvent) => { // Don't allow clicks to escape the context menu wrapper ev.stopPropagation(); }; + // We now only handle closing the ContextMenu in this keyDown handler. + // All of the item/option navigation is delegated to RovingTabIndex. private onKeyDown = (ev: React.KeyboardEvent) => { - // don't let keyboard handling escape the context menu - ev.stopPropagation(); + ev.stopPropagation(); // prevent keyboard propagating out of the context menu, we're focus-locked + // 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) { this.props.onFinished(); - ev.preventDefault(); } return; } - // only handle escape when in an input field - if (ev.key !== Key.ESCAPE && getInputableElement(ev.target as HTMLElement)) return; - - let handled = true; - - switch (ev.key) { - // XXX: this is imitating roving behaviour, it should really use the RovingTabIndex utils - // to inherit proper handling of unmount edge cases - case Key.TAB: - case Key.ESCAPE: - case Key.ARROW_LEFT: // close on left and right arrows too for when it is a context menu on a - case Key.ARROW_RIGHT: - this.props.onFinished(); - break; - case Key.ARROW_UP: - this.onMoveFocus(ev.target as Element, true); - break; - case Key.ARROW_DOWN: - this.onMoveFocus(ev.target as Element, false); - break; - case Key.HOME: - this.onMoveFocusHomeEnd(this.state.contextMenuElem, true); - break; - case Key.END: - this.onMoveFocusHomeEnd(this.state.contextMenuElem, false); - break; - default: - handled = false; + // When an is focused, only handle the Escape key + if (checkInputableElement(ev.target as HTMLElement) && ev.key !== Key.ESCAPE) { + return; } - if (handled) { - // consume all other keys in context menu - ev.preventDefault(); + if ( + ev.key === Key.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 || + // 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 + ) { + this.props.onFinished(); } }; @@ -408,23 +341,27 @@ export default class ContextMenu extends React.PureComponent { } return ( -
- { background } -
- { body } -
-
+ + { ({ onKeyDownHandler }) => ( +
+ { background } +
+ { body } +
+
+ ) } +
); } diff --git a/src/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx index b29a1a43d60..4b309a1838c 100644 --- a/src/components/structures/EmbeddedPage.tsx +++ b/src/components/structures/EmbeddedPage.tsx @@ -86,7 +86,7 @@ export default class EmbeddedPage extends React.PureComponent { return; } - body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1)); + body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1) => this.translate(g1)); if (this.props.replaceMap) { Object.keys(this.props.replaceMap).forEach(key => { diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index ea93c8b728d..110404e4646 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -27,9 +27,8 @@ import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from '../../MatrixClientPeg'; import EventIndexPeg from "../../indexing/EventIndexPeg"; import { _t } from '../../languageHandler'; -import BaseCard from "../views/right_panel/BaseCard"; -import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; import DesktopBuildsNotice, { WarningKind } from "../views/elements/DesktopBuildsNotice"; +import BaseCard from "../views/right_panel/BaseCard"; import { replaceableComponent } from "../../utils/replaceableComponent"; import ResizeNotifier from '../../utils/ResizeNotifier'; import TimelinePanel from "./TimelinePanel"; @@ -223,7 +222,6 @@ class FilePanel extends React.Component { return
{ _t("You must register to use this functionality", @@ -236,7 +234,6 @@ class FilePanel extends React.Component { return
{ _t("You must join the room to see its files") }
; @@ -260,7 +257,6 @@ class FilePanel extends React.Component { @@ -288,7 +284,6 @@ class FilePanel extends React.Component { diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index da32b4f8ae4..f891c4100e1 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -38,13 +38,14 @@ import GroupStore from '../../stores/GroupStore'; import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import { makeGroupPermalink, makeUserPermalink } from "../../utils/permalinks/Permalinks"; -import RightPanelStore from "../../stores/RightPanelStore"; +import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import AutoHideScrollbar from "./AutoHideScrollbar"; import { mediaFromMxc } from "../../customisations/Media"; import { replaceableComponent } from "../../utils/replaceableComponent"; import { createSpaceFromCommunity } from "../../utils/space"; import { Action } from "../../dispatcher/actions"; -import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases"; +import { UPDATE_EVENT } from "../../stores/AsyncStore"; const LONG_DESC_PLACEHOLDER = _td( `

HTML for your community's page

@@ -427,7 +428,7 @@ export default class GroupView extends React.Component { membershipBusy: false, publicityBusy: false, inviterProfile: null, - showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup, + showRightPanel: RightPanelStore.instance.isOpenForGroup, showUpgradeNotice: !localStorage.getItem(UPGRADE_NOTICE_LS_KEY), }; @@ -439,7 +440,7 @@ export default class GroupView extends React.Component { this._initGroupStore(this.props.groupId, true); this._dispatcherRef = dis.register(this._onAction); - this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate); + RightPanelStore.instance.on(UPDATE_EVENT, this._onRightPanelStoreUpdate); } componentWillUnmount() { @@ -447,10 +448,7 @@ export default class GroupView extends React.Component { this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership); dis.unregister(this._dispatcherRef); - // Remove RightPanelStore listener - if (this._rightPanelStoreToken) { - this._rightPanelStoreToken.remove(); - } + RightPanelStore.instance.off(UPDATE_EVENT, this._onRightPanelStoreUpdate); } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event @@ -468,7 +466,7 @@ export default class GroupView extends React.Component { _onRightPanelStoreUpdate = () => { this.setState({ - showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup, + showRightPanel: RightPanelStore.instance.isOpenForGroup, }); }; @@ -824,10 +822,7 @@ export default class GroupView extends React.Component { }; _onAdminsLinkClick = () => { - dis.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.GroupMemberList, - }); + RightPanelStore.instance.setCard({ phase: RightPanelPhases.GroupMemberList }); }; _getGroupSection() { diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index 9c8013f866e..2a931d760b2 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -19,7 +19,7 @@ import { useContext, useState } from "react"; import AutoHideScrollbar from './AutoHideScrollbar'; import { getHomePageUrl } from "../../utils/pages"; -import { _t } from "../../languageHandler"; +import { _tDom } from "../../languageHandler"; import SdkConfig from "../../SdkConfig"; import * as sdk from "../../index"; import dis from "../../dispatcher/dispatcher"; @@ -72,8 +72,8 @@ const UserWelcomeTop = () => { return
cli.setAvatarUrl(url)} > { /> -

{ _t("Welcome %(name)s", { name: ownProfile.displayName }) }

-

{ _t("Now, let's help you get started") }

+

{ _tDom("Welcome %(name)s", { name: ownProfile.displayName }) }

+

{ _tDom("Now, let's help you get started") }

; }; @@ -113,8 +113,8 @@ const HomePage: React.FC = ({ justRegistered = false }) => { introSection = {config.brand} -

{ _t("Welcome to %(appName)s", { appName: config.brand }) }

-

{ _t("Own your conversations.") }

+

{ _tDom("Welcome to %(appName)s", { appName: config.brand }) }

+

{ _tDom("Own your conversations.") }

; } @@ -123,13 +123,13 @@ const HomePage: React.FC = ({ justRegistered = false }) => { { introSection }
- { _t("Send a Direct Message") } + { _tDom("Send a Direct Message") } - { _t("Explore Public Rooms") } + { _tDom("Explore Public Rooms") } - { _t("Create a Group Chat") } + { _tDom("Create a Group Chat") }
diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index dad1b8f43fc..36761a1272b 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -84,7 +84,7 @@ export default class LeftPanel extends React.Component { } private static get breadcrumbsMode(): BreadcrumbsMode { - if (!SettingsStore.getValue("breadcrumbs")) return BreadcrumbsMode.Disabled; + if (!BreadcrumbsStore.instance.visible) return BreadcrumbsMode.Disabled; return SettingsStore.getValue("feature_breadcrumbs_v2") ? BreadcrumbsMode.Labs : BreadcrumbsMode.Legacy; } diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 53cc1503767..ad8bf8b7b6f 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -19,6 +19,8 @@ import { MatrixClient } from 'matrix-js-sdk/src/client'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import classNames from 'classnames'; +import { ISyncStateData, SyncState } from 'matrix-js-sdk/src/sync'; +import { IUsageLimit } from 'matrix-js-sdk/src/@types/partials'; import { Key } from '../../Keyboard'; import PageTypes from '../../PageTypes'; @@ -42,7 +44,6 @@ import CallContainer from '../views/voip/CallContainer'; import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload"; import RoomListStore from "../../stores/room-list/RoomListStore"; import NonUrgentToastContainer from "./NonUrgentToastContainer"; -import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload"; import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore"; import Modal from "../../Modal"; import { ICollapseConfig } from "../../resizer/distributors/collapse"; @@ -68,13 +69,17 @@ import { mediaFromMxc } from "../../customisations/Media"; import { RecheckThemePayload } from '../../dispatcher/payloads/RecheckThemePayload'; import LegacyCommunityPreview from "./LegacyCommunityPreview"; import { Layout } from '../../settings/enums/Layout'; +import RightPanelStore from '../../stores/right-panel/RightPanelStore'; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. // NB. this is just for server notices rather than pinned messages in general. const MAX_PINNED_NOTICES_PER_ROOM = 2; -export function getInputableElement(el: HTMLElement): HTMLElement | null { +// Used to find the closest inputable thing. Because of how our composer works, +// your caret might be within a paragraph/font/div/whatever within the +// contenteditable rather than directly in something inputable. +function getInputableElement(el: HTMLElement): HTMLElement | null { return el.closest("input, textarea, select, [contenteditable=true]"); } @@ -106,24 +111,8 @@ interface IProps { forceTimeline?: boolean; // see props on MatrixChat } -interface IUsageLimit { - // "hs_disabled" is NOT a specced string, but is used in Synapse - // This is tracked over at https://github.com/matrix-org/synapse/issues/9237 - // eslint-disable-next-line camelcase - limit_type: "monthly_active_user" | "hs_disabled" | string; - // eslint-disable-next-line camelcase - admin_contact?: string; -} - interface IState { - syncErrorData?: { - error: { - // This is not specced, but used in Synapse. See - // https://github.com/matrix-org/synapse/issues/9237#issuecomment-768238922 - data: IUsageLimit; - errcode: string; - }; - }; + syncErrorData?: ISyncStateData; usageLimitDismissed: boolean; usageLimitEventContent?: IUsageLimit; usageLimitEventTs?: number; @@ -306,45 +295,35 @@ class LoggedInView extends React.Component { } }; - onLayoutChanged = (setting, roomId, level, valueAtLevel, newValue) => { + private onLayoutChanged = () => { this.setState({ - layout: valueAtLevel, + layout: SettingsStore.getValue("layout"), }); }; - onCompactLayoutChanged = (setting, roomId, level, valueAtLevel, newValue) => { + private onCompactLayoutChanged = () => { this.setState({ - useCompactLayout: valueAtLevel, + useCompactLayout: SettingsStore.getValue("useCompactLayout"), }); }; - onSync = (syncState, oldSyncState, data) => { - const oldErrCode = ( - this.state.syncErrorData && - this.state.syncErrorData.error && - this.state.syncErrorData.error.errcode - ); + private onSync = (syncState: SyncState, oldSyncState?: SyncState, data?: ISyncStateData): void => { + const oldErrCode = this.state.syncErrorData?.error?.errcode; const newErrCode = data && data.error && data.error.errcode; if (syncState === oldSyncState && oldErrCode === newErrCode) return; - if (syncState === 'ERROR') { - this.setState({ - syncErrorData: data, - }); - } else { - this.setState({ - syncErrorData: null, - }); - } + this.setState({ + syncErrorData: syncState === SyncState.Error ? data : null, + }); - if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') { + if (oldSyncState === SyncState.Prepared && syncState === SyncState.Syncing) { this.updateServerNoticeEvents(); } else { this.calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent); } }; - onRoomStateEvents = (ev, state) => { + private onRoomStateEvents = (ev: MatrixEvent): void => { const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice]; if (serverNoticeList && serverNoticeList.some(r => r.roomId === ev.getRoomId())) { this.updateServerNoticeEvents(); @@ -360,7 +339,7 @@ class LoggedInView extends React.Component { private calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) { const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED"; if (error) { - usageLimitEventContent = syncError.error.data; + usageLimitEventContent = syncError.error.data as IUsageLimit; } // usageLimitDismissed is true when the user has explicitly hidden the toast @@ -524,10 +503,7 @@ class LoggedInView extends React.Component { break; case NavigationAction.ToggleRoomSidePanel: if (this.props.page_type === "room_view" || this.props.page_type === "group_view") { - dis.dispatch({ - action: Action.ToggleRightPanel, - type: this.props.page_type === "room_view" ? "room" : "group", - }); + RightPanelStore.instance.togglePanel(); handled = true; } break; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 233e07665da..4e9e3942a9a 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -15,7 +15,9 @@ limitations under the License. */ import React, { ComponentType, createRef } from 'react'; -import { createClient } from "matrix-js-sdk/src/matrix"; +import { createClient, EventType, MatrixClient } from 'matrix-js-sdk/src/matrix'; +import { ISyncStateData, SyncState } from 'matrix-js-sdk/src/sync'; +import { MatrixError } from 'matrix-js-sdk/src/http-api'; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { Error as ErrorEvent } from "matrix-analytics-events/types/typescript/Error"; @@ -31,7 +33,7 @@ import 'what-input'; import Analytics from "../../Analytics"; import CountlyAnalytics from "../../CountlyAnalytics"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; -import { MatrixClientPeg, IMatrixClientCreds } from "../../MatrixClientPeg"; +import { IMatrixClientCreds, MatrixClientPeg } from "../../MatrixClientPeg"; import PlatformPeg from "../../PlatformPeg"; import SdkConfig from "../../SdkConfig"; import dis from "../../dispatcher/dispatcher"; @@ -58,6 +60,7 @@ import { storeRoomAliasInCache } from '../../RoomAliasCache'; import ToastStore from "../../stores/ToastStore"; import * as StorageManager from "../../utils/StorageManager"; import type LoggedInViewType from "./LoggedInView"; +import LoggedInView from './LoggedInView'; import { Action } from "../../dispatcher/actions"; import { hideToast as hideAnalyticsToast, @@ -68,7 +71,10 @@ import { showToast as showScUpdateRoomToast } from "../../toasts/ScUpdateAnnounc import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast"; import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import ErrorDialog from "../views/dialogs/ErrorDialog"; -import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore"; +import { + RoomNotificationStateStore, + UPDATE_STATUS_INDICATOR, +} from "../../stores/notifications/RoomNotificationStateStore"; import { SettingLevel } from "../../settings/SettingLevel"; import { leaveRoomBehaviour } from "../../utils/membership"; import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog"; @@ -92,7 +98,6 @@ import RoomDirectory from './RoomDirectory'; import KeySignatureUploadFailedDialog from "../views/dialogs/KeySignatureUploadFailedDialog"; import IncomingSasDialog from "../views/dialogs/IncomingSasDialog"; import CompleteSecurity from "./auth/CompleteSecurity"; -import LoggedInView from './LoggedInView'; import Welcome from "../views/auth/Welcome"; import ForgotPassword from "./auth/ForgotPassword"; import E2eSetup from "./auth/E2eSetup"; @@ -114,6 +119,7 @@ import InfoDialog from "../views/dialogs/InfoDialog"; import FeedbackDialog from "../views/dialogs/FeedbackDialog"; import AccessibleButton from "../views/elements/AccessibleButton"; import { ActionPayload } from "../../dispatcher/payloads"; +import { SummarizedNotificationState } from "../../stores/notifications/SummarizedNotificationState"; /** constants for MatrixChat.state.view */ export enum Views { @@ -234,7 +240,7 @@ interface IState { // When showing Modal dialogs we need to set aria-hidden on the root app element // and disable it when there are no dialogs hideToSRUsers: boolean; - syncError?: Error; + syncError?: MatrixError; resizeNotifier: ResizeNotifier; serverConfig?: ValidatedServerConfig; ready: boolean; @@ -257,8 +263,8 @@ export default class MatrixChat extends React.PureComponent { onTokenLoginCompleted: () => {}, }; - firstSyncComplete: boolean; - firstSyncPromise: IDeferred; + private firstSyncComplete = false; + private firstSyncPromise: IDeferred; private screenAfterLogin?: IScreen; private pageChanging: boolean; @@ -270,12 +276,12 @@ export default class MatrixChat extends React.PureComponent { private prevWindowWidth: number; private readonly loggedInView: React.RefObject; - private readonly dispatcherRef: any; + private readonly dispatcherRef: string; private readonly themeWatcher: ThemeWatcher; private readonly fontWatcher: FontWatcher; - constructor(props, context) { - super(props, context); + constructor(props: IProps) { + super(props); this.state = { view: Views.LOADING, @@ -321,6 +327,8 @@ export default class MatrixChat extends React.PureComponent { // For PersistentElement this.state.resizeNotifier.on("middlePanelResized", this.dispatchTimelineResize); + RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatusIndicator); + // Force users to go through the soft logout page if they're soft logged out if (Lifecycle.isSoftLogout()) { // When the session loads it'll be detected as soft logged out and a dispatch @@ -494,15 +502,15 @@ export default class MatrixChat extends React.PureComponent { }); } - getFallbackHsUrl() { - if (this.props.serverConfig && this.props.serverConfig.isDefault) { + private getFallbackHsUrl(): string { + if (this.props.serverConfig?.isDefault) { return this.props.config.fallback_hs_url; } else { return null; } } - getServerProperties() { + private getServerProperties() { let props = this.state.serverConfig; if (!props) props = this.props.serverConfig; // for unit tests if (!props) props = SdkConfig.get()["validated_server_config"]; @@ -535,11 +543,11 @@ export default class MatrixChat extends React.PureComponent { // to try logging out. } - startPageChangeTimer() { + private startPageChangeTimer() { PerformanceMonitor.instance.start(PerformanceEntryNames.PAGE_CHANGE); } - stopPageChangeTimer() { + private stopPageChangeTimer() { const perfMonitor = PerformanceMonitor.instance; perfMonitor.stop(PerformanceEntryNames.PAGE_CHANGE); @@ -554,13 +562,13 @@ export default class MatrixChat extends React.PureComponent { : null; } - shouldTrackPageChange(prevState: IState, state: IState) { + private shouldTrackPageChange(prevState: IState, state: IState): boolean { return prevState.currentRoomId !== state.currentRoomId || prevState.view !== state.view || prevState.page_type !== state.page_type; } - setStateForNewView(state: Partial) { + private setStateForNewView(state: Partial): void { if (state.view === undefined) { throw new Error("setStateForNewView with no view!"); } @@ -572,7 +580,7 @@ export default class MatrixChat extends React.PureComponent { this.setState(newState); } - private onAction = (payload: ActionPayload) => { + private onAction = (payload: ActionPayload): void => { // console.log(`MatrixClientPeg.onAction: ${payload.action}`); // Start the onboarding process for certain actions @@ -787,6 +795,8 @@ export default class MatrixChat extends React.PureComponent { }); break; case 'focus_room_filter': // for CtrlOrCmd+K to work by expanding the left panel first + if (SettingsStore.getValue("feature_spotlight")) break; // don't expand if spotlight enabled + // fallthrough case 'show_left_panel': this.setState({ collapseLhs: false, @@ -1288,16 +1298,10 @@ export default class MatrixChat extends React.PureComponent { // run without the update to m.direct, making another welcome // user room (it doesn't wait for new data from the server, just // the saved sync to be loaded). - const saveWelcomeUser = (ev) => { - if ( - ev.getType() === 'm.direct' && - ev.getContent() && - ev.getContent()[this.props.config.welcomeUserId] - ) { + const saveWelcomeUser = (ev: MatrixEvent) => { + if (ev.getType() === EventType.Direct && ev.getContent()[this.props.config.welcomeUserId]) { MatrixClientPeg.get().store.save(true); - MatrixClientPeg.get().removeListener( - "accountData", saveWelcomeUser, - ); + MatrixClientPeg.get().removeListener("accountData", saveWelcomeUser); } }; MatrixClientPeg.get().on("accountData", saveWelcomeUser); @@ -1489,7 +1493,7 @@ export default class MatrixChat extends React.PureComponent { return this.loggedInView.current.canResetTimelineInRoom(roomId); }); - cli.on('sync', (state, prevState, data) => { + cli.on('sync', (state: SyncState, prevState?: SyncState, data?: ISyncStateData) => { // LifecycleStore and others cannot directly subscribe to matrix client for // events because flux only allows store state changes during flux dispatches. // So dispatch directly from here. Ideally we'd use a SyncStateStore that @@ -1497,21 +1501,20 @@ export default class MatrixChat extends React.PureComponent { // its own dispatch). dis.dispatch({ action: 'sync_state', prevState, state }); - if (state === "ERROR" || state === "RECONNECTING") { + if (state === SyncState.Error || state === SyncState.Reconnecting) { if (data.error instanceof InvalidStoreError) { Lifecycle.handleInvalidStoreError(data.error); } - this.setState({ syncError: data.error || true }); + this.setState({ syncError: data.error || {} as MatrixError }); } else if (this.state.syncError) { this.setState({ syncError: null }); } - this.updateStatusIndicator(state, prevState); - if (state === "SYNCING" && prevState === "SYNCING") { + if (state === SyncState.Syncing && prevState === SyncState.Syncing) { return; } logger.info("MatrixClient sync state => %s", state); - if (state !== "PREPARED") { return; } + if (state !== SyncState.Prepared) { return; } this.firstSyncComplete = true; this.firstSyncPromise.resolve(); @@ -1629,11 +1632,13 @@ export default class MatrixChat extends React.PureComponent { const dft = new DecryptionFailureTracker((total, errorCode) => { Analytics.trackEvent('E2E', 'Decryption failure', errorCode, String(total)); CountlyAnalytics.instance.track("decryption_failure", { errorCode }, null, { sum: total }); - PosthogAnalytics.instance.trackEvent({ - eventName: "Error", - domain: "E2EE", - name: errorCode, - }); + for (let i = 0; i < total; i++) { + PosthogAnalytics.instance.trackEvent({ + eventName: "Error", + domain: "E2EE", + name: errorCode, + }); + } }, (errorCode) => { // Map JS-SDK error codes to tracker codes for aggregation switch (errorCode) { @@ -1771,7 +1776,7 @@ export default class MatrixChat extends React.PureComponent { } } - showScreen(screen: string, params?: {[key: string]: any}) { + public showScreen(screen: string, params?: {[key: string]: any}) { const cli = MatrixClientPeg.get(); const isLoggedOutOrGuest = !cli || cli.isGuest(); if (!isLoggedOutOrGuest && AUTH_SCREENS.includes(screen)) { @@ -1946,13 +1951,14 @@ export default class MatrixChat extends React.PureComponent { } } - notifyNewScreen(screen: string, replaceLast = false) { + private notifyNewScreen(screen: string, replaceLast = false) { if (this.props.onNewScreen) { this.props.onNewScreen(screen, replaceLast); } this.setPageSubtitle(); } - onLogoutClick(event: React.MouseEvent) { + + private onLogoutClick(event: React.MouseEvent) { dis.dispatch({ action: 'logout', }); @@ -1960,7 +1966,7 @@ export default class MatrixChat extends React.PureComponent { event.preventDefault(); } - handleResize = () => { + private handleResize = () => { const LHS_THRESHOLD = 1000; const width = UIStore.instance.windowWidth; @@ -1980,28 +1986,28 @@ export default class MatrixChat extends React.PureComponent { dis.dispatch({ action: 'timeline_resize' }); } - onRegisterClick = () => { + private onRegisterClick = () => { this.showScreen("register"); }; - onLoginClick = () => { + private onLoginClick = () => { this.showScreen("login"); }; - onForgotPasswordClick = () => { + private onForgotPasswordClick = () => { this.showScreen("forgot_password"); }; - onRegisterFlowComplete = (credentials: IMatrixClientCreds, password: string) => { + private onRegisterFlowComplete = (credentials: IMatrixClientCreds, password: string): Promise => { return this.onUserCompletedLoginFlow(credentials, password); }; // returns a promise which resolves to the new MatrixClient - onRegistered(credentials: IMatrixClientCreds) { + private onRegistered(credentials: IMatrixClientCreds): Promise { return Lifecycle.setLoggedIn(credentials); } - onSendEvent(roomId: string, event: MatrixEvent) { + private onSendEvent(roomId: string, event: MatrixEvent): void { const cli = MatrixClientPeg.get(); if (!cli) return; @@ -2028,17 +2034,16 @@ export default class MatrixChat extends React.PureComponent { } } - updateStatusIndicator(state: string, prevState: string) { - const notificationState = RoomNotificationStateStore.instance.globalState; + private onUpdateStatusIndicator = (notificationState: SummarizedNotificationState, state: SyncState): void => { const numUnreadRooms = notificationState.numUnreadStates; // we know that states === rooms here if (PlatformPeg.get()) { - PlatformPeg.get().setErrorStatus(state === 'ERROR'); + PlatformPeg.get().setErrorStatus(state === SyncState.Error); PlatformPeg.get().setNotificationCount(numUnreadRooms); } this.subTitleStatus = ''; - if (state === "ERROR") { + if (state === SyncState.Error) { this.subTitleStatus += `[${_t("Offline")}] `; } if (numUnreadRooms > 0) { @@ -2046,13 +2051,9 @@ export default class MatrixChat extends React.PureComponent { } this.setPageSubtitle(); - } - - onCloseAllSettings() { - dis.dispatch({ action: 'close_settings' }); - } + }; - onServerConfigChange = (serverConfig: ValidatedServerConfig) => { + private onServerConfigChange = (serverConfig: ValidatedServerConfig) => { this.setState({ serverConfig }); }; @@ -2070,7 +2071,7 @@ export default class MatrixChat extends React.PureComponent { * Note: SSO users (and any others using token login) currently do not pass through * this, as they instead jump straight into the app after `attemptTokenLogin`. */ - onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds, password: string) => { + private onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds, password: string): Promise => { this.accountPassword = password; // self-destruct the password after 5mins if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer); @@ -2088,11 +2089,11 @@ export default class MatrixChat extends React.PureComponent { }; // complete security / e2e setup has finished - onCompleteSecurityE2eSetupFinished = () => { + private onCompleteSecurityE2eSetupFinished = (): void => { this.onLoggedIn(); }; - getFragmentAfterLogin() { + private getFragmentAfterLogin(): string { let fragmentAfterLogin = ""; const initialScreenAfterLogin = this.props.initialScreenAfterLogin; if (initialScreenAfterLogin && @@ -2163,9 +2164,9 @@ export default class MatrixChat extends React.PureComponent {
{ errorBox } - + { _t('Logout') } - +
); } diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index abd2a0e46ee..a5a80f199e3 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -51,6 +51,7 @@ import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import EditorStateTransfer from "../../utils/EditorStateTransfer"; import { UserNameColorMode } from '../../settings/enums/UserNameColorMode'; import { Action } from '../../dispatcher/actions'; +import { getEventDisplayInfo } from "../../utils/EventUtils"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = [EventType.Sticker, EventType.RoomMessage]; @@ -738,11 +739,14 @@ export default class MessagePanel extends React.Component { ret.push(dateSeparator); } - let willWantDateSeparator = false; let lastInSection = true; if (nextEventWithTile) { - willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEventWithTile.getDate() || new Date()); - lastInSection = willWantDateSeparator || mxEv.getSender() !== nextEventWithTile.getSender(); + const nextEv = nextEventWithTile; + const willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEv.getDate() || new Date()); + lastInSection = willWantDateSeparator || + mxEv.getSender() !== nextEv.getSender() || + getEventDisplayInfo(nextEv).isInfoMessage || + !shouldFormContinuation(mxEv, nextEv, this.showHiddenEvents, this.context.timelineRenderingType); } // is this a continuation of the previous message? diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 22c21ee55fd..7f0a249c9aa 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -20,24 +20,17 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { RoomState } from "matrix-js-sdk/src/models/room-state"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { throttle } from 'lodash'; import dis from '../../dispatcher/dispatcher'; import GroupStore from '../../stores/GroupStore'; -import { - RIGHT_PANEL_PHASES_NO_ARGS, - RIGHT_PANEL_SPACE_PHASES, - RightPanelPhases, -} from "../../stores/RightPanelStorePhases"; -import RightPanelStore from "../../stores/RightPanelStore"; +import { RightPanelPhases } from '../../stores/right-panel/RightPanelStorePhases'; +import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import MatrixClientContext from "../../contexts/MatrixClientContext"; -import { Action } from "../../dispatcher/actions"; import RoomSummaryCard from "../views/right_panel/RoomSummaryCard"; import WidgetCard from "../views/right_panel/WidgetCard"; import { replaceableComponent } from "../../utils/replaceableComponent"; import SettingsStore from "../../settings/SettingsStore"; -import { ActionPayload } from "../../dispatcher/payloads"; import MemberList from "../views/rooms/MemberList"; import GroupMemberList from "../views/groups/GroupMemberList"; import GroupRoomList from "../views/groups/GroupRoomList"; @@ -50,17 +43,17 @@ import ThreadPanel from "./ThreadPanel"; import NotificationPanel from "./NotificationPanel"; import ResizeNotifier from "../../utils/ResizeNotifier"; import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; -import SpaceStore from "../../stores/spaces/SpaceStore"; import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; import { UserNameColorMode } from '../../settings/enums/UserNameColorMode'; import { E2EStatus } from '../../utils/ShieldUtils'; -import { dispatchShowThreadsPanelEvent } from '../../dispatcher/dispatch-actions/threads'; import TimelineCard from '../views/right_panel/TimelineCard'; +import { UPDATE_EVENT } from '../../stores/AsyncStore'; +import { IRightPanelCard, IRightPanelCardState } from '../../stores/right-panel/RightPanelStoreIPanelState'; interface IProps { room?: Room; // if showing panels for a given room, this is set groupId?: string; // if showing panels for a given group, this is set - member?: RoomMember; // used if we know the room member ahead of opening the panel + overwriteCard?: IRightPanelCard; // used to display a custom card and ignoring the RightPanelStore (used for UserView) resizeNotifier: ResizeNotifier; permalinkCreator?: RoomPermalinkCreator; userNameColorMode?: UserNameColorMode; @@ -70,32 +63,21 @@ interface IProps { interface IState { phase: RightPanelPhases; isUserPrivilegedInGroup?: boolean; - member?: RoomMember; - verificationRequest?: VerificationRequest; - verificationRequestPromise?: Promise; - space?: Room; - widgetId?: string; - groupRoomId?: string; - groupId?: string; - event: MatrixEvent; - initialEvent?: MatrixEvent; - initialEventHighlighted?: boolean; searchQuery: string; + cardState?: IRightPanelCardState; } @replaceableComponent("structures.RightPanel") export default class RightPanel extends React.Component { static contextType = MatrixClientContext; - private dispatcherRef: string; - constructor(props, context) { super(props, context); + this.state = { - ...RightPanelStore.getSharedInstance().roomPanelPhaseParams, - phase: this.getPhaseFromProps(), + cardState: RightPanelStore.instance.currentCard?.state, + phase: RightPanelStore.instance.currentCard?.phase, isUserPrivilegedInGroup: null, - member: this.getUserForPanel(), searchQuery: "", }; } @@ -104,64 +86,18 @@ export default class RightPanel extends React.Component { this.forceUpdate(); }, 500, { leading: true, trailing: true }); - // Helper function to split out the logic for getPhaseFromProps() and the constructor - // as both are called at the same time in the constructor. - private getUserForPanel(): RoomMember { - if (this.state && this.state.member) return this.state.member; - const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams; - return this.props.member || lastParams['member']; - } - - // gets the current phase from the props and also maybe the store - private getPhaseFromProps() { - const rps = RightPanelStore.getSharedInstance(); - const userForPanel = this.getUserForPanel(); - if (this.props.groupId) { - if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) { - dis.dispatch({ action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList }); - return RightPanelPhases.GroupMemberList; - } - return rps.groupPanelPhase; - } else if (SpaceStore.spacesEnabled && this.props.room?.isSpaceRoom() - && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase) - ) { - return RightPanelPhases.SpaceMemberList; - } else if (userForPanel) { - // XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state - // from its props and some from a store, except if the contents of the store changes - // while it's mounted in which case it replaces all of its state with that of the store, - // except it uses a dispatch instead of a normal store listener? - // Unfortunately rewriting this would almost certainly break showing the right panel - // in some of the many cases, and I don't have time to re-architect it and test all - // the flows now, so adding yet another special case so if the store thinks there is - // a verification going on for the member we're displaying, we show that, otherwise - // we race if a verification is started while the panel isn't displayed because we're - // not mounted in time to get the dispatch. - // Until then, let this code serve as a warning from history. - if ( - rps.roomPanelPhaseParams.member && - userForPanel.userId === rps.roomPanelPhaseParams.member.userId && - rps.roomPanelPhaseParams.verificationRequest - ) { - return rps.roomPanelPhase; - } - return RightPanelPhases.RoomMemberInfo; - } - return rps.roomPanelPhase; - } - public componentDidMount(): void { - this.dispatcherRef = dis.register(this.onAction); const cli = this.context; cli.on("RoomState.members", this.onRoomStateMember); + RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); this.initGroupStore(this.props.groupId); } public componentWillUnmount(): void { - dis.unregister(this.dispatcherRef); if (this.context) { this.context.removeListener("RoomState.members", this.onRoomStateMember); } + RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); this.unregisterGroupStore(); } @@ -195,42 +131,28 @@ export default class RightPanel extends React.Component { // redraw the badge on the membership list if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) { this.delayedUpdate(); - } else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId && - member.userId === this.state.member.userId) { + } else if ( + this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId && + member.userId === this.state.cardState.member.userId + ) { // refresh the member info (e.g. new power level) this.delayedUpdate(); } }; - private onAction = (payload: ActionPayload) => { - const isChangingRoom = payload.action === Action.ViewRoom && payload.room_id !== this.props.room.roomId; - const isViewingThread = this.state.phase === RightPanelPhases.ThreadView; - if (isChangingRoom && isViewingThread) { - dispatchShowThreadsPanelEvent(); - } - - if (payload.action === Action.AfterRightPanelPhaseChange) { - this.setState({ - phase: payload.phase, - groupRoomId: payload.groupRoomId, - groupId: payload.groupId, - member: payload.member, - event: payload.event, - initialEvent: payload.initialEvent, - initialEventHighlighted: payload.highlighted, - verificationRequest: payload.verificationRequest, - verificationRequestPromise: payload.verificationRequestPromise, - widgetId: payload.widgetId, - space: payload.space, - }); - } + private onRightPanelStoreUpdate = () => { + const currentPanel = RightPanelStore.instance.currentCard; + this.setState({ + cardState: currentPanel.state, + phase: currentPanel.phase, + }); }; private onClose = () => { // XXX: There are three different ways of 'closing' this panel depending on what state // things are in... this knows far more than it should do about the state of the rest // of the app and is generally a bit silly. - if (this.props.member) { + if (this.props.overwriteCard?.state?.member) { // If we have a user prop then we're displaying a user from the 'user' page type // in LoggedInView, so need to change the page type to close the panel (we switch // to the home page which is not obviously the correct thing to do, but I'm not sure @@ -240,16 +162,12 @@ export default class RightPanel extends React.Component { }); } else if ( this.state.phase === RightPanelPhases.EncryptionPanel && - this.state.verificationRequest && this.state.verificationRequest.pending + this.state.cardState.verificationRequest && this.state.cardState.verificationRequest.pending ) { // When the user clicks close on the encryption panel cancel the pending request first if any - this.state.verificationRequest.cancel(); + this.state.cardState.verificationRequest.cancel(); } else { - // the RightPanelStore has no way of knowing which mode room/group it is in, so we handle closing here - dis.dispatch({ - action: Action.ToggleRightPanel, - type: this.props.groupId ? "group" : "room", - }); + RightPanelStore.instance.togglePanel(); } }; @@ -258,13 +176,14 @@ export default class RightPanel extends React.Component { }; public render(): JSX.Element { - let panel =
; + let card =
; const roomId = this.props.room ? this.props.room.roomId : undefined; - - switch (this.state.phase) { + const phase = this.props.overwriteCard?.phase ?? this.state.phase; + const cardState = this.props.overwriteCard?.state ?? this.state.cardState; + switch (phase) { case RightPanelPhases.RoomMemberList: if (roomId) { - panel = { } break; case RightPanelPhases.SpaceMemberList: - panel = { case RightPanelPhases.GroupMemberList: if (this.props.groupId) { - panel = ; + card = ; } break; case RightPanelPhases.GroupRoomList: - panel = ; + card = ; break; case RightPanelPhases.RoomMemberInfo: case RightPanelPhases.SpaceMemberInfo: - case RightPanelPhases.EncryptionPanel: - panel = ; break; - + } case RightPanelPhases.Room3pidMemberInfo: case RightPanelPhases.Space3pidMemberInfo: - panel = ; + card = ; break; case RightPanelPhases.GroupMemberInfo: - panel = ; + key={cardState.member.userId} + phase={phase} + onClose={this.onClose} + />; break; case RightPanelPhases.GroupRoomInfo: - panel = ; + key={cardState.groupRoomId} + />; break; case RightPanelPhases.NotificationPanel: - panel = ; + card = ; break; case RightPanelPhases.PinnedMessages: if (SettingsStore.getValue("feature_pinning")) { - panel = { } break; case RightPanelPhases.Timeline: - if (!SettingsStore.getValue("feature_maximised_widgets")) break; - panel = { />; break; case RightPanelPhases.FilePanel: - panel = { break; case RightPanelPhases.ThreadView: - panel = ; + e2eStatus={this.props.e2eStatus} + />; break; case RightPanelPhases.ThreadPanel: - panel = { break; case RightPanelPhases.RoomSummary: - panel = ; + card = ; break; case RightPanelPhases.Widget: - panel = ; + card = ; break; } return ( ); } diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index a60ee5f8057..9b6c068cda4 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -393,7 +393,7 @@ export default class RoomDirectory extends React.Component { private onFilterChange = (alias: string) => { this.setState({ - filterString: alias || "", + filterString: alias?.trim() || "", }); // don't send the request for a little bit, diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 0ac762409c4..0244d193247 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -93,11 +93,11 @@ export default class RoomSearch extends React.PureComponent { private onAction = (payload: ActionPayload) => { if (payload.action === Action.ViewRoom && payload.clear_search) { this.clearInput(); - } else if (payload.action === 'focus_room_filter' && this.inputRef.current) { + } else if (payload.action === 'focus_room_filter') { if (SettingsStore.getValue("feature_spotlight")) { this.openSpotlight(); } else { - this.inputRef.current.focus(); + this.inputRef.current?.focus(); } } }; @@ -109,8 +109,12 @@ export default class RoomSearch extends React.PureComponent { }; private openSearch = () => { - defaultDispatcher.dispatch({ action: "show_left_panel" }); - defaultDispatcher.dispatch({ action: "focus_room_filter" }); + if (SettingsStore.getValue("feature_spotlight")) { + this.openSpotlight(); + } else { + defaultDispatcher.dispatch({ action: "show_left_panel" }); + defaultDispatcher.dispatch({ action: "focus_room_filter" }); + } }; private onChange = () => { @@ -118,19 +122,17 @@ export default class RoomSearch extends React.PureComponent { this.setState({ query: this.inputRef.current.value }); }; - private onMouseDown = (ev: React.MouseEvent) => { + private onFocus = (ev: React.FocusEvent) => { if (SettingsStore.getValue("feature_spotlight")) { ev.preventDefault(); ev.stopPropagation(); this.openSpotlight(); + } else { + this.setState({ focused: true }); + ev.target.select(); } }; - private onFocus = (ev: React.FocusEvent) => { - this.setState({ focused: true }); - ev.target.select(); - }; - private onBlur = (ev: React.FocusEvent) => { this.setState({ focused: false }); }; @@ -156,7 +158,11 @@ export default class RoomSearch extends React.PureComponent { }; public focus = (): void => { - this.inputRef.current?.focus(); + if (SettingsStore.getValue("feature_spotlight")) { + this.openSpotlight(); + } else { + this.inputRef.current?.focus(); + } }; public render(): React.ReactNode { @@ -181,7 +187,6 @@ export default class RoomSearch extends React.PureComponent { ref={this.inputRef} className={inputClasses} value={this.state.query} - onMouseDown={this.onMouseDown} onFocus={this.onFocus} onBlur={this.onBlur} onChange={this.onChange} diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index 76835d7c29d..aad5bb5cc21 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -16,8 +16,7 @@ limitations under the License. import React from 'react'; import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { SyncState } from "matrix-js-sdk/src/sync.api"; -import { ISyncStateData } from "matrix-js-sdk/src/sync"; +import { SyncState, ISyncStateData } from "matrix-js-sdk/src/sync"; import { Room } from "matrix-js-sdk/src/models/room"; import { _t, _td } from '../../languageHandler'; @@ -85,6 +84,7 @@ interface IState { @replaceableComponent("structures.RoomStatusBar") export default class RoomStatusBar extends React.PureComponent { + private unmounted = false; public static contextType = MatrixClientContext; constructor(props: IProps, context: typeof MatrixClientContext) { @@ -111,6 +111,7 @@ export default class RoomStatusBar extends React.PureComponent { } public componentWillUnmount(): void { + this.unmounted = true; // we may have entirely lost our client as we're logging out before clicking login on the guest bar... const client = this.context; if (client) { @@ -123,6 +124,7 @@ export default class RoomStatusBar extends React.PureComponent { if (state === "SYNCING" && prevState === "SYNCING") { return; } + if (this.unmounted) return; this.setState({ syncState: state, syncStateData: data, diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 9b05104ca2d..f1bc1eec75a 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -33,6 +33,7 @@ import { EventType } from 'matrix-js-sdk/src/@types/event'; import { RoomState } from 'matrix-js-sdk/src/models/room-state'; import { CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { throttle } from "lodash"; +import { MatrixError } from 'matrix-js-sdk/src/http-api'; import shouldHideEvent from '../../shouldHideEvent'; import { _t } from '../../languageHandler'; @@ -52,7 +53,7 @@ import WidgetEchoStore from '../../stores/WidgetEchoStore'; import SettingsStore from "../../settings/SettingsStore"; import { Layout } from "../../settings/enums/Layout"; import AccessibleButton from "../views/elements/AccessibleButton"; -import RightPanelStore from "../../stores/RightPanelStore"; +import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import { haveTileForEvent } from "../views/rooms/EventTile"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import MatrixClientContext, { withMatrixClientHOC, MatrixClientProps } from "../../contexts/MatrixClientContext"; @@ -96,12 +97,12 @@ import SpaceStore from "../../stores/spaces/SpaceStore"; import { UserNameColorMode } from '../../settings/enums/UserNameColorMode'; import DMRoomMap from '../../utils/DMRoomMap'; -import { dispatchShowThreadEvent } from '../../dispatcher/dispatch-actions/threads'; +import { showThread } from '../../dispatcher/dispatch-actions/threads'; import { fetchInitialEvent } from "../../utils/EventUtils"; import { ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload"; import AppsDrawer from '../views/rooms/AppsDrawer'; -import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload'; -import { RightPanelPhases } from '../../stores/RightPanelStorePhases'; +import { RightPanelPhases } from '../../stores/right-panel/RightPanelStorePhases'; +import { ActionPayload } from "../../dispatcher/payloads"; const DEBUG = false; let debuglog = function(msg: string) {}; @@ -167,7 +168,7 @@ export interface IRoomState { // error object, as from the matrix client/server API // If we failed to load information about the room, // store the error here. - roomLoadError?: Error; + roomLoadError?: MatrixError; // Have we sent a request to join the room that we're waiting to complete? joining: boolean; // this is true if we are fully scrolled-down, and are looking at @@ -219,7 +220,6 @@ export interface IRoomState { export class RoomView extends React.Component { private readonly dispatcherRef: string; private readonly roomStoreToken: EventSubscription; - private readonly rightPanelStoreToken: EventSubscription; private settingWatchers: string[]; private unmounted = false; @@ -251,7 +251,7 @@ export class RoomView extends React.Component { canPeek: false, showApps: false, isPeeking: false, - showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, + showRightPanel: RightPanelStore.instance.isOpenForRoom, joining: false, atEndOfLiveTimeline: true, atEndOfLiveTimelineInit: false, @@ -297,7 +297,8 @@ export class RoomView extends React.Component { this.context.on("Event.decrypted", this.onEventDecrypted); // Start listening for RoomViewStore updates this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); - this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate); + + RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate); WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate); @@ -363,13 +364,16 @@ export class RoomView extends React.Component { }); if (WidgetLayoutStore.instance.hasMaximisedWidget(this.state.room)) { // Show chat in right panel when a widget is maximised - dis.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.Timeline, - }); + RightPanelStore.instance.setCard({ phase: RightPanelPhases.Timeline }); + } else if ( + RightPanelStore.instance.isOpenForRoom && + RightPanelStore.instance.roomPhaseHistory.some(card => (card.phase === RightPanelPhases.Timeline)) + ) { + // hide chat in right panel when the widget is minimized + RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary }); + RightPanelStore.instance.togglePanel(); } this.checkWidgets(this.state.room); - this.checkRightPanel(this.state.room); }; private checkWidgets = (room) => { @@ -387,22 +391,6 @@ export class RoomView extends React.Component { : MainSplitContentType.Timeline; }; - private checkRightPanel = (room) => { - // This is a hack to hide the chat. This should not be necessary once the right panel - // phase is stored per room. (need to be done after check widget so that mainSplitContentType is updated) - if ( - RightPanelStore.getSharedInstance().roomPanelPhase === RightPanelPhases.Timeline && - this.state.showRightPanel && - !WidgetLayoutStore.instance.hasMaximisedWidget(this.state.room) - ) { - // Two timelines are shown prevent this by hiding the right panel - dis.dispatch({ - action: Action.ToggleRightPanel, - type: "room", - }); - } - }; - private onReadReceiptsChange = () => { this.setState({ showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId), @@ -446,6 +434,7 @@ export class RoomView extends React.Component { showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId), showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId), wasContextSwitch: RoomViewStore.getWasContextSwitch(), + initialEventId: null, // default to clearing this, will get set later in the method if needed }; const initialEventId = RoomViewStore.getInitialEventId(); @@ -470,21 +459,21 @@ export class RoomView extends React.Component { const thread = initialEvent?.getThread(); if (thread && !initialEvent?.isThreadRoot) { - dispatchShowThreadEvent( - thread.rootEvent, + showThread({ + rootEvent: thread.rootEvent, initialEvent, - RoomViewStore.isInitialEventHighlighted(), - ); + highlighted: RoomViewStore.isInitialEventHighlighted(), + }); } else { newState.initialEventId = initialEventId; newState.isInitialEventHighlighted = RoomViewStore.isInitialEventHighlighted(); if (thread && initialEvent?.isThreadRoot) { - dispatchShowThreadEvent( - thread.rootEvent, + showThread({ + rootEvent: thread.rootEvent, initialEvent, - RoomViewStore.isInitialEventHighlighted(), - ); + highlighted: RoomViewStore.isInitialEventHighlighted(), + }); } } } @@ -684,6 +673,7 @@ export class RoomView extends React.Component { callState: callState, }); + CallHandler.instance.on(CallHandlerEvent.CallState, this.onCallState); window.addEventListener('beforeunload', this.onPageUnload); if (this.props.resizeNotifier) { @@ -708,7 +698,6 @@ export class RoomView extends React.Component { } componentDidUpdate() { - CallHandler.instance.addListener(CallHandlerEvent.CallState, this.onCallState); if (this.roomView.current) { const roomView = this.roomView.current; if (!roomView.ondrop) { @@ -793,11 +782,8 @@ export class RoomView extends React.Component { if (this.roomStoreToken) { this.roomStoreToken.remove(); } - // Remove RightPanelStore listener - if (this.rightPanelStoreToken) { - this.rightPanelStoreToken.remove(); - } + RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); WidgetEchoStore.removeListener(UPDATE_EVENT, this.onWidgetEchoStoreUpdate); WidgetStore.instance.removeListener(UPDATE_EVENT, this.onWidgetStoreUpdate); @@ -808,6 +794,8 @@ export class RoomView extends React.Component { ); } + CallHandler.instance.off(CallHandlerEvent.CallState, this.onCallState); + // cancel any pending calls to the throttled updated this.updateRoomMembers.cancel(); @@ -830,7 +818,7 @@ export class RoomView extends React.Component { private onRightPanelStoreUpdate = () => { this.setState({ - showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, + showRightPanel: RightPanelStore.instance.isOpenForRoom, }); }; @@ -879,7 +867,7 @@ export class RoomView extends React.Component { this.setState({ callState: call ? call.state : null }); }; - private onAction = payload => { + private onAction = async (payload: ActionPayload): Promise => { switch (payload.action) { case 'message_sent': this.checkDesktopNotifications(); @@ -958,6 +946,12 @@ export class RoomView extends React.Component { case Action.ComposerInsert: { if (payload.composerType) break; + + if (this.state.searching && payload.timelineRenderingType === TimelineRenderingType.Room) { + // we don't have the composer rendered in this state, so bring it back first + await this.onCancelSearchClick(); + } + // re-dispatch to the correct composer dis.dispatch({ ...payload, @@ -1109,7 +1103,6 @@ export class RoomView extends React.Component { this.updateE2EStatus(room); this.updatePermissions(room); this.checkWidgets(room); - this.checkRightPanel(room); this.setState({ liveTimeline: room.getLiveTimeline(), @@ -1715,10 +1708,12 @@ export class RoomView extends React.Component { }); }; - private onCancelSearchClick = () => { - this.setState({ - searching: false, - searchResults: null, + private onCancelSearchClick = (): Promise => { + return new Promise(resolve => { + this.setState({ + searching: false, + searchResults: null, + }, resolve); }); }; @@ -2295,7 +2290,6 @@ export class RoomView extends React.Component { // keep the timeline in as the mainSplitBody break; case MainSplitContentType.MaximisedWidget: - if (!SettingsStore.getValue("feature_maximised_widgets")) break; mainSplitBody = { > { this.props.fixedChildren }
-
    +
      { this.props.children }
diff --git a/src/components/structures/SearchBox.tsx b/src/components/structures/SearchBox.tsx index ea36f41033d..86058a957af 100644 --- a/src/components/structures/SearchBox.tsx +++ b/src/components/structures/SearchBox.tsx @@ -20,10 +20,8 @@ import { throttle } from 'lodash'; import classNames from 'classnames'; import { Key } from '../../Keyboard'; -import dis from '../../dispatcher/dispatcher'; import AccessibleButton from '../../components/views/elements/AccessibleButton'; import { replaceableComponent } from "../../utils/replaceableComponent"; -import { Action } from '../../dispatcher/actions'; interface IProps extends HTMLProps { onSearch?: (query: string) => void; @@ -37,11 +35,6 @@ interface IProps extends HTMLProps { autoFocus?: boolean; initialValue?: string; collapsed?: boolean; - - // If true, the search box will focus and clear itself - // on room search focus action (it would be nicer to take - // this functionality out, but not obvious how that would work) - enableRoomSearchFocus?: boolean; } interface IState { @@ -51,13 +44,8 @@ interface IState { @replaceableComponent("structures.SearchBox") export default class SearchBox extends React.Component { - private dispatcherRef: string; private search = createRef(); - static defaultProps: Partial = { - enableRoomSearchFocus: false, - }; - constructor(props: IProps) { super(props); @@ -67,31 +55,6 @@ export default class SearchBox extends React.Component { }; } - public componentDidMount(): void { - this.dispatcherRef = dis.register(this.onAction); - } - - public componentWillUnmount(): void { - dis.unregister(this.dispatcherRef); - } - - private onAction = (payload): void => { - if (!this.props.enableRoomSearchFocus) return; - - switch (payload.action) { - case Action.ViewRoom: - if (this.search.current && payload.clear_search) { - this.clearSearch(); - } - break; - case 'focus_room_filter': - if (this.search.current) { - this.search.current.focus(); - } - break; - } - }; - private onChange = (): void => { if (!this.search.current) return; this.setState({ searchTerm: this.search.current.value }); @@ -137,7 +100,7 @@ export default class SearchBox extends React.Component { public render(): JSX.Element { /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ const { onSearch, onCleared, onKeyDown, onFocus, onBlur, className = "", placeholder, blurredPlaceholder, - autoFocus, initialValue, collapsed, enableRoomSearchFocus, ...props } = this.props; + autoFocus, initialValue, collapsed, ...props } = this.props; // check for collapsed here and // not at parent so we keep diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index f684c28ff0a..9e7f1df272c 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -477,17 +477,19 @@ export const useRoomHierarchy = (space: Room): { loading: boolean; rooms: IHierarchyRoom[]; hierarchy: RoomHierarchy; - loadMore(pageSize?: number): Promise ; + error: Error; + loadMore(pageSize?: number): Promise; } => { const [rooms, setRooms] = useState([]); const [hierarchy, setHierarchy] = useState(); + const [error, setError] = useState(); const resetHierarchy = useCallback(() => { const hierarchy = new RoomHierarchy(space, INITIAL_PAGE_SIZE); hierarchy.load().then(() => { if (space !== hierarchy.root) return; // discard stale results setRooms(hierarchy.rooms); - }); + }, setError); setHierarchy(hierarchy); }, [space]); useEffect(resetHierarchy, [resetHierarchy]); @@ -501,12 +503,12 @@ export const useRoomHierarchy = (space: Room): { const loadMore = useCallback(async (pageSize?: number) => { if (hierarchy.loading || !hierarchy.canLoadMore || hierarchy.noSupport) return; - await hierarchy.load(pageSize); + await hierarchy.load(pageSize).catch(setError); setRooms(hierarchy.rooms); }, [hierarchy]); const loading = hierarchy?.loading ?? true; - return { loading, rooms, hierarchy, loadMore }; + return { loading, rooms, hierarchy, loadMore, error }; }; const useIntersectionObserver = (callback: () => void) => { @@ -649,7 +651,7 @@ const SpaceHierarchy = ({ const [selected, setSelected] = useState(new Map>()); // Map> - const { loading, rooms, hierarchy, loadMore } = useRoomHierarchy(space); + const { loading, rooms, hierarchy, loadMore, error: hierarchyError } = useRoomHierarchy(space); const filteredRoomSet = useMemo>(() => { if (!rooms?.length) return new Set(); @@ -677,6 +679,10 @@ const SpaceHierarchy = ({ }, [rooms, hierarchy, query]); const [error, setError] = useState(""); + let errorText = error; + if (!error && hierarchyError) { + errorText = _t("Failed to load list of rooms."); + } const loaderRef = useIntersectionObserver(loadMore); @@ -759,8 +765,8 @@ const SpaceHierarchy = ({ ) }
- { error &&
- { error } + { errorText &&
+ { errorText }
}
    { }; const SpaceInfo = ({ space }: { space: Room }) => { - // summary will begin as undefined whilst loading and go null if it fails to load. + // summary will begin as undefined whilst loading and go null if it fails to load or we are not invited. const summary = useAsyncMemo(async () => { - if (space.getMyMembership() !== "invite") return; + if (space.getMyMembership() !== "invite") return null; try { return space.client.getRoomSummary(space.roomId); } catch (e) { @@ -141,7 +140,7 @@ const SpaceInfo = ({ space }: { space: Room }) => { const membership = useMyRoomMembership(space); let visibilitySection; - if (joinRule === "public") { + if (joinRule === JoinRule.Public) { visibilitySection = { _t("Public space") } ; @@ -164,10 +163,9 @@ const SpaceInfo = ({ space }: { space: Room }) => { kind="link" className="mx_SpaceRoomView_info_memberCount" onClick={() => { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, + RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomMemberList, - refireParams: { space }, + state: { spaceId: space.roomId }, }); }} > @@ -473,11 +471,7 @@ const SpaceLanding = ({ space }: { space: Room }) => { } const onMembersClick = () => { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.RoomMemberList, - refireParams: { space }, - }); + RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomMemberList, state: { spaceId: space.roomId } }); }; return
    @@ -796,7 +790,6 @@ export default class SpaceRoomView extends React.PureComponent { private readonly creator: string; private readonly dispatcherRef: string; - private readonly rightPanelStoreToken: EventSubscription; constructor(props, context) { super(props, context); @@ -813,18 +806,18 @@ export default class SpaceRoomView extends React.PureComponent { this.state = { phase, - showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, + showRightPanel: RightPanelStore.instance.isOpenForRoom, myMembership: this.props.space.getMyMembership(), }; this.dispatcherRef = defaultDispatcher.register(this.onAction); - this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate); + RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); this.context.on("Room.myMembership", this.onMyMembership); } componentWillUnmount() { defaultDispatcher.unregister(this.dispatcherRef); - this.rightPanelStoreToken.remove(); + RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); this.context.off("Room.myMembership", this.onMyMembership); } @@ -836,7 +829,7 @@ export default class SpaceRoomView extends React.PureComponent { private onRightPanelStoreUpdate = () => { this.setState({ - showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, + showRightPanel: RightPanelStore.instance.isOpenForRoom, }); }; @@ -849,28 +842,19 @@ export default class SpaceRoomView extends React.PureComponent { if (payload.action !== Action.ViewUser && payload.action !== "view_3pid_invite") return; if (payload.action === Action.ViewUser && payload.member) { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, + RightPanelStore.instance.setCard({ phase: RightPanelPhases.SpaceMemberInfo, - refireParams: { - space: this.props.space, - member: payload.member, - }, + state: { spaceId: this.props.space.roomId, member: payload.member }, }); } else if (payload.action === "view_3pid_invite" && payload.event) { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, + RightPanelStore.instance.setCard({ phase: RightPanelPhases.Space3pidMemberInfo, - refireParams: { - space: this.props.space, - event: payload.event, - }, + state: { spaceId: this.props.space.roomId, member: payload.member }, }); } else { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, + RightPanelStore.instance.setCard({ phase: RightPanelPhases.SpaceMemberList, - refireParams: { space: this.props.space }, + state: { spaceId: this.props.space.roomId }, }); } }; diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index cdc6c63abb7..34ca6affdd1 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -14,10 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread'; +import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set'; import { Room } from 'matrix-js-sdk/src/models/room'; +import { RelationType } from 'matrix-js-sdk/src/@types/event'; +import { MatrixClient } from 'matrix-js-sdk/src/client'; +import { + Filter, + IFilterDefinition, + UNSTABLE_FILTER_RELATION_SENDERS, + UNSTABLE_FILTER_RELATION_TYPES, +} from 'matrix-js-sdk/src/filter'; import BaseCard from "../views/right_panel/BaseCard"; import ResizeNotifier from '../../utils/ResizeNotifier'; @@ -28,10 +35,65 @@ import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from './Conte import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext'; import TimelinePanel from './TimelinePanel'; import { Layout } from '../../settings/enums/Layout'; -import { useEventEmitter } from '../../hooks/useEventEmitter'; import { TileShape } from '../views/rooms/EventTile'; import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; +async function getThreadTimelineSet( + client: MatrixClient, + room: Room, + filterType = ThreadFilterType.All, +): Promise { + const capabilities = await client.getCapabilities(); + const serverSupportsThreads = capabilities['io.element.thread']?.enabled; + + if (serverSupportsThreads) { + const myUserId = client.getUserId(); + const filter = new Filter(myUserId); + + const definition: IFilterDefinition = { + "room": { + "timeline": { + [UNSTABLE_FILTER_RELATION_TYPES.name]: [RelationType.Thread], + }, + }, + }; + + if (filterType === ThreadFilterType.My) { + definition.room.timeline[UNSTABLE_FILTER_RELATION_SENDERS.name] = [myUserId]; + } + + filter.setDefinition(definition); + const filterId = await client.getOrCreateFilter( + `THREAD_PANEL_${room.roomId}_${filterType}`, + filter, + ); + filter.filterId = filterId; + const timelineSet = room.getOrCreateFilteredTimelineSet( + filter, + { prepopulateTimeline: false }, + ); + + timelineSet.resetLiveTimeline(); + await client.paginateEventTimeline( + timelineSet.getLiveTimeline(), + { backwards: true, limit: 20 }, + ); + return timelineSet; + } else { + // Filter creation fails if HomeServer does not support the new relation + // filter fields. We fallback to the threads that have been discovered in + // the main timeline + const timelineSet = new EventTimelineSet(room, {}); + for (const [, thread] of room.threads) { + const isOwnEvent = thread.rootEvent.getSender() === client.getUserId(); + if (filterType !== ThreadFilterType.My || isOwnEvent) { + timelineSet.getLiveTimeline().addEvent(thread.rootEvent, false); + } + } + return timelineSet; + } +} + interface IProps { roomId: string; onClose: () => void; @@ -50,44 +112,6 @@ type ThreadPanelHeaderOption = { key: ThreadFilterType; }; -const useFilteredThreadsTimelinePanel = ({ - threads, - room, - filterOption, - userId, - updateTimeline, -}: { - threads: Map; - room: Room; - userId: string; - filterOption: ThreadFilterType; - updateTimeline: () => void; -}) => { - const timelineSet = useMemo(() => new EventTimelineSet(null, { - timelineSupport: true, - unstableClientRelationAggregation: true, - pendingEvents: false, - }), []); - - const buildThreadList = useCallback(function(timelineSet: EventTimelineSet) { - timelineSet.resetLiveTimeline(""); - Array.from(threads) - .forEach(([, thread]) => { - if (filterOption !== ThreadFilterType.My || thread.hasCurrentUserParticipated) { - timelineSet.addLiveEvent(thread.rootEvent); - } - }); - updateTimeline(); - }, [filterOption, threads, updateTimeline]); - - useEffect(() => { buildThreadList(timelineSet); }, [timelineSet, buildThreadList]); - - useEventEmitter(room, ThreadEvent.Update, () => { buildThreadList(timelineSet); }); - useEventEmitter(room, ThreadEvent.New, () => { buildThreadList(timelineSet); }); - - return timelineSet; -}; - export const ThreadPanelHeaderFilterOptionItem = ({ label, description, @@ -185,19 +209,24 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => const [filterOption, setFilterOption] = useState(ThreadFilterType.All); const ref = useRef(); - const filteredTimelineSet = useFilteredThreadsTimelinePanel({ - threads: room.threads, - room, - filterOption, - userId: mxClient.getUserId(), - updateTimeline: () => ref.current?.refreshTimeline(), - }); + const [timelineSet, setTimelineSet] = useState(null); + const timelineSetPromise = useMemo( + async () => { + const timelineSet = getThreadTimelineSet(mxClient, room, filterOption); + return timelineSet; + }, + [mxClient, room, filterOption], + ); + useEffect(() => { + timelineSetPromise + .then(timelineSet => { setTimelineSet(timelineSet); }) + .catch(() => setTimelineSet(null)); + }, [timelineSetPromise]); return ( = ({ roomId, onClose, permalinkCreator }) => onClose={onClose} withoutScrollContainer={true} > - setFilterOption(ThreadFilterType.All)} - />} - alwaysShowTimestamps={true} - layout={Layout.Group} - hideThreadedMessages={false} - hidden={false} - showReactions={true} - className="mx_RoomView_messagePanel mx_GroupLayout" - membersLoaded={true} - permalinkCreator={permalinkCreator} - tileShape={TileShape.ThreadPanel} - disableGrouping={true} - /> + { timelineSet && ( + setFilterOption(ThreadFilterType.All)} + />} + alwaysShowTimestamps={true} + layout={Layout.Group} + hideThreadedMessages={false} + hidden={false} + showReactions={false} + className="mx_RoomView_messagePanel mx_GroupLayout" + membersLoaded={true} + permalinkCreator={permalinkCreator} + tileShape={TileShape.ThreadPanel} + disableGrouping={true} + /> + ) } ); diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index f7179525ef6..42cd3851e82 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -20,7 +20,7 @@ import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread'; import { RelationType } from 'matrix-js-sdk/src/@types/event'; import BaseCard from "../views/right_panel/BaseCard"; -import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases"; import { replaceableComponent } from "../../utils/replaceableComponent"; import ResizeNotifier from '../../utils/ResizeNotifier'; import { TileShape } from '../views/rooms/EventTile'; @@ -30,7 +30,6 @@ import { Layout } from '../../settings/enums/Layout'; import TimelinePanel from './TimelinePanel'; import dis from "../../dispatcher/dispatcher"; import { ActionPayload } from '../../dispatcher/payloads'; -import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload'; import { Action } from '../../dispatcher/actions'; import { MatrixClientPeg } from '../../MatrixClientPeg'; import { E2EStatus } from '../../utils/ShieldUtils'; @@ -40,9 +39,7 @@ import ContentMessages from '../../ContentMessages'; import UploadBar from './UploadBar'; import { _t } from '../../languageHandler'; import ThreadListContextMenu from '../views/context_menus/ThreadListContextMenu'; -import RightPanelStore from '../../stores/RightPanelStore'; -import SettingsStore from '../../settings/SettingsStore'; -import { WidgetLayoutStore } from '../../stores/widgets/WidgetLayoutStore'; +import RightPanelStore from '../../stores/right-panel/RightPanelStore'; interface IProps { room: Room; @@ -52,7 +49,7 @@ interface IProps { permalinkCreator?: RoomPermalinkCreator; e2eStatus?: E2EStatus; initialEvent?: MatrixEvent; - initialEventHighlighted?: boolean; + isInitialEventHighlighted?: boolean; } interface IState { thread?: Thread; @@ -94,10 +91,7 @@ export default class ThreadView extends React.Component { } if (prevProps.room !== this.props.room) { - dis.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.RoomSummary, - }); + RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary }); } } @@ -135,16 +129,7 @@ export default class ThreadView extends React.Component { private setupThread = (mxEv: MatrixEvent) => { let thread = this.props.room.threads.get(mxEv.getId()); if (!thread) { - const client = MatrixClientPeg.get(); - // Do not attach this thread object to the event for now - // TODO: When local echo gets reintroduced it will be important - // to add that back in, and the threads model should go through the - // same reconciliation algorithm as events - thread = new Thread( - [mxEv], - this.props.room, - client, - ); + thread = this.props.room.createThread([mxEv]); } thread.on(ThreadEvent.Update, this.updateThread); thread.once(ThreadEvent.Ready, this.updateThread); @@ -177,7 +162,7 @@ export default class ThreadView extends React.Component { }; private onScroll = (): void => { - if (this.props.initialEvent && this.props.initialEventHighlighted) { + if (this.props.initialEvent && this.props.isInitialEventHighlighted) { dis.dispatch({ action: Action.ViewRoom, room_id: this.props.room.roomId, @@ -198,7 +183,7 @@ export default class ThreadView extends React.Component { }; public render(): JSX.Element { - const highlightedEventId = this.props.initialEventHighlighted + const highlightedEventId = this.props.isInitialEventHighlighted ? this.props.initialEvent?.getId() : null; @@ -207,24 +192,6 @@ export default class ThreadView extends React.Component { event_id: this.state.thread?.id, }; - let previousPhase = RightPanelStore.getSharedInstance().previousPhase; - if (!SettingsStore.getValue("feature_maximised_widgets")) { - previousPhase = RightPanelPhases.ThreadPanel; - } - - // change the previous phase to the threadPanel in case there is no maximised widget anymore - if (!WidgetLayoutStore.instance.hasMaximisedWidget(this.props.room)) { - previousPhase = RightPanelPhases.ThreadPanel; - } - - // Make sure the previous Phase is always one of the two: Timeline or ThreadPanel - if (![RightPanelPhases.ThreadPanel, RightPanelPhases.Timeline].includes(previousPhase)) { - previousPhase = RightPanelPhases.ThreadPanel; - } - const previousPhaseLabels = {}; - previousPhaseLabels[RightPanelPhases.ThreadPanel] = _t("All threads"); - previousPhaseLabels[RightPanelPhases.Timeline] = _t("Chat"); - return ( { diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index f00c65ca5e9..c9396161cc7 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -22,7 +22,7 @@ import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set"; import { Direction, EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; import { TimelineWindow } from "matrix-js-sdk/src/timeline-window"; import { EventType, RelationType } from 'matrix-js-sdk/src/@types/event'; -import { SyncState } from 'matrix-js-sdk/src/sync.api'; +import { SyncState } from 'matrix-js-sdk/src/sync'; import { debounce } from 'lodash'; import { logger } from "matrix-js-sdk/src/logger"; @@ -599,7 +599,7 @@ class TimelinePanel extends React.Component { } this.setState(updatedState, () => { - this.messagePanel.current.updateTimelineMinHeight(); + this.messagePanel.current?.updateTimelineMinHeight(); if (callRMUpdated) { this.props.onReadMarkerUpdated?.(); } @@ -689,6 +689,7 @@ class TimelinePanel extends React.Component { }; private onSync = (clientSyncState: SyncState, prevState: SyncState, data: object): void => { + if (this.unmounted) return; this.setState({ clientSyncState }); }; @@ -1246,9 +1247,11 @@ class TimelinePanel extends React.Component { // should use this list, so that they don't advance into pending events. const liveEvents = [...events]; + const thread = events[0]?.getThread(); + // if we're at the end of the live timeline, append the pending events if (!this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) { - events.push(...this.props.timelineSet.getPendingEvents()); + events.push(...this.props.timelineSet.getPendingEvents(thread)); } return { diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index e62d877a846..738abce1835 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, useContext, useState } from "react"; +import React, { createRef, useContext, useRef, useState } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import * as fbEmitter from "fbemitter"; import classNames from "classnames"; @@ -32,13 +32,17 @@ import FeedbackDialog from "../views/dialogs/FeedbackDialog"; import Modal from "../../Modal"; import LogoutDialog from "../views/dialogs/LogoutDialog"; import SettingsStore from "../../settings/SettingsStore"; +import { + RovingAccessibleButton, + RovingAccessibleTooltipButton, + useRovingTabIndex, +} from "../../accessibility/RovingTabIndex"; import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; import SdkConfig from "../../SdkConfig"; import { getHomePageUrl } from "../../utils/pages"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import BaseAvatar from '../views/avatars/BaseAvatar'; -import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { SettingLevel } from "../../settings/SettingLevel"; import IconizedContextMenu, { IconizedContextMenuCheckbox, @@ -61,30 +65,43 @@ const CustomStatusSection = () => { const setStatus = cli.getUser(cli.getUserId()).unstable_statusMessage || ""; const [value, setValue] = useState(setStatus); + const ref = useRef(null); + const [onFocus, isActive] = useRovingTabIndex(ref); + + const classes = classNames({ + 'mx_UserMenu_CustomStatusSection_field': true, + 'mx_UserMenu_CustomStatusSection_field_hasQuery': value, + }); + let details: JSX.Element; if (value !== setStatus) { details = <>

    { _t("Your status will be shown to people you have a DM with.") }

    - cli._unstable_setStatusMessage(value)} kind="primary_outline" > { value ? _t("Set status") : _t("Clear status") } - + ; } - return
    -
    + return
    +
    setValue(e.target.value)} placeholder={_t("Set a new status")} autoComplete="off" + onFocus={onFocus} + ref={ref} + tabIndex={isActive ? 0 : -1} /> {
    { details } -
    ; + ; }; interface IProps { @@ -448,7 +465,7 @@ export default class UserMenu extends React.Component {
    { this.state.themeInUse !== Theme.System ? - { alt={_t("Switch theme")} width={16} /> - : null + : null }
    { customStatusSection } diff --git a/src/components/structures/UserView.tsx b/src/components/structures/UserView.tsx index 04e97bc0572..657b9ab6ff5 100644 --- a/src/components/structures/UserView.tsx +++ b/src/components/structures/UserView.tsx @@ -29,6 +29,7 @@ import MainSplit from "./MainSplit"; import RightPanel from "./RightPanel"; import Spinner from "../views/elements/Spinner"; import ResizeNotifier from "../../utils/ResizeNotifier"; +import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases"; interface IProps { userId?: string; @@ -88,7 +89,10 @@ export default class UserView extends React.Component { if (this.state.loading) { return ; } else if (this.state.member) { - const panel = ; + const panel = ; return ( ); diff --git a/src/components/structures/auth/CompleteSecurity.tsx b/src/components/structures/auth/CompleteSecurity.tsx index 91f378dc60b..7043878a729 100644 --- a/src/components/structures/auth/CompleteSecurity.tsx +++ b/src/components/structures/auth/CompleteSecurity.tsx @@ -69,20 +69,20 @@ export default class CompleteSecurity extends React.Component { } else if (phase === Phase.Intro) { if (lostKeys) { icon = ; - title = _t("Unable to verify this login"); + title = _t("Unable to verify this device"); } else { icon = ; - title = _t("Verify this login"); + title = _t("Verify this device"); } } else if (phase === Phase.Done) { icon = ; - title = _t("Session verified"); + title = _t("Device verified"); } else if (phase === Phase.ConfirmSkip) { icon = ; title = _t("Are you sure?"); } else if (phase === Phase.Busy) { icon = ; - title = _t("Verify this login"); + title = _t("Verify this device"); } else if (phase === Phase.ConfirmReset) { icon = ; title = _t("Really reset verification keys?"); diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index dee6f2dec68..79ae5e201f5 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -38,6 +38,7 @@ import ErrorDialog from "../../views/dialogs/ErrorDialog"; import AuthHeader from "../../views/auth/AuthHeader"; import AuthBody from "../../views/auth/AuthBody"; import PassphraseConfirmField from "../../views/auth/PassphraseConfirmField"; +import AccessibleButton from '../../views/elements/AccessibleButton'; enum Phase { // Show the forgot password inputs @@ -338,9 +339,9 @@ export default class ForgotPassword extends React.Component { value={_t('Send Reset Email')} /> - + { _t('Sign in instead') } - +
; } diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 4b849e1d03d..3fc47d66936 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -38,6 +38,7 @@ import ServerPicker from "../../views/elements/ServerPicker"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import AuthBody from "../../views/auth/AuthBody"; import AuthHeader from "../../views/auth/AuthHeader"; +import AccessibleButton from '../../views/elements/AccessibleButton'; // These are used in several places, and come from the js-sdk's autodiscovery // stuff. We define them here so that they'll be picked up by i18n. @@ -454,7 +455,7 @@ export default class LoginComponent extends React.PureComponent let errorText: ReactNode = _t("There was a problem communicating with the homeserver, " + "please try again later.") + (errCode ? " (" + errCode + ")" : ""); - if (err.cors === 'rejected') { + if (err["cors"] === 'rejected') { // browser-request specific error field if (window.location.protocol === 'https:' && (this.props.serverConfig.hsUrl.startsWith("http:") || !this.props.serverConfig.hsUrl.startsWith("http")) @@ -588,7 +589,10 @@ export default class LoginComponent extends React.PureComponent footer = ( { _t("New? Create account", {}, { - a: sub => { sub }, + a: sub => + + { sub } + , }) } ); diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 44f40a37d98..828ca8d79d7 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -363,7 +363,7 @@ export default class Registration extends React.Component { return Promise.resolve(); } const matrixClient = MatrixClientPeg.get(); - return matrixClient.getPushers().then((resp)=>{ + return matrixClient.getPushers().then((resp) => { const pushers = resp.pushers; for (let i = 0; i < pushers.length; ++i) { if (pushers[i].kind === 'email') { @@ -538,16 +538,20 @@ export default class Registration extends React.Component { const signIn = { _t("Already have an account? Sign in here", {}, { - a: sub => { sub }, + a: sub => { sub }, }) } ; // Only show the 'go back' button if you're not looking at the form let goBack; if (this.state.doingUIAuth) { - goBack = + goBack = { _t('Go back') } - ; + ; } let body; diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 8836d785e64..b18c56768cf 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -121,7 +121,7 @@ export default class SetupEncryptionBody extends React.Component store.returnAfterSkip(); }; - private onResetClick = (ev: React.MouseEvent) => { + private onResetClick = (ev: React.MouseEvent) => { ev.preventDefault(); const store = SetupEncryptionStore.sharedInstance(); store.reset(); @@ -198,7 +198,7 @@ export default class SetupEncryptionBody extends React.Component let verifyButton; if (store.hasDevicesToVerifyAgainst) { verifyButton = - { _t("Verify with another login") } + { _t("Verify with another device") } ; } @@ -214,10 +214,9 @@ export default class SetupEncryptionBody extends React.Component
{ _t("Forgotten or lost all recovery methods? Reset all", null, { - a: (sub) => , }) }
@@ -227,12 +226,12 @@ export default class SetupEncryptionBody extends React.Component let message; if (this.state.backupInfo) { message =

{ _t( - "Your new session is now verified. It has access to your " + + "Your new device is now verified. It has access to your " + "encrypted messages, and other users will see it as trusted.", ) }

; } else { message =

{ _t( - "Your new session is now verified. Other users will see it as trusted.", + "Your new device is now verified. Other users will see it as trusted.", ) }

; } return ( diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index c0396ff186c..63845f0e97c 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -755,7 +755,7 @@ export class SSOAuthEntry extends React.Component { private popupWindow: Window; - private fallbackButton = createRef(); + private fallbackButton = createRef(); constructor(props) { super(props); @@ -814,9 +814,9 @@ export class FallbackAuthEntry extends React.Component { } return (
- { + { _t("Start authentication") - } + } { errorSection }
); diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx index b9bba921ddf..516aae0eac1 100644 --- a/src/components/views/auth/PasswordLogin.tsx +++ b/src/components/views/auth/PasswordLogin.tsx @@ -325,7 +325,7 @@ export default class PasswordLogin extends React.PureComponent { onChange={this.onUsernameChanged} onFocus={this.onUsernameFocus} onBlur={this.onUsernameBlur} - disabled={this.props.disableSubmit} + disabled={this.props.busy} autoFocus={autoFocus} onValidate={this.onEmailValidate} fieldRef={field => this[LoginField.Email] = field} @@ -344,7 +344,7 @@ export default class PasswordLogin extends React.PureComponent { onChange={this.onUsernameChanged} onFocus={this.onUsernameFocus} onBlur={this.onUsernameBlur} - disabled={this.props.disableSubmit} + disabled={this.props.busy} autoFocus={autoFocus} onValidate={this.onUsernameValidate} ref={field => this[LoginField.MatrixId] = field} @@ -371,7 +371,7 @@ export default class PasswordLogin extends React.PureComponent { onChange={this.onPhoneNumberChanged} onFocus={this.onPhoneNumberFocus} onBlur={this.onPhoneNumberBlur} - disabled={this.props.disableSubmit} + disabled={this.props.busy} autoFocus={autoFocus} onValidate={this.onPhoneNumberValidate} ref={field => this[LoginField.Password] = field} @@ -422,7 +422,7 @@ export default class PasswordLogin extends React.PureComponent { element="select" value={this.state.loginType} onChange={this.onLoginTypeChange} - disabled={this.props.disableSubmit} + disabled={this.props.busy} >
; } else { @@ -721,7 +728,9 @@ export default class AddressPickerDialog extends React.Component "Use an identity server to invite by email. " + "Manage in Settings.", {}, { - settings: sub => { sub }, + settings: sub => + { sub } + , }, ) }; } diff --git a/src/components/views/dialogs/BaseDialog.tsx b/src/components/views/dialogs/BaseDialog.tsx index 15c9114ca2f..52773c13b93 100644 --- a/src/components/views/dialogs/BaseDialog.tsx +++ b/src/components/views/dialogs/BaseDialog.tsx @@ -27,6 +27,7 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { _t } from "../../../languageHandler"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Heading from '../typography/Heading'; import { IDialogProps } from "./IDialogProps"; interface IProps extends IDialogProps { @@ -141,10 +142,10 @@ export default class BaseDialog extends React.Component { 'mx_Dialog_headerWithButton': !!this.props.headerButton, 'mx_Dialog_headerWithCancel': !!cancelButton, })}> -
+ { headerImage } { this.props.title } -
+ { this.props.headerButton } { cancelButton } diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index 120b9c7ea52..8b698acd7c6 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useState, useEffect, ChangeEvent, MouseEvent } from 'react'; +import React, { useState, useEffect, ChangeEvent } from 'react'; import { PHASE_UNSENT, PHASE_REQUESTED, @@ -44,6 +44,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { SettingLevel } from '../../../settings/SettingLevel'; import BaseDialog from "./BaseDialog"; import TruncatedList from "../elements/TruncatedList"; +import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton'; interface IGenericEditorProps { onBack: () => void; @@ -965,12 +966,12 @@ class SettingsExplorer extends React.PureComponent { + private onViewClick = (ev: ButtonEvent, settingId: string) => { ev.preventDefault(); this.setState({ viewSetting: settingId }); }; - private onEditClick = (ev: MouseEvent, settingId: string) => { + private onEditClick = (ev: ButtonEvent, settingId: string) => { ev.preventDefault(); this.setState({ editSetting: settingId, @@ -1078,16 +1079,16 @@ class SettingsExplorer extends React.PureComponent ( - this.onViewClick(e, i)}> + this.onViewClick(e, i)}> { i } - - + this.onEditClick(e, i)} className='mx_DevTools_SettingsExplorer_edit' > ✏ - + { this.renderSettingValue(SettingsStore.getValue(i)) } diff --git a/src/components/views/dialogs/EndPollDialog.tsx b/src/components/views/dialogs/EndPollDialog.tsx index cf501936393..f3403839b9c 100644 --- a/src/components/views/dialogs/EndPollDialog.tsx +++ b/src/components/views/dialogs/EndPollDialog.tsx @@ -18,11 +18,12 @@ import React from "react"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { Relations } from "matrix-js-sdk/src/models/relations"; +import { IPollEndContent, POLL_END_EVENT_TYPE } from "matrix-js-sdk/src/@types/polls"; +import { TEXT_NODE_TYPE } from "matrix-js-sdk/src/@types/extensible_events"; import { _t } from "../../../languageHandler"; import { IDialogProps } from "./IDialogProps"; import QuestionDialog from "./QuestionDialog"; -import { IPollEndContent, POLL_END_EVENT_TYPE, TEXT_NODE_TYPE } from "../../../polls/consts"; import { findTopAnswer } from "../messages/MPollBody"; import Modal from "../../../Modal"; import ErrorDialog from "./ErrorDialog"; diff --git a/src/components/views/dialogs/ExportDialog.tsx b/src/components/views/dialogs/ExportDialog.tsx index e3d2b437cf2..4bbd68071ac 100644 --- a/src/components/views/dialogs/ExportDialog.tsx +++ b/src/components/views/dialogs/ExportDialog.tsx @@ -53,7 +53,7 @@ const ExportDialog: React.FC = ({ room, onFinished }) => { const [sizeLimit, setSizeLimit] = useState(8); const sizeLimitRef = useRef(); const messageCountRef = useRef(); - const [exportProgressText, setExportProgressText] = useState("Processing..."); + const [exportProgressText, setExportProgressText] = useState(_t("Processing...")); const [displayCancel, setCancelWarning] = useState(false); const [exportCancelled, setExportCancelled] = useState(false); const [exportSuccessful, setExportSuccessful] = useState(false); diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 13e48db702e..f352393c5be 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -1255,8 +1255,14 @@ export default class InviteDialog extends React.PureComponent { sub }, - settings: sub => { sub }, + default: sub => + + { sub } + , + settings: sub => + + { sub } + , }, ) } ); @@ -1266,7 +1272,10 @@ export default class InviteDialog extends React.PureComponentSettings.", {}, { - settings: sub => { sub }, + settings: sub => + + { sub } + , }, ) } ); diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx index 9d208944e12..67636cdf2db 100644 --- a/src/components/views/dialogs/ModalWidgetDialog.tsx +++ b/src/components/views/dialogs/ModalWidgetDialog.tsx @@ -80,7 +80,7 @@ export default class ModalWidgetDialog extends React.PureComponent { - return { sub }; + return + { sub } + ; }, }, ) } diff --git a/src/components/views/dialogs/SpacePreferencesDialog.tsx b/src/components/views/dialogs/SpacePreferencesDialog.tsx new file mode 100644 index 00000000000..dc047245ac4 --- /dev/null +++ b/src/components/views/dialogs/SpacePreferencesDialog.tsx @@ -0,0 +1,99 @@ +/* +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. +*/ + +import React, { ChangeEvent } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; + +import { _t, _td } from '../../../languageHandler'; +import BaseDialog from "../dialogs/BaseDialog"; +import { IDialogProps } from "./IDialogProps"; +import TabbedView, { Tab } from "../../structures/TabbedView"; +import StyledCheckbox from "../elements/StyledCheckbox"; +import { useSettingValue } from "../../../hooks/useSettings"; +import SettingsStore from "../../../settings/SettingsStore"; +import { SettingLevel } from "../../../settings/SettingLevel"; +import RoomName from "../elements/RoomName"; + +export enum SpacePreferenceTab { + Appearance = "SPACE_PREFERENCE_APPEARANCE_TAB", +} + +interface IProps extends IDialogProps { + space: Room; + initialTabId?: SpacePreferenceTab; +} + +const SpacePreferencesAppearanceTab = ({ space }: Pick) => { + const showPeople = useSettingValue("Spaces.showPeopleInSpace", space.roomId); + + return ( +
+
{ _t("Sections to show") }
+ +
+ ) => { + SettingsStore.setValue( + "Spaces.showPeopleInSpace", + space.roomId, + SettingLevel.ROOM_ACCOUNT, + !showPeople, + ); + }} + > + { _t("People") } + +

+ { _t("This groups your chats with members of this space. " + + "Turning this off will hide those chats from your view of %(spaceName)s.", { + spaceName: space.name, + }) } +

+
+
+ ); +}; + +const SpacePreferencesDialog: React.FC = ({ space, initialTabId, onFinished }) => { + const tabs = [ + new Tab( + SpacePreferenceTab.Appearance, + _td("Appearance"), + "mx_RoomSettingsDialog_notificationsIcon", + , + ), + ]; + + return ( + +

+ +

+
+ +
+
+ ); +}; + +export default SpacePreferencesDialog; diff --git a/src/components/views/dialogs/StorageEvictedDialog.tsx b/src/components/views/dialogs/StorageEvictedDialog.tsx index 8a41224d574..acbb62b43d4 100644 --- a/src/components/views/dialogs/StorageEvictedDialog.tsx +++ b/src/components/views/dialogs/StorageEvictedDialog.tsx @@ -24,6 +24,7 @@ import BaseDialog from "./BaseDialog"; import DialogButtons from "../elements/DialogButtons"; import BugReportDialog from "./BugReportDialog"; import { IDialogProps } from "./IDialogProps"; +import AccessibleButton from '../elements/AccessibleButton'; interface IProps extends IDialogProps { } @@ -45,7 +46,9 @@ export default class StorageEvictedDialog extends React.Component { "To help us prevent this in future, please send us logs.", {}, { - a: text => { text }, + a: text => + { text } + , }, ); } diff --git a/src/components/views/dialogs/UntrustedDeviceDialog.tsx b/src/components/views/dialogs/UntrustedDeviceDialog.tsx index 8c503e340d1..8039a67511e 100644 --- a/src/components/views/dialogs/UntrustedDeviceDialog.tsx +++ b/src/components/views/dialogs/UntrustedDeviceDialog.tsx @@ -57,10 +57,10 @@ const UntrustedDeviceDialog: React.FC = ({ device, user, onFinished }) =

{ askToVerifyText }

- onFinished("legacy")}> + onFinished("legacy")}> { _t("Manually Verify by Text") } - onFinished("sas")}> + onFinished("sas")}> { _t("Interactively verify by Emoji") } onFinished(false)}> diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index 4cf08ca24e1..e50c12e6fbe 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -52,7 +52,7 @@ export enum UserTab { } interface IProps extends IDialogProps { - initialTabId?: string; + initialTabId?: UserTab; } interface IState { diff --git a/src/components/views/dialogs/VerificationRequestDialog.tsx b/src/components/views/dialogs/VerificationRequestDialog.tsx index 5387a675c10..f73cda91f44 100644 --- a/src/components/views/dialogs/VerificationRequestDialog.tsx +++ b/src/components/views/dialogs/VerificationRequestDialog.tsx @@ -55,7 +55,7 @@ export default class VerificationRequestDialog extends React.Component { _t("Forgotten or lost all recovery methods? Reset all", null, { - a: (sub) => { sub }, + className="mx_AccessSecretStorageDialog_reset_link">{ sub }, }) }
); @@ -393,6 +393,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent | React.KeyboardEvent | React.FormEvent; +type AccessibleButtonKind = | 'primary' + | 'primary_outline' + | 'primary_sm' + | 'secondary' + | 'danger' + | 'danger_outline' + | 'danger_sm' + | 'link' + | 'link_inline' + | 'link_sm' + | 'confirm_sm' + | 'cancel_sm'; + /** * children: React's magic prop. Represents all children given to the element. * element: (optional) The base element type. "div" by default. @@ -32,7 +45,7 @@ interface IProps extends React.InputHTMLAttributes { element?: keyof ReactHTML; // The kind of button, similar to how Bootstrap works. // See available classes for AccessibleButton for options. - kind?: string; + kind?: AccessibleButtonKind | string; // The ARIA role role?: string; // The tabIndex diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index a8e2308976e..56543bbf187 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -77,6 +77,7 @@ interface IProps { // sets the pointer-events property on the iframe pointerEvents?: string; widgetPageTitle?: string; + hideMaximiseButton?: boolean; } interface IState { @@ -508,7 +509,7 @@ export default class AppTile extends React.Component { // Also wrap the PersistedElement in a div to fix the height, otherwise // AppTile's border is in the wrong place appTileBody =
- + { appTileBody }
; @@ -541,7 +542,7 @@ export default class AppTile extends React.Component { ); } let maxMinButton; - if (SettingsStore.getValue("feature_maximised_widgets")) { + if (!this.props.hideMaximiseButton) { const widgetIsMaximised = WidgetLayoutStore.instance. isInContainer(this.props.room, this.props.app, Container.Center); maxMinButton = { _t("Message search initialisation failed, check your settings for more information", {}, { - a: sub => ( { - evt.preventDefault(); - dis.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Security, - }); - }}> - { sub } - ), + a: sub => ( + { + evt.preventDefault(); + dis.dispatch({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Security, + }); + }} + > + { sub } + ), }) } ; } diff --git a/src/components/views/elements/DialPadBackspaceButton.tsx b/src/components/views/elements/DialPadBackspaceButton.tsx index ce9dbffe7e5..57c6ee474e6 100644 --- a/src/components/views/elements/DialPadBackspaceButton.tsx +++ b/src/components/views/elements/DialPadBackspaceButton.tsx @@ -16,6 +16,7 @@ limitations under the License. import * as React from "react"; +import { _t } from "../../../languageHandler"; import AccessibleButton, { ButtonEvent } from "./AccessibleButton"; interface IProps { @@ -26,7 +27,11 @@ interface IProps { export default class DialPadBackspaceButton extends React.PureComponent { render() { return
- +
; } } diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index 4a21898bed8..5689b7666f8 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -313,7 +313,7 @@ export default class Dropdown extends React.Component { ); }); if (options.length === 0) { - return [
+ return [
{ _t("No results") }
]; } @@ -331,6 +331,7 @@ export default class Dropdown extends React.Component { if (this.props.searchEnabled) { currentValue = ( { role="combobox" aria-autocomplete="list" aria-activedescendant={`${this.props.id}__${this.state.highlightedOption}`} - aria-owns={`${this.props.id}_listbox`} + aria-expanded={this.state.expanded} + aria-controls={`${this.props.id}_listbox`} aria-disabled={this.props.disabled} aria-label={this.props.label} onKeyDown={this.onKeyDown} @@ -382,6 +384,7 @@ export default class Dropdown extends React.Component { inputRef={this.buttonRef} aria-label={this.props.label} aria-describedby={`${this.props.id}_value`} + aria-owns={`${this.props.id}_input`} onKeyDown={this.onKeyDown} > { currentValue } diff --git a/src/components/views/elements/FacePile.tsx b/src/components/views/elements/FacePile.tsx index 0c19a7a63af..3710e032c96 100644 --- a/src/components/views/elements/FacePile.tsx +++ b/src/components/views/elements/FacePile.tsx @@ -38,7 +38,9 @@ const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsFor const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }: IProps) => { const cli = useContext(MatrixClientContext); + const isJoined = room.getMyMembership() === "join"; let members = useRoomMembers(room); + const count = members.length; // sort users with an explicit avatar first const iteratees = [member => !!member.getMxcAvatarUrl()]; @@ -59,26 +61,40 @@ const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, . let tooltip: ReactNode; if (props.onClick) { + let subText: string; + if (isJoined) { + subText = _t("Including you, %(commaSeparatedMembers)s", { commaSeparatedMembers }); + } else { + subText = _t("Including %(commaSeparatedMembers)s", { commaSeparatedMembers }); + } + tooltip =
- { _t("View all %(count)s members", { count: members.length }) } + { _t("View all %(count)s members", { count }) }
- { _t("Including %(commaSeparatedMembers)s", { commaSeparatedMembers }) } + { subText }
; } else { - tooltip = _t("%(count)s members including %(commaSeparatedMembers)s", { - count: members.length, - commaSeparatedMembers, - }); + if (isJoined) { + tooltip = _t("%(count)s members including you, %(commaSeparatedMembers)s", { + count: count - 1, + commaSeparatedMembers, + }); + } else { + tooltip = _t("%(count)s members including %(commaSeparatedMembers)s", { + count, + commaSeparatedMembers, + }); + } } return
{ members.length > numShown ? : null } { shownMembers.map(m => - ) } + ) } { onlyKnownUsers && { _t("%(count)s people you know have already joined", { count: members.length }) } diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 85f397834f2..1dedf77a8b9 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -230,7 +230,7 @@ export default class Field extends React.PureComponent { /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ const { element, prefixComponent, postfixComponent, className, onValidate, children, tooltipContent, forceValidity, tooltipClassName, list, validateOnBlur, validateOnChange, validateOnFocus, - usePlaceholderAsHint, + usePlaceholderAsHint, forceTooltipVisible, ...inputProps } = this.props; // Set some defaults for the element @@ -276,7 +276,7 @@ export default class Field extends React.PureComponent { if (tooltipContent || this.state.feedback) { fieldTooltip = ; diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index bfe957c62ad..5110716c5e2 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -26,19 +26,14 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import { isValid3pidInvite } from "../../../RoomInvite"; import EventListSummary from "./EventListSummary"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import defaultDispatcher from '../../../dispatcher/dispatcher'; -import { RightPanelPhases } from '../../../stores/RightPanelStorePhases'; -import { Action } from '../../../dispatcher/actions'; -import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload'; +import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases'; import { jsxJoin } from '../../../utils/ReactUtils'; import { Layout } from '../../../settings/enums/Layout'; +import RightPanelStore from '../../../stores/right-panel/RightPanelStore'; +import AccessibleButton from './AccessibleButton'; const onPinnedMessagesClick = (): void => { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.PinnedMessages, - allowClose: false, - }); + RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false); }; const SENDER_AS_DISPLAY_NAME_EVENTS = [EventType.RoomServerAcl, EventType.RoomPinnedEvents]; @@ -90,14 +85,15 @@ export default class MemberEventListSummary extends React.Component { layout: Layout.Group, }; - shouldComponentUpdate(nextProps) { + shouldComponentUpdate(nextProps: IProps): boolean { // Update if // - The number of summarised events has changed // - or if the summary is about to toggle to become collapsed // - or if there are fewEvents, meaning the child eventTiles are shown as-is return ( nextProps.events.length !== this.props.events.length || - nextProps.events.length < this.props.threshold + nextProps.events.length < this.props.threshold || + nextProps.layout !== this.props.layout ); } @@ -327,10 +323,18 @@ export default class MemberEventListSummary extends React.Component { res = (userCount > 1) ? _t("%(severalUsers)schanged the pinned messages for the room %(count)s times.", { severalUsers: "", count: repeats }, - { "a": (sub) => { sub } }) + { + "a": (sub) => + { sub } + , + }) : _t("%(oneUser)schanged the pinned messages for the room %(count)s times.", { oneUser: "", count: repeats }, - { "a": (sub) => { sub } }); + { + "a": (sub) => + { sub } + , + }); break; } diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index 22ff4bf4b32..837433e0137 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -24,14 +24,15 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { useTimeout } from "../../../hooks/useTimeout"; import Analytics from "../../../Analytics"; import CountlyAnalytics from '../../../CountlyAnalytics'; +import { TranslatedString } from '../../../languageHandler'; import RoomContext from "../../../contexts/RoomContext"; export const AVATAR_SIZE = 52; interface IProps { hasAvatar: boolean; - noAvatarLabel?: string; - hasAvatarLabel?: string; + noAvatarLabel?: TranslatedString; + hasAvatarLabel?: TranslatedString; setAvatarUrl(url: string): Promise; } diff --git a/src/components/views/elements/PersistentApp.tsx b/src/components/views/elements/PersistentApp.tsx index 80e57b537b5..aba42236bb5 100644 --- a/src/components/views/elements/PersistentApp.tsx +++ b/src/components/views/elements/PersistentApp.tsx @@ -25,6 +25,10 @@ import WidgetUtils from '../../../utils/WidgetUtils'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import AppTile from "./AppTile"; +import { Container, WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore'; +import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases'; +import RightPanelStore from '../../../stores/right-panel/RightPanelStore'; +import { UPDATE_EVENT } from '../../../stores/AsyncStore'; interface IProps { // none @@ -33,6 +37,7 @@ interface IProps { interface IState { roomId: string; persistentWidgetId: string; + rightPanelPhase?: RightPanelPhases; } @replaceableComponent("views.elements.PersistentApp") @@ -45,12 +50,14 @@ export default class PersistentApp extends React.Component { this.state = { roomId: RoomViewStore.getRoomId(), persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(), + rightPanelPhase: RightPanelStore.instance.currentCard.phase, }; } public componentDidMount(): void { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate); + RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership); } @@ -59,6 +66,7 @@ export default class PersistentApp extends React.Component { this.roomStoreToken.remove(); } ActiveWidgetStore.instance.removeListener(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate); + RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership); } @@ -71,6 +79,12 @@ export default class PersistentApp extends React.Component { }); }; + private onRightPanelStoreUpdate = () => { + this.setState({ + rightPanelPhase: RightPanelStore.instance.currentCard.phase, + }); + }; + private onActiveWidgetStoreUpdate = (): void => { this.setState({ persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(), @@ -88,8 +102,9 @@ export default class PersistentApp extends React.Component { }; public render(): JSX.Element { - if (this.state.persistentWidgetId) { - const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId); + const wId = this.state.persistentWidgetId; + if (wId) { + const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(wId); const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId); @@ -97,8 +112,24 @@ export default class PersistentApp extends React.Component { // thus no room is associated anymore. if (!persistentWidgetInRoom) return null; - const myMembership = persistentWidgetInRoom.getMyMembership(); - if (this.state.roomId !== persistentWidgetInRoomId && myMembership === "join") { + const wls = WidgetLayoutStore.instance; + + const userIsPartOfTheRoom = persistentWidgetInRoom.getMyMembership() == "join"; + const fromAnotherRoom = this.state.roomId !== persistentWidgetInRoomId; + + const notInRightPanel = + !(this.state.rightPanelPhase == RightPanelPhases.Widget && + wId == RightPanelStore.instance.currentCard.state?.widgetId); + const notInCenterContainer = + !wls.getContainerWidgets(persistentWidgetInRoom, Container.Center).some((app) => app.id == wId); + const notInTopContainer = + !wls.getContainerWidgets(persistentWidgetInRoom, Container.Top).some(app => app.id == wId); + if ( + // the widget should only be shown as a persistent app (in a floating pip container) if it is not visible on screen + // either, because we are viewing a different room OR because it is in none of the possible containers of the room view. + (fromAnotherRoom && userIsPartOfTheRoom) || + (notInRightPanel && notInCenterContainer && notInTopContainer && userIsPartOfTheRoom) + ) { // get the widget data const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => { return ev.getStateKey() === ActiveWidgetStore.instance.getPersistentWidgetId(); diff --git a/src/components/views/elements/PollCreateDialog.tsx b/src/components/views/elements/PollCreateDialog.tsx index 4e1e76d23f9..73e552f7725 100644 --- a/src/components/views/elements/PollCreateDialog.tsx +++ b/src/components/views/elements/PollCreateDialog.tsx @@ -16,6 +16,8 @@ limitations under the License. import React, { ChangeEvent, createRef } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; +import { makePollContent } from "matrix-js-sdk/src/content-helpers"; +import { POLL_KIND_DISCLOSED, POLL_START_EVENT_TYPE } from "matrix-js-sdk/src/@types/polls"; import ScrollableBaseModal, { IScrollableBaseState } from "../dialogs/ScrollableBaseModal"; import { IDialogProps } from "../dialogs/IDialogProps"; @@ -25,7 +27,6 @@ import { _t } from "../../../languageHandler"; import { arrayFastClone, arraySeed } from "../../../utils/arrays"; import Field from "./Field"; import AccessibleButton from "./AccessibleButton"; -import { makePollContent, POLL_KIND_DISCLOSED, POLL_START_EVENT_TYPE } from "../../../polls/consts"; import Spinner from "./Spinner"; interface IProps extends IDialogProps { diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx index 5b207ebb16f..08625791828 100644 --- a/src/components/views/elements/ReplyChain.tsx +++ b/src/components/views/elements/ReplyChain.tsx @@ -38,6 +38,7 @@ import Spinner from './Spinner'; import ReplyTile from "../rooms/ReplyTile"; import Pill from './Pill'; import { UserNameColorMode } from '../../../settings/enums/UserNameColorMode'; +import { ButtonEvent } from './AccessibleButton'; /** * This number is based on the previous behavior - if we have message of height @@ -347,7 +348,7 @@ export default class ReplyChain extends React.Component { this.initialize(); }; - private onQuoteClick = async (event: React.MouseEvent): Promise => { + private onQuoteClick = async (event: ButtonEvent): Promise => { const events = [this.state.loadedEv, ...this.state.events]; let loadedEv = null; @@ -385,7 +386,11 @@ export default class ReplyChain extends React.Component { header =
{ _t('In reply to ', {}, { - 'a': (sub) => { sub }, + 'a': (sub) => ( + + ), 'pill': ( { + static contextType = MatrixClientContext; + public context!: React.ContextType; + private fieldRef = createRef(); constructor(props, context) { @@ -50,25 +53,38 @@ export default class RoomAliasField extends React.PureComponent } private asFullAlias(localpart: string): string { - return `#${localpart}:${this.props.domain}`; + const hashAlias = `#${ localpart }`; + if (this.props.domain) { + return `${hashAlias}:${this.props.domain}`; + } + return hashAlias; + } + + private get domainProps() { + const { domain } = this.props; + const prefix = #; + const postfix = domain ? ({ `:${domain}` }) : ; + const maxlength = domain ? 255 - domain.length - 2 : 255 - 1; // 2 for # and : + const value = domain ? + this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1) : + this.props.value.substring(1); + + return { prefix, postfix, value, maxlength }; } render() { - const poundSign = (#); - const aliasPostfix = ":" + this.props.domain; - const domain = ({ aliasPostfix }); - const maxlength = 255 - this.props.domain.length - 2; // 2 for # and : + const { prefix, postfix, value, maxlength } = this.domainProps; return ( private validationRules = withValidation({ rules: [ + { key: "hasDomain", + test: async ({ value }) => { + // Ignore if we have passed domain + if (!value || this.props.domain) { + return true; + } + + if (value.split(':').length < 2) { + return false; + } + return true; + }, + invalid: () => _t("Missing domain separator e.g. (:domain.org)"), + }, + { + key: "hasLocalpart", + test: async ({ value }) => { + if (!value || this.props.domain) { + return true; + } + + const split = value.split(':'); + if (split.length < 2) { + return true; // hasDomain check will fail here instead + } + + // Define the value invalid if there's no first part (roomname) + if (split[0].length < 1) { + return false; + } + return true; + }, + invalid: () => _t("Missing room name or separator e.g. (my-room:domain.org)"), + }, { key: "safeLocalpart", test: async ({ value }) => { if (!value) { return true; } - const fullAlias = this.asFullAlias(value); - // XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668 - return !value.includes("#") && !value.includes(":") && !value.includes(",") && - encodeURI(fullAlias) === fullAlias; + if (!this.props.domain) { + return true; + } else { + const fullAlias = this.asFullAlias(value); + const hasColon = this.props.domain ? !value.includes(":") : true; + // XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668 + // NOTE: We could probably use linkifyjs to parse those aliases here? + return !value.includes("#") && + hasColon && + !value.includes(",") && + encodeURI(fullAlias) === fullAlias; + } }, invalid: () => _t("Some characters not allowed"), }, { @@ -114,12 +172,13 @@ export default class RoomAliasField extends React.PureComponent if (!value) { return true; } - const client = MatrixClientPeg.get(); + const client = this.context; try { await client.getRoomIdForAlias(this.asFullAlias(value)); // we got a room id, so the alias is taken return false; } catch (err) { + console.log(err); // any server error code will do, // either it M_NOT_FOUND or the alias is invalid somehow, // in which case we don't want to show the invalid message @@ -127,7 +186,9 @@ export default class RoomAliasField extends React.PureComponent } }, valid: () => _t("This address is available to use"), - invalid: () => _t("This address is already in use"), + invalid: () => this.props.domain ? + _t("This address is already in use") : + _t("This address had invalid server or is already in use"), }, ], }); diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx index c2d27f8ab72..a0af045c269 100644 --- a/src/components/views/elements/SettingsFlag.tsx +++ b/src/components/views/elements/SettingsFlag.tsx @@ -96,9 +96,9 @@ export default class SettingsFlag extends React.Component {
{ + inputRef?: React.RefObject; kind?: CheckboxStyle; } @@ -48,7 +49,8 @@ export default class StyledCheckbox extends React.PureComponent public render() { /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ - const { children, className, kind = CheckboxStyle.Solid, ...otherProps } = this.props; + const { children, className, kind = CheckboxStyle.Solid, inputRef, ...otherProps } = this.props; + const newClassName = classnames( "mx_Checkbox", className, @@ -58,7 +60,13 @@ export default class StyledCheckbox extends React.PureComponent }, ); return - +