diff --git a/.eslintignore b/.eslintignore index c4f7298047..e453170087 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,4 @@ src/component-index.js test/end-to-end-tests/node_modules/ -test/end-to-end-tests/riot/ +test/end-to-end-tests/element/ test/end-to-end-tests/synapse/ diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles deleted file mode 100644 index 9973cfb120..0000000000 --- a/.eslintignore.errorfiles +++ /dev/null @@ -1,62 +0,0 @@ -# autogenerated file: run scripts/generate-eslint-error-ignore-file to update. - -src/components/structures/RoomDirectory.js -src/components/structures/RoomStatusBar.js -src/components/structures/RoomView.js -src/components/structures/ScrollPanel.js -src/components/structures/SearchBox.js -src/components/structures/UploadBar.js -src/components/views/avatars/BaseAvatar.js -src/components/views/avatars/MemberAvatar.js -src/components/views/create_room/RoomAlias.js -src/components/views/dialogs/SetPasswordDialog.js -src/components/views/dialogs/UnknownDeviceDialog.js -src/components/views/elements/AddressSelector.js -src/components/views/elements/DirectorySearchBox.js -src/components/views/elements/MemberEventListSummary.js -src/components/views/elements/UserSelector.js -src/components/views/globals/MatrixToolbar.js -src/components/views/globals/NewVersionBar.js -src/components/views/globals/UpdateCheckBar.js -src/components/views/messages/MFileBody.js -src/components/views/messages/TextualBody.js -src/components/views/room_settings/ColorSettings.js -src/components/views/rooms/Autocomplete.js -src/components/views/rooms/AuxPanel.js -src/components/views/rooms/LinkPreviewWidget.js -src/components/views/rooms/MemberDeviceInfo.js -src/components/views/rooms/MemberInfo.js -src/components/views/rooms/MemberList.js -src/components/views/rooms/RoomList.js -src/components/views/rooms/RoomPreviewBar.js -src/components/views/rooms/SearchResultTile.js -src/components/views/settings/ChangeAvatar.js -src/components/views/settings/ChangePassword.js -src/components/views/settings/DevicesPanel.js -src/components/views/settings/Notifications.js -src/HtmlUtils.js -src/ImageUtils.js -src/Markdown.js -src/notifications/ContentRules.js -src/notifications/PushRuleVectorState.js -src/PlatformPeg.js -src/rageshake/rageshake.js -src/ratelimitedfunc.js -src/Rooms.js -src/Unread.js -src/utils/DecryptFile.js -src/utils/DirectoryUtils.js -src/utils/DMRoomMap.js -src/utils/FormattingUtils.js -src/utils/MultiInviter.js -src/utils/Receipt.js -src/Velociraptor.js -test/components/structures/MessagePanel-test.js -test/components/views/dialogs/InteractiveAuthDialog-test.js -test/mock-clock.js -test/notifications/ContentRules-test.js -test/notifications/PushRuleVectorState-test.js -src/component-index.js -test/end-to-end-tests/node_modules/ -test/end-to-end-tests/riot/ -test/end-to-end-tests/synapse/ diff --git a/.eslintrc.js b/.eslintrc.js index 6a0576c58a..827b373949 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,121 +1,80 @@ -const path = require('path'); - -// get the path of the js-sdk so we can extend the config -// eslint supports loading extended configs by module, -// but only if they come from a module that starts with eslint-config- -// So we load the filename directly (and it could be in node_modules/ -// or or ../node_modules/ etc) -// -// We add a `..` to the end because the js-sdk lives out of lib/, but the eslint -// config is at the project root. -const matrixJsSdkPath = path.join(path.dirname(require.resolve('matrix-js-sdk')), '..'); - module.exports = { - parser: "babel-eslint", - extends: [matrixJsSdkPath + "/.eslintrc.js"], - plugins: [ - "react", - "react-hooks", - "flowtype", - "babel" + plugins: ["matrix-org"], + extends: [ + "plugin:matrix-org/babel", + "plugin:matrix-org/react", ], + env: { + browser: true, + node: true, + }, globals: { LANGUAGES_FILE: "readonly", }, - env: { - es6: true, - }, - parserOptions: { - ecmaFeatures: { - jsx: true, - legacyDecorators: true, - } - }, rules: { - // eslint's built in no-invalid-this rule breaks with class properties - "no-invalid-this": "off", - // so we replace it with a version that is class property aware - "babel/no-invalid-this": "error", + // Things we do that break the ideal style + "no-constant-condition": "off", + "prefer-promise-reject-errors": "off", + "no-async-promise-executor": "off", + "quotes": "off", + "no-extra-boolean-cast": "off", - // We appear to follow this most of the time, so let's enforce it instead - // of occasionally following it (or catching it in review) - "keyword-spacing": "error", - - /** react **/ - // This just uses the react plugin to help eslint known when - // variables have been used in JSX - "react/jsx-uses-vars": "error", - // Don't mark React as unused if we're using JSX - "react/jsx-uses-react": "error", - - // bind or arrow function in props causes performance issues - // (but we currently use them in some places) + // Bind or arrow functions in props causes performance issues (but we + // currently use them in some places). // It's disabled here, but we should using it sparingly. "react/jsx-no-bind": "off", "react/jsx-key": ["error"], - // Components in JSX should always be defined. - "react/jsx-no-undef": "error", - - // Assert no spacing in JSX curly brackets - // - // - // https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/docs/rules/jsx-curly-spacing.md - // - // Disabled for now - if anything we'd like to *enforce* spacing in JSX - // curly brackets for legibility, but in practice it's not clear that the - // consistency particularly improves legibility here. --Matthew - // - // "react/jsx-curly-spacing": ["error", {"when": "never", "children": {"when": "always"}}], - - // Assert spacing before self-closing JSX tags, and no spacing before or - // after the closing slash, and no spacing after the opening bracket of - // the opening tag or closing tag. - // - // https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/docs/rules/jsx-tag-spacing.md - "react/jsx-tag-spacing": ["error"], - - /** flowtype **/ - "flowtype/require-parameter-type": ["warn", { - "excludeArrowFunctions": true, - }], - "flowtype/define-flow-type": "warn", - "flowtype/require-return-type": ["warn", - "always", - { - "annotateUndefined": "never", - "excludeArrowFunctions": true, - } + "no-restricted-properties": [ + "error", + ...buildRestrictedPropertiesOptions( + ["window.innerHeight", "window.innerWidth", "window.visualViewport"], + "Use UIStore to access window dimensions instead.", + ), + ...buildRestrictedPropertiesOptions( + ["*.mxcUrlToHttp", "*.getHttpUriForMxc"], + "Use Media helper instead to centralise access for customisation.", + ), ], - "flowtype/space-after-type-colon": ["warn", "always"], - "flowtype/space-before-type-colon": ["warn", "never"], - - /* - * things that are errors in the js-sdk config that the current - * code does not adhere to, turned down to warn - */ - "max-len": ["warn", { - // apparently people believe the length limit shouldn't apply - // to JSX. - ignorePattern: '^\\s*<', - ignoreComments: true, - ignoreRegExpLiterals: true, - code: 120, - }], - "valid-jsdoc": ["warn"], - "new-cap": ["warn"], - "key-spacing": ["warn"], - "prefer-const": ["warn"], - - // crashes currently: https://github.com/eslint/eslint/issues/6274 - "generator-star-spacing": "off", - - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn", }, - settings: { - flowtype: { - onlyFilesWithFlowAnnotation: true + overrides: [{ + files: [ + "src/**/*.{ts,tsx}", + "test/**/*.{ts,tsx}", + ], + extends: [ + "plugin:matrix-org/typescript", + "plugin:matrix-org/react", + ], + rules: { + // Things we do that break the ideal style + "prefer-promise-reject-errors": "off", + "quotes": "off", + "no-extra-boolean-cast": "off", + + // Remove Babel things manually due to override limitations + "@babel/no-invalid-this": ["off"], + + // We're okay being explicit at the moment + "@typescript-eslint/no-empty-interface": "off", + // We disable this while we're transitioning + "@typescript-eslint/no-explicit-any": "off", + // We'd rather not do this but we do + "@typescript-eslint/ban-ts-comment": "off", }, - }, + }], }; + +function buildRestrictedPropertiesOptions(properties, message) { + return properties.map(prop => { + let [object, property] = prop.split("."); + if (object === "*") { + object = undefined; + } + return { + object, + property, + message, + }; + }); +} diff --git a/.flowconfig b/.flowconfig deleted file mode 100644 index 81770c6585..0000000000 --- a/.flowconfig +++ /dev/null @@ -1,6 +0,0 @@ -[include] -src/**/*.js -test/**/*.js - -[ignore] -node_modules/ diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..e9ede862d2 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ + + + + + diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml new file mode 100644 index 0000000000..0ae59da09a --- /dev/null +++ b/.github/workflows/develop.yml @@ -0,0 +1,44 @@ +name: Develop +on: + # These tests won't work for non-develop branches at the moment as they + # won't pull in the right versions of other repos, so they're only enabled + # on develop. + push: + branches: [develop] + pull_request: + branches: [develop] +jobs: + end-to-end: + runs-on: ubuntu-latest + container: vectorim/element-web-ci-e2etests-env:latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Prepare End-to-End tests + run: ./scripts/ci/prepare-end-to-end-tests.sh + - name: Run End-to-End tests + run: ./scripts/ci/run-end-to-end-tests.sh + - name: Archive logs + uses: actions/upload-artifact@v2 + if: ${{ always() }} + with: + path: | + test/end-to-end-tests/logs/**/* + test/end-to-end-tests/synapse/installations/consent/homeserver.log + retention-days: 14 + - name: Download previous benchmark data + uses: actions/cache@v1 + with: + path: ./cache + key: ${{ runner.os }}-benchmark + - name: Store benchmark result + uses: matrix-org/github-action-benchmark@jsperfentry-1 + with: + tool: 'jsperformanceentry' + output-file-path: test/end-to-end-tests/performance-entries.json + fail-on-alert: false + comment-on-alert: false + # Only temporary to monitor where failures occur + alert-comment-cc-users: '@gsouquet' + github-token: ${{ secrets.DEPLOY_GH_PAGES }} + auto-push: ${{ github.ref == 'refs/heads/develop' }} diff --git a/.gitignore b/.gitignore index 33e8bfc7ac..102f4b5ec1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /*.log package-lock.json +/coverage /node_modules /lib @@ -13,3 +14,7 @@ package-lock.json /src/component-index.js .DS_Store +*.tmp + +.vscode +.vscode/ diff --git a/.stylelintrc.js b/.stylelintrc.js index 1690f2186f..0e6de7000f 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -4,6 +4,7 @@ module.exports = { "stylelint-scss", ], "rules": { + "color-hex-case": null, "indentation": 4, "comment-empty-line-before": null, "declaration-empty-line-before": null, @@ -17,7 +18,7 @@ module.exports = { "at-rule-no-unknown": null, "no-descending-specificity": null, "scss/at-rule-no-unknown": [true, { - // https://github.com/vector-im/riot-web/issues/10544 + // https://github.com/vector-im/element-web/issues/10544 "ignoreAtRules": ["define-mixin"], }], } diff --git a/CHANGELOG.md b/CHANGELOG.md index b6f25f1858..22b35b7c59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4066 @@ +Changes in [3.25.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.25.0) (2021-07-05) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.25.0-rc.1...v3.25.0) + + * Remove reminescent references to the tinter + [\#6316](https://github.com/matrix-org/matrix-react-sdk/pull/6316) + * Update to released version of js-sdk + +Changes in [3.25.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.25.0-rc.1) (2021-06-29) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.24.0...v3.25.0-rc.1) + + * Update to js-sdk v12.0.1-rc.1 + * Translations update from Weblate + [\#6286](https://github.com/matrix-org/matrix-react-sdk/pull/6286) + * Fix back button on user info card after clicking a permalink + [\#6277](https://github.com/matrix-org/matrix-react-sdk/pull/6277) + * Group ACLs with MELS + [\#6280](https://github.com/matrix-org/matrix-react-sdk/pull/6280) + * Fix editState not getting passed through + [\#6282](https://github.com/matrix-org/matrix-react-sdk/pull/6282) + * Migrate message context menu to IconizedContextMenu + [\#5671](https://github.com/matrix-org/matrix-react-sdk/pull/5671) + * Improve audio recording performance + [\#6240](https://github.com/matrix-org/matrix-react-sdk/pull/6240) + * Fix multiple timeline panels handling composer and edit events + [\#6278](https://github.com/matrix-org/matrix-react-sdk/pull/6278) + * Let m.notice messages mark a room as unread + [\#6281](https://github.com/matrix-org/matrix-react-sdk/pull/6281) + * Removes the override on the Bubble Container + [\#5953](https://github.com/matrix-org/matrix-react-sdk/pull/5953) + * Fix IRC layout regressions + [\#6193](https://github.com/matrix-org/matrix-react-sdk/pull/6193) + * Fix trashcan.svg by exporting it with its viewbox + [\#6248](https://github.com/matrix-org/matrix-react-sdk/pull/6248) + * Fix tiny scrollbar dot on chrome/electron in Forward Dialog + [\#6276](https://github.com/matrix-org/matrix-react-sdk/pull/6276) + * Upgrade puppeteer to use newer version of Chrome + [\#6268](https://github.com/matrix-org/matrix-react-sdk/pull/6268) + * Make toast dismiss button less prominent + [\#6275](https://github.com/matrix-org/matrix-react-sdk/pull/6275) + * Encrypt the voice message file if needed + [\#6269](https://github.com/matrix-org/matrix-react-sdk/pull/6269) + * Fix hyper-precise presence + [\#6270](https://github.com/matrix-org/matrix-react-sdk/pull/6270) + * Fix issues around private spaces, including previewable + [\#6265](https://github.com/matrix-org/matrix-react-sdk/pull/6265) + * Make _pinned messages_ in `m.room.pinned_events` event clickable + [\#6257](https://github.com/matrix-org/matrix-react-sdk/pull/6257) + * Fix space avatar management layout being broken + [\#6266](https://github.com/matrix-org/matrix-react-sdk/pull/6266) + * Convert EntityTile, MemberTile and PresenceLabel to TS + [\#6251](https://github.com/matrix-org/matrix-react-sdk/pull/6251) + * Fix UserInfo not working when rendered without a room + [\#6260](https://github.com/matrix-org/matrix-react-sdk/pull/6260) + * Update membership reason handling, including leave reason displaying + [\#6253](https://github.com/matrix-org/matrix-react-sdk/pull/6253) + * Consolidate types with js-sdk changes + [\#6220](https://github.com/matrix-org/matrix-react-sdk/pull/6220) + * Fix edit history modal + [\#6258](https://github.com/matrix-org/matrix-react-sdk/pull/6258) + * Convert MemberList to TS + [\#6249](https://github.com/matrix-org/matrix-react-sdk/pull/6249) + * Fix two PRs duplicating the css attribute + [\#6259](https://github.com/matrix-org/matrix-react-sdk/pull/6259) + * Improve invite error messages in InviteDialog for room invites + [\#6201](https://github.com/matrix-org/matrix-react-sdk/pull/6201) + * Fix invite dialog being cut off when it has limited results + [\#6256](https://github.com/matrix-org/matrix-react-sdk/pull/6256) + * Fix pinning event in a room which hasn't had events pinned in before + [\#6255](https://github.com/matrix-org/matrix-react-sdk/pull/6255) + * Allow modal widget buttons to be disabled when the modal opens + [\#6178](https://github.com/matrix-org/matrix-react-sdk/pull/6178) + * Decrease e2e shield fill mask size so that it doesn't overlap + [\#6250](https://github.com/matrix-org/matrix-react-sdk/pull/6250) + * Dial Pad UI bug fixes + [\#5786](https://github.com/matrix-org/matrix-react-sdk/pull/5786) + * Simple handling of mid-call output changes + [\#6247](https://github.com/matrix-org/matrix-react-sdk/pull/6247) + * Improve ForwardDialog performance by using TruncatedList + [\#6228](https://github.com/matrix-org/matrix-react-sdk/pull/6228) + * Fix dependency and lockfile mismatch + [\#6246](https://github.com/matrix-org/matrix-react-sdk/pull/6246) + * Improve room directory click behaviour + [\#6234](https://github.com/matrix-org/matrix-react-sdk/pull/6234) + * Fix keyboard accessibility of the space panel + [\#6239](https://github.com/matrix-org/matrix-react-sdk/pull/6239) + * Add ways to manage addresses for Spaces + [\#6151](https://github.com/matrix-org/matrix-react-sdk/pull/6151) + * Hide communities invites and the community autocompleter when Spaces on + [\#6244](https://github.com/matrix-org/matrix-react-sdk/pull/6244) + * Convert bunch of files to TS + [\#6241](https://github.com/matrix-org/matrix-react-sdk/pull/6241) + * Open local addresses section by default when there are no existing local + addresses + [\#6179](https://github.com/matrix-org/matrix-react-sdk/pull/6179) + * Allow reordering of the space panel via Drag and Drop + [\#6137](https://github.com/matrix-org/matrix-react-sdk/pull/6137) + * Replace drag and drop mechanism in communities with something simpler + [\#6134](https://github.com/matrix-org/matrix-react-sdk/pull/6134) + * EventTilePreview fixes + [\#6000](https://github.com/matrix-org/matrix-react-sdk/pull/6000) + * Upgrade @types/react and @types/react-dom + [\#6233](https://github.com/matrix-org/matrix-react-sdk/pull/6233) + * Fix type error in the SpaceStore + [\#6242](https://github.com/matrix-org/matrix-react-sdk/pull/6242) + * Add experimental options to the Spaces beta + [\#6199](https://github.com/matrix-org/matrix-react-sdk/pull/6199) + * Consolidate types with js-sdk changes + [\#6215](https://github.com/matrix-org/matrix-react-sdk/pull/6215) + * Fix branch matching for Buildkite + [\#6236](https://github.com/matrix-org/matrix-react-sdk/pull/6236) + * Migrate SearchBar to TypeScript + [\#6230](https://github.com/matrix-org/matrix-react-sdk/pull/6230) + * Add support to keyboard shortcuts dialog for [digits] + [\#6088](https://github.com/matrix-org/matrix-react-sdk/pull/6088) + * Fix modal opening race condition + [\#6238](https://github.com/matrix-org/matrix-react-sdk/pull/6238) + * Deprecate FormButton in favour of AccessibleButton + [\#6229](https://github.com/matrix-org/matrix-react-sdk/pull/6229) + * Add PR template + [\#6216](https://github.com/matrix-org/matrix-react-sdk/pull/6216) + * Prefer canonical aliases while autocompleting rooms + [\#6222](https://github.com/matrix-org/matrix-react-sdk/pull/6222) + * Fix quote button + [\#6232](https://github.com/matrix-org/matrix-react-sdk/pull/6232) + * Restore branch matching support for GitHub Actions e2e tests + [\#6224](https://github.com/matrix-org/matrix-react-sdk/pull/6224) + * Fix View Source accessing renamed private field on MatrixEvent + [\#6225](https://github.com/matrix-org/matrix-react-sdk/pull/6225) + * Fix ConfirmUserActionDialog returning an input field rather than text + [\#6219](https://github.com/matrix-org/matrix-react-sdk/pull/6219) + * Revert "Partially restore immutable event objects at the rendering layer" + [\#6221](https://github.com/matrix-org/matrix-react-sdk/pull/6221) + * Add jq to e2e tests Dockerfile + [\#6218](https://github.com/matrix-org/matrix-react-sdk/pull/6218) + * Partially restore immutable event objects at the rendering layer + [\#6196](https://github.com/matrix-org/matrix-react-sdk/pull/6196) + * Update MSC number references for voice messages + [\#6197](https://github.com/matrix-org/matrix-react-sdk/pull/6197) + * Fix phase enum usage in JS modules as well + [\#6214](https://github.com/matrix-org/matrix-react-sdk/pull/6214) + * Migrate some dialogs to TypeScript + [\#6185](https://github.com/matrix-org/matrix-react-sdk/pull/6185) + * Typescript fixes due to MatrixEvent being TSified + [\#6208](https://github.com/matrix-org/matrix-react-sdk/pull/6208) + * Allow click-to-ping, quote & emoji picker for edit composer too + [\#5858](https://github.com/matrix-org/matrix-react-sdk/pull/5858) + * Add call silencing + [\#6082](https://github.com/matrix-org/matrix-react-sdk/pull/6082) + * Fix types in SlashCommands + [\#6207](https://github.com/matrix-org/matrix-react-sdk/pull/6207) + * Benchmark multiple common user scenario + [\#6190](https://github.com/matrix-org/matrix-react-sdk/pull/6190) + * Fix forward dialog message preview display names + [\#6204](https://github.com/matrix-org/matrix-react-sdk/pull/6204) + * Remove stray bullet point in reply preview + [\#6206](https://github.com/matrix-org/matrix-react-sdk/pull/6206) + * Stop requesting null next replies from the server + [\#6203](https://github.com/matrix-org/matrix-react-sdk/pull/6203) + * Fix soft crash caused by a broken shouldComponentUpdate + [\#6202](https://github.com/matrix-org/matrix-react-sdk/pull/6202) + * Keep composer reply when scrolling away from a highlighted event + [\#6200](https://github.com/matrix-org/matrix-react-sdk/pull/6200) + * Cache virtual/native room mappings when they're created + [\#6194](https://github.com/matrix-org/matrix-react-sdk/pull/6194) + * Disable comment-on-alert + [\#6191](https://github.com/matrix-org/matrix-react-sdk/pull/6191) + * Bump postcss from 7.0.35 to 7.0.36 + [\#6195](https://github.com/matrix-org/matrix-react-sdk/pull/6195) + +Changes in [3.24.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.24.0) (2021-06-21) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.24.0-rc.1...v3.24.0) + + * Upgrade to JS SDK 12.0.0 + * [Release] Keep composer reply when scrolling away from a highlighted event + [\#6211](https://github.com/matrix-org/matrix-react-sdk/pull/6211) + * [Release] Remove stray bullet point in reply preview + [\#6210](https://github.com/matrix-org/matrix-react-sdk/pull/6210) + * [Release] Stop requesting null next replies from the server + [\#6209](https://github.com/matrix-org/matrix-react-sdk/pull/6209) + +Changes in [3.24.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.24.0-rc.1) (2021-06-15) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0...v3.24.0-rc.1) + + * Upgrade to JS SDK 12.0.0-rc.1 + * Translations update from Weblate + [\#6192](https://github.com/matrix-org/matrix-react-sdk/pull/6192) + * Disable comment-on-alert for PR coming from a fork + [\#6189](https://github.com/matrix-org/matrix-react-sdk/pull/6189) + * Add JS benchmark tracking in CI + [\#6177](https://github.com/matrix-org/matrix-react-sdk/pull/6177) + * Upgrade matrix-react-test-utils for React 17 peer deps + [\#6187](https://github.com/matrix-org/matrix-react-sdk/pull/6187) + * Fix display name overlaps on the IRC layout + [\#6186](https://github.com/matrix-org/matrix-react-sdk/pull/6186) + * Small fixes to the spaces experience + [\#6184](https://github.com/matrix-org/matrix-react-sdk/pull/6184) + * Add footer and privacy note to the start dm dialog + [\#6111](https://github.com/matrix-org/matrix-react-sdk/pull/6111) + * Format mxids when disambiguation needed + [\#5880](https://github.com/matrix-org/matrix-react-sdk/pull/5880) + * Move various createRoom types to the js-sdk + [\#6183](https://github.com/matrix-org/matrix-react-sdk/pull/6183) + * Fix HTML tag for Event Tile when not rendered in a list + [\#6175](https://github.com/matrix-org/matrix-react-sdk/pull/6175) + * Remove legacy polyfills and unused dependencies + [\#6176](https://github.com/matrix-org/matrix-react-sdk/pull/6176) + * Fix buggy hovering/selecting of event tiles + [\#6173](https://github.com/matrix-org/matrix-react-sdk/pull/6173) + * Add room intro warning when e2ee is not enabled + [\#5929](https://github.com/matrix-org/matrix-react-sdk/pull/5929) + * Migrate end to end tests to GitHub actions + [\#6156](https://github.com/matrix-org/matrix-react-sdk/pull/6156) + * Fix expanding last collapsed sticky session when zoomed in + [\#6171](https://github.com/matrix-org/matrix-react-sdk/pull/6171) + * ⚛️ Upgrade to React@17 + [\#6165](https://github.com/matrix-org/matrix-react-sdk/pull/6165) + * Revert refreshStickyHeaders optimisations + [\#6168](https://github.com/matrix-org/matrix-react-sdk/pull/6168) + * Add logging for which rooms calls are in + [\#6170](https://github.com/matrix-org/matrix-react-sdk/pull/6170) + * Restore read receipt animation from event to event + [\#6169](https://github.com/matrix-org/matrix-react-sdk/pull/6169) + * Restore copy button icon when sharing permalink + [\#6166](https://github.com/matrix-org/matrix-react-sdk/pull/6166) + * Restore Page Up/Down key bindings when focusing the composer + [\#6167](https://github.com/matrix-org/matrix-react-sdk/pull/6167) + * Timeline rendering optimizations + [\#6143](https://github.com/matrix-org/matrix-react-sdk/pull/6143) + * Bump css-what from 5.0.0 to 5.0.1 + [\#6164](https://github.com/matrix-org/matrix-react-sdk/pull/6164) + * Bump ws from 6.2.1 to 6.2.2 in /test/end-to-end-tests + [\#6145](https://github.com/matrix-org/matrix-react-sdk/pull/6145) + * Bump trim-newlines from 3.0.0 to 3.0.1 + [\#6163](https://github.com/matrix-org/matrix-react-sdk/pull/6163) + * Fix upgrade to element home button in top left menu + [\#6162](https://github.com/matrix-org/matrix-react-sdk/pull/6162) + * Fix unpinning of pinned messages and panel empty state + [\#6140](https://github.com/matrix-org/matrix-react-sdk/pull/6140) + * Better handling for widgets that fail to load + [\#6161](https://github.com/matrix-org/matrix-react-sdk/pull/6161) + * Improved forwarding UI + [\#5999](https://github.com/matrix-org/matrix-react-sdk/pull/5999) + * Fixes for sharing room links + [\#6118](https://github.com/matrix-org/matrix-react-sdk/pull/6118) + * Fix setting watchers + [\#6160](https://github.com/matrix-org/matrix-react-sdk/pull/6160) + * Fix Stickerpicker context menu + [\#6152](https://github.com/matrix-org/matrix-react-sdk/pull/6152) + * Add warning to private space creation flow + [\#6155](https://github.com/matrix-org/matrix-react-sdk/pull/6155) + * Add prop to alwaysShowTimestamps on TimelinePanel + [\#6159](https://github.com/matrix-org/matrix-react-sdk/pull/6159) + * Fix notif panel timestamp padding + [\#6157](https://github.com/matrix-org/matrix-react-sdk/pull/6157) + * Fixes and refactoring for the ImageView + [\#6149](https://github.com/matrix-org/matrix-react-sdk/pull/6149) + * Fix timestamps + [\#6148](https://github.com/matrix-org/matrix-react-sdk/pull/6148) + * Make it easier to pan images in the lightbox + [\#6147](https://github.com/matrix-org/matrix-react-sdk/pull/6147) + * Fix scroll token for EventTile and EventListSummary node type + [\#6154](https://github.com/matrix-org/matrix-react-sdk/pull/6154) + * Convert bunch of things to Typescript + [\#6153](https://github.com/matrix-org/matrix-react-sdk/pull/6153) + * Lint the typescript tests + [\#6142](https://github.com/matrix-org/matrix-react-sdk/pull/6142) + * Fix jumping to bottom without a highlighted event + [\#6146](https://github.com/matrix-org/matrix-react-sdk/pull/6146) + * Repair event status position in timeline + [\#6141](https://github.com/matrix-org/matrix-react-sdk/pull/6141) + * Adapt for js-sdk MatrixClient conversion to TS + [\#6132](https://github.com/matrix-org/matrix-react-sdk/pull/6132) + * Improve pinned messages in Labs + [\#6096](https://github.com/matrix-org/matrix-react-sdk/pull/6096) + * Map phone number lookup results to their native rooms + [\#6136](https://github.com/matrix-org/matrix-react-sdk/pull/6136) + * Fix mx_Event containment rules and empty read avatar row + [\#6138](https://github.com/matrix-org/matrix-react-sdk/pull/6138) + * Improve switch room rendering + [\#6079](https://github.com/matrix-org/matrix-react-sdk/pull/6079) + * Add CSS containment rules for shorter reflow operations + [\#6127](https://github.com/matrix-org/matrix-react-sdk/pull/6127) + * ignore hash/fragment when de-duplicating links for url previews + [\#6135](https://github.com/matrix-org/matrix-react-sdk/pull/6135) + * Clicking jump to bottom resets room hash + [\#5823](https://github.com/matrix-org/matrix-react-sdk/pull/5823) + * Use passive option for scroll handlers + [\#6113](https://github.com/matrix-org/matrix-react-sdk/pull/6113) + * Optimise memberSort performance for large list + [\#6130](https://github.com/matrix-org/matrix-react-sdk/pull/6130) + * Tweak event border radius to match action bar + [\#6133](https://github.com/matrix-org/matrix-react-sdk/pull/6133) + * Log when we ignore a second call in a room + [\#6131](https://github.com/matrix-org/matrix-react-sdk/pull/6131) + * Performance monitoring measurements + [\#6041](https://github.com/matrix-org/matrix-react-sdk/pull/6041) + +Changes in [3.23.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0) (2021-06-07) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0-rc.1...v3.23.0) + + * Upgrade to JS SDK 11.2.0 + * [Release] Fix notif panel timestamp padding + [\#6158](https://github.com/matrix-org/matrix-react-sdk/pull/6158) + +Changes in [3.23.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0-rc.1) (2021-06-01) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0...v3.23.0-rc.1) + + * Upgrade to JS SDK 11.2.0-rc.1 + * Translations update from Weblate + [\#6128](https://github.com/matrix-org/matrix-react-sdk/pull/6128) + * Fix all DMs wrongly appearing in room list when `m.direct` is changed + [\#6122](https://github.com/matrix-org/matrix-react-sdk/pull/6122) + * Update way of checking for registration disabled + [\#6123](https://github.com/matrix-org/matrix-react-sdk/pull/6123) + * Fix the ability to remove avatar from a space via settings + [\#6126](https://github.com/matrix-org/matrix-react-sdk/pull/6126) + * Switch to stable endpoint/fields for MSC2858 + [\#6125](https://github.com/matrix-org/matrix-react-sdk/pull/6125) + * Clear stored editor state when canceling editing using a shortcut + [\#6117](https://github.com/matrix-org/matrix-react-sdk/pull/6117) + * Respect newlines in space topics + [\#6124](https://github.com/matrix-org/matrix-react-sdk/pull/6124) + * Add url param `defaultUsername` to prefill the login username field + [\#5674](https://github.com/matrix-org/matrix-react-sdk/pull/5674) + * Bump ws from 7.4.2 to 7.4.6 + [\#6115](https://github.com/matrix-org/matrix-react-sdk/pull/6115) + * Sticky headers repositioning without layout trashing + [\#6110](https://github.com/matrix-org/matrix-react-sdk/pull/6110) + * Handle user_busy in voip calls + [\#6112](https://github.com/matrix-org/matrix-react-sdk/pull/6112) + * Avoid showing warning modals from the invite dialog after it unmounts + [\#6105](https://github.com/matrix-org/matrix-react-sdk/pull/6105) + * Fix misleading child counts in spaces + [\#6109](https://github.com/matrix-org/matrix-react-sdk/pull/6109) + * Close creation menu when expanding space panel via expand hierarchy + [\#6090](https://github.com/matrix-org/matrix-react-sdk/pull/6090) + * Prevent having duplicates in pending room state + [\#6108](https://github.com/matrix-org/matrix-react-sdk/pull/6108) + * Update reactions row on event decryption + [\#6106](https://github.com/matrix-org/matrix-react-sdk/pull/6106) + * Destroy playback instance on voice message unmount + [\#6101](https://github.com/matrix-org/matrix-react-sdk/pull/6101) + * Fix message preview not up to date + [\#6102](https://github.com/matrix-org/matrix-react-sdk/pull/6102) + * Convert some Flow typed files to TS (round 2) + [\#6076](https://github.com/matrix-org/matrix-react-sdk/pull/6076) + * Remove unused middlePanelResized event listener + [\#6086](https://github.com/matrix-org/matrix-react-sdk/pull/6086) + * Fix accessing currentState on an invalid joinedRoom + [\#6100](https://github.com/matrix-org/matrix-react-sdk/pull/6100) + * Remove Promise allSettled polyfill as js-sdk uses it directly + [\#6097](https://github.com/matrix-org/matrix-react-sdk/pull/6097) + * Prevent DecoratedRoomAvatar to update its state for the same value + [\#6099](https://github.com/matrix-org/matrix-react-sdk/pull/6099) + * Skip generatePreview if event is not part of the live timeline + [\#6098](https://github.com/matrix-org/matrix-react-sdk/pull/6098) + * fix sticky headers when results num get displayed + [\#6095](https://github.com/matrix-org/matrix-react-sdk/pull/6095) + * Improve addEventsToTimeline performance scoping WhoIsTypingTile::setState + [\#6094](https://github.com/matrix-org/matrix-react-sdk/pull/6094) + * Safeguards to prevent layout trashing for window dimensions + [\#6092](https://github.com/matrix-org/matrix-react-sdk/pull/6092) + * Use local room state to render space hierarchy if the room is known + [\#6089](https://github.com/matrix-org/matrix-react-sdk/pull/6089) + * Add spinner in UserMenu to list pending long running actions + [\#6085](https://github.com/matrix-org/matrix-react-sdk/pull/6085) + * Stop overscroll in Firefox Nightly for macOS + [\#6093](https://github.com/matrix-org/matrix-react-sdk/pull/6093) + * Move SettingsStore watchers/monitors over to ES6 maps for performance + [\#6063](https://github.com/matrix-org/matrix-react-sdk/pull/6063) + * Bump libolm version. + [\#6080](https://github.com/matrix-org/matrix-react-sdk/pull/6080) + * Improve styling of the message action bar + [\#6066](https://github.com/matrix-org/matrix-react-sdk/pull/6066) + * Improve explore rooms when no results are found + [\#6070](https://github.com/matrix-org/matrix-react-sdk/pull/6070) + * Remove logo spinner + [\#6078](https://github.com/matrix-org/matrix-react-sdk/pull/6078) + * Fix add reaction prompt showing even when user is not joined to room + [\#6073](https://github.com/matrix-org/matrix-react-sdk/pull/6073) + * Vectorize spinners + [\#5680](https://github.com/matrix-org/matrix-react-sdk/pull/5680) + * Fix handling of via servers for suggested rooms + [\#6077](https://github.com/matrix-org/matrix-react-sdk/pull/6077) + * Upgrade showChatEffects to room-level setting exposure + [\#6075](https://github.com/matrix-org/matrix-react-sdk/pull/6075) + * Delete RoomView dead code + [\#6071](https://github.com/matrix-org/matrix-react-sdk/pull/6071) + * Reduce noise in tests + [\#6074](https://github.com/matrix-org/matrix-react-sdk/pull/6074) + * Fix room name issues in right panel summary card + [\#6069](https://github.com/matrix-org/matrix-react-sdk/pull/6069) + * Cache normalized room name + [\#6072](https://github.com/matrix-org/matrix-react-sdk/pull/6072) + * Update MemberList to reflect changes for invite permission change + [\#6061](https://github.com/matrix-org/matrix-react-sdk/pull/6061) + * Delete RoomView dead code + [\#6065](https://github.com/matrix-org/matrix-react-sdk/pull/6065) + * Show subspace rooms count even if it is 0 for consistency + [\#6067](https://github.com/matrix-org/matrix-react-sdk/pull/6067) + +Changes in [3.22.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0) (2021-05-24) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0-rc.1...v3.22.0) + + * Upgrade to JS SDK 11.1.0 + * [Release] Bump libolm version + [\#6087](https://github.com/matrix-org/matrix-react-sdk/pull/6087) + +Changes in [3.22.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0-rc.1) (2021-05-19) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.21.0...v3.22.0-rc.1) + + * Upgrade to JS SDK 11.1.0-rc.1 + * Translations update from Weblate + [\#6068](https://github.com/matrix-org/matrix-react-sdk/pull/6068) + * Show DMs in space for invited members too, to match Android impl + [\#6062](https://github.com/matrix-org/matrix-react-sdk/pull/6062) + * Support filtering by alias in add existing to space dialog + [\#6057](https://github.com/matrix-org/matrix-react-sdk/pull/6057) + * Fix issue when a room without a name or alias is marked as suggested + [\#6064](https://github.com/matrix-org/matrix-react-sdk/pull/6064) + * Fix space room hierarchy not updating when removing a room + [\#6055](https://github.com/matrix-org/matrix-react-sdk/pull/6055) + * Revert "Try putting room list handling behind a lock" + [\#6060](https://github.com/matrix-org/matrix-react-sdk/pull/6060) + * Stop assuming encrypted messages are decrypted ahead of time + [\#6052](https://github.com/matrix-org/matrix-react-sdk/pull/6052) + * Add error detail when languges fail to load + [\#6059](https://github.com/matrix-org/matrix-react-sdk/pull/6059) + * Add space invaders chat effect + [\#6053](https://github.com/matrix-org/matrix-react-sdk/pull/6053) + * Create SpaceProvider and hide Spaces from the RoomProvider autocompleter + [\#6051](https://github.com/matrix-org/matrix-react-sdk/pull/6051) + * Don't mark a room as unread when redacted event is present + [\#6049](https://github.com/matrix-org/matrix-react-sdk/pull/6049) + * Add support for MSC2873: Client information for Widgets + [\#6023](https://github.com/matrix-org/matrix-react-sdk/pull/6023) + * Support UI for MSC2762: Widgets reading events from rooms + [\#5960](https://github.com/matrix-org/matrix-react-sdk/pull/5960) + * Fix crash on opening notification panel + [\#6047](https://github.com/matrix-org/matrix-react-sdk/pull/6047) + * Remove custom LoggedInView::shouldComponentUpdate logic + [\#6046](https://github.com/matrix-org/matrix-react-sdk/pull/6046) + * Fix edge cases with the new add reactions prompt button + [\#6045](https://github.com/matrix-org/matrix-react-sdk/pull/6045) + * Add ids to homeserver and passphrase fields + [\#6043](https://github.com/matrix-org/matrix-react-sdk/pull/6043) + * Update space order field validity requirements to match msc update + [\#6042](https://github.com/matrix-org/matrix-react-sdk/pull/6042) + * Try putting room list handling behind a lock + [\#6024](https://github.com/matrix-org/matrix-react-sdk/pull/6024) + * Improve progress bar progression for smaller voice messages + [\#6035](https://github.com/matrix-org/matrix-react-sdk/pull/6035) + * Fix share space edge case where space is public but not invitable + [\#6039](https://github.com/matrix-org/matrix-react-sdk/pull/6039) + * Add missing 'rel' to image view download button + [\#6033](https://github.com/matrix-org/matrix-react-sdk/pull/6033) + * Improve visible waveform for voice messages + [\#6034](https://github.com/matrix-org/matrix-react-sdk/pull/6034) + * Fix roving tab index intercepting home/end in space create menu + [\#6040](https://github.com/matrix-org/matrix-react-sdk/pull/6040) + * Decorate room avatars with publicity in add existing to space flow + [\#6030](https://github.com/matrix-org/matrix-react-sdk/pull/6030) + * Improve Spaces "Just Me" wizard + [\#6025](https://github.com/matrix-org/matrix-react-sdk/pull/6025) + * Increase hover feedback on room sub list buttons + [\#6037](https://github.com/matrix-org/matrix-react-sdk/pull/6037) + * Show alternative button during space creation wizard if no rooms + [\#6029](https://github.com/matrix-org/matrix-react-sdk/pull/6029) + * Swap rotation buttons in the image viewer + [\#6032](https://github.com/matrix-org/matrix-react-sdk/pull/6032) + * Typo: initilisation -> initialisation + [\#5915](https://github.com/matrix-org/matrix-react-sdk/pull/5915) + * Save edited state of a message when switching rooms + [\#6001](https://github.com/matrix-org/matrix-react-sdk/pull/6001) + * Fix shield icon in Untrusted Device Dialog + [\#6022](https://github.com/matrix-org/matrix-react-sdk/pull/6022) + * Do not eagerly decrypt breadcrumb rooms + [\#6028](https://github.com/matrix-org/matrix-react-sdk/pull/6028) + * Update spaces.png + [\#6031](https://github.com/matrix-org/matrix-react-sdk/pull/6031) + * Encourage more diverse reactions to content + [\#6027](https://github.com/matrix-org/matrix-react-sdk/pull/6027) + * Wrap decodeURIComponent in try-catch to protect against malformed URIs + [\#6026](https://github.com/matrix-org/matrix-react-sdk/pull/6026) + * Iterate beta feedback dialog + [\#6021](https://github.com/matrix-org/matrix-react-sdk/pull/6021) + * Disable space fields whilst their form is busy + [\#6020](https://github.com/matrix-org/matrix-react-sdk/pull/6020) + * Add missing space on beta feedback dialog + [\#6018](https://github.com/matrix-org/matrix-react-sdk/pull/6018) + * Fix colours used for the back button in space create menu + [\#6017](https://github.com/matrix-org/matrix-react-sdk/pull/6017) + * Prioritise and reduce the amount of events decrypted on application startup + [\#5980](https://github.com/matrix-org/matrix-react-sdk/pull/5980) + * Linkify topics in space room directory results + [\#6015](https://github.com/matrix-org/matrix-react-sdk/pull/6015) + * Persistent space collapsed states + [\#5972](https://github.com/matrix-org/matrix-react-sdk/pull/5972) + * Catch another instance of unlabeled avatars. + [\#6010](https://github.com/matrix-org/matrix-react-sdk/pull/6010) + * Rescale and smooth voice message playback waveform to better match + expectation + [\#5996](https://github.com/matrix-org/matrix-react-sdk/pull/5996) + * Scale voice message clock with user's font size + [\#5993](https://github.com/matrix-org/matrix-react-sdk/pull/5993) + * Remove "in development" flag from voice messages + [\#5995](https://github.com/matrix-org/matrix-react-sdk/pull/5995) + * Support voice messages on Safari + [\#5989](https://github.com/matrix-org/matrix-react-sdk/pull/5989) + * Translations update from Weblate + [\#6011](https://github.com/matrix-org/matrix-react-sdk/pull/6011) + +Changes in [3.21.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.21.0) (2021-05-17) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.21.0-rc.1...v3.21.0) + +## Security notice + +matrix-react-sdk 3.21.0 fixes a low severity issue (GHSA-8796-gc9j-63rv) +related to file upload. When uploading a file, the local file preview can lead +to execution of scripts embedded in the uploaded file, but only after several +user interactions to open the preview in a separate tab. This only impacts the +local user while in the process of uploading. It cannot be exploited remotely +or by other users. Thanks to [Muhammad Zaid Ghifari](https://github.com/MR-ZHEEV) +for responsibly disclosing this via Matrix's Security Disclosure Policy. + +## All changes + + * Upgrade to JS SDK 11.0.0 + * [Release] Add missing space on beta feedback dialog + [\#6019](https://github.com/matrix-org/matrix-react-sdk/pull/6019) + * [Release] Add feedback mechanism for beta features, namely Spaces + [\#6013](https://github.com/matrix-org/matrix-react-sdk/pull/6013) + * Add feedback mechanism for beta features, namely Spaces + [\#6012](https://github.com/matrix-org/matrix-react-sdk/pull/6012) + +Changes in [3.21.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.21.0-rc.1) (2021-05-11) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0...v3.21.0-rc.1) + + * Upgrade to JS SDK 11.0.0-rc.1 + * Add disclaimer about subspaces being experimental in add existing dialog + [\#5978](https://github.com/matrix-org/matrix-react-sdk/pull/5978) + * Spaces Beta release + [\#5933](https://github.com/matrix-org/matrix-react-sdk/pull/5933) + * Improve permissions error when adding new server to room directory + [\#6009](https://github.com/matrix-org/matrix-react-sdk/pull/6009) + * Allow user to progress through space creation & setup using Enter + [\#6006](https://github.com/matrix-org/matrix-react-sdk/pull/6006) + * Upgrade sanitize types + [\#6008](https://github.com/matrix-org/matrix-react-sdk/pull/6008) + * Upgrade `cheerio` and resolve type errors + [\#6007](https://github.com/matrix-org/matrix-react-sdk/pull/6007) + * Add slash commands support to edit message composer + [\#5865](https://github.com/matrix-org/matrix-react-sdk/pull/5865) + * Fix the two todays problem + [\#5940](https://github.com/matrix-org/matrix-react-sdk/pull/5940) + * Switch the Home Space out for an All rooms space + [\#5969](https://github.com/matrix-org/matrix-react-sdk/pull/5969) + * Show device ID in UserInfo when there is no device name + [\#5985](https://github.com/matrix-org/matrix-react-sdk/pull/5985) + * Switch back to release version of `sanitize-html` + [\#6005](https://github.com/matrix-org/matrix-react-sdk/pull/6005) + * Bump hosted-git-info from 2.8.8 to 2.8.9 + [\#5998](https://github.com/matrix-org/matrix-react-sdk/pull/5998) + * Don't use the event's metadata to calc the scale of an image + [\#5982](https://github.com/matrix-org/matrix-react-sdk/pull/5982) + * Adjust MIME type of upload confirmation if needed + [\#5981](https://github.com/matrix-org/matrix-react-sdk/pull/5981) + * Forbid redaction of encryption events + [\#5991](https://github.com/matrix-org/matrix-react-sdk/pull/5991) + * Fix voice message playback being squished up against send button + [\#5988](https://github.com/matrix-org/matrix-react-sdk/pull/5988) + * Improve style of notification badges on the space panel + [\#5983](https://github.com/matrix-org/matrix-react-sdk/pull/5983) + * Add dev dependency for parse5 typings + [\#5990](https://github.com/matrix-org/matrix-react-sdk/pull/5990) + * Iterate Spaces admin UX around room management + [\#5977](https://github.com/matrix-org/matrix-react-sdk/pull/5977) + * Guard all isSpaceRoom calls behind the labs flag + [\#5979](https://github.com/matrix-org/matrix-react-sdk/pull/5979) + * Bump lodash from 4.17.20 to 4.17.21 + [\#5986](https://github.com/matrix-org/matrix-react-sdk/pull/5986) + * Bump lodash from 4.17.19 to 4.17.21 in /test/end-to-end-tests + [\#5987](https://github.com/matrix-org/matrix-react-sdk/pull/5987) + * Bump ua-parser-js from 0.7.23 to 0.7.28 + [\#5984](https://github.com/matrix-org/matrix-react-sdk/pull/5984) + * Update visual style of plain files in the timeline + [\#5971](https://github.com/matrix-org/matrix-react-sdk/pull/5971) + * Support for multiple streams (not MSC3077) + [\#5833](https://github.com/matrix-org/matrix-react-sdk/pull/5833) + * Update space ordering behaviour to match updates in MSC + [\#5963](https://github.com/matrix-org/matrix-react-sdk/pull/5963) + * Improve performance of search all spaces and space switching + [\#5976](https://github.com/matrix-org/matrix-react-sdk/pull/5976) + * Update colours and sizing for voice messages + [\#5970](https://github.com/matrix-org/matrix-react-sdk/pull/5970) + * Update link to Android SDK + [\#5973](https://github.com/matrix-org/matrix-react-sdk/pull/5973) + * Add cleanup functions for image view + [\#5962](https://github.com/matrix-org/matrix-react-sdk/pull/5962) + * Add a note about sharing your IP in P2P calls + [\#5961](https://github.com/matrix-org/matrix-react-sdk/pull/5961) + * Only aggregate DM notifications on the Space Panel in the Home Space + [\#5968](https://github.com/matrix-org/matrix-react-sdk/pull/5968) + * Add retry mechanism and progress bar to add existing to space dialog + [\#5975](https://github.com/matrix-org/matrix-react-sdk/pull/5975) + * Warn on access token reveal + [\#5755](https://github.com/matrix-org/matrix-react-sdk/pull/5755) + * Fix newly joined room appearing under the wrong space + [\#5945](https://github.com/matrix-org/matrix-react-sdk/pull/5945) + * Early rendering for voice messages in the timeline + [\#5955](https://github.com/matrix-org/matrix-react-sdk/pull/5955) + * Calculate the real waveform in the Playback class for voice messages + [\#5956](https://github.com/matrix-org/matrix-react-sdk/pull/5956) + * Don't recurse on arrayFastResample + [\#5957](https://github.com/matrix-org/matrix-react-sdk/pull/5957) + * Support a dark theme for voice messages + [\#5958](https://github.com/matrix-org/matrix-react-sdk/pull/5958) + * Handle no/blocked microphones in voice messages + [\#5959](https://github.com/matrix-org/matrix-react-sdk/pull/5959) + +Changes in [3.20.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.20.0) (2021-05-10) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0-rc.1...v3.20.0) + + * Upgrade to JS SDK 10.1.0 + * [Release] Don't use the event's metadata to calc the scale of an image + [\#6004](https://github.com/matrix-org/matrix-react-sdk/pull/6004) + +Changes in [3.20.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.20.0-rc.1) (2021-05-04) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0...v3.20.0-rc.1) + + * Upgrade to JS SDK 10.1.0-rc.1 + * Translations update from Weblate + [\#5966](https://github.com/matrix-org/matrix-react-sdk/pull/5966) + * Fix more space panel layout and hover behaviour issues + [\#5965](https://github.com/matrix-org/matrix-react-sdk/pull/5965) + * Fix edge case with space panel alignment with subspaces on ff + [\#5964](https://github.com/matrix-org/matrix-react-sdk/pull/5964) + * Fix saving room pill part to history + [\#5951](https://github.com/matrix-org/matrix-react-sdk/pull/5951) + * Generate room preview even when minimized + [\#5948](https://github.com/matrix-org/matrix-react-sdk/pull/5948) + * Another change from recovery passphrase to Security Phrase + [\#5934](https://github.com/matrix-org/matrix-react-sdk/pull/5934) + * Sort rooms in the add existing to space dialog based on recency + [\#5943](https://github.com/matrix-org/matrix-react-sdk/pull/5943) + * Inhibit sending RR when context switching to a room + [\#5944](https://github.com/matrix-org/matrix-react-sdk/pull/5944) + * Prevent room list keyboard handling from landing focus on hidden nodes + [\#5950](https://github.com/matrix-org/matrix-react-sdk/pull/5950) + * Make the text filter search all spaces instead of just the selected one + [\#5942](https://github.com/matrix-org/matrix-react-sdk/pull/5942) + * Enable indent rule and fix indent + [\#5931](https://github.com/matrix-org/matrix-react-sdk/pull/5931) + * Prevent peeking members from reacting + [\#5946](https://github.com/matrix-org/matrix-react-sdk/pull/5946) + * Disallow inline display maths + [\#5939](https://github.com/matrix-org/matrix-react-sdk/pull/5939) + * Space creation prompt user to add existing rooms for "Just Me" spaces + [\#5923](https://github.com/matrix-org/matrix-react-sdk/pull/5923) + * Add test coverage collection script + [\#5937](https://github.com/matrix-org/matrix-react-sdk/pull/5937) + * Fix joining room using via servers regression + [\#5936](https://github.com/matrix-org/matrix-react-sdk/pull/5936) + * Revert "Fixes the two Todays problem in Redaction" + [\#5938](https://github.com/matrix-org/matrix-react-sdk/pull/5938) + * Handle encoded matrix URLs + [\#5903](https://github.com/matrix-org/matrix-react-sdk/pull/5903) + * Render ignored users setting regardless of if there are any + [\#5860](https://github.com/matrix-org/matrix-react-sdk/pull/5860) + * Fix inserting trailing colon after mention/pill + [\#5830](https://github.com/matrix-org/matrix-react-sdk/pull/5830) + * Fixes the two Todays problem in Redaction + [\#5917](https://github.com/matrix-org/matrix-react-sdk/pull/5917) + * Fix page up/down scrolling only half a page + [\#5920](https://github.com/matrix-org/matrix-react-sdk/pull/5920) + * Voice messages: Composer controls + [\#5935](https://github.com/matrix-org/matrix-react-sdk/pull/5935) + * Support MSC3086 asserted identity + [\#5886](https://github.com/matrix-org/matrix-react-sdk/pull/5886) + * Handle possible edge case with getting stuck in "unsent messages" bar + [\#5930](https://github.com/matrix-org/matrix-react-sdk/pull/5930) + * Fix suggested rooms not showing up regression from room list optimisation + [\#5932](https://github.com/matrix-org/matrix-react-sdk/pull/5932) + * Broadcast language change to ElectronPlatform + [\#5913](https://github.com/matrix-org/matrix-react-sdk/pull/5913) + * Fix VoIP PIP frame color + [\#5701](https://github.com/matrix-org/matrix-react-sdk/pull/5701) + * Convert some Flow-typed files to TypeScript + [\#5912](https://github.com/matrix-org/matrix-react-sdk/pull/5912) + * Initial SpaceStore tests work + [\#5906](https://github.com/matrix-org/matrix-react-sdk/pull/5906) + * Fix issues with space hierarchy in layout and with incompatible servers + [\#5926](https://github.com/matrix-org/matrix-react-sdk/pull/5926) + * Scale all mxc thumbs using device pixel ratio for hidpi + [\#5928](https://github.com/matrix-org/matrix-react-sdk/pull/5928) + * Fix add existing to space dialog no longer showing rooms for public spaces + [\#5918](https://github.com/matrix-org/matrix-react-sdk/pull/5918) + * Disable spaces context switching for when exploring a space + [\#5924](https://github.com/matrix-org/matrix-react-sdk/pull/5924) + * Autofocus search box in the add existing to space dialog + [\#5921](https://github.com/matrix-org/matrix-react-sdk/pull/5921) + * Use label element in add existing to space dialog for easier hit target + [\#5922](https://github.com/matrix-org/matrix-react-sdk/pull/5922) + * Dynamic max and min zoom in the new ImageView + [\#5916](https://github.com/matrix-org/matrix-react-sdk/pull/5916) + * Improve message error states + [\#5897](https://github.com/matrix-org/matrix-react-sdk/pull/5897) + * Check for null room in `VisibilityProvider` + [\#5914](https://github.com/matrix-org/matrix-react-sdk/pull/5914) + * Add unit tests for various collection-based utility functions + [\#5910](https://github.com/matrix-org/matrix-react-sdk/pull/5910) + * Spaces visual fixes + [\#5909](https://github.com/matrix-org/matrix-react-sdk/pull/5909) + * Remove reliance on DOM API to generated message preview + [\#5908](https://github.com/matrix-org/matrix-react-sdk/pull/5908) + * Expand upon voice message event & include overall waveform + [\#5888](https://github.com/matrix-org/matrix-react-sdk/pull/5888) + * Use floats for image background opacity + [\#5905](https://github.com/matrix-org/matrix-react-sdk/pull/5905) + * Show invites to spaces at the top of the space panel + [\#5902](https://github.com/matrix-org/matrix-react-sdk/pull/5902) + * Improve edge cases with spaces context switching + [\#5899](https://github.com/matrix-org/matrix-react-sdk/pull/5899) + * Fix spaces notification dots wrongly including upgraded (hidden) rooms + [\#5900](https://github.com/matrix-org/matrix-react-sdk/pull/5900) + * Iterate the spaces face pile design + [\#5898](https://github.com/matrix-org/matrix-react-sdk/pull/5898) + * Fix alignment issue with nested spaces being cut off wrong + [\#5890](https://github.com/matrix-org/matrix-react-sdk/pull/5890) + +Changes in [3.19.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0) (2021-04-26) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0-rc.1...v3.19.0) + + * Upgrade to JS SDK 10.0.0 + * [Release] Dynamic max and min zoom in the new ImageView + [\#5927](https://github.com/matrix-org/matrix-react-sdk/pull/5927) + * [Release] Add a WheelEvent normalization function + [\#5911](https://github.com/matrix-org/matrix-react-sdk/pull/5911) + * Add a WheelEvent normalization function + [\#5904](https://github.com/matrix-org/matrix-react-sdk/pull/5904) + * [Release] Use floats for image background opacity + [\#5907](https://github.com/matrix-org/matrix-react-sdk/pull/5907) + +Changes in [3.19.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0-rc.1) (2021-04-21) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.18.0...v3.19.0-rc.1) + + * Upgrade to JS SDK 10.0.0-rc.1 + * Translations update from Weblate + [\#5896](https://github.com/matrix-org/matrix-react-sdk/pull/5896) + * Fix sticky tags header in room list + [\#5895](https://github.com/matrix-org/matrix-react-sdk/pull/5895) + * Fix spaces filtering sometimes lagging behind or behaving oddly + [\#5893](https://github.com/matrix-org/matrix-react-sdk/pull/5893) + * Fix issue with spaces context switching looping and breaking + [\#5894](https://github.com/matrix-org/matrix-react-sdk/pull/5894) + * Improve RoomList render time when filtering + [\#5874](https://github.com/matrix-org/matrix-react-sdk/pull/5874) + * Avoid being stuck in a space + [\#5891](https://github.com/matrix-org/matrix-react-sdk/pull/5891) + * [Spaces] Context switching + [\#5795](https://github.com/matrix-org/matrix-react-sdk/pull/5795) + * Warn when you attempt to leave room that you are the only member of + [\#5415](https://github.com/matrix-org/matrix-react-sdk/pull/5415) + * Ensure PersistedElement are unmounted on application logout + [\#5884](https://github.com/matrix-org/matrix-react-sdk/pull/5884) + * Add missing space in seshat dialog and the corresponding string + [\#5866](https://github.com/matrix-org/matrix-react-sdk/pull/5866) + * A tiny change to make the Add existing rooms dialog a little nicer + [\#5885](https://github.com/matrix-org/matrix-react-sdk/pull/5885) + * Remove weird margin from the file panel + [\#5889](https://github.com/matrix-org/matrix-react-sdk/pull/5889) + * Trigger lazy loading when filtering using spaces + [\#5882](https://github.com/matrix-org/matrix-react-sdk/pull/5882) + * Fix typo in method call in add existing to space dialog + [\#5883](https://github.com/matrix-org/matrix-react-sdk/pull/5883) + * New Image View fixes/improvements + [\#5872](https://github.com/matrix-org/matrix-react-sdk/pull/5872) + * Limit voice recording length + [\#5871](https://github.com/matrix-org/matrix-react-sdk/pull/5871) + * Clean up add existing to space dialog and include DMs in it too + [\#5881](https://github.com/matrix-org/matrix-react-sdk/pull/5881) + * Fix unknown slash command error exploding + [\#5853](https://github.com/matrix-org/matrix-react-sdk/pull/5853) + * Switch to a spec conforming email validation Regexp + [\#5852](https://github.com/matrix-org/matrix-react-sdk/pull/5852) + * Cleanup unused state in MessageComposer + [\#5877](https://github.com/matrix-org/matrix-react-sdk/pull/5877) + * Pulse animation for voice messages recording state + [\#5869](https://github.com/matrix-org/matrix-react-sdk/pull/5869) + * Don't include invisible rooms in notify summary + [\#5875](https://github.com/matrix-org/matrix-react-sdk/pull/5875) + * Properly disable composer access when recording a voice message + [\#5870](https://github.com/matrix-org/matrix-react-sdk/pull/5870) + * Stabilise starting a DM with multiple people flow + [\#5862](https://github.com/matrix-org/matrix-react-sdk/pull/5862) + * Render msgOption only if showReadReceipts is enabled + [\#5864](https://github.com/matrix-org/matrix-react-sdk/pull/5864) + * Labs: Add quick/cheap "do not disturb" flag + [\#5873](https://github.com/matrix-org/matrix-react-sdk/pull/5873) + * Fix ReadReceipts animations + [\#5836](https://github.com/matrix-org/matrix-react-sdk/pull/5836) + * Add tooltips to message previews + [\#5859](https://github.com/matrix-org/matrix-react-sdk/pull/5859) + * IRC Layout fix layout spacing in replies + [\#5855](https://github.com/matrix-org/matrix-react-sdk/pull/5855) + * Move user to welcome_page if continuing with previous session + [\#5849](https://github.com/matrix-org/matrix-react-sdk/pull/5849) + * Improve image view + [\#5521](https://github.com/matrix-org/matrix-react-sdk/pull/5521) + * Add a button to reset personal encryption state during login + [\#5819](https://github.com/matrix-org/matrix-react-sdk/pull/5819) + * Fix js-sdk import in SlashCommands + [\#5850](https://github.com/matrix-org/matrix-react-sdk/pull/5850) + * Fix useRoomPowerLevels hook + [\#5854](https://github.com/matrix-org/matrix-react-sdk/pull/5854) + * Prevent state events being rendered with invalid state keys + [\#5851](https://github.com/matrix-org/matrix-react-sdk/pull/5851) + * Give server ACLs a name in 'roles & permissions' tab + [\#5838](https://github.com/matrix-org/matrix-react-sdk/pull/5838) + * Don't hide notification badge on the home space button as it has no menu + [\#5845](https://github.com/matrix-org/matrix-react-sdk/pull/5845) + * User Info hide disambiguation as we always show MXID anyway + [\#5843](https://github.com/matrix-org/matrix-react-sdk/pull/5843) + * Improve kick state to not show if the target was not joined to begin with + [\#5846](https://github.com/matrix-org/matrix-react-sdk/pull/5846) + * Fix space store wrongly switching to a non-space filter + [\#5844](https://github.com/matrix-org/matrix-react-sdk/pull/5844) + * Tweak appearance of invite reason + [\#5847](https://github.com/matrix-org/matrix-react-sdk/pull/5847) + * Update Inter font to v3.18 + [\#5840](https://github.com/matrix-org/matrix-react-sdk/pull/5840) + * Enable sharing historical keys on invite + [\#5839](https://github.com/matrix-org/matrix-react-sdk/pull/5839) + * Add ability to hide post-login encryption setup with customisation point + [\#5834](https://github.com/matrix-org/matrix-react-sdk/pull/5834) + * Use LaTeX and TeX delimiters by default + [\#5515](https://github.com/matrix-org/matrix-react-sdk/pull/5515) + * Clone author's deps fork for Netlify previews + [\#5837](https://github.com/matrix-org/matrix-react-sdk/pull/5837) + * Show drop file UI only if dragging a file + [\#5827](https://github.com/matrix-org/matrix-react-sdk/pull/5827) + * Ignore punctuation when filtering rooms + [\#5824](https://github.com/matrix-org/matrix-react-sdk/pull/5824) + * Resizable CallView + [\#5710](https://github.com/matrix-org/matrix-react-sdk/pull/5710) + +Changes in [3.18.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.18.0) (2021-04-12) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.18.0-rc.1...v3.18.0) + + * Upgrade to JS SDK 9.11.0 + * [Release] Tweak appearance of invite reason + [\#5848](https://github.com/matrix-org/matrix-react-sdk/pull/5848) + +Changes in [3.18.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.18.0-rc.1) (2021-04-07) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.17.0...v3.18.0-rc.1) + + * Upgrade to JS SDK 9.11.0-rc.1 + * Translations update from Weblate + [\#5832](https://github.com/matrix-org/matrix-react-sdk/pull/5832) + * Add fake fallback thumbnail URL for encrypted videos + [\#5826](https://github.com/matrix-org/matrix-react-sdk/pull/5826) + * Fix broken "Go to Home View" shortcut on macOS + [\#5818](https://github.com/matrix-org/matrix-react-sdk/pull/5818) + * Remove status area UI defects when in-call + [\#5828](https://github.com/matrix-org/matrix-react-sdk/pull/5828) + * Fix viewing invitations when the inviter has no avatar set + [\#5829](https://github.com/matrix-org/matrix-react-sdk/pull/5829) + * Restabilize room list ordering with prefiltering on spaces/communities + [\#5825](https://github.com/matrix-org/matrix-react-sdk/pull/5825) + * Show invite reasons + [\#5694](https://github.com/matrix-org/matrix-react-sdk/pull/5694) + * Require strong password in forgot password form + [\#5744](https://github.com/matrix-org/matrix-react-sdk/pull/5744) + * Attended transfer + [\#5798](https://github.com/matrix-org/matrix-react-sdk/pull/5798) + * Make user autocomplete query search beyond prefix + [\#5822](https://github.com/matrix-org/matrix-react-sdk/pull/5822) + * Add reset option for corrupted event index store + [\#5806](https://github.com/matrix-org/matrix-react-sdk/pull/5806) + * Prevent Re-request encryption keys from appearing under redacted messages + [\#5816](https://github.com/matrix-org/matrix-react-sdk/pull/5816) + * Keybindings follow up + [\#5815](https://github.com/matrix-org/matrix-react-sdk/pull/5815) + * Increase default visible tiles for room sublists + [\#5821](https://github.com/matrix-org/matrix-react-sdk/pull/5821) + * Change copy to point to native node modules docs in element desktop + [\#5817](https://github.com/matrix-org/matrix-react-sdk/pull/5817) + * Show waveform and timer in voice messages + [\#5801](https://github.com/matrix-org/matrix-react-sdk/pull/5801) + * Label unlabeled avatar button in event panel + [\#5585](https://github.com/matrix-org/matrix-react-sdk/pull/5585) + * Fix the theme engine breaking with some web theming extensions + [\#5810](https://github.com/matrix-org/matrix-react-sdk/pull/5810) + * Add /spoiler command + [\#5696](https://github.com/matrix-org/matrix-react-sdk/pull/5696) + * Don't specify sample rates for voice messages + [\#5802](https://github.com/matrix-org/matrix-react-sdk/pull/5802) + * Tweak security key error handling + [\#5812](https://github.com/matrix-org/matrix-react-sdk/pull/5812) + * Add user settings for warn before exit + [\#5793](https://github.com/matrix-org/matrix-react-sdk/pull/5793) + * Decouple key bindings from event handling + [\#5720](https://github.com/matrix-org/matrix-react-sdk/pull/5720) + * Fixing spaces papercuts + [\#5792](https://github.com/matrix-org/matrix-react-sdk/pull/5792) + * Share keys for historical messages when inviting users to encrypted rooms + [\#5763](https://github.com/matrix-org/matrix-react-sdk/pull/5763) + * Fix upload bar not populating when starting uploads + [\#5804](https://github.com/matrix-org/matrix-react-sdk/pull/5804) + * Fix crash on login when using social login + [\#5803](https://github.com/matrix-org/matrix-react-sdk/pull/5803) + * Convert AccessSecretStorageDialog to TypeScript + [\#5805](https://github.com/matrix-org/matrix-react-sdk/pull/5805) + * Tweak cross-signing copy + [\#5807](https://github.com/matrix-org/matrix-react-sdk/pull/5807) + * Fix password change popup message + [\#5791](https://github.com/matrix-org/matrix-react-sdk/pull/5791) + * View Source: make Event ID go below Event ID + [\#5790](https://github.com/matrix-org/matrix-react-sdk/pull/5790) + * Fix line numbers when missing trailing newline + [\#5800](https://github.com/matrix-org/matrix-react-sdk/pull/5800) + * Remember reply when switching rooms + [\#5796](https://github.com/matrix-org/matrix-react-sdk/pull/5796) + * Fix edge case with redaction grouper messing up continuations + [\#5797](https://github.com/matrix-org/matrix-react-sdk/pull/5797) + * Only show the ask anyway modal for explicit user lookup failures + [\#5785](https://github.com/matrix-org/matrix-react-sdk/pull/5785) + * Improve error reporting when EventIndex fails on a supported environment + [\#5787](https://github.com/matrix-org/matrix-react-sdk/pull/5787) + * Tweak and fix some space features + [\#5789](https://github.com/matrix-org/matrix-react-sdk/pull/5789) + * Support replying with a message command + [\#5686](https://github.com/matrix-org/matrix-react-sdk/pull/5686) + * Labs feature: Early implementation of voice messages + [\#5769](https://github.com/matrix-org/matrix-react-sdk/pull/5769) + +Changes in [3.17.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.17.0) (2021-03-29) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.17.0-rc.1...v3.17.0) + + * Upgrade to JS SDK 9.10.0 + * [Release] Tweak cross-signing copy + [\#5808](https://github.com/matrix-org/matrix-react-sdk/pull/5808) + * [Release] Fix crash on login when using social login + [\#5809](https://github.com/matrix-org/matrix-react-sdk/pull/5809) + * [Release] Fix edge case with redaction grouper messing up continuations + [\#5799](https://github.com/matrix-org/matrix-react-sdk/pull/5799) + +Changes in [3.17.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.17.0-rc.1) (2021-03-25) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0...v3.17.0-rc.1) + + * Upgrade to JS SDK 9.10.0-rc.1 + * Translations update from Weblate + [\#5788](https://github.com/matrix-org/matrix-react-sdk/pull/5788) + * Track next event [tile] over group boundaries + [\#5784](https://github.com/matrix-org/matrix-react-sdk/pull/5784) + * Fixing the minor UI issues in the email discovery + [\#5780](https://github.com/matrix-org/matrix-react-sdk/pull/5780) + * Don't overwrite callback with undefined if no customization provided + [\#5783](https://github.com/matrix-org/matrix-react-sdk/pull/5783) + * Fix redaction event list summaries breaking sender profiles + [\#5781](https://github.com/matrix-org/matrix-react-sdk/pull/5781) + * Fix CIDER formatting buttons on Safari + [\#5782](https://github.com/matrix-org/matrix-react-sdk/pull/5782) + * Improve discovery of rooms in a space + [\#5776](https://github.com/matrix-org/matrix-react-sdk/pull/5776) + * Spaces improve creation journeys + [\#5777](https://github.com/matrix-org/matrix-react-sdk/pull/5777) + * Make buttons in verify dialog respect the system font + [\#5778](https://github.com/matrix-org/matrix-react-sdk/pull/5778) + * Collapse redactions into an event list summary + [\#5728](https://github.com/matrix-org/matrix-react-sdk/pull/5728) + * Added invite option to room's context menu + [\#5648](https://github.com/matrix-org/matrix-react-sdk/pull/5648) + * Add an optional config option to make the welcome page the login page + [\#5658](https://github.com/matrix-org/matrix-react-sdk/pull/5658) + * Fix username showing instead of display name in Jitsi widgets + [\#5770](https://github.com/matrix-org/matrix-react-sdk/pull/5770) + * Convert a bunch more js-sdk imports to absolute paths + [\#5774](https://github.com/matrix-org/matrix-react-sdk/pull/5774) + * Remove forgotten rooms from the room list once forgotten + [\#5775](https://github.com/matrix-org/matrix-react-sdk/pull/5775) + * Log error when failing to list usermedia devices + [\#5771](https://github.com/matrix-org/matrix-react-sdk/pull/5771) + * Fix weird timeline jumps + [\#5772](https://github.com/matrix-org/matrix-react-sdk/pull/5772) + * Replace type declaration in Registration.tsx + [\#5773](https://github.com/matrix-org/matrix-react-sdk/pull/5773) + * Add possibility to delay rageshake persistence in app startup + [\#5767](https://github.com/matrix-org/matrix-react-sdk/pull/5767) + * Fix left panel resizing and lower min-width improving flexibility + [\#5764](https://github.com/matrix-org/matrix-react-sdk/pull/5764) + * Work around more cases where a rageshake server might not be present + [\#5766](https://github.com/matrix-org/matrix-react-sdk/pull/5766) + * Iterate space panel visually and functionally + [\#5761](https://github.com/matrix-org/matrix-react-sdk/pull/5761) + * Make some dispatches async + [\#5765](https://github.com/matrix-org/matrix-react-sdk/pull/5765) + * fix: make room directory correct when using a homeserver with explicit port + [\#5762](https://github.com/matrix-org/matrix-react-sdk/pull/5762) + * Hangup all calls on logout + [\#5756](https://github.com/matrix-org/matrix-react-sdk/pull/5756) + * Remove now-unused assets and CSS from CompleteSecurity step + [\#5757](https://github.com/matrix-org/matrix-react-sdk/pull/5757) + * Add details and summary to allowed HTML tags + [\#5760](https://github.com/matrix-org/matrix-react-sdk/pull/5760) + * Support a media handling customisation endpoint + [\#5714](https://github.com/matrix-org/matrix-react-sdk/pull/5714) + * Edit button on View Source dialog that takes you to devtools -> + SendCustomEvent + [\#5718](https://github.com/matrix-org/matrix-react-sdk/pull/5718) + * Show room alias in plain/formatted body + [\#5748](https://github.com/matrix-org/matrix-react-sdk/pull/5748) + * Allow pills on the beginning of a part string + [\#5754](https://github.com/matrix-org/matrix-react-sdk/pull/5754) + * [SK-3] Decorate easy components with replaceableComponent + [\#5734](https://github.com/matrix-org/matrix-react-sdk/pull/5734) + * Use fsync in reskindex to ensure file is written to disk + [\#5753](https://github.com/matrix-org/matrix-react-sdk/pull/5753) + * Remove unused common CSS classes + [\#5752](https://github.com/matrix-org/matrix-react-sdk/pull/5752) + * Rebuild space previews with new designs + [\#5751](https://github.com/matrix-org/matrix-react-sdk/pull/5751) + * Rework cross-signing login flow + [\#5727](https://github.com/matrix-org/matrix-react-sdk/pull/5727) + * Change read receipt drift to be non-fractional + [\#5745](https://github.com/matrix-org/matrix-react-sdk/pull/5745) + +Changes in [3.16.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0) (2021-03-15) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0-rc.2...v3.16.0) + + * Upgrade to JS SDK 9.9.0 + * [Release] Change read receipt drift to be non-fractional + [\#5746](https://github.com/matrix-org/matrix-react-sdk/pull/5746) + * [Release] Properly gate SpaceRoomView behind labs + [\#5750](https://github.com/matrix-org/matrix-react-sdk/pull/5750) + +Changes in [3.16.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0-rc.2) (2021-03-10) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0-rc.1...v3.16.0-rc.2) + + * Fixed incorrect build output in rc.1 + +Changes in [3.16.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0-rc.1) (2021-03-10) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0...v3.16.0-rc.1) + + * Upgrade to JS SDK 9.9.0-rc.1 + * Translations update from Weblate + [\#5743](https://github.com/matrix-org/matrix-react-sdk/pull/5743) + * Document behaviour of showReadReceipts=false for sent receipts + [\#5739](https://github.com/matrix-org/matrix-react-sdk/pull/5739) + * Tweak sent marker code style + [\#5741](https://github.com/matrix-org/matrix-react-sdk/pull/5741) + * Fix sent markers disappearing for edits/reactions + [\#5737](https://github.com/matrix-org/matrix-react-sdk/pull/5737) + * Ignore to-device decryption in the room list store + [\#5740](https://github.com/matrix-org/matrix-react-sdk/pull/5740) + * Spaces suggested rooms support + [\#5736](https://github.com/matrix-org/matrix-react-sdk/pull/5736) + * Add tooltips to sent/sending receipts + [\#5738](https://github.com/matrix-org/matrix-react-sdk/pull/5738) + * Remove a bunch of useless 'use strict' definitions + [\#5735](https://github.com/matrix-org/matrix-react-sdk/pull/5735) + * [SK-1] Fix types for replaceableComponent + [\#5732](https://github.com/matrix-org/matrix-react-sdk/pull/5732) + * [SK-2] Make debugging skinning problems easier + [\#5733](https://github.com/matrix-org/matrix-react-sdk/pull/5733) + * Support sending invite reasons with /invite command + [\#5695](https://github.com/matrix-org/matrix-react-sdk/pull/5695) + * Fix clicking on the avatar for opening member info requires pixel-perfect + accuracy + [\#5717](https://github.com/matrix-org/matrix-react-sdk/pull/5717) + * Display decrypted and encrypted event source on the same dialog + [\#5713](https://github.com/matrix-org/matrix-react-sdk/pull/5713) + * Fix units of TURN server expiry time + [\#5730](https://github.com/matrix-org/matrix-react-sdk/pull/5730) + * Display room name in pills instead of address + [\#5624](https://github.com/matrix-org/matrix-react-sdk/pull/5624) + * Refresh UI for file uploads + [\#5723](https://github.com/matrix-org/matrix-react-sdk/pull/5723) + * UI refresh for uploaded files + [\#5719](https://github.com/matrix-org/matrix-react-sdk/pull/5719) + * Improve message sending states to match new designs + [\#5699](https://github.com/matrix-org/matrix-react-sdk/pull/5699) + * Add clipboard write permission for widgets + [\#5725](https://github.com/matrix-org/matrix-react-sdk/pull/5725) + * Fix widget resizing + [\#5722](https://github.com/matrix-org/matrix-react-sdk/pull/5722) + * Option for audio streaming + [\#5707](https://github.com/matrix-org/matrix-react-sdk/pull/5707) + * Show a specific error for hs_disabled + [\#5576](https://github.com/matrix-org/matrix-react-sdk/pull/5576) + * Add Edge to the targets list + [\#5721](https://github.com/matrix-org/matrix-react-sdk/pull/5721) + * File drop UI fixes and improvements + [\#5505](https://github.com/matrix-org/matrix-react-sdk/pull/5505) + * Fix Bottom border of state counters is white on the dark theme + [\#5715](https://github.com/matrix-org/matrix-react-sdk/pull/5715) + * Trim spurious whitespace of nicknames + [\#5332](https://github.com/matrix-org/matrix-react-sdk/pull/5332) + * Ensure HostSignupDialog border colour matches light theme + [\#5716](https://github.com/matrix-org/matrix-react-sdk/pull/5716) + * Don't place another call if there's already one ongoing + [\#5712](https://github.com/matrix-org/matrix-react-sdk/pull/5712) + * Space room hierarchies + [\#5706](https://github.com/matrix-org/matrix-react-sdk/pull/5706) + * Iterate Space view and right panel + [\#5705](https://github.com/matrix-org/matrix-react-sdk/pull/5705) + * Add a scroll to bottom on message sent setting + [\#5692](https://github.com/matrix-org/matrix-react-sdk/pull/5692) + * Add .tmp files to gitignore + [\#5708](https://github.com/matrix-org/matrix-react-sdk/pull/5708) + * Initial Space Room View and Creation UX + [\#5704](https://github.com/matrix-org/matrix-react-sdk/pull/5704) + * Add multi language spell check + [\#5452](https://github.com/matrix-org/matrix-react-sdk/pull/5452) + * Fix tetris effect (holes) in read receipts + [\#5697](https://github.com/matrix-org/matrix-react-sdk/pull/5697) + * Fixed edit for markdown images + [\#5703](https://github.com/matrix-org/matrix-react-sdk/pull/5703) + * Iterate Space Panel + [\#5702](https://github.com/matrix-org/matrix-react-sdk/pull/5702) + * Fix read receipts for compact layout + [\#5700](https://github.com/matrix-org/matrix-react-sdk/pull/5700) + * Space Store and Space Panel for Room List filtering + [\#5689](https://github.com/matrix-org/matrix-react-sdk/pull/5689) + * Log when turn creds expire + [\#5691](https://github.com/matrix-org/matrix-react-sdk/pull/5691) + * Null check for maxHeight in call view + [\#5690](https://github.com/matrix-org/matrix-react-sdk/pull/5690) + * Autocomplete invited users + [\#5687](https://github.com/matrix-org/matrix-react-sdk/pull/5687) + * Add send message button + [\#5535](https://github.com/matrix-org/matrix-react-sdk/pull/5535) + * Move call buttons to the room header + [\#5693](https://github.com/matrix-org/matrix-react-sdk/pull/5693) + * Use the default SSSS key if the default is set + [\#5638](https://github.com/matrix-org/matrix-react-sdk/pull/5638) + * Initial Spaces feature flag + [\#5668](https://github.com/matrix-org/matrix-react-sdk/pull/5668) + * Clean up code edge cases and add helpers + [\#5667](https://github.com/matrix-org/matrix-react-sdk/pull/5667) + * Clean up widgets when leaving the room + [\#5684](https://github.com/matrix-org/matrix-react-sdk/pull/5684) + * Fix read receipts? + [\#5567](https://github.com/matrix-org/matrix-react-sdk/pull/5567) + * Fix MAU usage alerts + [\#5678](https://github.com/matrix-org/matrix-react-sdk/pull/5678) + +Changes in [3.15.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.15.0) (2021-03-01) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0-rc.1...v3.15.0) + +## Security notice + +matrix-react-sdk 3.15.0 fixes a moderate severity issue (CVE-2021-21320) where +the user content sandbox can be abused to trick users into opening unexpected +documents after several user interactions. The content can be opened with a +`blob` origin from the Matrix client, so it is possible for a malicious document +to access user messages and secrets. Thanks to @keerok for responsibly +disclosing this via Matrix's Security Disclosure Policy. + +## All changes + + * Upgrade to JS SDK 9.8.0 + +Changes in [3.15.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.15.0-rc.1) (2021-02-24) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.14.0...v3.15.0-rc.1) + + * Upgrade to JS SDK 9.8.0-rc.1 + * Translations update from Weblate + [\#5683](https://github.com/matrix-org/matrix-react-sdk/pull/5683) + * Fix object diffing when objects have different keys + [\#5681](https://github.com/matrix-org/matrix-react-sdk/pull/5681) + * Add if it's missing + [\#5673](https://github.com/matrix-org/matrix-react-sdk/pull/5673) + * Add email only if the verification is complete + [\#5629](https://github.com/matrix-org/matrix-react-sdk/pull/5629) + * Fix portrait videocalls + [\#5676](https://github.com/matrix-org/matrix-react-sdk/pull/5676) + * Tweak code block icon positions + [\#5643](https://github.com/matrix-org/matrix-react-sdk/pull/5643) + * Revert "Improve URL preview formatting and image upload thumbnail size" + [\#5677](https://github.com/matrix-org/matrix-react-sdk/pull/5677) + * Fix context menu leaving visible area + [\#5644](https://github.com/matrix-org/matrix-react-sdk/pull/5644) + * Jitsi conferences names, take 3 + [\#5675](https://github.com/matrix-org/matrix-react-sdk/pull/5675) + * Update isUserOnDarkTheme to take use_system_theme in account + [\#5670](https://github.com/matrix-org/matrix-react-sdk/pull/5670) + * Discard some dead code + [\#5665](https://github.com/matrix-org/matrix-react-sdk/pull/5665) + * Add developer tool to explore and edit settings + [\#5664](https://github.com/matrix-org/matrix-react-sdk/pull/5664) + * Use and create new room helpers + [\#5663](https://github.com/matrix-org/matrix-react-sdk/pull/5663) + * Clear message previews when the maximum limit is reached for history + [\#5661](https://github.com/matrix-org/matrix-react-sdk/pull/5661) + * VoIP virtual rooms, mk II + [\#5639](https://github.com/matrix-org/matrix-react-sdk/pull/5639) + * Disable chat effects when reduced motion preferred + [\#5660](https://github.com/matrix-org/matrix-react-sdk/pull/5660) + * Improve URL preview formatting and image upload thumbnail size + [\#5637](https://github.com/matrix-org/matrix-react-sdk/pull/5637) + * Fix border radius when the panel is collapsed + [\#5641](https://github.com/matrix-org/matrix-react-sdk/pull/5641) + * Use a more generic layout setting - useIRCLayout → layout + [\#5571](https://github.com/matrix-org/matrix-react-sdk/pull/5571) + * Remove redundant lockOrigin parameter from usercontent + [\#5657](https://github.com/matrix-org/matrix-react-sdk/pull/5657) + * Set ICE candidate pool size option + [\#5655](https://github.com/matrix-org/matrix-react-sdk/pull/5655) + * Prepare to encrypt when a call arrives + [\#5654](https://github.com/matrix-org/matrix-react-sdk/pull/5654) + * Use config for host signup branding + [\#5650](https://github.com/matrix-org/matrix-react-sdk/pull/5650) + * Use randomly generated conference names for Jitsi + [\#5649](https://github.com/matrix-org/matrix-react-sdk/pull/5649) + * Modified regex to account for an immediate new line after slash commands + [\#5647](https://github.com/matrix-org/matrix-react-sdk/pull/5647) + * Fix codeblock scrollbar color for non-Firefox + [\#5642](https://github.com/matrix-org/matrix-react-sdk/pull/5642) + * Fix codeblock scrollbar colors + [\#5630](https://github.com/matrix-org/matrix-react-sdk/pull/5630) + * Added loading and disabled the button while searching for server + [\#5634](https://github.com/matrix-org/matrix-react-sdk/pull/5634) + +Changes in [3.14.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.14.0) (2021-02-16) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.14.0-rc.1...v3.14.0) + + * Upgrade to JS SDK 9.7.0 + * [Release] Use config for host signup branding + [\#5651](https://github.com/matrix-org/matrix-react-sdk/pull/5651) + +Changes in [3.14.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.14.0-rc.1) (2021-02-10) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.13.1...v3.14.0-rc.1) + + * Upgrade to JS SDK 9.7.0-rc.1 + * Translations update from Weblate + [\#5636](https://github.com/matrix-org/matrix-react-sdk/pull/5636) + * Add host signup modal with iframe + [\#5450](https://github.com/matrix-org/matrix-react-sdk/pull/5450) + * Fix duplication of codeblock elements + [\#5633](https://github.com/matrix-org/matrix-react-sdk/pull/5633) + * Handle undefined call stats + [\#5632](https://github.com/matrix-org/matrix-react-sdk/pull/5632) + * Avoid delayed displaying of sources in source picker + [\#5631](https://github.com/matrix-org/matrix-react-sdk/pull/5631) + * Give breadcrumbs toolbar an accessibility label. + [\#5628](https://github.com/matrix-org/matrix-react-sdk/pull/5628) + * Fix the %s in logs + [\#5627](https://github.com/matrix-org/matrix-react-sdk/pull/5627) + * Fix jumpy notifications settings UI + [\#5625](https://github.com/matrix-org/matrix-react-sdk/pull/5625) + * Improve displaying of code blocks + [\#5559](https://github.com/matrix-org/matrix-react-sdk/pull/5559) + * Fix desktop Matrix screen sharing and add a screen/window picker + [\#5525](https://github.com/matrix-org/matrix-react-sdk/pull/5525) + * Call "MatrixClientPeg.get()" only once in method "findOverrideMuteRule" + [\#5498](https://github.com/matrix-org/matrix-react-sdk/pull/5498) + * Close current modal when session is logged out + [\#5616](https://github.com/matrix-org/matrix-react-sdk/pull/5616) + * Switch room explorer list to CSS grid + [\#5551](https://github.com/matrix-org/matrix-react-sdk/pull/5551) + * Improve SSO login start screen and 3pid invite handling somewhat + [\#5622](https://github.com/matrix-org/matrix-react-sdk/pull/5622) + * Don't jump to bottom on reaction + [\#5621](https://github.com/matrix-org/matrix-react-sdk/pull/5621) + * Fix several profile settings oddities + [\#5620](https://github.com/matrix-org/matrix-react-sdk/pull/5620) + * Add option to hide the stickers button in the composer + [\#5530](https://github.com/matrix-org/matrix-react-sdk/pull/5530) + * Fix confusing right panel button behaviour + [\#5598](https://github.com/matrix-org/matrix-react-sdk/pull/5598) + * Fix jumping timestamp if hovering a message with e2e indicator bar + [\#5601](https://github.com/matrix-org/matrix-react-sdk/pull/5601) + * Fix avatar and trash alignment + [\#5614](https://github.com/matrix-org/matrix-react-sdk/pull/5614) + * Fix z-index of stickerpicker + [\#5617](https://github.com/matrix-org/matrix-react-sdk/pull/5617) + * Fix permalink via parsing for rooms + [\#5615](https://github.com/matrix-org/matrix-react-sdk/pull/5615) + * Fix "Terms and Conditions" checkbox alignment + [\#5613](https://github.com/matrix-org/matrix-react-sdk/pull/5613) + * Fix flair height after accent changes + [\#5611](https://github.com/matrix-org/matrix-react-sdk/pull/5611) + * Iterate Social Logins work around edge cases and branding + [\#5609](https://github.com/matrix-org/matrix-react-sdk/pull/5609) + * Lock widget room ID when added + [\#5607](https://github.com/matrix-org/matrix-react-sdk/pull/5607) + * Better errors for SSO failures + [\#5605](https://github.com/matrix-org/matrix-react-sdk/pull/5605) + * Increase language search bar width + [\#5549](https://github.com/matrix-org/matrix-react-sdk/pull/5549) + * Scroll to bottom on message_sent + [\#5565](https://github.com/matrix-org/matrix-react-sdk/pull/5565) + * Fix new rooms being titled 'Empty Room' + [\#5587](https://github.com/matrix-org/matrix-react-sdk/pull/5587) + * Fix saving the collapsed state of the left panel + [\#5593](https://github.com/matrix-org/matrix-react-sdk/pull/5593) + * Fix app-url hint in the e2e-test run script output + [\#5600](https://github.com/matrix-org/matrix-react-sdk/pull/5600) + * Fix RoomView re-mounting breaking peeking + [\#5602](https://github.com/matrix-org/matrix-react-sdk/pull/5602) + * Tweak a few room ID checks + [\#5592](https://github.com/matrix-org/matrix-react-sdk/pull/5592) + * Remove pills from event permalinks with text + [\#5575](https://github.com/matrix-org/matrix-react-sdk/pull/5575) + +Changes in [3.13.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.13.1) (2021-02-04) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.13.0...v3.13.1) + + * [Release] Fix z-index of stickerpicker + [\#5618](https://github.com/matrix-org/matrix-react-sdk/pull/5618) + +Changes in [3.13.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.13.0) (2021-02-03) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.13.0-rc.1...v3.13.0) + + * Upgrade to JS SDK 9.6.0 + * [Release] Fix flair height after accent changes + [\#5612](https://github.com/matrix-org/matrix-react-sdk/pull/5612) + * [Release] Iterate Social Logins work around edge cases and branding + [\#5610](https://github.com/matrix-org/matrix-react-sdk/pull/5610) + * [Release] Lock widget room ID when added + [\#5608](https://github.com/matrix-org/matrix-react-sdk/pull/5608) + * [Release] Better errors for SSO failures + [\#5606](https://github.com/matrix-org/matrix-react-sdk/pull/5606) + * [Release] Fix RoomView re-mounting breaking peeking + [\#5603](https://github.com/matrix-org/matrix-react-sdk/pull/5603) + +Changes in [3.13.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.13.0-rc.1) (2021-01-29) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.12.1...v3.13.0-rc.1) + + * Upgrade to JS SDK 9.6.0-rc.1 + * Translations update from Weblate + [\#5597](https://github.com/matrix-org/matrix-react-sdk/pull/5597) + * Support managed hybrid widgets from config + [\#5596](https://github.com/matrix-org/matrix-react-sdk/pull/5596) + * Add managed hybrid call widgets when supported + [\#5594](https://github.com/matrix-org/matrix-react-sdk/pull/5594) + * Tweak mobile guide toast copy + [\#5595](https://github.com/matrix-org/matrix-react-sdk/pull/5595) + * Improve SSO auth flow + [\#5578](https://github.com/matrix-org/matrix-react-sdk/pull/5578) + * Add optional mobile guide toast + [\#5586](https://github.com/matrix-org/matrix-react-sdk/pull/5586) + * Fix invisible text after logging out in the dark theme + [\#5588](https://github.com/matrix-org/matrix-react-sdk/pull/5588) + * Fix escape for cancelling replies + [\#5591](https://github.com/matrix-org/matrix-react-sdk/pull/5591) + * Update widget-api to beta.12 + [\#5589](https://github.com/matrix-org/matrix-react-sdk/pull/5589) + * Add commands for DM conversion + [\#5540](https://github.com/matrix-org/matrix-react-sdk/pull/5540) + * Run a UI refresh over the OIDC Exchange confirmation dialog + [\#5580](https://github.com/matrix-org/matrix-react-sdk/pull/5580) + * Allow stickerpickers the legacy "visibility" capability + [\#5581](https://github.com/matrix-org/matrix-react-sdk/pull/5581) + * Hide local video if it is muted + [\#5529](https://github.com/matrix-org/matrix-react-sdk/pull/5529) + * Don't use name width in reply thread for IRC layout + [\#5518](https://github.com/matrix-org/matrix-react-sdk/pull/5518) + * Update code_style.md + [\#5554](https://github.com/matrix-org/matrix-react-sdk/pull/5554) + * Fix Czech capital letters like ŠČŘ... + [\#5569](https://github.com/matrix-org/matrix-react-sdk/pull/5569) + * Add optional search shortcut + [\#5548](https://github.com/matrix-org/matrix-react-sdk/pull/5548) + * Fix Sudden 'find a room' UI shows up when the only room moves to favourites + [\#5584](https://github.com/matrix-org/matrix-react-sdk/pull/5584) + * Increase PersistedElement's z-index + [\#5568](https://github.com/matrix-org/matrix-react-sdk/pull/5568) + * Remove check that prevents Jitsi widgets from being unpinned + [\#5582](https://github.com/matrix-org/matrix-react-sdk/pull/5582) + * Fix Jitsi widgets causing localized tile crashes + [\#5583](https://github.com/matrix-org/matrix-react-sdk/pull/5583) + * Log candidates for calls + [\#5573](https://github.com/matrix-org/matrix-react-sdk/pull/5573) + * Upgrade deps 2021-01 + [\#5579](https://github.com/matrix-org/matrix-react-sdk/pull/5579) + * Fix "Continuing without email" dialog bug + [\#5566](https://github.com/matrix-org/matrix-react-sdk/pull/5566) + * Require registration for verification actions + [\#5574](https://github.com/matrix-org/matrix-react-sdk/pull/5574) + * Don't play the hangup sound when the call is answered from elsewhere + [\#5572](https://github.com/matrix-org/matrix-react-sdk/pull/5572) + * Move to newer base image for end-to-end tests + [\#5570](https://github.com/matrix-org/matrix-react-sdk/pull/5570) + * Update widgets in the room upon join + [\#5564](https://github.com/matrix-org/matrix-react-sdk/pull/5564) + * Update AuxPanel and related buttons when widgets change or on reload + [\#5563](https://github.com/matrix-org/matrix-react-sdk/pull/5563) + * Add VoIP user mapper + [\#5560](https://github.com/matrix-org/matrix-react-sdk/pull/5560) + * Improve styling of SSO Buttons for multiple IdPs + [\#5558](https://github.com/matrix-org/matrix-react-sdk/pull/5558) + * Fixes for the general tab in the room dialog + [\#5522](https://github.com/matrix-org/matrix-react-sdk/pull/5522) + * fix issue 16226 to allow switching back to default HS. + [\#5561](https://github.com/matrix-org/matrix-react-sdk/pull/5561) + * Support room-defined widget layouts + [\#5553](https://github.com/matrix-org/matrix-react-sdk/pull/5553) + * Change a bunch of strings from Recovery Key/Phrase to Security Key/Phrase + [\#5533](https://github.com/matrix-org/matrix-react-sdk/pull/5533) + * Give a bigger target area to AppsDrawer vertical resizer + [\#5557](https://github.com/matrix-org/matrix-react-sdk/pull/5557) + * Fix minimized left panel avatar alignment + [\#5493](https://github.com/matrix-org/matrix-react-sdk/pull/5493) + * Ensure component index has been written before renaming + [\#5556](https://github.com/matrix-org/matrix-react-sdk/pull/5556) + * Fixed continue button while selecting home-server + [\#5552](https://github.com/matrix-org/matrix-react-sdk/pull/5552) + * Wire up MSC2931 widget navigation + [\#5527](https://github.com/matrix-org/matrix-react-sdk/pull/5527) + * Various fixes for Bridge Info page (MSC2346) + [\#5454](https://github.com/matrix-org/matrix-react-sdk/pull/5454) + * Use room-specific listeners for message preview and community prototype + [\#5547](https://github.com/matrix-org/matrix-react-sdk/pull/5547) + * Fix some misc. React warnings when viewing timeline + [\#5546](https://github.com/matrix-org/matrix-react-sdk/pull/5546) + * Use device storage for allowed widgets if account data not supported + [\#5544](https://github.com/matrix-org/matrix-react-sdk/pull/5544) + * Fix incoming call box on dark theme + [\#5542](https://github.com/matrix-org/matrix-react-sdk/pull/5542) + * Convert DMRoomMap to typescript + [\#5541](https://github.com/matrix-org/matrix-react-sdk/pull/5541) + * Add in-call dialpad for DTMF sending + [\#5532](https://github.com/matrix-org/matrix-react-sdk/pull/5532) + +Changes in [3.12.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.12.1) (2021-01-26) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.12.0...v3.12.1) + + * Upgrade to JS SDK 9.5.1 + +Changes in [3.12.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.12.0) (2021-01-18) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.12.0-rc.1...v3.12.0) + + * Upgrade to JS SDK 9.5.0 + * Fix incoming call box on dark theme + [\#5543](https://github.com/matrix-org/matrix-react-sdk/pull/5543) + +Changes in [3.12.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.12.0-rc.1) (2021-01-13) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.11.1...v3.12.0-rc.1) + + * Upgrade to JS SDK 9.5.0-rc.1 + * Fix soft crash on soft logout page + [\#5539](https://github.com/matrix-org/matrix-react-sdk/pull/5539) + * Translations update from Weblate + [\#5538](https://github.com/matrix-org/matrix-react-sdk/pull/5538) + * Run TypeScript tests + [\#5537](https://github.com/matrix-org/matrix-react-sdk/pull/5537) + * Add a basic widget explorer to devtools (per-room) + [\#5528](https://github.com/matrix-org/matrix-react-sdk/pull/5528) + * Add to security key field + [\#5534](https://github.com/matrix-org/matrix-react-sdk/pull/5534) + * Fix avatar upload prompt/tooltip floating wrong and permissions + [\#5526](https://github.com/matrix-org/matrix-react-sdk/pull/5526) + * Add a dialpad UI for PSTN lookup + [\#5523](https://github.com/matrix-org/matrix-react-sdk/pull/5523) + * Basic call transfer initiation support + [\#5494](https://github.com/matrix-org/matrix-react-sdk/pull/5494) + * Fix #15988 + [\#5524](https://github.com/matrix-org/matrix-react-sdk/pull/5524) + * Bump node-notifier from 8.0.0 to 8.0.1 + [\#5520](https://github.com/matrix-org/matrix-react-sdk/pull/5520) + * Use TypeScript source for development, swap to build during release + [\#5503](https://github.com/matrix-org/matrix-react-sdk/pull/5503) + * Look for emoji in the body that will be displayed + [\#5517](https://github.com/matrix-org/matrix-react-sdk/pull/5517) + * Bump ini from 1.3.5 to 1.3.7 + [\#5486](https://github.com/matrix-org/matrix-react-sdk/pull/5486) + * Recognise `*.element.io` links as Element permalinks + [\#5514](https://github.com/matrix-org/matrix-react-sdk/pull/5514) + * Fixes for call UI + [\#5509](https://github.com/matrix-org/matrix-react-sdk/pull/5509) + * Add a snowfall chat effect (with /snowfall command) + [\#5511](https://github.com/matrix-org/matrix-react-sdk/pull/5511) + * fireworks effect + [\#5507](https://github.com/matrix-org/matrix-react-sdk/pull/5507) + * Don't play call end sound for calls that never started + [\#5506](https://github.com/matrix-org/matrix-react-sdk/pull/5506) + * Add /tableflip slash command + [\#5485](https://github.com/matrix-org/matrix-react-sdk/pull/5485) + * Import from src in IncomingCallBox.tsx + [\#5504](https://github.com/matrix-org/matrix-react-sdk/pull/5504) + * Social Login support both https and mxc icons + [\#5499](https://github.com/matrix-org/matrix-react-sdk/pull/5499) + * Fix padding in confirmation email registration prompt + [\#5501](https://github.com/matrix-org/matrix-react-sdk/pull/5501) + * Fix room list help prompt alignment + [\#5500](https://github.com/matrix-org/matrix-react-sdk/pull/5500) + +Changes in [3.11.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.11.1) (2020-12-21) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.11.0...v3.11.1) + + * Upgrade JS SDK to 9.4.1 + +Changes in [3.11.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.11.0) (2020-12-21) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.11.0-rc.2...v3.11.0) + + * Upgrade JS SDK to 9.4.0 + * [Release] Look for emoji in the body that will be displayed + [\#5519](https://github.com/matrix-org/matrix-react-sdk/pull/5519) + * [Release] Recognise `*.element.io` links as Element permalinks + [\#5516](https://github.com/matrix-org/matrix-react-sdk/pull/5516) + * [Release] Fixes for call UI + [\#5513](https://github.com/matrix-org/matrix-react-sdk/pull/5513) + * [RELEASE] Add a snowfall chat effect (with /snowfall command) + [\#5512](https://github.com/matrix-org/matrix-react-sdk/pull/5512) + * [Release] Fix padding in confirmation email registration prompt + [\#5502](https://github.com/matrix-org/matrix-react-sdk/pull/5502) + +Changes in [3.11.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.11.0-rc.2) (2020-12-16) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.11.0-rc.1...v3.11.0-rc.2) + + * Upgrade JS SDK to 9.4.0-rc.2 + +Changes in [3.11.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.11.0-rc.1) (2020-12-16) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.10.0...v3.11.0-rc.1) + + * Upgrade JS SDK to 9.4.0-rc.1 + * Translations update from Weblate + [\#5497](https://github.com/matrix-org/matrix-react-sdk/pull/5497) + * Unregister from the dispatcher in CallHandler + [\#5495](https://github.com/matrix-org/matrix-react-sdk/pull/5495) + * Better adhere to MSC process + [\#5496](https://github.com/matrix-org/matrix-react-sdk/pull/5496) + * Use random pickle key on all platforms + [\#5483](https://github.com/matrix-org/matrix-react-sdk/pull/5483) + * Fix mx_MemberList icons + [\#5492](https://github.com/matrix-org/matrix-react-sdk/pull/5492) + * Convert InviteDialog to TypeScript + [\#5491](https://github.com/matrix-org/matrix-react-sdk/pull/5491) + * Add keyboard shortcut for emoji reactions + [\#5425](https://github.com/matrix-org/matrix-react-sdk/pull/5425) + * Run chat effects on events sent by widgets too + [\#5488](https://github.com/matrix-org/matrix-react-sdk/pull/5488) + * Fix being unable to pin widgets + [\#5487](https://github.com/matrix-org/matrix-react-sdk/pull/5487) + * Line 1 / 2 Support + [\#5468](https://github.com/matrix-org/matrix-react-sdk/pull/5468) + * Remove impossible labs feature: sending hidden read receipts + [\#5484](https://github.com/matrix-org/matrix-react-sdk/pull/5484) + * Fix height of Remote Video in call + [\#5456](https://github.com/matrix-org/matrix-react-sdk/pull/5456) + * Add UI for hold functionality + [\#5446](https://github.com/matrix-org/matrix-react-sdk/pull/5446) + * Allow SearchBox to expand to fill width + [\#5411](https://github.com/matrix-org/matrix-react-sdk/pull/5411) + * Use room alias in generated permalink for rooms + [\#5451](https://github.com/matrix-org/matrix-react-sdk/pull/5451) + * Only show confetti if the current room is receiving an appropriate event + [\#5482](https://github.com/matrix-org/matrix-react-sdk/pull/5482) + * Throttle RoomState.members handler to improve performance + [\#5481](https://github.com/matrix-org/matrix-react-sdk/pull/5481) + * Handle manual hs urls better for the server picker + [\#5477](https://github.com/matrix-org/matrix-react-sdk/pull/5477) + * Add Olm as a dev dependency for types + [\#5479](https://github.com/matrix-org/matrix-react-sdk/pull/5479) + * Hide Invite to this room CTA if no permission + [\#5476](https://github.com/matrix-org/matrix-react-sdk/pull/5476) + * Fix width of underline in server picker dialog + [\#5478](https://github.com/matrix-org/matrix-react-sdk/pull/5478) + * Fix confetti room unread state check + [\#5475](https://github.com/matrix-org/matrix-react-sdk/pull/5475) + * Show confetti in a chat room on command or emoji + [\#5140](https://github.com/matrix-org/matrix-react-sdk/pull/5140) + * Fix inverted settings default value + [\#5391](https://github.com/matrix-org/matrix-react-sdk/pull/5391) + * Improve usability of the Server Picker Dialog + [\#5474](https://github.com/matrix-org/matrix-react-sdk/pull/5474) + * Fix typos in some strings + [\#5473](https://github.com/matrix-org/matrix-react-sdk/pull/5473) + * Bump highlight.js from 10.1.2 to 10.4.1 + [\#5472](https://github.com/matrix-org/matrix-react-sdk/pull/5472) + * Remove old app test script path + [\#5471](https://github.com/matrix-org/matrix-react-sdk/pull/5471) + * add support for giving reason when redacting + [\#5260](https://github.com/matrix-org/matrix-react-sdk/pull/5260) + * Add support for Netlify to fetchdep script + [\#5469](https://github.com/matrix-org/matrix-react-sdk/pull/5469) + * Nest other layers inside on automation + [\#5467](https://github.com/matrix-org/matrix-react-sdk/pull/5467) + * Rebrand various CI scripts and modules + [\#5466](https://github.com/matrix-org/matrix-react-sdk/pull/5466) + * Add more widget sanity checking + [\#5462](https://github.com/matrix-org/matrix-react-sdk/pull/5462) + * Fix React complaining about unknown DOM props + [\#5465](https://github.com/matrix-org/matrix-react-sdk/pull/5465) + * Jump to home page when leaving a room + [\#5464](https://github.com/matrix-org/matrix-react-sdk/pull/5464) + * Fix SSO buttons for Social Logins + [\#5463](https://github.com/matrix-org/matrix-react-sdk/pull/5463) + * Social Login and login delight tweaks + [\#5426](https://github.com/matrix-org/matrix-react-sdk/pull/5426) + +Changes in [3.10.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.10.0) (2020-12-07) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.10.0-rc.1...v3.10.0) + + * Upgrade to JS SDK 9.3.0 + +Changes in [3.10.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.10.0-rc.1) (2020-12-02) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.9.0...v3.10.0-rc.1) + + * Upgrade to JS SDK 9.3.0-rc.1 + * Translations update from Weblate + [\#5461](https://github.com/matrix-org/matrix-react-sdk/pull/5461) + * Fix VoIP call plinth on dark theme + [\#5460](https://github.com/matrix-org/matrix-react-sdk/pull/5460) + * Add sanity checking around widget pinning + [\#5459](https://github.com/matrix-org/matrix-react-sdk/pull/5459) + * Update i18n for Appearance User Settings + [\#5457](https://github.com/matrix-org/matrix-react-sdk/pull/5457) + * Only show 'answered elsewhere' if we tried to answer too + [\#5455](https://github.com/matrix-org/matrix-react-sdk/pull/5455) + * Fixed Avatar for 3PID invites + [\#5442](https://github.com/matrix-org/matrix-react-sdk/pull/5442) + * Slightly better error if we can't capture user media + [\#5449](https://github.com/matrix-org/matrix-react-sdk/pull/5449) + * Make it possible in-code to hide rooms from the room list + [\#5445](https://github.com/matrix-org/matrix-react-sdk/pull/5445) + * Fix the stickerpicker + [\#5447](https://github.com/matrix-org/matrix-react-sdk/pull/5447) + * Add live password validation to change password dialog + [\#5436](https://github.com/matrix-org/matrix-react-sdk/pull/5436) + * LaTeX rendering in element-web using KaTeX + [\#5244](https://github.com/matrix-org/matrix-react-sdk/pull/5244) + * Add lifecycle customisation point after logout + [\#5448](https://github.com/matrix-org/matrix-react-sdk/pull/5448) + * Simplify UserMenu for Guests as they can't use most of the options + [\#5421](https://github.com/matrix-org/matrix-react-sdk/pull/5421) + * Fix known issues with modal widgets + [\#5444](https://github.com/matrix-org/matrix-react-sdk/pull/5444) + * Fix existing widgets not having approved capabilities for their function + [\#5443](https://github.com/matrix-org/matrix-react-sdk/pull/5443) + * Use the WidgetDriver to run OIDC requests + [\#5440](https://github.com/matrix-org/matrix-react-sdk/pull/5440) + * Add a customisation point for widget permissions and fix amnesia issues + [\#5439](https://github.com/matrix-org/matrix-react-sdk/pull/5439) + * Fix Widget event notification text including spurious space + [\#5441](https://github.com/matrix-org/matrix-react-sdk/pull/5441) + * Move call listener out of MatrixChat + [\#5438](https://github.com/matrix-org/matrix-react-sdk/pull/5438) + * New Look in-Call View + [\#5432](https://github.com/matrix-org/matrix-react-sdk/pull/5432) + * Support arbitrary widgets sticking to the screen + sending stickers + [\#5435](https://github.com/matrix-org/matrix-react-sdk/pull/5435) + * Auth typescripting and validation tweaks + [\#5433](https://github.com/matrix-org/matrix-react-sdk/pull/5433) + * Add new widget API actions for changing rooms and sending/receiving events + [\#5385](https://github.com/matrix-org/matrix-react-sdk/pull/5385) + * Revert room header click behaviour to opening room settings + [\#5434](https://github.com/matrix-org/matrix-react-sdk/pull/5434) + * Add option to send/edit a message with Ctrl + Enter / Command + Enter + [\#5160](https://github.com/matrix-org/matrix-react-sdk/pull/5160) + * Add Analytics instrumentation to the Homepage + [\#5409](https://github.com/matrix-org/matrix-react-sdk/pull/5409) + * Fix encrypted video playback in Chrome-based browsers + [\#5430](https://github.com/matrix-org/matrix-react-sdk/pull/5430) + * Add border-radius for video + [\#5333](https://github.com/matrix-org/matrix-react-sdk/pull/5333) + * Push name to the end, near text, in IRC layout + [\#5166](https://github.com/matrix-org/matrix-react-sdk/pull/5166) + * Disable notifications for the room you have recently been active in + [\#5325](https://github.com/matrix-org/matrix-react-sdk/pull/5325) + * Search through the list of unfiltered rooms rather than the rooms in the + state which are already filtered by the search text + [\#5331](https://github.com/matrix-org/matrix-react-sdk/pull/5331) + * Lighten blockquote colour in dark mode + [\#5353](https://github.com/matrix-org/matrix-react-sdk/pull/5353) + * Specify community description img must be mxc urls + [\#5364](https://github.com/matrix-org/matrix-react-sdk/pull/5364) + * Add keyboard shortcut to close the current conversation + [\#5253](https://github.com/matrix-org/matrix-react-sdk/pull/5253) + * Redirect user home from auth screens if they are already logged in + [\#5423](https://github.com/matrix-org/matrix-react-sdk/pull/5423) + +Changes in [3.9.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.9.0) (2020-11-23) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.9.0-rc.1...v3.9.0) + + * Upgrade JS SDK to 9.2.0 + * [Release] Fix encrypted video playback in Chrome-based browsers + [\#5431](https://github.com/matrix-org/matrix-react-sdk/pull/5431) + +Changes in [3.9.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.9.0-rc.1) (2020-11-18) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.8.0...v3.9.0-rc.1) + + * Upgrade JS SDK to 9.2.0-rc.1 + * Translations update from Weblate + [\#5429](https://github.com/matrix-org/matrix-react-sdk/pull/5429) + * Fix message search summary text + [\#5428](https://github.com/matrix-org/matrix-react-sdk/pull/5428) + * Shrink new room intro top margin to half for encryption bubble tile + [\#5427](https://github.com/matrix-org/matrix-react-sdk/pull/5427) + * Small delight tweaks to improve rough corners in the app + [\#5418](https://github.com/matrix-org/matrix-react-sdk/pull/5418) + * Fix DM logic to always pick a more reliable DM room + [\#5424](https://github.com/matrix-org/matrix-react-sdk/pull/5424) + * Update styling of the Analytics toast + [\#5408](https://github.com/matrix-org/matrix-react-sdk/pull/5408) + * Fix vertical centering of the Homepage and button layout + [\#5420](https://github.com/matrix-org/matrix-react-sdk/pull/5420) + * Fix BaseAvatar sometimes messing up and duplicating the url + [\#5422](https://github.com/matrix-org/matrix-react-sdk/pull/5422) + * Disable buttons when required by MSC2790 + [\#5412](https://github.com/matrix-org/matrix-react-sdk/pull/5412) + * Fix drag drop file to upload for Safari + [\#5414](https://github.com/matrix-org/matrix-react-sdk/pull/5414) + * Fix poorly i18n'd string + [\#5416](https://github.com/matrix-org/matrix-react-sdk/pull/5416) + * Fix the feedback not closing without feedback/countly + [\#5417](https://github.com/matrix-org/matrix-react-sdk/pull/5417) + * Fix New Room Intro invite to this room button + [\#5419](https://github.com/matrix-org/matrix-react-sdk/pull/5419) + * Change how we expose Role in User Info and hide in DMs + [\#5413](https://github.com/matrix-org/matrix-react-sdk/pull/5413) + * Disallow sending of empty messages + [\#5390](https://github.com/matrix-org/matrix-react-sdk/pull/5390) + * hide some validation tooltips if fields are valid. + [\#5403](https://github.com/matrix-org/matrix-react-sdk/pull/5403) + * Improvements around new room empty space interactions + [\#5398](https://github.com/matrix-org/matrix-react-sdk/pull/5398) + * Implement call hold + [\#5366](https://github.com/matrix-org/matrix-react-sdk/pull/5366) + * Fix Skeleton UI showing up when not intended. + [\#5407](https://github.com/matrix-org/matrix-react-sdk/pull/5407) + * Close context menu when user clicks the Home button + [\#5406](https://github.com/matrix-org/matrix-react-sdk/pull/5406) + * Skip e2ee warn logout prompt if user has no megolm sessions to lose + [\#5410](https://github.com/matrix-org/matrix-react-sdk/pull/5410) + * Allow country names to be translated + [\#5405](https://github.com/matrix-org/matrix-react-sdk/pull/5405) + * Support thirdparty lookup for phone numbers + [\#5396](https://github.com/matrix-org/matrix-react-sdk/pull/5396) + * Change "Password" to "New Password" + [\#5371](https://github.com/matrix-org/matrix-react-sdk/pull/5371) + * Add customisation point for dehydration key + [\#5397](https://github.com/matrix-org/matrix-react-sdk/pull/5397) + * Rebrand Riot -> Element in the permalink classes + [\#5386](https://github.com/matrix-org/matrix-react-sdk/pull/5386) + * Invite / Create DM UX tweaks + [\#5387](https://github.com/matrix-org/matrix-react-sdk/pull/5387) + * Tweaks to toasts and post-registration landing + [\#5383](https://github.com/matrix-org/matrix-react-sdk/pull/5383) + +Changes in [3.8.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.8.0) (2020-11-09) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.8.0-rc.1...v3.8.0) + + * Upgrade JS SDK to 9.1.0 + +Changes in [3.8.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.8.0-rc.1) (2020-11-04) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.7.1...v3.8.0-rc.1) + + * Upgrade JS SDK to 9.1.0-rc.1 + * Log when saving profile + [\#5394](https://github.com/matrix-org/matrix-react-sdk/pull/5394) + * Translations update from Weblate + [\#5395](https://github.com/matrix-org/matrix-react-sdk/pull/5395) + * Hide prompt to add email for notifications if 3pid ui feature is off + [\#5392](https://github.com/matrix-org/matrix-react-sdk/pull/5392) + * Fix room list message preview copy for hangup events + [\#5388](https://github.com/matrix-org/matrix-react-sdk/pull/5388) + * Track UISIs as Countly Events + [\#5382](https://github.com/matrix-org/matrix-react-sdk/pull/5382) + * Don't let users accidentally redact ACL events + [\#5384](https://github.com/matrix-org/matrix-react-sdk/pull/5384) + * Two more easy files to remove from eslintignore + [\#5378](https://github.com/matrix-org/matrix-react-sdk/pull/5378) + * Fix Widget OpenID Permissions for realsies + [\#5381](https://github.com/matrix-org/matrix-react-sdk/pull/5381) + * Fix regression with OpenID permissions on widgets + [\#5380](https://github.com/matrix-org/matrix-react-sdk/pull/5380) + * Fix room directory events happening in the wrong order for Funnels + [\#5379](https://github.com/matrix-org/matrix-react-sdk/pull/5379) + * Remove a couple more files from eslintignore + [\#5377](https://github.com/matrix-org/matrix-react-sdk/pull/5377) + * Fix countly method bindings and errors + [\#5376](https://github.com/matrix-org/matrix-react-sdk/pull/5376) + * Fix a bunch of silly lint errors + [\#5375](https://github.com/matrix-org/matrix-react-sdk/pull/5375) + * Typescript: ImageUtils + [\#5374](https://github.com/matrix-org/matrix-react-sdk/pull/5374) + * Convert AuxPanel to TypeScript + [\#5373](https://github.com/matrix-org/matrix-react-sdk/pull/5373) + * Only pass metrics if they exist otherwise Countly will be unhappy! + [\#5372](https://github.com/matrix-org/matrix-react-sdk/pull/5372) + * Fix CountlyAnalytics NPE on MatrixClientPeg + [\#5370](https://github.com/matrix-org/matrix-react-sdk/pull/5370) + * fix CountlyAnalytics canEnable on wrong target + [\#5369](https://github.com/matrix-org/matrix-react-sdk/pull/5369) + * Initial Countly work + [\#5365](https://github.com/matrix-org/matrix-react-sdk/pull/5365) + * Fix videos not playing in non-encrypted rooms + [\#5368](https://github.com/matrix-org/matrix-react-sdk/pull/5368) + * Fix custom tag layout which regressed in #5309 + [\#5367](https://github.com/matrix-org/matrix-react-sdk/pull/5367) + * Watch replyToEvent at RoomView to prevent races + [\#5360](https://github.com/matrix-org/matrix-react-sdk/pull/5360) + * Add a UI Feature flag for room history settings + [\#5362](https://github.com/matrix-org/matrix-react-sdk/pull/5362) + * Hide inline images when preference disabled + [\#5361](https://github.com/matrix-org/matrix-react-sdk/pull/5361) + * Fix React warning by moving handler to each button + [\#5359](https://github.com/matrix-org/matrix-react-sdk/pull/5359) + * Do not preload encrypted videos|images unless autoplay or thumbnailing is on + [\#5352](https://github.com/matrix-org/matrix-react-sdk/pull/5352) + * Fix theme variable passed to Jitsi + [\#5357](https://github.com/matrix-org/matrix-react-sdk/pull/5357) + * docs: added comment explanation + [\#5349](https://github.com/matrix-org/matrix-react-sdk/pull/5349) + * Modal Widgets - MSC2790 + [\#5252](https://github.com/matrix-org/matrix-react-sdk/pull/5252) + * Widgets fixes + [\#5350](https://github.com/matrix-org/matrix-react-sdk/pull/5350) + * Fix User Menu avatar colouring being based on wrong string + [\#5348](https://github.com/matrix-org/matrix-react-sdk/pull/5348) + * Support 'answered elsewhere' + [\#5345](https://github.com/matrix-org/matrix-react-sdk/pull/5345) + +Changes in [3.7.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.7.1) (2020-10-28) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.7.0...v3.7.1) + + * Upgrade JS SDK to 9.0.1 + * [Release] Fix theme variable passed to Jitsi + [\#5358](https://github.com/matrix-org/matrix-react-sdk/pull/5358) + * [Release] Widget fixes + [\#5351](https://github.com/matrix-org/matrix-react-sdk/pull/5351) + +Changes in [3.7.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.7.0) (2020-10-26) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.7.0-rc.2...v3.7.0) + + * Upgrade JS SDK to 9.0.0 + +Changes in [3.7.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.7.0-rc.2) (2020-10-21) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.7.0-rc.1...v3.7.0-rc.2) + + * Fix JS SDK dependency to use 9.0.0-rc.1 as intended + +Changes in [3.7.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.7.0-rc.1) (2020-10-21) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.6.1...v3.7.0-rc.1) + + * Upgrade JS SDK to 9.0.0-rc.1 + * Update Weblate URL + [\#5346](https://github.com/matrix-org/matrix-react-sdk/pull/5346) + * Translations update from Weblate + [\#5347](https://github.com/matrix-org/matrix-react-sdk/pull/5347) + * Left Panel Widget support + [\#5247](https://github.com/matrix-org/matrix-react-sdk/pull/5247) + * Pinned widgets work + [\#5266](https://github.com/matrix-org/matrix-react-sdk/pull/5266) + * Convert resizer to Typescript + [\#5343](https://github.com/matrix-org/matrix-react-sdk/pull/5343) + * Hide filtering microcopy when left panel is minimized + [\#5338](https://github.com/matrix-org/matrix-react-sdk/pull/5338) + * Skip editor confirmation of upgrades + [\#5344](https://github.com/matrix-org/matrix-react-sdk/pull/5344) + * Spec compliance, /search doesn't have to return results + [\#5337](https://github.com/matrix-org/matrix-react-sdk/pull/5337) + * Fix excessive hosting link padding + [\#5336](https://github.com/matrix-org/matrix-react-sdk/pull/5336) + * Adjust for new widget messaging APIs + [\#5341](https://github.com/matrix-org/matrix-react-sdk/pull/5341) + * Fix case where sublist context menu missed an update + [\#5339](https://github.com/matrix-org/matrix-react-sdk/pull/5339) + * Add analytics to VoIP + [\#5340](https://github.com/matrix-org/matrix-react-sdk/pull/5340) + * Fix Jitsi OpenIDC auth + [\#5334](https://github.com/matrix-org/matrix-react-sdk/pull/5334) + * Support rejecting calls + [\#5324](https://github.com/matrix-org/matrix-react-sdk/pull/5324) + * Don't show admin tooling if we're not in the room + [\#5330](https://github.com/matrix-org/matrix-react-sdk/pull/5330) + * Show Integrations error if iframe failed to load too + [\#5328](https://github.com/matrix-org/matrix-react-sdk/pull/5328) + * Add security customisation points + [\#5327](https://github.com/matrix-org/matrix-react-sdk/pull/5327) + * Discard all mx_fadable legacy cruft which is totally useless + [\#5326](https://github.com/matrix-org/matrix-react-sdk/pull/5326) + * Fix background-image: url(null) for backdrop filter + [\#5319](https://github.com/matrix-org/matrix-react-sdk/pull/5319) + * Make the ACL update message less noisy + [\#5316](https://github.com/matrix-org/matrix-react-sdk/pull/5316) + * Fix aspect ratio of avatar before clicking Save + [\#5318](https://github.com/matrix-org/matrix-react-sdk/pull/5318) + * Don't supply popout widgets with widget parameters + [\#5323](https://github.com/matrix-org/matrix-react-sdk/pull/5323) + * Changed rainbow algorithm + [\#5301](https://github.com/matrix-org/matrix-react-sdk/pull/5301) + * Renamed TagPanel and TagOrderStore + [\#5309](https://github.com/matrix-org/matrix-react-sdk/pull/5309) + * Fix/clarify boolean logic for reaction previews + [\#5321](https://github.com/matrix-org/matrix-react-sdk/pull/5321) + * Support glare for VoIP calls + [\#5311](https://github.com/matrix-org/matrix-react-sdk/pull/5311) + * Round of Typescript conversions + [\#5314](https://github.com/matrix-org/matrix-react-sdk/pull/5314) + * Fix broken rendering of Room Create when showHiddenEvents enabled + [\#5317](https://github.com/matrix-org/matrix-react-sdk/pull/5317) + * Improve LHS resize performance and tidy stale props&classes + [\#5313](https://github.com/matrix-org/matrix-react-sdk/pull/5313) + * event-index: Pass the user/device id pair when initializing the event index. + [\#5312](https://github.com/matrix-org/matrix-react-sdk/pull/5312) + * Fix various aspects of (jitsi) widgets + [\#5315](https://github.com/matrix-org/matrix-react-sdk/pull/5315) + * Fix rogue (partial) call bar + [\#5310](https://github.com/matrix-org/matrix-react-sdk/pull/5310) + * Rewrite call state machine + [\#5308](https://github.com/matrix-org/matrix-react-sdk/pull/5308) + * Convert `src/SecurityManager.js` to TypeScript + [\#5307](https://github.com/matrix-org/matrix-react-sdk/pull/5307) + * Fix templating for v1 jitsi widgets + [\#5305](https://github.com/matrix-org/matrix-react-sdk/pull/5305) + * Use new preparing event for widget communications + [\#5303](https://github.com/matrix-org/matrix-react-sdk/pull/5303) + * Fix parsing issue in event tile preview for appearance tab + [\#5302](https://github.com/matrix-org/matrix-react-sdk/pull/5302) + * Track replyToEvent along with Cider state & history + [\#5284](https://github.com/matrix-org/matrix-react-sdk/pull/5284) + * Roving Tab Index should not interfere with inputs + [\#5299](https://github.com/matrix-org/matrix-react-sdk/pull/5299) + * Visual tweaks from 2020-10-06 polishing + [\#5298](https://github.com/matrix-org/matrix-react-sdk/pull/5298) + * Convert auth lifecycle to TS, remove dead ILAG code + [\#5296](https://github.com/matrix-org/matrix-react-sdk/pull/5296) + +Changes in [3.6.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.6.1) (2020-10-20) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.6.0...v3.6.1) + + * [Release] Adjust for new widget messaging APIs + [\#5342](https://github.com/matrix-org/matrix-react-sdk/pull/5342) + * [Release] Fix Jitsi OpenIDC auth + [\#5335](https://github.com/matrix-org/matrix-react-sdk/pull/5335) + +Changes in [3.6.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.6.0) (2020-10-12) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.6.0-rc.1...v3.6.0) + + * Upgrade JS SDK to 8.5.0 + * [Release] Fix templating for v1 jitsi widgets + [\#5306](https://github.com/matrix-org/matrix-react-sdk/pull/5306) + * [Release] Use new preparing event for widget communications + [\#5304](https://github.com/matrix-org/matrix-react-sdk/pull/5304) + +Changes in [3.6.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.6.0-rc.1) (2020-10-07) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.5.0...v3.6.0-rc.1) + + * Upgrade JS SDK to 8.5.0-rc.1 + * Update from Weblate + [\#5297](https://github.com/matrix-org/matrix-react-sdk/pull/5297) + * Fix edited replies being wrongly treated as big emoji + [\#5295](https://github.com/matrix-org/matrix-react-sdk/pull/5295) + * Fix StopGapWidget infinitely recursing + [\#5294](https://github.com/matrix-org/matrix-react-sdk/pull/5294) + * Fix editing and redactions not updating the Reply Thread + [\#5281](https://github.com/matrix-org/matrix-react-sdk/pull/5281) + * Hide Jump to Read Receipt button for users who have not yet sent an RR + [\#5282](https://github.com/matrix-org/matrix-react-sdk/pull/5282) + * fix img tags not always being rendered correctly + [\#5279](https://github.com/matrix-org/matrix-react-sdk/pull/5279) + * Hopefully fix righhtpanel crash + [\#5293](https://github.com/matrix-org/matrix-react-sdk/pull/5293) + * Fix naive pinning limit and app tile widgetMessaging NPE + [\#5283](https://github.com/matrix-org/matrix-react-sdk/pull/5283) + * Show server errors from saving profile settings + [\#5272](https://github.com/matrix-org/matrix-react-sdk/pull/5272) + * Update copy for `redact` permission + [\#5273](https://github.com/matrix-org/matrix-react-sdk/pull/5273) + * Remove width limit on widgets + [\#5265](https://github.com/matrix-org/matrix-react-sdk/pull/5265) + * Fix call container avatar initial centering + [\#5280](https://github.com/matrix-org/matrix-react-sdk/pull/5280) + * Fix right panel for peeking rooms + [\#5268](https://github.com/matrix-org/matrix-react-sdk/pull/5268) + * Add support for dehydrated devices + [\#5239](https://github.com/matrix-org/matrix-react-sdk/pull/5239) + * Use Own Profile Store for the Profile Settings + [\#5277](https://github.com/matrix-org/matrix-react-sdk/pull/5277) + * null-guard defaultAvatarUrlForString + [\#5270](https://github.com/matrix-org/matrix-react-sdk/pull/5270) + * Choose first result on enter in the emoji picker + [\#5257](https://github.com/matrix-org/matrix-react-sdk/pull/5257) + * Fix room directory clipping links in the room's topic + [\#5276](https://github.com/matrix-org/matrix-react-sdk/pull/5276) + * Decorate failed e2ee downgrade attempts better + [\#5278](https://github.com/matrix-org/matrix-react-sdk/pull/5278) + * MELS use latest avatar rather than the first avatar + [\#5262](https://github.com/matrix-org/matrix-react-sdk/pull/5262) + * Fix Encryption Panel close button clashing with Base Card + [\#5261](https://github.com/matrix-org/matrix-react-sdk/pull/5261) + * Wrap canEncryptToAllUsers in a try/catch to handle server errors + [\#5275](https://github.com/matrix-org/matrix-react-sdk/pull/5275) + * Fix conditional on communities prototype room creation dialog + [\#5274](https://github.com/matrix-org/matrix-react-sdk/pull/5274) + * Fix ensureDmExists for encryption detection + [\#5271](https://github.com/matrix-org/matrix-react-sdk/pull/5271) + * Switch to using the Widget API SDK for widget messaging + [\#5171](https://github.com/matrix-org/matrix-react-sdk/pull/5171) + * Ensure package links exist when releasing + [\#5269](https://github.com/matrix-org/matrix-react-sdk/pull/5269) + * Fix the call preview when not in same room as the call + [\#5267](https://github.com/matrix-org/matrix-react-sdk/pull/5267) + * Make the hangup button do things for conference calls + [\#5223](https://github.com/matrix-org/matrix-react-sdk/pull/5223) + * Render Jitsi widget state events in a more obvious way + [\#5222](https://github.com/matrix-org/matrix-react-sdk/pull/5222) + * Make the PIP Jitsi look and feel like the 1:1 PIP + [\#5226](https://github.com/matrix-org/matrix-react-sdk/pull/5226) + * Trim range when formatting so that it excludes leading/trailing spaces + [\#5263](https://github.com/matrix-org/matrix-react-sdk/pull/5263) + * Fix button label on the Set Password Dialog + [\#5264](https://github.com/matrix-org/matrix-react-sdk/pull/5264) + * fix link to classic yarn's `yarn link` + [\#5259](https://github.com/matrix-org/matrix-react-sdk/pull/5259) + * Fix index mismatch between username colors styles and custom theming + [\#5256](https://github.com/matrix-org/matrix-react-sdk/pull/5256) + * Disable autocompletion on security key input during login + [\#5258](https://github.com/matrix-org/matrix-react-sdk/pull/5258) + * fix uninitialised state and eventlistener leak in RoomUpgradeWarningBar + [\#5255](https://github.com/matrix-org/matrix-react-sdk/pull/5255) + * Only set title when it changes + [\#5254](https://github.com/matrix-org/matrix-react-sdk/pull/5254) + * Convert CallHandler to typescript + [\#5248](https://github.com/matrix-org/matrix-react-sdk/pull/5248) + * Retry loading i18n language if it fails + [\#5209](https://github.com/matrix-org/matrix-react-sdk/pull/5209) + * Rework profile area for user and room settings to be more clear + [\#5243](https://github.com/matrix-org/matrix-react-sdk/pull/5243) + * Validation improve pattern for derived data + [\#5241](https://github.com/matrix-org/matrix-react-sdk/pull/5241) + +Changes in [3.5.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.5.0) (2020-09-28) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.5.0-rc.1...v3.5.0) + + * Upgrade JS SDK to 8.4.1 + +Changes in [3.5.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.5.0-rc.1) (2020-09-23) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.4.1...v3.5.0-rc.1) + + * Upgrade JS SDK to 8.4.0-rc.1 + * Update from Weblate + [\#5246](https://github.com/matrix-org/matrix-react-sdk/pull/5246) + * Upgrade sanitize-html, set nesting limit + [\#5245](https://github.com/matrix-org/matrix-react-sdk/pull/5245) + * Add a note to use the desktop builds when seshat isn't available + [\#5225](https://github.com/matrix-org/matrix-react-sdk/pull/5225) + * Add some permission checks to the communities v2 prototype + [\#5240](https://github.com/matrix-org/matrix-react-sdk/pull/5240) + * Support HS-preferred Secure Backup setup methods + [\#5242](https://github.com/matrix-org/matrix-react-sdk/pull/5242) + * Only show User Info verify button if the other user has e2ee devices + [\#5234](https://github.com/matrix-org/matrix-react-sdk/pull/5234) + * Fix New Room List arrow key management + [\#5237](https://github.com/matrix-org/matrix-react-sdk/pull/5237) + * Fix Room Directory View & Preview actions for federated joins + [\#5235](https://github.com/matrix-org/matrix-react-sdk/pull/5235) + * Add a UI feature to disable advanced encryption options + [\#5238](https://github.com/matrix-org/matrix-react-sdk/pull/5238) + * UI Feature Flag: Communities + [\#5216](https://github.com/matrix-org/matrix-react-sdk/pull/5216) + * Rename apps back to widgets + [\#5236](https://github.com/matrix-org/matrix-react-sdk/pull/5236) + * Adjust layout and formatting of notifications / files cards + [\#5229](https://github.com/matrix-org/matrix-react-sdk/pull/5229) + * Fix Search Results Tile undefined variable access regression + [\#5232](https://github.com/matrix-org/matrix-react-sdk/pull/5232) + * Fix Cmd/Ctrl+Shift+U for File Upload + [\#5233](https://github.com/matrix-org/matrix-react-sdk/pull/5233) + * Disable the e2ee toggle when creating a room on a server with forced e2e + [\#5231](https://github.com/matrix-org/matrix-react-sdk/pull/5231) + * UI Feature Flag: Disable advanced options and tidy up some copy + [\#5215](https://github.com/matrix-org/matrix-react-sdk/pull/5215) + * UI Feature Flag: 3PIDs + [\#5228](https://github.com/matrix-org/matrix-react-sdk/pull/5228) + * Defer encryption setup until first E2EE room + [\#5219](https://github.com/matrix-org/matrix-react-sdk/pull/5219) + * Tidy devDeps, all the webpack stuff lives in the layer above + [\#5179](https://github.com/matrix-org/matrix-react-sdk/pull/5179) + * UI Feature Flag: Hide flair + [\#5214](https://github.com/matrix-org/matrix-react-sdk/pull/5214) + * UI Feature Flag: Identity server + [\#5218](https://github.com/matrix-org/matrix-react-sdk/pull/5218) + * UI Feature Flag: Share dialog QR code and social icons + [\#5221](https://github.com/matrix-org/matrix-react-sdk/pull/5221) + * UI Feature Flag: Registration, Password Reset, Deactivate + [\#5227](https://github.com/matrix-org/matrix-react-sdk/pull/5227) + * Retry joinRoom up to 5 times in the case of a 504 GATEWAY TIMEOUT + [\#5204](https://github.com/matrix-org/matrix-react-sdk/pull/5204) + * UI Feature Flag: Disable VoIP + [\#5217](https://github.com/matrix-org/matrix-react-sdk/pull/5217) + * Fix setState() usage in the constructor of RoomDirectory + [\#5224](https://github.com/matrix-org/matrix-react-sdk/pull/5224) + * Hide Analytics sections if piwik config is not provided + [\#5211](https://github.com/matrix-org/matrix-react-sdk/pull/5211) + * UI Feature Flag: Disable feedback button + [\#5213](https://github.com/matrix-org/matrix-react-sdk/pull/5213) + * Clean up UserInfo to not show a blank Power Selector for users not in room + [\#5220](https://github.com/matrix-org/matrix-react-sdk/pull/5220) + * Also hide bug reporting prompts from the Error Boundaries + [\#5212](https://github.com/matrix-org/matrix-react-sdk/pull/5212) + * Tactical improvements to 3PID invites + [\#5201](https://github.com/matrix-org/matrix-react-sdk/pull/5201) + * If no bug_report_endpoint_url, hide rageshaking from the App + [\#5210](https://github.com/matrix-org/matrix-react-sdk/pull/5210) + * Introduce a concept of UI features, using it for URL previews at first + [\#5208](https://github.com/matrix-org/matrix-react-sdk/pull/5208) + * Remove defunct "always show encryption icons" setting + [\#5207](https://github.com/matrix-org/matrix-react-sdk/pull/5207) + * Don't show Notifications Prompt Toast if user has master rule enabled + [\#5203](https://github.com/matrix-org/matrix-react-sdk/pull/5203) + * Fix Bridges tab crashing when the room does not have bridges + [\#5206](https://github.com/matrix-org/matrix-react-sdk/pull/5206) + * Don't count widgets which no longer exist towards pinned count + [\#5202](https://github.com/matrix-org/matrix-react-sdk/pull/5202) + * Fix crashes with cannot read isResizing of undefined + [\#5205](https://github.com/matrix-org/matrix-react-sdk/pull/5205) + * Prompt to remove the jitsi widget when pressing the call button + [\#5193](https://github.com/matrix-org/matrix-react-sdk/pull/5193) + * Show verification status in the room summary card + [\#5195](https://github.com/matrix-org/matrix-react-sdk/pull/5195) + * Fix user info scrolling in new card view + [\#5198](https://github.com/matrix-org/matrix-react-sdk/pull/5198) + * Fix sticker picker height + [\#5197](https://github.com/matrix-org/matrix-react-sdk/pull/5197) + * Call jitsi widgets 'group calls' + [\#5191](https://github.com/matrix-org/matrix-react-sdk/pull/5191) + * Don't show 'unpin' for persistent widgets + [\#5194](https://github.com/matrix-org/matrix-react-sdk/pull/5194) + * Split up cross-signing and secure backup settings + [\#5182](https://github.com/matrix-org/matrix-react-sdk/pull/5182) + * Fix onNewScreen to use replace when going from roomId->roomAlias + [\#5185](https://github.com/matrix-org/matrix-react-sdk/pull/5185) + * bring back 1.2M style badge counts rather than 99+ + [\#5192](https://github.com/matrix-org/matrix-react-sdk/pull/5192) + * Run the rageshake command through the bug report dialog + [\#5189](https://github.com/matrix-org/matrix-react-sdk/pull/5189) + * Account for via in pill matching regex + [\#5188](https://github.com/matrix-org/matrix-react-sdk/pull/5188) + * Remove now-unused create-react-class from lockfile + [\#5187](https://github.com/matrix-org/matrix-react-sdk/pull/5187) + * Fixed 1px jump upwards + [\#5163](https://github.com/matrix-org/matrix-react-sdk/pull/5163) + * Always allow widgets when using the local version + [\#5184](https://github.com/matrix-org/matrix-react-sdk/pull/5184) + * Migrate RoomView and RoomContext to Typescript + [\#5175](https://github.com/matrix-org/matrix-react-sdk/pull/5175) + +Changes in [3.4.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.4.1) (2020-09-14) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.4.0...v3.4.1) + + * Don't count widgets which no longer exist towards pinned count + [\#5202](https://github.com/matrix-org/matrix-react-sdk/pull/5202) + * Fix crashes with cannot read isResizing of undefined + [\#5205](https://github.com/matrix-org/matrix-react-sdk/pull/5205) + +Changes in [3.4.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.4.0) (2020-09-14) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.4.0-rc.1...v3.4.0) + + * Upgrade to JS SDK 8.3.0 + * [Release] Show verification status in the room summary card + [\#5196](https://github.com/matrix-org/matrix-react-sdk/pull/5196) + * Fix user info scrolling in new card view + [\#5200](https://github.com/matrix-org/matrix-react-sdk/pull/5200) + * Fix sticker picker height + [\#5199](https://github.com/matrix-org/matrix-react-sdk/pull/5199) + * [Release] Account for via in pill matching regex + [\#5190](https://github.com/matrix-org/matrix-react-sdk/pull/5190) + +Changes in [3.4.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.4.0-rc.1) (2020-09-09) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.3.0...v3.4.0-rc.1) + + * Upgrade to JS SDK 8.3.0-rc.1 + * Update from Weblate + [\#5183](https://github.com/matrix-org/matrix-react-sdk/pull/5183) + * Right Panel Room Summary and Widgets + [\#5167](https://github.com/matrix-org/matrix-react-sdk/pull/5167) + * null-guard roomId in RightPanel and pass Room to UserView + [\#5180](https://github.com/matrix-org/matrix-react-sdk/pull/5180) + * Fix create-react-class regression. + [\#5178](https://github.com/matrix-org/matrix-react-sdk/pull/5178) + * Fix WatchManager for global room watchers and tidy widget code a little + [\#5176](https://github.com/matrix-org/matrix-react-sdk/pull/5176) + * Fix permalink local linkification to not strip via servers + [\#5174](https://github.com/matrix-org/matrix-react-sdk/pull/5174) + * Support creation of Jitsi widgets with "openidtoken-jwt" auth + [\#5173](https://github.com/matrix-org/matrix-react-sdk/pull/5173) + * Fix create-react-class regression. + [\#5177](https://github.com/matrix-org/matrix-react-sdk/pull/5177) + * Update openid_credentials Widget API action for MSC1960 updates + [\#5172](https://github.com/matrix-org/matrix-react-sdk/pull/5172) + * Allow persistent resizing of the widget app drawer + [\#5138](https://github.com/matrix-org/matrix-react-sdk/pull/5138) + * add lenny face command + [\#5158](https://github.com/matrix-org/matrix-react-sdk/pull/5158) + * Prep work for Settings changes with cross-signing deferral + [\#5169](https://github.com/matrix-org/matrix-react-sdk/pull/5169) + * Small code clean ups and tweaks + [\#5168](https://github.com/matrix-org/matrix-react-sdk/pull/5168) + * Fix soft crash from TruncatedList in the createReactClass conversion + [\#5170](https://github.com/matrix-org/matrix-react-sdk/pull/5170) + * Remove create-react-class + [\#5157](https://github.com/matrix-org/matrix-react-sdk/pull/5157) + * Consolidate Lodash files in bundle + [\#5162](https://github.com/matrix-org/matrix-react-sdk/pull/5162) + * Communities v2 prototype: "In community" view + [\#5161](https://github.com/matrix-org/matrix-react-sdk/pull/5161) + * Respect user preference for whether pills should have an avatar or not + [\#5165](https://github.com/matrix-org/matrix-react-sdk/pull/5165) + * Communities v2 prototype: DM copy updates + [\#5153](https://github.com/matrix-org/matrix-react-sdk/pull/5153) + * Only wait for public keys during verification + [\#5164](https://github.com/matrix-org/matrix-react-sdk/pull/5164) + * Fix eslint ts override tsx matching and delint + [\#5155](https://github.com/matrix-org/matrix-react-sdk/pull/5155) + * Fix react error about functional components can't take refs + [\#5159](https://github.com/matrix-org/matrix-react-sdk/pull/5159) + * Remove redundant components and devDependencies + [\#5156](https://github.com/matrix-org/matrix-react-sdk/pull/5156) + * Add display-capture to iframe allow for widgets + [\#5154](https://github.com/matrix-org/matrix-react-sdk/pull/5154) + * Update create room dialog copy & community prototype home icon + [\#5151](https://github.com/matrix-org/matrix-react-sdk/pull/5151) + * Migrate to new, separate APIs for cross-signing and secret storage + [\#5149](https://github.com/matrix-org/matrix-react-sdk/pull/5149) + * Fix clicking the background of the tag panel not clearing the filter + [\#5152](https://github.com/matrix-org/matrix-react-sdk/pull/5152) + * Communities v2 prototype: Associate created rooms with the selected + community + [\#5147](https://github.com/matrix-org/matrix-react-sdk/pull/5147) + * Communities v2 prototype: Tag panel selection changes + [\#5145](https://github.com/matrix-org/matrix-react-sdk/pull/5145) + * Communities v2 prototype: Create community flow + [\#5144](https://github.com/matrix-org/matrix-react-sdk/pull/5144) + * Communities v2 prototype: Override invite aesthetics for community-as-room + invites + [\#5143](https://github.com/matrix-org/matrix-react-sdk/pull/5143) + +Changes in [3.3.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.3.0) (2020-09-01) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.3.0-rc.1...v3.3.0) + + * Upgrade to JS SDK 8.2.0 + +Changes in [3.3.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.3.0-rc.1) (2020-08-26) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.2.0...v3.3.0-rc.1) + + * Upgrade to JS SDK 8.2.0-rc.1 + * Update from Weblate + [\#5146](https://github.com/matrix-org/matrix-react-sdk/pull/5146) + * BaseAvatar avoid initial render with default avatar + [\#5142](https://github.com/matrix-org/matrix-react-sdk/pull/5142) + * Enforce Secure Backup completion when requested by HS + [\#5130](https://github.com/matrix-org/matrix-react-sdk/pull/5130) + * Communities v2 prototype: Explore rooms, global state, and default room + [\#5139](https://github.com/matrix-org/matrix-react-sdk/pull/5139) + * Add communities v2 prototyping feature flag + initial tag panel prototypes + [\#5133](https://github.com/matrix-org/matrix-react-sdk/pull/5133) + * Remove some unused components + [\#5134](https://github.com/matrix-org/matrix-react-sdk/pull/5134) + * Allow avatar image view for 1:1 rooms + [\#5137](https://github.com/matrix-org/matrix-react-sdk/pull/5137) + * Send mx_local_settings in rageshake + [\#5136](https://github.com/matrix-org/matrix-react-sdk/pull/5136) + * Run all room leaving behaviour through a single function + [\#5132](https://github.com/matrix-org/matrix-react-sdk/pull/5132) + * Add clarifying comment in media device selection + [\#5131](https://github.com/matrix-org/matrix-react-sdk/pull/5131) + * Settings v3: Feature flag changes + [\#5124](https://github.com/matrix-org/matrix-react-sdk/pull/5124) + * Clear url previews if they all get edited out of the event + [\#5129](https://github.com/matrix-org/matrix-react-sdk/pull/5129) + * Consider tab completions as modifications for editing purposes to unlock + sending + [\#5128](https://github.com/matrix-org/matrix-react-sdk/pull/5128) + * Use matrix-doc for SAS emoji translations + [\#5125](https://github.com/matrix-org/matrix-react-sdk/pull/5125) + * Add a rageshake function to download the logs locally + [\#3849](https://github.com/matrix-org/matrix-react-sdk/pull/3849) + * Room List filtering visual tweaks + [\#5123](https://github.com/matrix-org/matrix-react-sdk/pull/5123) + * Make reply preview not an overlay so you can see new messages + [\#5072](https://github.com/matrix-org/matrix-react-sdk/pull/5072) + * Allow room tile context menu when minimized using right click + [\#5113](https://github.com/matrix-org/matrix-react-sdk/pull/5113) + * Add null guard to group inviter for corrupted groups + [\#5121](https://github.com/matrix-org/matrix-react-sdk/pull/5121) + * Room List styling tweaks + [\#5118](https://github.com/matrix-org/matrix-react-sdk/pull/5118) + * Fix corner rounding on images not always affecting right side + [\#5120](https://github.com/matrix-org/matrix-react-sdk/pull/5120) + * Change add room action for rooms to context menu + [\#5108](https://github.com/matrix-org/matrix-react-sdk/pull/5108) + * Switch out the globe icon and colour it depending on theme + [\#5106](https://github.com/matrix-org/matrix-react-sdk/pull/5106) + * Message Action Bar watch for event send changes + [\#5115](https://github.com/matrix-org/matrix-react-sdk/pull/5115) + * Put message previews for Emoji behind Labs + [\#5110](https://github.com/matrix-org/matrix-react-sdk/pull/5110) + * Fix styling for selected community marker + [\#5107](https://github.com/matrix-org/matrix-react-sdk/pull/5107) + * Fix action bar safe area regression + [\#5111](https://github.com/matrix-org/matrix-react-sdk/pull/5111) + * Fix /op slash command + [\#5109](https://github.com/matrix-org/matrix-react-sdk/pull/5109) + +Changes in [3.2.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.2.0) (2020-08-17) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.2.0-rc.1...v3.2.0) + + * Upgrade to JS SDK 8.1.0 + * [Release] Fix corner rounding on images not always affecting right side + [\#5122](https://github.com/matrix-org/matrix-react-sdk/pull/5122) + * [Release] Message Action Bar watch for event send changes + [\#5116](https://github.com/matrix-org/matrix-react-sdk/pull/5116) + * Fix /op slash command to release + [\#5114](https://github.com/matrix-org/matrix-react-sdk/pull/5114) + * Fix action bar safe area regression + [\#5112](https://github.com/matrix-org/matrix-react-sdk/pull/5112) + +Changes in [3.2.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.2.0-rc.1) (2020-08-13) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.1.0...v3.2.0-rc.1) + + * Upgrade to JS SDK 8.1.0-rc.1 + * Update from Weblate + [\#5105](https://github.com/matrix-org/matrix-react-sdk/pull/5105) + * padding the timeline so that its scrollbar has its own space from the + resizer + [\#5103](https://github.com/matrix-org/matrix-react-sdk/pull/5103) + * Try to close notification on all platforms which support it, not just + electron + [\#5102](https://github.com/matrix-org/matrix-react-sdk/pull/5102) + * Fix exception when stripping replies from an event with a non-string body + [\#5101](https://github.com/matrix-org/matrix-react-sdk/pull/5101) + * Quick win session 24/07/2020 + [\#5056](https://github.com/matrix-org/matrix-react-sdk/pull/5056) + * Remove rebranding toast + [\#5100](https://github.com/matrix-org/matrix-react-sdk/pull/5100) + * Generate previews for rooms when the option changes + [\#5098](https://github.com/matrix-org/matrix-react-sdk/pull/5098) + * Fix Bridge Settings tab + [\#5095](https://github.com/matrix-org/matrix-react-sdk/pull/5095) + * get screen type from app prop + [\#5081](https://github.com/matrix-org/matrix-react-sdk/pull/5081) + * Update rageshake app name + [\#5093](https://github.com/matrix-org/matrix-react-sdk/pull/5093) + * Factor out Iconized Context menu for reusability + [\#5085](https://github.com/matrix-org/matrix-react-sdk/pull/5085) + * Decouple Audible notifications from Desktop notifications + [\#5088](https://github.com/matrix-org/matrix-react-sdk/pull/5088) + * Make the room sublist show more/less buttons treeitems + [\#5087](https://github.com/matrix-org/matrix-react-sdk/pull/5087) + * Share and debug master cross-signing key + [\#5092](https://github.com/matrix-org/matrix-react-sdk/pull/5092) + * Create Map comparison utilities and convert Hooks to Typescript + [\#5086](https://github.com/matrix-org/matrix-react-sdk/pull/5086) + * Fix room list scrolling in Safari + [\#5090](https://github.com/matrix-org/matrix-react-sdk/pull/5090) + * Replace Riot with Element in docs and comments + [\#5083](https://github.com/matrix-org/matrix-react-sdk/pull/5083) + * When the room view isn't active don't highlight it in room list + [\#5027](https://github.com/matrix-org/matrix-react-sdk/pull/5027) + * remove emoji icons in autocomplete/reply by designer request + [\#5073](https://github.com/matrix-org/matrix-react-sdk/pull/5073) + * Add title and icon to empty state of file and notification panel + [\#5079](https://github.com/matrix-org/matrix-react-sdk/pull/5079) + * Mass redact ignore room creation events + [\#5045](https://github.com/matrix-org/matrix-react-sdk/pull/5045) + * Replace all chevrons with a single icon + [\#5067](https://github.com/matrix-org/matrix-react-sdk/pull/5067) + * Replace i18n generation script with something matching our project + [\#5077](https://github.com/matrix-org/matrix-react-sdk/pull/5077) + * Handle tag changes in sticky room updates + [\#5078](https://github.com/matrix-org/matrix-react-sdk/pull/5078) + * Remove leftover bits of TSLint + [\#5075](https://github.com/matrix-org/matrix-react-sdk/pull/5075) + * Clean up documentation of Whenable + fix other code concerns + [\#5076](https://github.com/matrix-org/matrix-react-sdk/pull/5076) + * Center the jump down/up icon, looks misaligned + [\#5074](https://github.com/matrix-org/matrix-react-sdk/pull/5074) + * [WIP] Support a new settings structure + [\#5058](https://github.com/matrix-org/matrix-react-sdk/pull/5058) + * Convert SettingsStore to TypeScript + [\#5062](https://github.com/matrix-org/matrix-react-sdk/pull/5062) + +Changes in [3.1.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.1.0) (2020-08-05) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.1.0-rc.1...v3.1.0) + + * Upgrade JS SDK to 8.0.1 + * Fix room list scrolling in Safari + [\#5091](https://github.com/matrix-org/matrix-react-sdk/pull/5091) + * Add null guard in InviteDialog + [\#5084](https://github.com/matrix-org/matrix-react-sdk/pull/5084) + * Add null guard in InviteDialog + [\#5082](https://github.com/matrix-org/matrix-react-sdk/pull/5082) + * Handle tag changes in sticky room updates + [\#5080](https://github.com/matrix-org/matrix-react-sdk/pull/5080) + +Changes in [3.1.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.1.0-rc.1) (2020-07-31) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.0.0...v3.1.0-rc.1) + + * Upgrade JS SDK to 8.0.1-rc.1 + * Update from Weblate + [\#5071](https://github.com/matrix-org/matrix-react-sdk/pull/5071) + * Add local echo for notifications in the new room list + [\#5065](https://github.com/matrix-org/matrix-react-sdk/pull/5065) + * Fix various small regressions in the room list's behaviour + [\#5070](https://github.com/matrix-org/matrix-react-sdk/pull/5070) + * Remove redundant lint dependencies + [\#5059](https://github.com/matrix-org/matrix-react-sdk/pull/5059) + * Fix key backup warning on soft logout page + [\#5069](https://github.com/matrix-org/matrix-react-sdk/pull/5069) + * Bump elliptic from 6.5.2 to 6.5.3 + [\#5066](https://github.com/matrix-org/matrix-react-sdk/pull/5066) + * Fix crash on logging in again after soft logout + [\#5068](https://github.com/matrix-org/matrix-react-sdk/pull/5068) + * Convert right_panel to TS + [\#5036](https://github.com/matrix-org/matrix-react-sdk/pull/5036) + * Remove all unreferenced images + [\#5063](https://github.com/matrix-org/matrix-react-sdk/pull/5063) + * Provide nicer error for no known servers error when accepting an invite + [\#5061](https://github.com/matrix-org/matrix-react-sdk/pull/5061) + * add logging for keytar/pickle key + [\#5057](https://github.com/matrix-org/matrix-react-sdk/pull/5057) + * Don't speak the outgoing message if it is in the Sending state. + [\#4075](https://github.com/matrix-org/matrix-react-sdk/pull/4075) + * Remove poorly contrasted "dark style" heading in Room Preview Bar + [\#5052](https://github.com/matrix-org/matrix-react-sdk/pull/5052) + * Fix Query Matcher regression with certain unhomoglyph'd characters + [\#5050](https://github.com/matrix-org/matrix-react-sdk/pull/5050) + * Fix handlebar interaction + [\#4989](https://github.com/matrix-org/matrix-react-sdk/pull/4989) + * Minor improvements to filtering performance + [\#5054](https://github.com/matrix-org/matrix-react-sdk/pull/5054) + * Fix TextWithTooltip "leaking" tooltip wrappers + [\#5055](https://github.com/matrix-org/matrix-react-sdk/pull/5055) + +Changes in [3.0.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.0.0) (2020-07-27) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.10.1...v3.0.0) + +BREAKING CHANGES +--- + + * The room list components have been replaced as part of this release, so the list, tiles, and other associated components now use a different prop / state contract. + + +All Changes +--- + + * Upgrade to JS SDK 8.0.0 + * Update from Weblate + [\#5053](https://github.com/matrix-org/matrix-react-sdk/pull/5053) + * RoomList listen to notificationState updates for bolding + [\#5051](https://github.com/matrix-org/matrix-react-sdk/pull/5051) + * Ensure notification badges stop listening when they unmount + [\#5049](https://github.com/matrix-org/matrix-react-sdk/pull/5049) + * Improve RoomTile performance + [\#5048](https://github.com/matrix-org/matrix-react-sdk/pull/5048) + * Reward users for using stable ordering in their room list + [\#5047](https://github.com/matrix-org/matrix-react-sdk/pull/5047) + * Fix autocomplete suggesting a different thing mid-composition + [\#5030](https://github.com/matrix-org/matrix-react-sdk/pull/5030) + * Put low priority xor toggle back in the room list context menu + [\#5026](https://github.com/matrix-org/matrix-react-sdk/pull/5026) + * Fix autocompletion of Community IDs + [\#5040](https://github.com/matrix-org/matrix-react-sdk/pull/5040) + * Use OpenType tabular numbers in timestamps + [\#5042](https://github.com/matrix-org/matrix-react-sdk/pull/5042) + * Update packages to modern versions + [\#5046](https://github.com/matrix-org/matrix-react-sdk/pull/5046) + * Add dismiss button to rebrand toast + [\#5044](https://github.com/matrix-org/matrix-react-sdk/pull/5044) + * Fix Firefox composer regression exception + [\#5039](https://github.com/matrix-org/matrix-react-sdk/pull/5039) + * Fix BaseAvatar wrongly using Buttons when it needs not + [\#5037](https://github.com/matrix-org/matrix-react-sdk/pull/5037) + * Performance improvements round 2: Maps, freezing, dispatching, and flexbox + obliteration + [\#5038](https://github.com/matrix-org/matrix-react-sdk/pull/5038) + * Mixed bag of performance improvements: ScrollPanel and notifications + [\#5034](https://github.com/matrix-org/matrix-react-sdk/pull/5034) + * Update message previews + [\#5025](https://github.com/matrix-org/matrix-react-sdk/pull/5025) + * Translate create room buttons + [\#5035](https://github.com/matrix-org/matrix-react-sdk/pull/5035) + * Escape single quotes in composer placeholder + [\#5033](https://github.com/matrix-org/matrix-react-sdk/pull/5033) + * Don't hammer on the layout engine with avatar updates for the background + [\#5032](https://github.com/matrix-org/matrix-react-sdk/pull/5032) + * Ensure incremental updates to the ImportanceAlgorithm trigger A-Z order + [\#5031](https://github.com/matrix-org/matrix-react-sdk/pull/5031) + * don't syntax highlight languages that begin with "_" + [\#5029](https://github.com/matrix-org/matrix-react-sdk/pull/5029) + * Convert Modal to TypeScript + [\#4956](https://github.com/matrix-org/matrix-react-sdk/pull/4956) + * Use new eslint dependency and remove tslint + [\#4815](https://github.com/matrix-org/matrix-react-sdk/pull/4815) + * Support custom tags in the room list again + [\#5024](https://github.com/matrix-org/matrix-react-sdk/pull/5024) + * Fix the tag panel context menu + [\#5028](https://github.com/matrix-org/matrix-react-sdk/pull/5028) + * Tag Watcher don't create new filter if not needed, confuses references + [\#5021](https://github.com/matrix-org/matrix-react-sdk/pull/5021) + * Convert editor to TypeScript + [\#4978](https://github.com/matrix-org/matrix-react-sdk/pull/4978) + * Query Matcher use unhomoglyph for a little bit more leniency + [\#4977](https://github.com/matrix-org/matrix-react-sdk/pull/4977) + * Fix Breadcrumbs2 ending up with 2 tabIndexes on Firefox + [\#5017](https://github.com/matrix-org/matrix-react-sdk/pull/5017) + * Add min-width to floating Jitsi + [\#5023](https://github.com/matrix-org/matrix-react-sdk/pull/5023) + * Update crypto event icon to match rest of app styling + [\#5020](https://github.com/matrix-org/matrix-react-sdk/pull/5020) + * Fix Reactions Row Button vertical misalignment due to forced height + [\#5019](https://github.com/matrix-org/matrix-react-sdk/pull/5019) + * Use mouseleave instead of mouseout for hover events. Fix tooltip flicker + [\#5016](https://github.com/matrix-org/matrix-react-sdk/pull/5016) + * Fix slash commands null guard + [\#5015](https://github.com/matrix-org/matrix-react-sdk/pull/5015) + * Fix field tooltips + [\#5014](https://github.com/matrix-org/matrix-react-sdk/pull/5014) + * Fix community right panel button regression + [\#5022](https://github.com/matrix-org/matrix-react-sdk/pull/5022) + * [BREAKING] Remove the old room list + [\#5013](https://github.com/matrix-org/matrix-react-sdk/pull/5013) + * ellipse senders for images and videos + [\#4990](https://github.com/matrix-org/matrix-react-sdk/pull/4990) + * Sprinkle and consolidate some tooltips + [\#5012](https://github.com/matrix-org/matrix-react-sdk/pull/5012) + * Hopefully make cancel dialog a bit less weird + [\#4833](https://github.com/matrix-org/matrix-react-sdk/pull/4833) + * Fix emoji filterString + [\#5011](https://github.com/matrix-org/matrix-react-sdk/pull/5011) + * Fix size call for devtools state events + [\#5008](https://github.com/matrix-org/matrix-react-sdk/pull/5008) + * Fix `this` context in _setupHomeserverManagers for IntegrationManagers + [\#5010](https://github.com/matrix-org/matrix-react-sdk/pull/5010) + * Sync recently used reactions list across sessions + [\#4993](https://github.com/matrix-org/matrix-react-sdk/pull/4993) + * Null guard no e2ee for UserInfo + [\#5009](https://github.com/matrix-org/matrix-react-sdk/pull/5009) + * stop Inter from clobbering Twemoji + [\#5007](https://github.com/matrix-org/matrix-react-sdk/pull/5007) + * use a proper HTML sanitizer to strip , rather than a regexp + [\#5006](https://github.com/matrix-org/matrix-react-sdk/pull/5006) + * Convert room list log setting to a real setting + [\#5005](https://github.com/matrix-org/matrix-react-sdk/pull/5005) + * Bump lodash from 4.17.15 to 4.17.19 in /test/end-to-end-tests + [\#5003](https://github.com/matrix-org/matrix-react-sdk/pull/5003) + * Bump lodash from 4.17.15 to 4.17.19 + [\#5004](https://github.com/matrix-org/matrix-react-sdk/pull/5004) + * Convert devtools dialog to use new room state format + [\#4936](https://github.com/matrix-org/matrix-react-sdk/pull/4936) + * Update checkbox + [\#5000](https://github.com/matrix-org/matrix-react-sdk/pull/5000) + * Increase width for country code dropdown + [\#5001](https://github.com/matrix-org/matrix-react-sdk/pull/5001) + +Changes in [2.10.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.10.1) (2020-07-16) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.10.0...v2.10.1) + + * Post-launch Element Web polish + [\#5002](https://github.com/matrix-org/matrix-react-sdk/pull/5002) + * Move e2e icon + [\#4992](https://github.com/matrix-org/matrix-react-sdk/pull/4992) + * Wire up new room list breadcrumbs as an ARIA Toolbar + [\#4976](https://github.com/matrix-org/matrix-react-sdk/pull/4976) + * Fix Room Tile Icon to not ignore DMs in other tags + [\#4999](https://github.com/matrix-org/matrix-react-sdk/pull/4999) + * Fix filtering by community not showing DM rooms with community members + [\#4997](https://github.com/matrix-org/matrix-react-sdk/pull/4997) + * Fix enter in new room list filter breaking things + [\#4996](https://github.com/matrix-org/matrix-react-sdk/pull/4996) + * Notify left panel of resizing when it is collapsed&expanded + [\#4995](https://github.com/matrix-org/matrix-react-sdk/pull/4995) + * When removing a filter condition, try recalculate in case it wasn't last + [\#4994](https://github.com/matrix-org/matrix-react-sdk/pull/4994) + * Create a generic ARIA toolbar component + [\#4975](https://github.com/matrix-org/matrix-react-sdk/pull/4975) + * Fix /op Slash Command + [\#4604](https://github.com/matrix-org/matrix-react-sdk/pull/4604) + * Fix copy button in share dialog + [\#4998](https://github.com/matrix-org/matrix-react-sdk/pull/4998) + * Add tooltip to Room Tile Icon + [\#4987](https://github.com/matrix-org/matrix-react-sdk/pull/4987) + * Fix names jumping on hover in irc layout + [\#4991](https://github.com/matrix-org/matrix-react-sdk/pull/4991) + * check that encryptionInfo.sender is set + [\#4988](https://github.com/matrix-org/matrix-react-sdk/pull/4988) + * Update help link + [\#4986](https://github.com/matrix-org/matrix-react-sdk/pull/4986) + * Update cover photo link + [\#4985](https://github.com/matrix-org/matrix-react-sdk/pull/4985) + +Changes in [2.10.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.10.0) (2020-07-15) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.9.0...v2.10.0) + + * Incorporate new toasts into end-to-end tests + [\#4983](https://github.com/matrix-org/matrix-react-sdk/pull/4983) + * Fix TS lint errors + [\#4982](https://github.com/matrix-org/matrix-react-sdk/pull/4982) + * Fix js lint errors after rebrand merge + [\#4981](https://github.com/matrix-org/matrix-react-sdk/pull/4981) + * Fix style lint + [\#4980](https://github.com/matrix-org/matrix-react-sdk/pull/4980) + * Fix alignment of login/syncing spinner + [\#4979](https://github.com/matrix-org/matrix-react-sdk/pull/4979) + * De labs font-scaling + [\#4899](https://github.com/matrix-org/matrix-react-sdk/pull/4899) + * Remove debug logging from new room list + [\#4972](https://github.com/matrix-org/matrix-react-sdk/pull/4972) + * Tweak sticky header hiding to avoid pop + [\#4974](https://github.com/matrix-org/matrix-react-sdk/pull/4974) + * Fix show-all keyboard focus regression + [\#4973](https://github.com/matrix-org/matrix-react-sdk/pull/4973) + * Clean up TODOs, comments, and imports in the new room list + [\#4970](https://github.com/matrix-org/matrix-react-sdk/pull/4970) + * Make EffectiveMembership utils generic + [\#4971](https://github.com/matrix-org/matrix-react-sdk/pull/4971) + * Update sticky headers when breadcrumbs pop in or out + [\#4969](https://github.com/matrix-org/matrix-react-sdk/pull/4969) + * Fix show less button occluding the last tile + [\#4967](https://github.com/matrix-org/matrix-react-sdk/pull/4967) + * Ensure breadcrumbs don't keep turning themselves back on + [\#4968](https://github.com/matrix-org/matrix-react-sdk/pull/4968) + * Update top vs. bottom sticky styles separately + [\#4966](https://github.com/matrix-org/matrix-react-sdk/pull/4966) + * Ensure RoomListStore2 gets reset when the client becomes invalidated + [\#4965](https://github.com/matrix-org/matrix-react-sdk/pull/4965) + * Add fade to show more button on room list + [\#4963](https://github.com/matrix-org/matrix-react-sdk/pull/4963) + * Fix extra room tiles being rendered on smaller sublists + [\#4964](https://github.com/matrix-org/matrix-react-sdk/pull/4964) + * Ensure tag changes (leaving rooms) causes rooms to move between lists + [\#4962](https://github.com/matrix-org/matrix-react-sdk/pull/4962) + * Fix badges for font size 20 + [\#4958](https://github.com/matrix-org/matrix-react-sdk/pull/4958) + * Fix default sorting mechanics for new room list + [\#4960](https://github.com/matrix-org/matrix-react-sdk/pull/4960) + * Fix room sub list header collapse/jump interactions on bottom-most sublist + [\#4961](https://github.com/matrix-org/matrix-react-sdk/pull/4961) + * Fix room tile context menu for Historical rooms + [\#4959](https://github.com/matrix-org/matrix-react-sdk/pull/4959) + * "ignore"/"unignore" commands: validate user ID + [\#4895](https://github.com/matrix-org/matrix-react-sdk/pull/4895) + * Stop classname from overwritting baseavatar's + [\#4957](https://github.com/matrix-org/matrix-react-sdk/pull/4957) + * Remove redundant scroll-margins and fix RoomTile wrongly scrolling + [\#4952](https://github.com/matrix-org/matrix-react-sdk/pull/4952) + * Fix RoomAvatar viewAvatarOnClick to work on actual avatars instead of + default ones + [\#4953](https://github.com/matrix-org/matrix-react-sdk/pull/4953) + * Be consistent with the at-room pill avatar configurability + [\#4955](https://github.com/matrix-org/matrix-react-sdk/pull/4955) + * Room List v2 Enter in the filter field should select the first result + [\#4954](https://github.com/matrix-org/matrix-react-sdk/pull/4954) + * Enable the new room list by default + [\#4919](https://github.com/matrix-org/matrix-react-sdk/pull/4919) + * Convert ImportanceAlgorithm over to using NotificationColor instead + [\#4949](https://github.com/matrix-org/matrix-react-sdk/pull/4949) + * Internalize algorithm updates in the new room list store + [\#4951](https://github.com/matrix-org/matrix-react-sdk/pull/4951) + * Remove now-dead code from sublist resizing + [\#4950](https://github.com/matrix-org/matrix-react-sdk/pull/4950) + * Ensure triggered updates get fired for filters in the new room list + [\#4948](https://github.com/matrix-org/matrix-react-sdk/pull/4948) + * Handle off-cycle filtering updates in the new room list + [\#4947](https://github.com/matrix-org/matrix-react-sdk/pull/4947) + * Make the show more button do a clean cut on the room list while transparent + [\#4941](https://github.com/matrix-org/matrix-react-sdk/pull/4941) + * Stop safari from aggressively shrinking flex items + [\#4945](https://github.com/matrix-org/matrix-react-sdk/pull/4945) + * Fix search padding + [\#4946](https://github.com/matrix-org/matrix-react-sdk/pull/4946) + * Reduce event loop load caused by duplicate calculations in the new room list + [\#4943](https://github.com/matrix-org/matrix-react-sdk/pull/4943) + * Add an option to disable room list logging, and improve logging + [\#4944](https://github.com/matrix-org/matrix-react-sdk/pull/4944) + * Scroll fade for breadcrumbs + [\#4942](https://github.com/matrix-org/matrix-react-sdk/pull/4942) + * Auto expand room list on search + [\#4927](https://github.com/matrix-org/matrix-react-sdk/pull/4927) + * Fix rough badge alignment for community invite tiles again + [\#4939](https://github.com/matrix-org/matrix-react-sdk/pull/4939) + * Improve safety of new rooms in the room list + [\#4940](https://github.com/matrix-org/matrix-react-sdk/pull/4940) + * Don't destroy room notification states when replacing them + [\#4938](https://github.com/matrix-org/matrix-react-sdk/pull/4938) + * Move irc layout option to advanced + [\#4937](https://github.com/matrix-org/matrix-react-sdk/pull/4937) + * Potential solution to supporting transparent 'show more' buttons + [\#4932](https://github.com/matrix-org/matrix-react-sdk/pull/4932) + * Improve performance and stability in sticky headers for new room list + [\#4931](https://github.com/matrix-org/matrix-react-sdk/pull/4931) + * Move and improve notification state handling + [\#4935](https://github.com/matrix-org/matrix-react-sdk/pull/4935) + * Move list layout management to its own store + [\#4934](https://github.com/matrix-org/matrix-react-sdk/pull/4934) + * Noop first breadcrumb + [\#4933](https://github.com/matrix-org/matrix-react-sdk/pull/4933) + * Highlight "Jump to Bottom" badge when appropriate + [\#4892](https://github.com/matrix-org/matrix-react-sdk/pull/4892) + * Don't render the context menu within its trigger otherwise unhandled clicks + bubble + [\#4930](https://github.com/matrix-org/matrix-react-sdk/pull/4930) + * Protect rooms from getting lost due to complex transitions + [\#4929](https://github.com/matrix-org/matrix-react-sdk/pull/4929) + * Hide archive button + [\#4928](https://github.com/matrix-org/matrix-react-sdk/pull/4928) + * Enable options to favourite and low priority rooms + [\#4920](https://github.com/matrix-org/matrix-react-sdk/pull/4920) + * Move voip previews to bottom right corner + [\#4904](https://github.com/matrix-org/matrix-react-sdk/pull/4904) + * Focus room filter on openSearch + [\#4923](https://github.com/matrix-org/matrix-react-sdk/pull/4923) + * Swap out the resizer lib for something more stable in the new room list + [\#4924](https://github.com/matrix-org/matrix-react-sdk/pull/4924) + * Add wrapper to room list so sticky headers don't need a background + [\#4912](https://github.com/matrix-org/matrix-react-sdk/pull/4912) + * New room list view_room show_room_tile support + [\#4908](https://github.com/matrix-org/matrix-react-sdk/pull/4908) + * Convert Context Menu to TypeScript + [\#4871](https://github.com/matrix-org/matrix-react-sdk/pull/4871) + * Use html innerText for org.matrix.custom.html m.room.message room list + previews + [\#4925](https://github.com/matrix-org/matrix-react-sdk/pull/4925) + * Fix MELS summary of 3pid invite revocations + [\#4913](https://github.com/matrix-org/matrix-react-sdk/pull/4913) + * Fix sticky headers being left on display:none if they change too quickly + [\#4926](https://github.com/matrix-org/matrix-react-sdk/pull/4926) + * Fix gaps under resize handle + [\#4922](https://github.com/matrix-org/matrix-react-sdk/pull/4922) + * Fix DM handling in new room list + [\#4921](https://github.com/matrix-org/matrix-react-sdk/pull/4921) + * Respect and fix understanding of legacy options in new room list + [\#4918](https://github.com/matrix-org/matrix-react-sdk/pull/4918) + * Ensure DMs are not lost in the new room list, and clean up tag logging + [\#4916](https://github.com/matrix-org/matrix-react-sdk/pull/4916) + * Mute "Unknown room caused setting update" spam + [\#4915](https://github.com/matrix-org/matrix-react-sdk/pull/4915) + * Remove comment claiming encrypted rooms are handled incorrectly in the new + room list + [\#4917](https://github.com/matrix-org/matrix-react-sdk/pull/4917) + * Try using requestAnimationFrame if available for sticky headers + [\#4914](https://github.com/matrix-org/matrix-react-sdk/pull/4914) + * Show more/Show less keep focus in a relevant place + [\#4911](https://github.com/matrix-org/matrix-react-sdk/pull/4911) + * Change orange to our orange and do some lints + [\#4910](https://github.com/matrix-org/matrix-react-sdk/pull/4910) + * New Room List implement view_room_delta for keyboard shortcuts + [\#4900](https://github.com/matrix-org/matrix-react-sdk/pull/4900) + * New Room List accessibility + [\#4896](https://github.com/matrix-org/matrix-react-sdk/pull/4896) + * Improve room safety in the new room list + [\#4905](https://github.com/matrix-org/matrix-react-sdk/pull/4905) + * Fix a number of issues with the new room list's invites + [\#4906](https://github.com/matrix-org/matrix-react-sdk/pull/4906) + * Decrease default visible rooms down to 5 + [\#4907](https://github.com/matrix-org/matrix-react-sdk/pull/4907) + * swap order of context menu buttons so it does not jump when muted + [\#4909](https://github.com/matrix-org/matrix-react-sdk/pull/4909) + * Fix some room list sticky header instabilities + [\#4901](https://github.com/matrix-org/matrix-react-sdk/pull/4901) + * null-guard against groups with a null name in new Room List + [\#4903](https://github.com/matrix-org/matrix-react-sdk/pull/4903) + * Allow vertical scrolling on the new room list breadcrumbs + [\#4902](https://github.com/matrix-org/matrix-react-sdk/pull/4902) + * Convert things to Typescript, including languageHandler + [\#4883](https://github.com/matrix-org/matrix-react-sdk/pull/4883) + * Fix minor issues with the badges in the new room list + [\#4894](https://github.com/matrix-org/matrix-react-sdk/pull/4894) + * Radio button outline fixes including for new room list context menu + [\#4893](https://github.com/matrix-org/matrix-react-sdk/pull/4893) + * First step towards a11y in the new room list + [\#4882](https://github.com/matrix-org/matrix-react-sdk/pull/4882) + * Fix theme selector clicks bubbling out and causing context menu to float + away + [\#4891](https://github.com/matrix-org/matrix-react-sdk/pull/4891) + * Revert "Remove a bunch of noisy logging from the room list" + [\#4890](https://github.com/matrix-org/matrix-react-sdk/pull/4890) + * Remove duplicate compact settings, handle device level updates + [\#4888](https://github.com/matrix-org/matrix-react-sdk/pull/4888) + * fix notifications icons some more + [\#4887](https://github.com/matrix-org/matrix-react-sdk/pull/4887) + * Remove a bunch of noisy logging from the room list + [\#4886](https://github.com/matrix-org/matrix-react-sdk/pull/4886) + * Fix bell icon mismatch on room tile between hover and context menu + [\#4884](https://github.com/matrix-org/matrix-react-sdk/pull/4884) + * Add a null guard for message event previews + [\#4885](https://github.com/matrix-org/matrix-react-sdk/pull/4885) + * Enable the new room list by default and trigger an initial render + [\#4881](https://github.com/matrix-org/matrix-react-sdk/pull/4881) + * Fix selection states of room tiles in the new room list + [\#4879](https://github.com/matrix-org/matrix-react-sdk/pull/4879) + * Update mute icon behaviour for new room list designs + [\#4876](https://github.com/matrix-org/matrix-react-sdk/pull/4876) + * Fix alignment of avatars on community invites + [\#4878](https://github.com/matrix-org/matrix-react-sdk/pull/4878) + * Don't include empty badge container in minimized view + [\#4880](https://github.com/matrix-org/matrix-react-sdk/pull/4880) + * Fix alignment of dot badges in new room list + [\#4877](https://github.com/matrix-org/matrix-react-sdk/pull/4877) + * Reorganize and match new room list badges to old list behaviour + [\#4861](https://github.com/matrix-org/matrix-react-sdk/pull/4861) + * Implement breadcrumb notifications and scrolling + [\#4862](https://github.com/matrix-org/matrix-react-sdk/pull/4862) + * Add click-to-jump on badge in the room sublist header + [\#4875](https://github.com/matrix-org/matrix-react-sdk/pull/4875) + * Room List v2 context menu interactions + [\#4870](https://github.com/matrix-org/matrix-react-sdk/pull/4870) + * Wedge community invites into the new room list + [\#4874](https://github.com/matrix-org/matrix-react-sdk/pull/4874) + * Check whether crypto is enabled in room recovery reminder + [\#4873](https://github.com/matrix-org/matrix-react-sdk/pull/4873) + * Fix room list 2's room tile wrapping wrongly + [\#4872](https://github.com/matrix-org/matrix-react-sdk/pull/4872) + * Hide scrollbar without pixel jumping + [\#4863](https://github.com/matrix-org/matrix-react-sdk/pull/4863) + * Room Tile context menu, notifications, indicator and placement + [\#4858](https://github.com/matrix-org/matrix-react-sdk/pull/4858) + * Improve resizing interactions in the new room list + [\#4865](https://github.com/matrix-org/matrix-react-sdk/pull/4865) + * Disable use of account-level ordering options in new room list + [\#4866](https://github.com/matrix-org/matrix-react-sdk/pull/4866) + * Remove context menu on invites in new room list + [\#4867](https://github.com/matrix-org/matrix-react-sdk/pull/4867) + * Fix reaction event crashes in message previews + [\#4868](https://github.com/matrix-org/matrix-react-sdk/pull/4868) + +Changes in [2.9.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.9.0) (2020-07-03) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.9.0-rc.1...v2.9.0) + + * Upgrade to JS SDK 7.1.0 + * Remove duplicate compact settings, handle device level updates + [\#4889](https://github.com/matrix-org/matrix-react-sdk/pull/4889) + +Changes in [2.9.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.9.0-rc.1) (2020-07-01) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.8.1...v2.9.0-rc.1) + + * Upgrade to JS SDK 7.1.0-rc.1 + * Update from Weblate + [\#4869](https://github.com/matrix-org/matrix-react-sdk/pull/4869) + * Fix a number of proliferation issues in the new room list + [\#4828](https://github.com/matrix-org/matrix-react-sdk/pull/4828) + * Fix jumping to read marker for events without tiles + [\#4860](https://github.com/matrix-org/matrix-react-sdk/pull/4860) + * De-duplicate rooms from the room autocomplete provider + [\#4859](https://github.com/matrix-org/matrix-react-sdk/pull/4859) + * Add file upload button to recovery key input + [\#4847](https://github.com/matrix-org/matrix-react-sdk/pull/4847) + * Implement new design on security setup & login + [\#4831](https://github.com/matrix-org/matrix-react-sdk/pull/4831) + * Fix /join slash command via servers including room id as a via + [\#4856](https://github.com/matrix-org/matrix-react-sdk/pull/4856) + * Add Generic Expiring Toast and timing hooks + [\#4855](https://github.com/matrix-org/matrix-react-sdk/pull/4855) + * Fix Room Custom Sounds regression and make ProgressBar relevant again + [\#4846](https://github.com/matrix-org/matrix-react-sdk/pull/4846) + * Including start_sso and start_cas in redirect loop prevention + [\#4854](https://github.com/matrix-org/matrix-react-sdk/pull/4854) + * Clean up TODO comments for new room list + [\#4850](https://github.com/matrix-org/matrix-react-sdk/pull/4850) + * Show timestamp of redaction on hover + [\#4622](https://github.com/matrix-org/matrix-react-sdk/pull/4622) + * Remove the DM button from new room tiles + [\#4849](https://github.com/matrix-org/matrix-react-sdk/pull/4849) + * Hide room list show less button if it would do nothing + [\#4848](https://github.com/matrix-org/matrix-react-sdk/pull/4848) + * Improve message preview copy in new room list + [\#4823](https://github.com/matrix-org/matrix-react-sdk/pull/4823) + * Allow the tag panel to be disabled in the new room list + [\#4844](https://github.com/matrix-org/matrix-react-sdk/pull/4844) + * Make the whole user row clickable in the new room list + [\#4843](https://github.com/matrix-org/matrix-react-sdk/pull/4843) + * Add a new spinner design behind a labs flag + [\#4842](https://github.com/matrix-org/matrix-react-sdk/pull/4842) + * ts-ignore because something is made of fail + [\#4845](https://github.com/matrix-org/matrix-react-sdk/pull/4845) + * Fix Welcome.html CAS and SSO URLs not working + [\#4838](https://github.com/matrix-org/matrix-react-sdk/pull/4838) + * More small tweaks in preparation for Notifications rework + [\#4835](https://github.com/matrix-org/matrix-react-sdk/pull/4835) + * Iterate on the new room list resize handle + [\#4840](https://github.com/matrix-org/matrix-react-sdk/pull/4840) + * Update sublists for new hover states + [\#4837](https://github.com/matrix-org/matrix-react-sdk/pull/4837) + * Tweak parts of the new room list design + [\#4839](https://github.com/matrix-org/matrix-react-sdk/pull/4839) + * Implement new resize handle for dogfooding + [\#4836](https://github.com/matrix-org/matrix-react-sdk/pull/4836) + * Hide app badge count for hidden upgraded rooms (non-highlight) + [\#4834](https://github.com/matrix-org/matrix-react-sdk/pull/4834) + * Move compact modern layout checkbox to 'advanced' + [\#4822](https://github.com/matrix-org/matrix-react-sdk/pull/4822) + * Allow the user to resize the new sublists to 1 tile + [\#4825](https://github.com/matrix-org/matrix-react-sdk/pull/4825) + * Make LoggedInView a real component because it uses shouldComponentUpdate + [\#4832](https://github.com/matrix-org/matrix-react-sdk/pull/4832) + * Small tweaks in preparation for Notifications rework + [\#4829](https://github.com/matrix-org/matrix-react-sdk/pull/4829) + * Remove extraneous debug from the new left panel + [\#4826](https://github.com/matrix-org/matrix-react-sdk/pull/4826) + * Fix icons in the new user menu not showing up + [\#4824](https://github.com/matrix-org/matrix-react-sdk/pull/4824) + * Fix sticky room disappearing/jumping in search results + [\#4817](https://github.com/matrix-org/matrix-react-sdk/pull/4817) + * Show cross-signing / secret storage reset button in more cases + [\#4821](https://github.com/matrix-org/matrix-react-sdk/pull/4821) + * Use theme-capable icons in the user menu + [\#4819](https://github.com/matrix-org/matrix-react-sdk/pull/4819) + * Font support in custom themes + [\#4814](https://github.com/matrix-org/matrix-react-sdk/pull/4814) + * Decrease margin between new sublists + [\#4816](https://github.com/matrix-org/matrix-react-sdk/pull/4816) + * Update profile information in User Menu and truncate where needed + [\#4818](https://github.com/matrix-org/matrix-react-sdk/pull/4818) + * Fix MessageActionBar in irc layout + [\#4802](https://github.com/matrix-org/matrix-react-sdk/pull/4802) + * Mark messages with a black shield if the megolm session isn't trusted + [\#4797](https://github.com/matrix-org/matrix-react-sdk/pull/4797) + * Custom font selection + [\#4761](https://github.com/matrix-org/matrix-react-sdk/pull/4761) + * Use the correct timeline reference for message previews + [\#4812](https://github.com/matrix-org/matrix-react-sdk/pull/4812) + * Fix read receipt handling in the new room list + [\#4811](https://github.com/matrix-org/matrix-react-sdk/pull/4811) + * Improve unread/badge states in new room list (mk II) + [\#4805](https://github.com/matrix-org/matrix-react-sdk/pull/4805) + * Only fire setting changes for changed settings + [\#4803](https://github.com/matrix-org/matrix-react-sdk/pull/4803) + * Trigger room-specific watchers whenever a higher level change happens + [\#4804](https://github.com/matrix-org/matrix-react-sdk/pull/4804) + * Have the theme switcher set the device-level theme to match settings + [\#4810](https://github.com/matrix-org/matrix-react-sdk/pull/4810) + * Fix layout of minimized view for new room list + [\#4808](https://github.com/matrix-org/matrix-react-sdk/pull/4808) + * Fix sticky headers over/under extending themselves in the new room list + [\#4809](https://github.com/matrix-org/matrix-react-sdk/pull/4809) + * Update read receipt remainder for internal font size change + [\#4806](https://github.com/matrix-org/matrix-react-sdk/pull/4806) + * Fix some appearance tab crash and implement style nits + [\#4801](https://github.com/matrix-org/matrix-react-sdk/pull/4801) + * Add message preview for font slider + [\#4770](https://github.com/matrix-org/matrix-react-sdk/pull/4770) + * Add layout options to the appearance tab + [\#4773](https://github.com/matrix-org/matrix-react-sdk/pull/4773) + * Update from Weblate + [\#4800](https://github.com/matrix-org/matrix-react-sdk/pull/4800) + * Support accounts with cross signing but no SSSS + [\#4717](https://github.com/matrix-org/matrix-react-sdk/pull/4717) + * Look for existing verification requests after login + [\#4762](https://github.com/matrix-org/matrix-react-sdk/pull/4762) + * Add a checkpoint to index newly encrypted rooms. + [\#4611](https://github.com/matrix-org/matrix-react-sdk/pull/4611) + * Add support to paginate search results when using Seshat. + [\#4705](https://github.com/matrix-org/matrix-react-sdk/pull/4705) + * User versions in the event index. + [\#4788](https://github.com/matrix-org/matrix-react-sdk/pull/4788) + * Fix crash when filtering new room list too fast + [\#4796](https://github.com/matrix-org/matrix-react-sdk/pull/4796) + * hide search results from unknown rooms + [\#4795](https://github.com/matrix-org/matrix-react-sdk/pull/4795) + * Mark the new room list as ready for general testing + [\#4794](https://github.com/matrix-org/matrix-react-sdk/pull/4794) + * Extend QueryMatcher's sorting heuristic + [\#4784](https://github.com/matrix-org/matrix-react-sdk/pull/4784) + * Lint ts semicolons (aka. The great semicolon migration) + [\#4791](https://github.com/matrix-org/matrix-react-sdk/pull/4791) + * Revert "Use recovery keys over passphrases" + [\#4790](https://github.com/matrix-org/matrix-react-sdk/pull/4790) + * Clear `top` when not sticking headers to the top + [\#4783](https://github.com/matrix-org/matrix-react-sdk/pull/4783) + * Don't show a 'show less' button when it's impossible to collapse + [\#4785](https://github.com/matrix-org/matrix-react-sdk/pull/4785) + * Fix show less/more button occluding the list automatically + [\#4786](https://github.com/matrix-org/matrix-react-sdk/pull/4786) + * Improve room switching in the new room list + [\#4787](https://github.com/matrix-org/matrix-react-sdk/pull/4787) + * Remove labs option to cache 'passphrase' + [\#4789](https://github.com/matrix-org/matrix-react-sdk/pull/4789) + * Remove escape backslashes in non-Markdown messages + [\#4694](https://github.com/matrix-org/matrix-react-sdk/pull/4694) + +Changes in [2.8.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.8.1) (2020-06-29) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.8.0...v2.8.1) + + * Support accounts with cross signing but no SSSS + [\#4852](https://github.com/matrix-org/matrix-react-sdk/pull/4852) + +Changes in [2.8.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.8.0) (2020-06-23) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.8.0-rc.1...v2.8.0) + + * Upgrade to JS SDK 7.0.0 + * Update read receipt remainder for internal font size change + [\#4807](https://github.com/matrix-org/matrix-react-sdk/pull/4807) + * Revert "Use recovery keys over passphrases" + [\#4793](https://github.com/matrix-org/matrix-react-sdk/pull/4793) + +Changes in [2.8.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.8.0-rc.1) (2020-06-17) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.2...v2.8.0-rc.1) + + * Upgrade to JS SDK 7.0.0-rc.1 + * Fix Styled Checkbox and Radio Button disabled state + [\#4778](https://github.com/matrix-org/matrix-react-sdk/pull/4778) + * clean up and fix the isMasterRuleEnabled logic + [\#4782](https://github.com/matrix-org/matrix-react-sdk/pull/4782) + * Fix case-sensitivity of /me to match rest of slash commands + [\#4763](https://github.com/matrix-org/matrix-react-sdk/pull/4763) + * Add a 'show less' button to the new room list + [\#4765](https://github.com/matrix-org/matrix-react-sdk/pull/4765) + * Update from Weblate + [\#4781](https://github.com/matrix-org/matrix-react-sdk/pull/4781) + * Sticky and collapsing headers for new room list + [\#4758](https://github.com/matrix-org/matrix-react-sdk/pull/4758) + * Make the room list labs setting reload on change + [\#4780](https://github.com/matrix-org/matrix-react-sdk/pull/4780) + * Handle/hide old rooms in the room list + [\#4767](https://github.com/matrix-org/matrix-react-sdk/pull/4767) + * Add some media queries to improve UI on mobile (#3991) + [\#4656](https://github.com/matrix-org/matrix-react-sdk/pull/4656) + * Match fuzzy filtering a bit more reliably in the new room list + [\#4769](https://github.com/matrix-org/matrix-react-sdk/pull/4769) + * Improve Field ts definitions some more + [\#4777](https://github.com/matrix-org/matrix-react-sdk/pull/4777) + * Fix alignment of checkboxes in new room list's context menu + [\#4776](https://github.com/matrix-org/matrix-react-sdk/pull/4776) + * Fix Field ts def, fix LocalEchoWrapper and NotificationsEnabledController + [\#4775](https://github.com/matrix-org/matrix-react-sdk/pull/4775) + * Add presence indicators and globes to new room list + [\#4774](https://github.com/matrix-org/matrix-react-sdk/pull/4774) + * Include the sticky room when filtering in the new room list + [\#4772](https://github.com/matrix-org/matrix-react-sdk/pull/4772) + * Add a home button to the new room list menu when available + [\#4771](https://github.com/matrix-org/matrix-react-sdk/pull/4771) + * use group layout for search results + [\#4764](https://github.com/matrix-org/matrix-react-sdk/pull/4764) + * Fix m.id.phone spec compliance + [\#4757](https://github.com/matrix-org/matrix-react-sdk/pull/4757) + * User Info default power levels for ban/kick/redact to 50 as per spec + [\#4759](https://github.com/matrix-org/matrix-react-sdk/pull/4759) + * Match new room list's text search to old room list + [\#4768](https://github.com/matrix-org/matrix-react-sdk/pull/4768) + * Fix ordering of recent rooms in the new room list + [\#4766](https://github.com/matrix-org/matrix-react-sdk/pull/4766) + * Change theme selector to use new styled radio buttons + [\#4731](https://github.com/matrix-org/matrix-react-sdk/pull/4731) + * Use recovery keys over passphrases + [\#4686](https://github.com/matrix-org/matrix-react-sdk/pull/4686) + * Update from Weblate + [\#4760](https://github.com/matrix-org/matrix-react-sdk/pull/4760) + * Initial dark theme support for new room list + [\#4756](https://github.com/matrix-org/matrix-react-sdk/pull/4756) + * Support per-list options and algorithms on the new room list + [\#4754](https://github.com/matrix-org/matrix-react-sdk/pull/4754) + * Send read marker updates immediately after moving visually + [\#4755](https://github.com/matrix-org/matrix-react-sdk/pull/4755) + * Add a minimized view to the new room list + [\#4753](https://github.com/matrix-org/matrix-react-sdk/pull/4753) + * Fix e2e icon alignment in irc-layout + [\#4752](https://github.com/matrix-org/matrix-react-sdk/pull/4752) + * Add some resource leak protection to new room list badges + [\#4750](https://github.com/matrix-org/matrix-react-sdk/pull/4750) + * Fix read-receipt alignment + [\#4747](https://github.com/matrix-org/matrix-react-sdk/pull/4747) + * Show message previews on the new room list tiles + [\#4751](https://github.com/matrix-org/matrix-react-sdk/pull/4751) + * Fix various layout concerns with the new room list + [\#4749](https://github.com/matrix-org/matrix-react-sdk/pull/4749) + * Prioritize text on the clipboard over file + [\#4748](https://github.com/matrix-org/matrix-react-sdk/pull/4748) + * Move Settings flag to ts + [\#4729](https://github.com/matrix-org/matrix-react-sdk/pull/4729) + * Add a context menu to rooms in the new room list + [\#4743](https://github.com/matrix-org/matrix-react-sdk/pull/4743) + * Add hover states and basic context menu to new room list + [\#4742](https://github.com/matrix-org/matrix-react-sdk/pull/4742) + * Update resize handle for new designs in new room list + [\#4741](https://github.com/matrix-org/matrix-react-sdk/pull/4741) + * Improve general stability in the new room list + [\#4740](https://github.com/matrix-org/matrix-react-sdk/pull/4740) + * Reimplement breadcrumbs for new room list + [\#4735](https://github.com/matrix-org/matrix-react-sdk/pull/4735) + * Add styled radio buttons + [\#4744](https://github.com/matrix-org/matrix-react-sdk/pull/4744) + * Hide checkbox tick on dark backgrounds + [\#4730](https://github.com/matrix-org/matrix-react-sdk/pull/4730) + * Make checkboxes a11y friendly + [\#4746](https://github.com/matrix-org/matrix-react-sdk/pull/4746) + * EventIndex: Store and restore the encryption info for encrypted events. + [\#4738](https://github.com/matrix-org/matrix-react-sdk/pull/4738) + * Use IDestroyable instead of IDisposable + [\#4739](https://github.com/matrix-org/matrix-react-sdk/pull/4739) + * Add/improve badge counts in new room list + [\#4734](https://github.com/matrix-org/matrix-react-sdk/pull/4734) + * Convert FormattingUtils to TypeScript and add badge utility function + [\#4732](https://github.com/matrix-org/matrix-react-sdk/pull/4732) + * Add filtering and exploring to the new room list + [\#4736](https://github.com/matrix-org/matrix-react-sdk/pull/4736) + * Support prioritized room list filters + [\#4737](https://github.com/matrix-org/matrix-react-sdk/pull/4737) + * Clean up font scaling appearance + [\#4733](https://github.com/matrix-org/matrix-react-sdk/pull/4733) + * Add user menu to new room list + [\#4722](https://github.com/matrix-org/matrix-react-sdk/pull/4722) + * New room list basic styling and layout + [\#4711](https://github.com/matrix-org/matrix-react-sdk/pull/4711) + * Fix read receipt overlap + [\#4727](https://github.com/matrix-org/matrix-react-sdk/pull/4727) + * Load correct default font size + [\#4726](https://github.com/matrix-org/matrix-react-sdk/pull/4726) + * send state of lowBandwidth in rageshakes + [\#4724](https://github.com/matrix-org/matrix-react-sdk/pull/4724) + * Change internal font size from from 15 to 10 + [\#4725](https://github.com/matrix-org/matrix-react-sdk/pull/4725) + * Upgrade deps + [\#4723](https://github.com/matrix-org/matrix-react-sdk/pull/4723) + * Ensure active Jitsi conference is closed on widget pop-out + [\#4444](https://github.com/matrix-org/matrix-react-sdk/pull/4444) + * Introduce sticky rooms to the new room list + [\#4720](https://github.com/matrix-org/matrix-react-sdk/pull/4720) + * Handle remaining cases for room updates in new room list + [\#4721](https://github.com/matrix-org/matrix-react-sdk/pull/4721) + * Allow searching the emoji picker using other emoji + [\#4719](https://github.com/matrix-org/matrix-react-sdk/pull/4719) + * New room list scrolling and resizing + [\#4697](https://github.com/matrix-org/matrix-react-sdk/pull/4697) + * Don't show FormatBar if composer is empty + [\#4696](https://github.com/matrix-org/matrix-react-sdk/pull/4696) + * Split the left panel into new and old for new room list designs + [\#4687](https://github.com/matrix-org/matrix-react-sdk/pull/4687) + * Fix compact layout regression + [\#4712](https://github.com/matrix-org/matrix-react-sdk/pull/4712) + * fix emoji in safari + [\#4710](https://github.com/matrix-org/matrix-react-sdk/pull/4710) + * Fix not being able to dismiss new login toasts + [\#4709](https://github.com/matrix-org/matrix-react-sdk/pull/4709) + * Fix exceptions from Tooltip + [\#4708](https://github.com/matrix-org/matrix-react-sdk/pull/4708) + * Stop removing variation selector from quick reactions + [\#4707](https://github.com/matrix-org/matrix-react-sdk/pull/4707) + * Tidy up continuation algorithm and make it work for hidden profile changes + [\#4704](https://github.com/matrix-org/matrix-react-sdk/pull/4704) + * Profile settings should never show a disambiguated display name + [\#4699](https://github.com/matrix-org/matrix-react-sdk/pull/4699) + * Prevent (double) 4S bootstrap from RestoreKeyBackupDialog + [\#4701](https://github.com/matrix-org/matrix-react-sdk/pull/4701) + * Stop checkbox styling bleeding through room address selector + [\#4691](https://github.com/matrix-org/matrix-react-sdk/pull/4691) + * Center HeaderButtons + [\#4695](https://github.com/matrix-org/matrix-react-sdk/pull/4695) + * Add .well-known option to control default e2ee behaviour + [\#4605](https://github.com/matrix-org/matrix-react-sdk/pull/4605) + * Add max-width to right and left panels + [\#4692](https://github.com/matrix-org/matrix-react-sdk/pull/4692) + * Fix login loop where the sso flow returns to `#/login` + [\#4685](https://github.com/matrix-org/matrix-react-sdk/pull/4685) + * Don't clear MAU toasts when a successful sync comes in + [\#4690](https://github.com/matrix-org/matrix-react-sdk/pull/4690) + * Add initial filtering support to new room list + [\#4681](https://github.com/matrix-org/matrix-react-sdk/pull/4681) + * Bubble up a decline-to-render of verification events to outside wrapper + [\#4664](https://github.com/matrix-org/matrix-react-sdk/pull/4664) + * upgrade to twemoji 13.0.0 + [\#4672](https://github.com/matrix-org/matrix-react-sdk/pull/4672) + * Apply FocusLock to ImageView to capture Escape handling + [\#4666](https://github.com/matrix-org/matrix-react-sdk/pull/4666) + * Fix the 'complete security' screen + [\#4689](https://github.com/matrix-org/matrix-react-sdk/pull/4689) + * add null-guard for Autocomplete containerRef + [\#4688](https://github.com/matrix-org/matrix-react-sdk/pull/4688) + * Remove legacy codepaths for Unknown Device Error (UDE/UDD) handling + [\#4660](https://github.com/matrix-org/matrix-react-sdk/pull/4660) + * Remove feature_cross_signing + [\#4655](https://github.com/matrix-org/matrix-react-sdk/pull/4655) + * Autocomplete: use scrollIntoView for auto-scroll to fix it + [\#4670](https://github.com/matrix-org/matrix-react-sdk/pull/4670) + +Changes in [2.7.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.2) (2020-06-16) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.1...v2.7.2) + + * Upgrade to JS SDK 6.2.2 + +Changes in [2.7.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.1) (2020-06-05) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.0...v2.7.1) + + * Upgrade to JS SDK 6.2.1 + * Fix exceptions from Tooltip + [\#4716](https://github.com/matrix-org/matrix-react-sdk/pull/4716) + * Fix not being able to dismiss new login toasts + [\#4715](https://github.com/matrix-org/matrix-react-sdk/pull/4715) + * Fix compact layout regression + [\#4714](https://github.com/matrix-org/matrix-react-sdk/pull/4714) + +Changes in [2.7.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.0) (2020-06-04) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.0-rc.2...v2.7.0) + + * Upgrade to JS SDK 6.2.0 + * Prevent (double) 4S bootstrap from RestoreKeyBackupDialog + [\#4703](https://github.com/matrix-org/matrix-react-sdk/pull/4703) + * Fix checkbox bleed + [\#4702](https://github.com/matrix-org/matrix-react-sdk/pull/4702) + * Fix login loop where the sso flow returns to `#/login` to release + [\#4693](https://github.com/matrix-org/matrix-react-sdk/pull/4693) + +Changes in [2.7.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.0-rc.2) (2020-06-02) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.7.0-rc.1...v2.7.0-rc.2) + + * Rewire the Sticker button to be an Emoji Picker + [\#3747](https://github.com/matrix-org/matrix-react-sdk/pull/3747) + +Changes in [2.7.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.7.0-rc.1) (2020-06-02) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.6.1...v2.7.0-rc.1) + + * Upgrade to JS SDK 6.2.0-rc.1 + * Update from Weblate + [\#4683](https://github.com/matrix-org/matrix-react-sdk/pull/4683) + * Make auth argument in the register request compliant with r0.6.0 + [\#4347](https://github.com/matrix-org/matrix-react-sdk/pull/4347) + * Revert "Prevent PersistedElements overflowing scrolled areas" + [\#4682](https://github.com/matrix-org/matrix-react-sdk/pull/4682) + * Remove unused TagPanelButtons + [\#4680](https://github.com/matrix-org/matrix-react-sdk/pull/4680) + * Pass roomId to IRCTimelineProfileResizer + [\#4679](https://github.com/matrix-org/matrix-react-sdk/pull/4679) + * Remove logging to console for irc name resize + [\#4678](https://github.com/matrix-org/matrix-react-sdk/pull/4678) + * Use arrow functions instead of binding `this` + [\#4677](https://github.com/matrix-org/matrix-react-sdk/pull/4677) + * Increase specificity of compact layout selectors + [\#4675](https://github.com/matrix-org/matrix-react-sdk/pull/4675) + * Create and use stylised checkboxes + [\#4665](https://github.com/matrix-org/matrix-react-sdk/pull/4665) + * useIRCLayout moved to props + [\#4676](https://github.com/matrix-org/matrix-react-sdk/pull/4676) + * Fix paste image to upload + [\#4674](https://github.com/matrix-org/matrix-react-sdk/pull/4674) + * Fix FilePanel and NotificationsPanel regression + [\#4647](https://github.com/matrix-org/matrix-react-sdk/pull/4647) + * Allow deferring of Update Toast until the next morning + [\#4669](https://github.com/matrix-org/matrix-react-sdk/pull/4669) + * Give contextual feedback for manual update check instead of banner + [\#4668](https://github.com/matrix-org/matrix-react-sdk/pull/4668) + * Dialog wrap title instead of taking same space as the close/cancel button + [\#4659](https://github.com/matrix-org/matrix-react-sdk/pull/4659) + * Update Modular hosting link + [\#4627](https://github.com/matrix-org/matrix-react-sdk/pull/4627) + * Fix field placeholder regression + [\#4663](https://github.com/matrix-org/matrix-react-sdk/pull/4663) + * Fix/document a number of UIA oddities + [\#4667](https://github.com/matrix-org/matrix-react-sdk/pull/4667) + * Stop copy icon repeating weirdly + [\#4662](https://github.com/matrix-org/matrix-react-sdk/pull/4662) + * Try and fix the Notifier race + [\#4661](https://github.com/matrix-org/matrix-react-sdk/pull/4661) + * set the client's pickle key if the platform can store one + [\#4657](https://github.com/matrix-org/matrix-react-sdk/pull/4657) + * Migrate Banners to Toasts + [\#4624](https://github.com/matrix-org/matrix-react-sdk/pull/4624) + * Move Appearance tab to ts + [\#4658](https://github.com/matrix-org/matrix-react-sdk/pull/4658) + * Fix room alias lookup vs peeking race condition + [\#4606](https://github.com/matrix-org/matrix-react-sdk/pull/4606) + * Fix encryption icon miss-alignment + [\#4651](https://github.com/matrix-org/matrix-react-sdk/pull/4651) + * Fix sublist sizing regression + [\#4649](https://github.com/matrix-org/matrix-react-sdk/pull/4649) + * Fix lines overflowing room list width + [\#4650](https://github.com/matrix-org/matrix-react-sdk/pull/4650) + * Remove the keyshare dialog + [\#4648](https://github.com/matrix-org/matrix-react-sdk/pull/4648) + * Update badge counts in new room list as needed + [\#4654](https://github.com/matrix-org/matrix-react-sdk/pull/4654) + * EventIndex: Handle invalid m.room.redaction events correctly. + [\#4653](https://github.com/matrix-org/matrix-react-sdk/pull/4653) + * EventIndex: Print out the checkpoint if there was an error during a crawl + [\#4652](https://github.com/matrix-org/matrix-react-sdk/pull/4652) + * Move Field to Typescript + [\#4635](https://github.com/matrix-org/matrix-react-sdk/pull/4635) + * Use connection error to detect network problem + [\#4646](https://github.com/matrix-org/matrix-react-sdk/pull/4646) + * Revert default font size to 15px + [\#4641](https://github.com/matrix-org/matrix-react-sdk/pull/4641) + * Add logging when room join fails + [\#4645](https://github.com/matrix-org/matrix-react-sdk/pull/4645) + * Remove EncryptedEventDialog + [\#4644](https://github.com/matrix-org/matrix-react-sdk/pull/4644) + * Migrate Toasts to Typescript and to granular priority system + [\#4618](https://github.com/matrix-org/matrix-react-sdk/pull/4618) + * Update Crypto Store Too New copy + [\#4632](https://github.com/matrix-org/matrix-react-sdk/pull/4632) + * MemberAvatar should not have its own letter fallback, it should use + BaseAvatar + [\#4643](https://github.com/matrix-org/matrix-react-sdk/pull/4643) + * Fix media upload issues with abort and status bar + [\#4630](https://github.com/matrix-org/matrix-react-sdk/pull/4630) + * fix viewGroup to actually show the group if possible + [\#4633](https://github.com/matrix-org/matrix-react-sdk/pull/4633) + * Update confirm passphrase copy + [\#4634](https://github.com/matrix-org/matrix-react-sdk/pull/4634) + * Improve accessibility of the emoji picker + [\#4636](https://github.com/matrix-org/matrix-react-sdk/pull/4636) + * Fix Emoji Picker footer being too small if text overflows + [\#4631](https://github.com/matrix-org/matrix-react-sdk/pull/4631) + * Improve style of toasts to match Figma + [\#4613](https://github.com/matrix-org/matrix-react-sdk/pull/4613) + * Iterate toast count indicator more logically + [\#4620](https://github.com/matrix-org/matrix-react-sdk/pull/4620) + * Fix reacting to redactions + [\#4626](https://github.com/matrix-org/matrix-react-sdk/pull/4626) + * Fix sentMessageAndIsAlone by dispatching `message_sent` more consistently + [\#4628](https://github.com/matrix-org/matrix-react-sdk/pull/4628) + * Update from Weblate + [\#4640](https://github.com/matrix-org/matrix-react-sdk/pull/4640) + * Replace `alias` with `address` in copy for consistency + [\#4402](https://github.com/matrix-org/matrix-react-sdk/pull/4402) + * Convert MatrixClientPeg to TypeScript + [\#4638](https://github.com/matrix-org/matrix-react-sdk/pull/4638) + * Fix BaseAvatar wrongly retrying urls + [\#4629](https://github.com/matrix-org/matrix-react-sdk/pull/4629) + * Fix event highlights not being updated to reflect edits + [\#4637](https://github.com/matrix-org/matrix-react-sdk/pull/4637) + * Calculate badges in the new room list more reliably + [\#4625](https://github.com/matrix-org/matrix-react-sdk/pull/4625) + * Transition BaseAvatar to hooks + [\#4101](https://github.com/matrix-org/matrix-react-sdk/pull/4101) + * Convert BasePlatform and BaseEventIndexManager to Typescript + [\#4614](https://github.com/matrix-org/matrix-react-sdk/pull/4614) + * Fix: Tag_DM is not defined + [\#4619](https://github.com/matrix-org/matrix-react-sdk/pull/4619) + * Fix visibility of message timestamps + [\#4615](https://github.com/matrix-org/matrix-react-sdk/pull/4615) + * Rewrite the room list store + [\#4253](https://github.com/matrix-org/matrix-react-sdk/pull/4253) + * Update code style to mention switch statements + [\#4610](https://github.com/matrix-org/matrix-react-sdk/pull/4610) + * Fix key backup restore with SSSS + [\#4612](https://github.com/matrix-org/matrix-react-sdk/pull/4612) + * Handle null tokens in the crawler loop. + [\#4608](https://github.com/matrix-org/matrix-react-sdk/pull/4608) + * Font scaling settings and slider + [\#4424](https://github.com/matrix-org/matrix-react-sdk/pull/4424) + * Prevent PersistedElements overflowing scrolled areas + [\#4494](https://github.com/matrix-org/matrix-react-sdk/pull/4494) + * IRC ui layout + [\#4531](https://github.com/matrix-org/matrix-react-sdk/pull/4531) + * Remove SSSS key upgrade check from rageshake + [\#4607](https://github.com/matrix-org/matrix-react-sdk/pull/4607) + * Label the create room button better than "Add room" + [\#4603](https://github.com/matrix-org/matrix-react-sdk/pull/4603) + * Convert the dispatcher to TypeScript + [\#4593](https://github.com/matrix-org/matrix-react-sdk/pull/4593) + * Consolidate password/passphrase fields into a component & add dynamic colour + to progress + [\#4599](https://github.com/matrix-org/matrix-react-sdk/pull/4599) + * UserView, show Welcome page in the mid panel instead of empty space + [\#4590](https://github.com/matrix-org/matrix-react-sdk/pull/4590) + * Update from Weblate + [\#4601](https://github.com/matrix-org/matrix-react-sdk/pull/4601) + * Make email auth component fail better if server claims email isn't validated + [\#4600](https://github.com/matrix-org/matrix-react-sdk/pull/4600) + * Add new keyboard shortcuts for jump to unread and upload file + [\#4588](https://github.com/matrix-org/matrix-react-sdk/pull/4588) + * accept and linkify local domains like those from mDNS + [\#4594](https://github.com/matrix-org/matrix-react-sdk/pull/4594) + * Revert "ImageView make clicking off it easier" + [\#4586](https://github.com/matrix-org/matrix-react-sdk/pull/4586) + * wrap node-qrcode in a React FC and use it for ShareDialog + [\#4394](https://github.com/matrix-org/matrix-react-sdk/pull/4394) + * Pass screenAfterLogin through SSO in the callback url + [\#4585](https://github.com/matrix-org/matrix-react-sdk/pull/4585) + * Remove debugging that causes email addresses to load forever + [\#4597](https://github.com/matrix-org/matrix-react-sdk/pull/4597) + +Changes in [2.6.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.6.1) (2020-05-22) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.6.0...v2.6.1) + + * Fix key backup restore with SSSS + [\#4617](https://github.com/matrix-org/matrix-react-sdk/pull/4617) + * Remove SSSS key upgrade check from rageshake + [\#4616](https://github.com/matrix-org/matrix-react-sdk/pull/4616) + +Changes in [2.6.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.6.0) (2020-05-19) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.6.0-rc.1...v2.6.0) + + * Upgrade to JS SDK 6.1.0 + * Revert "ImageView make clicking off it easier" + [\#4602](https://github.com/matrix-org/matrix-react-sdk/pull/4602) + * Remove debugging that causes email addresses to load forever (to release) + [\#4598](https://github.com/matrix-org/matrix-react-sdk/pull/4598) + +Changes in [2.6.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.6.0-rc.1) (2020-05-14) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.5.0...v2.6.0-rc.1) + + * Upgrade to JS SDK 6.1.0-rc.1 + * Update from Weblate + [\#4596](https://github.com/matrix-org/matrix-react-sdk/pull/4596) + * Fix message edits dialog being wrong and sometimes crashing + [\#4595](https://github.com/matrix-org/matrix-react-sdk/pull/4595) + * Acquire a new session before enacting deactivation + [\#4584](https://github.com/matrix-org/matrix-react-sdk/pull/4584) + * Remove UI for upgrading 4S to symmetric encryption + [\#4581](https://github.com/matrix-org/matrix-react-sdk/pull/4581) + * Add copy to SSO prompts during cross-signing setup + [\#4555](https://github.com/matrix-org/matrix-react-sdk/pull/4555) + * Re-fix OpenID requests from widgets + [\#4592](https://github.com/matrix-org/matrix-react-sdk/pull/4592) + * Fix persistent widgets on desktop / http + [\#4591](https://github.com/matrix-org/matrix-react-sdk/pull/4591) + * Updated link and added:Yarn two is not yet used. + [\#4589](https://github.com/matrix-org/matrix-react-sdk/pull/4589) + * Fix topic dialog not supporting escape as it didn't have a "Close" + [\#4578](https://github.com/matrix-org/matrix-react-sdk/pull/4578) + * Default to public room when creating room from room directory + [\#4579](https://github.com/matrix-org/matrix-react-sdk/pull/4579) + * Replace png flags and add Kosovo to country code dropdown + [\#4576](https://github.com/matrix-org/matrix-react-sdk/pull/4576) + * Rename `trash (custom).svg` as electron doesn't like paths with spaces + [\#4583](https://github.com/matrix-org/matrix-react-sdk/pull/4583) + * Fix sign in / up links on previewed rooms + [\#4582](https://github.com/matrix-org/matrix-react-sdk/pull/4582) + * Avoid soft crash if unknown device in verification + [\#4580](https://github.com/matrix-org/matrix-react-sdk/pull/4580) + * Add slash commands /query and /msg to match IRC + [\#4568](https://github.com/matrix-org/matrix-react-sdk/pull/4568) + * Send cross-signing debug booleans over rageshake + [\#4570](https://github.com/matrix-org/matrix-react-sdk/pull/4570) + * Prompt user to specify an alternate server if theirs has registration off + [\#4575](https://github.com/matrix-org/matrix-react-sdk/pull/4575) + * Don't try and redact redactions for "Remove recent messages" + [\#4573](https://github.com/matrix-org/matrix-react-sdk/pull/4573) + * View Source should target the replacing event rather than the root one + [\#4571](https://github.com/matrix-org/matrix-react-sdk/pull/4571) + * Fix passphrase reset in key backup restore dialog + [\#4569](https://github.com/matrix-org/matrix-react-sdk/pull/4569) + * Ensure key backup gets dealt with correctly during secret storage reset + [\#4556](https://github.com/matrix-org/matrix-react-sdk/pull/4556) + * Fix crash for broken invites + [\#4565](https://github.com/matrix-org/matrix-react-sdk/pull/4565) + * Fix rageshake with no matrix client + [\#4572](https://github.com/matrix-org/matrix-react-sdk/pull/4572) + * Update from Weblate + [\#4567](https://github.com/matrix-org/matrix-react-sdk/pull/4567) + * Bring back UnknownBody for UISIs + [\#4564](https://github.com/matrix-org/matrix-react-sdk/pull/4564) + * clear tag panel selection if the community selected is left + [\#4559](https://github.com/matrix-org/matrix-react-sdk/pull/4559) + * Close ImageView when redacting + [\#4560](https://github.com/matrix-org/matrix-react-sdk/pull/4560) + * Redesign redactions + [\#4484](https://github.com/matrix-org/matrix-react-sdk/pull/4484) + * Don't try to reload profile information when closing the user panel + [\#4547](https://github.com/matrix-org/matrix-react-sdk/pull/4547) + * Fix right panel hiding when viewing room member + [\#4558](https://github.com/matrix-org/matrix-react-sdk/pull/4558) + * Don't erase password confirm on registration error + [\#4540](https://github.com/matrix-org/matrix-react-sdk/pull/4540) + * Add a loading state for email addresses/phone numbers in settings + [\#4557](https://github.com/matrix-org/matrix-react-sdk/pull/4557) + * set the meta tag for theme-color to the same theme css background + [\#4554](https://github.com/matrix-org/matrix-react-sdk/pull/4554) + * Update Invite Dialog copy to include email addresses + [\#4497](https://github.com/matrix-org/matrix-react-sdk/pull/4497) + * Fix slider toggle regression. + [\#4546](https://github.com/matrix-org/matrix-react-sdk/pull/4546) + * Fix a crash where a name could unexpectedly be an empty list + [\#4552](https://github.com/matrix-org/matrix-react-sdk/pull/4552) + * Solves communities can be dragged from context menu + [\#4492](https://github.com/matrix-org/matrix-react-sdk/pull/4492) + * Remove prefixes for composer avatar urls + [\#4553](https://github.com/matrix-org/matrix-react-sdk/pull/4553) + * Fix reply RR spacing getting doubled + [\#4541](https://github.com/matrix-org/matrix-react-sdk/pull/4541) + * Differentiate copy for own untrusted device dialog + [\#4549](https://github.com/matrix-org/matrix-react-sdk/pull/4549) + * EventIndex: Reduce the logging the event index is producing. + [\#4548](https://github.com/matrix-org/matrix-react-sdk/pull/4548) + * Increase rageshake size limit to 5mb + [\#4543](https://github.com/matrix-org/matrix-react-sdk/pull/4543) + * Update from Weblate + [\#4542](https://github.com/matrix-org/matrix-react-sdk/pull/4542) + * Guard against race when waiting for cross-signing to be ready + [\#4539](https://github.com/matrix-org/matrix-react-sdk/pull/4539) + * Wait for user to be verified in e2e setup + [\#4537](https://github.com/matrix-org/matrix-react-sdk/pull/4537) + * Convert MatrixChat to a TypeScript class + [\#4462](https://github.com/matrix-org/matrix-react-sdk/pull/4462) + * Mark room as read when escape is pressed + [\#4271](https://github.com/matrix-org/matrix-react-sdk/pull/4271) + * Only show key backup reminder when confirmed by server to be missing + [\#4534](https://github.com/matrix-org/matrix-react-sdk/pull/4534) + * Add device name to unverified session toast + [\#4535](https://github.com/matrix-org/matrix-react-sdk/pull/4535) + * Show progress when loading keys + [\#4507](https://github.com/matrix-org/matrix-react-sdk/pull/4507) + * Fix device verification toasts not disappearing + [\#4532](https://github.com/matrix-org/matrix-react-sdk/pull/4532) + * Update toast copy again + [\#4529](https://github.com/matrix-org/matrix-react-sdk/pull/4529) + * Re-apply theme after login + [\#4518](https://github.com/matrix-org/matrix-react-sdk/pull/4518) + * Reduce maximum width of toasts & allow multiple lines + [\#4525](https://github.com/matrix-org/matrix-react-sdk/pull/4525) + * Treat sessions that are there when we log in as old + [\#4524](https://github.com/matrix-org/matrix-react-sdk/pull/4524) + * Allow resetting storage from the access dialog + [\#4521](https://github.com/matrix-org/matrix-react-sdk/pull/4521) + * Update (bulk) unverified device toast copy + [\#4522](https://github.com/matrix-org/matrix-react-sdk/pull/4522) + * Make new device toasts appear above review toasts + [\#4519](https://github.com/matrix-org/matrix-react-sdk/pull/4519) + * Separate toasts for existing & new device verification + [\#4511](https://github.com/matrix-org/matrix-react-sdk/pull/4511) + * Slightly darker toggle off bg color + [\#4477](https://github.com/matrix-org/matrix-react-sdk/pull/4477) + * Fix pill vertical align + [\#4514](https://github.com/matrix-org/matrix-react-sdk/pull/4514) + * Fix set up encryption toast to use "set up" as action + [\#4502](https://github.com/matrix-org/matrix-react-sdk/pull/4502) + * Don't enable e2ee when inviting a 3pid + [\#4509](https://github.com/matrix-org/matrix-react-sdk/pull/4509) + * Fix internal link styling in Security Settings + [\#4510](https://github.com/matrix-org/matrix-react-sdk/pull/4510) + * Small custom theming fixes + [\#4508](https://github.com/matrix-org/matrix-react-sdk/pull/4508) + * Fix scaling issues + [\#4355](https://github.com/matrix-org/matrix-react-sdk/pull/4355) + * Aggregate device verify toasts + [\#4506](https://github.com/matrix-org/matrix-react-sdk/pull/4506) + * Support setting username and avatar colors in custom themes + [\#4503](https://github.com/matrix-org/matrix-react-sdk/pull/4503) + * only clear on continuations where the clear isn't done by SenderProfile + [\#4501](https://github.com/matrix-org/matrix-react-sdk/pull/4501) + * cap width of editable item list item to leave space for its X button + [\#4495](https://github.com/matrix-org/matrix-react-sdk/pull/4495) + * Add a link from settings / devices to your user profile + [\#4498](https://github.com/matrix-org/matrix-react-sdk/pull/4498) + * Update from Weblate + [\#4496](https://github.com/matrix-org/matrix-react-sdk/pull/4496) + * Make icon change in SetupEncryptionDialog + [\#4485](https://github.com/matrix-org/matrix-react-sdk/pull/4485) + * Remove invite only padlocks feature flag + [\#4487](https://github.com/matrix-org/matrix-react-sdk/pull/4487) + * Fix incorrect toast if security setup skipped + [\#4486](https://github.com/matrix-org/matrix-react-sdk/pull/4486) + * Revert "Update emojibase for fixed emoji codepoints and Emoji 13 support" + [\#4482](https://github.com/matrix-org/matrix-react-sdk/pull/4482) + * Fix widget URL templating (again) + [\#4481](https://github.com/matrix-org/matrix-react-sdk/pull/4481) + * Fix recovery link on login verification flow + [\#4479](https://github.com/matrix-org/matrix-react-sdk/pull/4479) + * Make avatars in pills occupy the entire space using cropping + [\#4476](https://github.com/matrix-org/matrix-react-sdk/pull/4476) + * Use WidgetType more often to avoid breaking new sticker pickers + [\#4458](https://github.com/matrix-org/matrix-react-sdk/pull/4458) + * Update logging for unmanaged widgets, and add TODO comments for other areas + [\#4460](https://github.com/matrix-org/matrix-react-sdk/pull/4460) + * Fix OpenID requests from widgets + [\#4459](https://github.com/matrix-org/matrix-react-sdk/pull/4459) + * Take encrypted message search out of labs + [\#4467](https://github.com/matrix-org/matrix-react-sdk/pull/4467) + * Fix BigEmoji for replies + [\#4475](https://github.com/matrix-org/matrix-react-sdk/pull/4475) + * Update login security copy and design to match Figma + [\#4472](https://github.com/matrix-org/matrix-react-sdk/pull/4472) + * Fix i18n of SSO UIA copy in Deactivate Account Dialog + [\#4471](https://github.com/matrix-org/matrix-react-sdk/pull/4471) + * Assert type of domNode as HTMLElement to fix build + [\#4470](https://github.com/matrix-org/matrix-react-sdk/pull/4470) + * Unignored in settings + [\#4466](https://github.com/matrix-org/matrix-react-sdk/pull/4466) + * Skip auth flow test for signing upload when password present + [\#4464](https://github.com/matrix-org/matrix-react-sdk/pull/4464) + * If user cannot set email during registration don't tell them to + [\#4461](https://github.com/matrix-org/matrix-react-sdk/pull/4461) + * Fix post-ts autocomplete, it is not null + [\#4463](https://github.com/matrix-org/matrix-react-sdk/pull/4463) + * Convert autocomplete stuff to TypeScript + [\#4452](https://github.com/matrix-org/matrix-react-sdk/pull/4452) + * Add a back button to the devtools verifications panel + [\#4455](https://github.com/matrix-org/matrix-react-sdk/pull/4455) + * Fix: wait until cross-signing keys are fetched to show verify button + [\#4456](https://github.com/matrix-org/matrix-react-sdk/pull/4456) + * Handle load error in create secret storage dialog + [\#4451](https://github.com/matrix-org/matrix-react-sdk/pull/4451) + * Allow iframes and Jitsi URLs in /addwidget + [\#4382](https://github.com/matrix-org/matrix-react-sdk/pull/4382) + * Support m.jitsi-typed widgets as Jitsi widgets + [\#4379](https://github.com/matrix-org/matrix-react-sdk/pull/4379) + * Don't recheck DeviceListener until after initial sync is finished + [\#4450](https://github.com/matrix-org/matrix-react-sdk/pull/4450) + * Fix CSS class in ButtonPlaceholder + [\#4449](https://github.com/matrix-org/matrix-react-sdk/pull/4449) + * Password Login make sure tab takes user to password field + [\#4441](https://github.com/matrix-org/matrix-react-sdk/pull/4441) + * Network Dropdown fix things not scrolling properly + [\#4439](https://github.com/matrix-org/matrix-react-sdk/pull/4439) + * ImageView make clicking off it easier + [\#4448](https://github.com/matrix-org/matrix-react-sdk/pull/4448) + * Add slash command to send a rageshake + [\#4443](https://github.com/matrix-org/matrix-react-sdk/pull/4443) + * EventIndex: Filter out events that don't have a propper content value. + [\#4446](https://github.com/matrix-org/matrix-react-sdk/pull/4446) + * Revert "Fix Filepanel scroll position state lost when room is changed" + [\#4445](https://github.com/matrix-org/matrix-react-sdk/pull/4445) + * Update seshat copy to remove trailing full stop + [\#4442](https://github.com/matrix-org/matrix-react-sdk/pull/4442) + * Fix Filepanel scroll position state lost when room is changed + [\#4388](https://github.com/matrix-org/matrix-react-sdk/pull/4388) + * Fix end-to-end tests for end-to-end encryption verification + [\#4436](https://github.com/matrix-org/matrix-react-sdk/pull/4436) + * Don't explode if the e2e test directory exists when crashing + [\#4437](https://github.com/matrix-org/matrix-react-sdk/pull/4437) + * Bump https-proxy-agent from 2.2.1 to 2.2.4 in /test/end-to-end-tests + [\#4430](https://github.com/matrix-org/matrix-react-sdk/pull/4430) + * Minor updates to e2e test instructions on Windows + [\#4432](https://github.com/matrix-org/matrix-react-sdk/pull/4432) + * Fix typo + [\#4435](https://github.com/matrix-org/matrix-react-sdk/pull/4435) + * Catch errors sooner so users can recover more easily + [\#4122](https://github.com/matrix-org/matrix-react-sdk/pull/4122) + * Rageshake: remind user of unsupported browser and send modernizr report + [\#4381](https://github.com/matrix-org/matrix-react-sdk/pull/4381) + * Design tweaks for DM Room Tiles + [\#4338](https://github.com/matrix-org/matrix-react-sdk/pull/4338) + * Don't break spills over multiple lines, ellipsis them at max-1-line + [\#4434](https://github.com/matrix-org/matrix-react-sdk/pull/4434) + * Turn the end-to-end tests back on and fix the lazy-loading tests + [\#4433](https://github.com/matrix-org/matrix-react-sdk/pull/4433) + * Fix key backup debug panel + [\#4431](https://github.com/matrix-org/matrix-react-sdk/pull/4431) + * Convert cross-signing feature flag to setting + [\#4416](https://github.com/matrix-org/matrix-react-sdk/pull/4416) + * Make RoomPublishSetting import-skinnable + [\#4428](https://github.com/matrix-org/matrix-react-sdk/pull/4428) + * Iterate cross-signing copy + [\#4425](https://github.com/matrix-org/matrix-react-sdk/pull/4425) + * Fix: ensure twemoji font is loaded when showing SAS emojis + [\#4422](https://github.com/matrix-org/matrix-react-sdk/pull/4422) + * Revert "Fix: load Twemoji before login so complete security gets the right + emojis during SAS" + [\#4421](https://github.com/matrix-org/matrix-react-sdk/pull/4421) + * Fix: load Twemoji before login so complete security gets the right emojis + during SAS + [\#4419](https://github.com/matrix-org/matrix-react-sdk/pull/4419) + * consolidate and fix copy to clipboard + [\#4410](https://github.com/matrix-org/matrix-react-sdk/pull/4410) + * Fix Message Context Menu options not displaying: block + [\#4418](https://github.com/matrix-org/matrix-react-sdk/pull/4418) + * Fix pills being broken by unescaped characters + [\#4411](https://github.com/matrix-org/matrix-react-sdk/pull/4411) + +Changes in [2.5.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.5.0) (2020-05-05) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.5.0-rc.6...v2.5.0) + + * Upgrade to JS SDK 6.0.0 + * EventIndex: Reduce the logging the event index is producing. + [\#4551](https://github.com/matrix-org/matrix-react-sdk/pull/4551) + * Differentiate copy for own untrusted device dialog + [\#4550](https://github.com/matrix-org/matrix-react-sdk/pull/4550) + * More detailed progress for key backup progress + [\#4545](https://github.com/matrix-org/matrix-react-sdk/pull/4545) + * Increase rageshake size limit to 5mb + [\#4544](https://github.com/matrix-org/matrix-react-sdk/pull/4544) + +Changes in [2.5.0-rc.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.5.0-rc.6) (2020-05-01) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.5.0-rc.5...v2.5.0-rc.6) + + * Upgrade to JS SDK 6.0.0-rc.2 + * Wait for user to be verified in e2e setup + [\#4538](https://github.com/matrix-org/matrix-react-sdk/pull/4538) + * Add device name to unverified session toast + [\#4536](https://github.com/matrix-org/matrix-react-sdk/pull/4536) + +Changes in [2.5.0-rc.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.5.0-rc.5) (2020-04-30) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.5.0-rc.4...v2.5.0-rc.5) + + * Upgrade to JS SDK 6.0.0-rc.1 + * Fix device verification toasts not disappearing + [\#4533](https://github.com/matrix-org/matrix-react-sdk/pull/4533) + * Allow resetting storage from the access dialog + [\#4526](https://github.com/matrix-org/matrix-react-sdk/pull/4526) + * Update toast copy again + [\#4530](https://github.com/matrix-org/matrix-react-sdk/pull/4530) + * Reduce maximum width of toasts & allow multiple lines + [\#4528](https://github.com/matrix-org/matrix-react-sdk/pull/4528) + * Treat sessions that are there when we log in as old + [\#4527](https://github.com/matrix-org/matrix-react-sdk/pull/4527) + * Update (bulk) unverified device toast copy + [\#4523](https://github.com/matrix-org/matrix-react-sdk/pull/4523) + * Make new device toasts appear above review toasts + [\#4520](https://github.com/matrix-org/matrix-react-sdk/pull/4520) + * Separate toasts for existing & new device verification + [\#4517](https://github.com/matrix-org/matrix-react-sdk/pull/4517) + * Aggregate device verify toasts + [\#4516](https://github.com/matrix-org/matrix-react-sdk/pull/4516) + * Fix set up encryption toast to use "set up" as action + [\#4515](https://github.com/matrix-org/matrix-react-sdk/pull/4515) + * Fix internal link styling in Security Settings + [\#4512](https://github.com/matrix-org/matrix-react-sdk/pull/4512) + * Don't enable e2ee when inviting a 3pid + [\#4513](https://github.com/matrix-org/matrix-react-sdk/pull/4513) + * only clear on continuations where the clear isn't done by SenderProfile + [\#4505](https://github.com/matrix-org/matrix-react-sdk/pull/4505) + * cap width of editable item list item to leave space for its X button + [\#4504](https://github.com/matrix-org/matrix-react-sdk/pull/4504) + * Add a link from settings / devices to your user profile + [\#4499](https://github.com/matrix-org/matrix-react-sdk/pull/4499) + * Make icon change in SetupEncryptionDialog + [\#4490](https://github.com/matrix-org/matrix-react-sdk/pull/4490) + * Remove invite only padlocks feature flag for release + [\#4488](https://github.com/matrix-org/matrix-react-sdk/pull/4488) + * Fix incorrect toast if security setup skipped + [\#4489](https://github.com/matrix-org/matrix-react-sdk/pull/4489) + * Revert "Update emojibase for fixed emoji codepoints and Emoji 13 support" + [\#4483](https://github.com/matrix-org/matrix-react-sdk/pull/4483) + * Fix recovery link on login verification flow + [\#4480](https://github.com/matrix-org/matrix-react-sdk/pull/4480) + +Changes in [2.5.0-rc.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.5.0-rc.4) (2020-04-23) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.5.0-rc.3...v2.5.0-rc.4) + + * Upgrade to JS SDK 5.3.1-rc.4 + * Take encrypted message search out of labs for release + [\#4468](https://github.com/matrix-org/matrix-react-sdk/pull/4468) + * Update login security copy and design to match Figma [to release] + [\#4474](https://github.com/matrix-org/matrix-react-sdk/pull/4474) + * Fix i18n of SSO UIA copy in Deactivate Account Dialog on release + [\#4473](https://github.com/matrix-org/matrix-react-sdk/pull/4473) + * Skip auth flow test for signing upload when password present + [\#4465](https://github.com/matrix-org/matrix-react-sdk/pull/4465) + * Fix: wait until cross-signing keys are fetched to show verify button + [\#4457](https://github.com/matrix-org/matrix-react-sdk/pull/4457) + * Handle load error in create secret storage dialog + [\#4454](https://github.com/matrix-org/matrix-react-sdk/pull/4454) + * Don't recheck DeviceListener until after initial sync is finished + [\#4450](https://github.com/matrix-org/matrix-react-sdk/pull/4450) + * EventIndex: Filter out events that don't have a propper content value. + [\#4447](https://github.com/matrix-org/matrix-react-sdk/pull/4447) + +Changes in [2.5.0-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.5.0-rc.3) (2020-04-17) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.5.0-rc.2...v2.5.0-rc.3) + + * Upgrade to JS SDK 5.3.1-rc.3 + +Changes in [2.5.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.5.0-rc.2) (2020-04-16) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.5.0-rc.1...v2.5.0-rc.2) + + * Upgrade to JS SDK 5.3.1-rc.2 + * [Release] Convert cross-signing flag to a setting + [\#4429](https://github.com/matrix-org/matrix-react-sdk/pull/4429) + * Iterate cross-signing copy + [\#4426](https://github.com/matrix-org/matrix-react-sdk/pull/4426) + * Fix: ensure twemoji font is loaded when showing SAS emojis + [\#4423](https://github.com/matrix-org/matrix-react-sdk/pull/4423) + +Changes in [2.5.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.5.0-rc.1) (2020-04-15) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.4.0-rc.1...v2.5.0-rc.1) + + * Upgrade to JS SDK 5.3.1-rc.1 + * null-guard MatrixClientPeg in RoomViewStore + [\#4415](https://github.com/matrix-org/matrix-react-sdk/pull/4415) + * Fix: prevent spurious notifications from indexer + [\#4414](https://github.com/matrix-org/matrix-react-sdk/pull/4414) + * Login block on initialSync with spinners + [\#4413](https://github.com/matrix-org/matrix-react-sdk/pull/4413) + * Allow network dropdown to be scrollable and fix context menu padding calc + [\#4408](https://github.com/matrix-org/matrix-react-sdk/pull/4408) + * Remove end-to-end message info option when cross-signing is used + [\#4412](https://github.com/matrix-org/matrix-react-sdk/pull/4412) + * Minimize widgets by default + [\#4378](https://github.com/matrix-org/matrix-react-sdk/pull/4378) + * Add comments to highlight where we'll need m.widget support + [\#4380](https://github.com/matrix-org/matrix-react-sdk/pull/4380) + * Fix: dont try to enable 4S if cross-signing is disabled + [\#4407](https://github.com/matrix-org/matrix-react-sdk/pull/4407) + * Fix: don't confuse user with spinner during complete security step + [\#4406](https://github.com/matrix-org/matrix-react-sdk/pull/4406) + * Fix: avoid potential crash during certain verification paths + [\#4405](https://github.com/matrix-org/matrix-react-sdk/pull/4405) + * Add riot-desktop shortcuts for forward/back matching browsers&slack + [\#4392](https://github.com/matrix-org/matrix-react-sdk/pull/4392) + * Convert LoggedInView to an ES6 PureComponent Class & TypeScript + [\#4398](https://github.com/matrix-org/matrix-react-sdk/pull/4398) + * Fix width of MVideoBody in FilePanel + [\#4396](https://github.com/matrix-org/matrix-react-sdk/pull/4396) + * Remove unused react-addons-css-transition-group + [\#4397](https://github.com/matrix-org/matrix-react-sdk/pull/4397) + * Fix emoji tooltip flickering + [\#4395](https://github.com/matrix-org/matrix-react-sdk/pull/4395) + * Pass along key backup for bootstrap + [\#4374](https://github.com/matrix-org/matrix-react-sdk/pull/4374) + * Fix create room dialog e2ee private room setting + [\#4403](https://github.com/matrix-org/matrix-react-sdk/pull/4403) + * Sort emoji by shortcodes for autocomplete primarily for :-1 and :+1 + [\#4391](https://github.com/matrix-org/matrix-react-sdk/pull/4391) + * Fix invalid commands when figuring out whether to set isTyping + [\#4390](https://github.com/matrix-org/matrix-react-sdk/pull/4390) + * op/deop return error if trying to affect an unknown user + [\#4389](https://github.com/matrix-org/matrix-react-sdk/pull/4389) + * Composer pills respect showPillAvatar setting + [\#4384](https://github.com/matrix-org/matrix-react-sdk/pull/4384) + * Only send typing notification when composing commands which send messages + [\#4385](https://github.com/matrix-org/matrix-react-sdk/pull/4385) + * Reverse order of they match/they don't match buttons + [\#4386](https://github.com/matrix-org/matrix-react-sdk/pull/4386) + * Use singular text on 'delete sessions' button for SSO + [\#4383](https://github.com/matrix-org/matrix-react-sdk/pull/4383) + * Pass widget data through from sticker picker + [\#4377](https://github.com/matrix-org/matrix-react-sdk/pull/4377) + * Obliterate widgets when they are minimized + [\#4376](https://github.com/matrix-org/matrix-react-sdk/pull/4376) + * Fix image thumbnail width when read receipts are hidden + [\#4370](https://github.com/matrix-org/matrix-react-sdk/pull/4370) + * Add toggle for e2ee when creating private room + [\#4362](https://github.com/matrix-org/matrix-react-sdk/pull/4362) + * Fix logging for failed searches + [\#4372](https://github.com/matrix-org/matrix-react-sdk/pull/4372) + * Ensure UI is updated when cross-signing gets disabled + [\#4369](https://github.com/matrix-org/matrix-react-sdk/pull/4369) + * Retry the request for the master key from SSSS on login + [\#4371](https://github.com/matrix-org/matrix-react-sdk/pull/4371) + * Upgrade deps + [\#4365](https://github.com/matrix-org/matrix-react-sdk/pull/4365) + * App load tweaks, i18n and localStorage + [\#4367](https://github.com/matrix-org/matrix-react-sdk/pull/4367) + * Fix encoding of widget arguments + [\#4366](https://github.com/matrix-org/matrix-react-sdk/pull/4366) + +Changes in [2.4.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.4.0-rc.1) (2020-04-08) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.3.1...v2.4.0-rc.1) + + * Upgrade to JS SDK to 5.3.0-rc.1 + * EventIndex: Log if we had all events in a checkpoint but are continuing. + [\#4363](https://github.com/matrix-org/matrix-react-sdk/pull/4363) + * Update from Weblate + [\#4364](https://github.com/matrix-org/matrix-react-sdk/pull/4364) + * Support deactivating your account with SSO + [\#4356](https://github.com/matrix-org/matrix-react-sdk/pull/4356) + * Add debug status for cached backup key format + [\#4359](https://github.com/matrix-org/matrix-react-sdk/pull/4359) + * Fix composer placeholder not updating + [\#4361](https://github.com/matrix-org/matrix-react-sdk/pull/4361) + * Fix sas verification buttons to match figma + [\#4358](https://github.com/matrix-org/matrix-react-sdk/pull/4358) + * Don't show fallback text for verification requests + [\#4345](https://github.com/matrix-org/matrix-react-sdk/pull/4345) + * Fix share dialog correctly + [\#4360](https://github.com/matrix-org/matrix-react-sdk/pull/4360) + * Use singular copy when only deleting one device + [\#4357](https://github.com/matrix-org/matrix-react-sdk/pull/4357) + * Deem m.sticker events as actionable for reacting + [\#4288](https://github.com/matrix-org/matrix-react-sdk/pull/4288) + * Don't show spinner over encryption setup dialogs + [\#4354](https://github.com/matrix-org/matrix-react-sdk/pull/4354) + * Support Jitsi information from client .well-known + [\#4348](https://github.com/matrix-org/matrix-react-sdk/pull/4348) + * Add new default home page fallback + [\#4350](https://github.com/matrix-org/matrix-react-sdk/pull/4350) + * Check more account data in toast listener + [\#4351](https://github.com/matrix-org/matrix-react-sdk/pull/4351) + * Don't try to send presence updates until the client is started + [\#4353](https://github.com/matrix-org/matrix-react-sdk/pull/4353) + * Fix copy button on code blocks when there is no code tag just pre + [\#4352](https://github.com/matrix-org/matrix-react-sdk/pull/4352) + * Clear sessionStorage on sign out + [\#4346](https://github.com/matrix-org/matrix-react-sdk/pull/4346) + * Re-request room keys after auth + [\#4341](https://github.com/matrix-org/matrix-react-sdk/pull/4341) + * Update emojibase for fixed emoji codepoints and Emoji 13 support + [\#4344](https://github.com/matrix-org/matrix-react-sdk/pull/4344) + * App load order tweaks for code splitting + [\#4343](https://github.com/matrix-org/matrix-react-sdk/pull/4343) + * Fix alignment of e2e icon in userinfo and expose full displayname in title + [\#4312](https://github.com/matrix-org/matrix-react-sdk/pull/4312) + * Adjust copy & UX for self-verification + [\#4342](https://github.com/matrix-org/matrix-react-sdk/pull/4342) + * QR code reciprocation + [\#4334](https://github.com/matrix-org/matrix-react-sdk/pull/4334) + * Fix Hangul typing does not work properly + [\#4339](https://github.com/matrix-org/matrix-react-sdk/pull/4339) + * Fix: dismiss setup encryption toast if cross-signing is ready + [\#4336](https://github.com/matrix-org/matrix-react-sdk/pull/4336) + * Fix read marker visibility for grouped events + [\#4340](https://github.com/matrix-org/matrix-react-sdk/pull/4340) + * Make all 'font-size's and 'line-height's rem + [\#4305](https://github.com/matrix-org/matrix-react-sdk/pull/4305) + * Fix spurious extra devices on registration + [\#4337](https://github.com/matrix-org/matrix-react-sdk/pull/4337) + * Fix the edit messager composer + [\#4333](https://github.com/matrix-org/matrix-react-sdk/pull/4333) + * Fix Room Settings Dialog Notifications tab icon + [\#4321](https://github.com/matrix-org/matrix-react-sdk/pull/4321) + * Fix various cases of React warnings by silencing them + [\#4331](https://github.com/matrix-org/matrix-react-sdk/pull/4331) + * Only apply padding to standard textual buttons (kind buttons) + [\#4332](https://github.com/matrix-org/matrix-react-sdk/pull/4332) + * Use console.log in place of console.warn for less warnings + [\#4330](https://github.com/matrix-org/matrix-react-sdk/pull/4330) + * Revert componentDidMount changes on breadcrumbs + [\#4329](https://github.com/matrix-org/matrix-react-sdk/pull/4329) + * Use new method for checking secret storage key + [\#4309](https://github.com/matrix-org/matrix-react-sdk/pull/4309) + * Label and use UNSAFE_componentWillMount to minimize warnings + [\#4315](https://github.com/matrix-org/matrix-react-sdk/pull/4315) + * Fix a number of minor code quality issues + [\#4314](https://github.com/matrix-org/matrix-react-sdk/pull/4314) + * Use componentDidMount in place of componentWillMount where possible + [\#4313](https://github.com/matrix-org/matrix-react-sdk/pull/4313) + * EventIndex: Mark the initial checkpoints for a full crawl. + [\#4325](https://github.com/matrix-org/matrix-react-sdk/pull/4325) + * Fix UserInfo e2e buttons to match Figma + [\#4320](https://github.com/matrix-org/matrix-react-sdk/pull/4320) + * Only auto-scroll to RoomTile when clicking on RoomTile or via shortcuts + [\#4316](https://github.com/matrix-org/matrix-react-sdk/pull/4316) + * Support SSO for interactive authentication + [\#4292](https://github.com/matrix-org/matrix-react-sdk/pull/4292) + * Fix /invite Slash Command + [\#4328](https://github.com/matrix-org/matrix-react-sdk/pull/4328) + * Fix jitsi popout URL + [\#4326](https://github.com/matrix-org/matrix-react-sdk/pull/4326) + * Use our own jitsi widget for the popout URL + [\#4323](https://github.com/matrix-org/matrix-react-sdk/pull/4323) + * Fix popout support for jitsi widgets + [\#4319](https://github.com/matrix-org/matrix-react-sdk/pull/4319) + * Fix: legacy verify user throwing error + [\#4318](https://github.com/matrix-org/matrix-react-sdk/pull/4318) + * Document settingDefaults + [\#3046](https://github.com/matrix-org/matrix-react-sdk/pull/3046) + * Fix Ctrl+/ for Finnish keyboard where it includes Shift + [\#4317](https://github.com/matrix-org/matrix-react-sdk/pull/4317) + * Rework SlashCommands to better expose aliases + [\#4302](https://github.com/matrix-org/matrix-react-sdk/pull/4302) + * Fix EventListSummary when RR rendering is disabled + [\#4311](https://github.com/matrix-org/matrix-react-sdk/pull/4311) + * Update link to css location. + [\#4299](https://github.com/matrix-org/matrix-react-sdk/pull/4299) + * Fix peeking keeping two timeline update mechanisms in play + [\#4310](https://github.com/matrix-org/matrix-react-sdk/pull/4310) + * Pass new secret storage key to bootstrap path + [\#4308](https://github.com/matrix-org/matrix-react-sdk/pull/4308) + * Show red shield for users that become unverified + [\#4303](https://github.com/matrix-org/matrix-react-sdk/pull/4303) + * Accessibility fixed for Event List Summary and Composer Format Bar + [\#4295](https://github.com/matrix-org/matrix-react-sdk/pull/4295) + * Support $riot: Templates for SSO/CAS urls in the welcome.html page + [\#4279](https://github.com/matrix-org/matrix-react-sdk/pull/4279) + * Added the /html command + [\#4296](https://github.com/matrix-org/matrix-react-sdk/pull/4296) + * EventIndex: Better logging on how many events are added. + [\#4301](https://github.com/matrix-org/matrix-react-sdk/pull/4301) + * Field: mark id as optional in propTypes + [\#4307](https://github.com/matrix-org/matrix-react-sdk/pull/4307) + * Fix view community link icon contrast + [\#4254](https://github.com/matrix-org/matrix-react-sdk/pull/4254) + * Remove underscore from Jitsi conference names + [\#4304](https://github.com/matrix-org/matrix-react-sdk/pull/4304) + * Refactor shield display logic; changed rules for DMs + [\#4290](https://github.com/matrix-org/matrix-react-sdk/pull/4290) + * Fix: bring back global thin scrollbars + [\#4300](https://github.com/matrix-org/matrix-react-sdk/pull/4300) + * Keyboard shortcuts: Escape cancel reply and fix Ctrl+K + [\#4297](https://github.com/matrix-org/matrix-react-sdk/pull/4297) + * Field: make id optional, generate one if not provided + [\#4298](https://github.com/matrix-org/matrix-react-sdk/pull/4298) + * Fix ugly scrollbars in TabbedView (settings), emojipicker and widgets + [\#4293](https://github.com/matrix-org/matrix-react-sdk/pull/4293) + * Rename secret storage force-reset variable to avoid confusion + [\#4274](https://github.com/matrix-org/matrix-react-sdk/pull/4274) + * Fix: can't dismiss unverified session toast when encryption hasn't been + upgraded + [\#4291](https://github.com/matrix-org/matrix-react-sdk/pull/4291) + * Blank out UserInfo avatar when changing between members + [\#4289](https://github.com/matrix-org/matrix-react-sdk/pull/4289) + * Add cancel button to verification panel + [\#4283](https://github.com/matrix-org/matrix-react-sdk/pull/4283) + * Show ongoing verification request straight away when navigating to member + [\#4284](https://github.com/matrix-org/matrix-react-sdk/pull/4284) + * Fix: allow scrolling while window is not focused & remove scrollbar hack + [\#4276](https://github.com/matrix-org/matrix-react-sdk/pull/4276) + * Show whether backup key is cached + [\#4287](https://github.com/matrix-org/matrix-react-sdk/pull/4287) + * Rename unverified session toast + [\#4285](https://github.com/matrix-org/matrix-react-sdk/pull/4285) + * Fix: pick last active DM for verification request + [\#4286](https://github.com/matrix-org/matrix-react-sdk/pull/4286) + * Fix formatBar not hidden after highlight and backspacing some text + [\#4269](https://github.com/matrix-org/matrix-react-sdk/pull/4269) + Changes in [2.3.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.3.1) (2020-04-01) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.3.0...v2.3.1) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.rst rename to CONTRIBUTING.md diff --git a/README.md b/README.md index 69aafeb724..b3e96ef001 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,14 @@ a 'skin'. A skin provides: * The containing application * Zero or more 'modules' containing non-UI functionality -As of Aug 2018, the only skin that exists is `vector-im/riot-web`; it and +As of Aug 2018, the only skin that exists is `vector-im/element-web`; it and `matrix-org/matrix-react-sdk` should effectively be considered as a single project (for instance, matrix-react-sdk bugs -are currently filed against vector-im/riot-web rather than this project). +are currently filed against vector-im/element-web rather than this project). Translation Status ================== -[![Translation status](https://translate.riot.im/widgets/riot-web/-/multi-auto.svg)](https://translate.riot.im/engage/riot-web/?utm_source=widget) +[![Translation status](https://translate.element.io/widgets/element-web/-/multi-auto.svg)](https://translate.element.io/engage/element-web/?utm_source=widget) Developer Guide =============== @@ -28,7 +28,7 @@ Platform Targets: * WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox. * Mobile Web is not currently a target platform - instead please use the native iOS (https://github.com/matrix-org/matrix-ios-kit) and Android - (https://github.com/matrix-org/matrix-android-sdk) SDKs. + (https://github.com/matrix-org/matrix-android-sdk2) SDKs. All code lands on the `develop` branch - `master` is only used for stable releases. **Please file PRs against `develop`!!** @@ -41,10 +41,10 @@ https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md Code should be committed as follows: * All new components: https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components - * Riot-specific components: https://github.com/vector-im/riot-web/tree/master/src/components + * Element-specific components: https://github.com/vector-im/element-web/tree/master/src/components * In practice, `matrix-react-sdk` is still evolving so fast that the maintenance - burden of customising and overriding these components for Riot can seriously - impede development. So right now, there should be very few (if any) customisations for Riot. + burden of customising and overriding these components for Element can seriously + impede development. So right now, there should be very few (if any) customisations for Element. * CSS: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/css * Theme specific CSS & resources: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes @@ -71,7 +71,7 @@ practices that anyone working with the SDK needs to be be aware of and uphold: * The view's CSS file MUST have the same name (e.g. view/rooms/MessageTile.css). CSS for matrix-react-sdk currently resides in - https://github.com/vector-im/riot-web/tree/master/src/skins/vector/css/matrix-react-sdk. + https://github.com/vector-im/element-web/tree/master/src/skins/vector/css/matrix-react-sdk. * Per-view CSS is optional - it could choose to inherit all its styling from the context of the rest of the app, although this is unusual for any but @@ -125,7 +125,7 @@ from it. Github Issues ============= -All issues should be filed under https://github.com/vector-im/riot-web/issues +All issues should be filed under https://github.com/vector-im/element-web/issues for now. Development @@ -133,8 +133,10 @@ Development Ensure you have the latest LTS version of Node.js installed. -Using `yarn` instead of `npm` is recommended. Please see the Yarn [install -guide](https://yarnpkg.com/docs/install/) if you do not have it already. +Using `yarn` instead of `npm` is recommended. Please see the Yarn 1 [install +guide](https://classic.yarnpkg.com/docs/install) if you do not have it +already. This project has not yet been migrated to Yarn 2, so please ensure +`yarn --version` shows a version from the 1.x series. `matrix-react-sdk` depends on `matrix-js-sdk`. To make use of changes in the latter and to ensure tests run against the develop branch of `matrix-js-sdk`, @@ -158,8 +160,8 @@ yarn link matrix-js-sdk yarn install ``` -See the [help for `yarn link`](https://yarnpkg.com/docs/cli/link) for more -details about this. +See the [help for `yarn link`](https://classic.yarnpkg.com/docs/cli/link) for +more details about this. Running tests ============= @@ -172,5 +174,5 @@ yarn test ## End-to-End tests -Make sure you've got your Riot development server running (by doing `yarn start` in riot-web), and then in this project, run `yarn run e2etests`. +Make sure you've got your Element development server running (by doing `yarn start` in element-web), and then in this project, run `yarn run e2etests`. See `test/end-to-end-tests/README.md` for more information. diff --git a/__mocks__/FontManager.js b/__mocks__/FontManager.js new file mode 100644 index 0000000000..41eab4bf94 --- /dev/null +++ b/__mocks__/FontManager.js @@ -0,0 +1,6 @@ +// Stub out FontManager for tests as it doesn't validate anything we don't already know given +// our fixed test environment and it requires the installation of node-canvas. + +module.exports = { + fixupColorFonts: () => Promise.resolve(), +}; diff --git a/__mocks__/browser-request.js b/__mocks__/browser-request.js index 7d231fb9db..4c59e8a43a 100644 --- a/__mocks__/browser-request.js +++ b/__mocks__/browser-request.js @@ -1,5 +1,10 @@ const en = require("../src/i18n/strings/en_EN"); +const de = require("../src/i18n/strings/de_DE"); +// Mock the browser-request for the languageHandler tests to return +// Fake languages.json containing references to en_EN and de_DE +// en_EN.json +// de_DE.json module.exports = jest.fn((opts, cb) => { const url = opts.url || opts.uri; if (url && url.endsWith("languages.json")) { @@ -8,9 +13,15 @@ module.exports = jest.fn((opts, cb) => { "fileName": "en_EN.json", "label": "English", }, + "de": { + "fileName": "de_DE.json", + "label": "German", + }, })); } 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 { cb(true, {status: 404}, ""); } diff --git a/__mocks__/empty.js b/__mocks__/empty.js new file mode 100644 index 0000000000..51fb4fe937 --- /dev/null +++ b/__mocks__/empty.js @@ -0,0 +1,2 @@ +// Yes, this is empty. +module.exports = {}; diff --git a/__mocks__/workerMock.js b/__mocks__/workerMock.js new file mode 100644 index 0000000000..6ee585673e --- /dev/null +++ b/__mocks__/workerMock.js @@ -0,0 +1 @@ +module.exports = jest.fn(); diff --git a/__test-utils__/environment.js b/__test-utils__/environment.js new file mode 100644 index 0000000000..9870c133a2 --- /dev/null +++ b/__test-utils__/environment.js @@ -0,0 +1,17 @@ +const BaseEnvironment = require("jest-environment-jsdom-sixteen"); + +class Environment extends BaseEnvironment { + constructor(config, options) { + super(Object.assign({}, config, { + globals: Object.assign({}, config.globals, { + // Explicitly specify the correct globals to workaround Jest bug + // https://github.com/facebook/jest/issues/7780 + Uint32Array: Uint32Array, + Uint8Array: Uint8Array, + ArrayBuffer: ArrayBuffer, + }), + }), options); + } +} + +module.exports = Environment; diff --git a/babel.config.js b/babel.config.js index d5a97d56ce..f00e83652c 100644 --- a/babel.config.js +++ b/babel.config.js @@ -3,12 +3,14 @@ module.exports = { "presets": [ ["@babel/preset-env", { "targets": [ - "last 2 Chrome versions", "last 2 Firefox versions", "last 2 Safari versions" + "last 2 Chrome versions", + "last 2 Firefox versions", + "last 2 Safari versions", + "last 2 Edge versions", ], }], "@babel/preset-typescript", - "@babel/preset-flow", - "@babel/preset-react" + "@babel/preset-react", ], "plugins": [ ["@babel/plugin-proposal-decorators", {legacy: true}], @@ -16,8 +18,7 @@ module.exports = { "@babel/plugin-proposal-numeric-separator", "@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-object-rest-spread", - "@babel/plugin-transform-flow-comments", "@babel/plugin-syntax-dynamic-import", - "@babel/plugin-transform-runtime" - ] + "@babel/plugin-transform-runtime", + ], }; diff --git a/code_style.md b/code_style.md index 3ad0d38873..5747540a76 100644 --- a/code_style.md +++ b/code_style.md @@ -4,7 +4,7 @@ Matrix JavaScript/ECMAScript Style Guide The intention of this guide is to make Matrix's JavaScript codebase clean, consistent with other popular JavaScript styles and consistent with the rest of the Matrix codebase. For reference, the Matrix Python style guide can be found -at https://github.com/matrix-org/synapse/blob/master/docs/code_style.rst +at https://github.com/matrix-org/synapse/blob/master/docs/code_style.md This document reflects how we would like Matrix JavaScript code to look, with acknowledgement that a significant amount of code is written to older @@ -17,7 +17,7 @@ writing in modern ECMAScript and using a transpile step to generate the file that applications can then include. There are significant benefits in being able to use modern ECMAScript, although the tooling for doing so can be awkward for library code, especially with regard to translating source maps and line -number throgh from the original code to the final application. +number through from the original code to the final application. General Style ------------- @@ -35,12 +35,6 @@ General Style - lowerCamelCase for functions and variables. - Single line ternary operators are fine. - UPPER_SNAKE_CASE for constants -- Single quotes for strings by default, for consistency with most JavaScript styles: - - ```javascript - "bad" // Bad - 'good' // Good - ``` - Use parentheses or `` ` `` instead of `\` for line continuation where ever possible - Open braces on the same line (consistent with Node): @@ -151,6 +145,7 @@ General Style Don't set things to undefined. Reserve that value to mean "not yet set to anything." Boolean objects are verboten. - Use JSDoc +- Use switch-case statements where there are 5 or more branches running against the same variable. ECMAScript ---------- @@ -161,7 +156,14 @@ ECMAScript - Be careful mixing arrow functions and regular functions, eg. if one function in a promise chain is an arrow function, they probably all should be. - Apart from that, newer ES features should be used whenever the author deems them to be appropriate. -- Flow annotations are welcome and encouraged. + +TypeScript +---------- +- TypeScript is preferred over the use of JavaScript +- It's desirable to convert existing JavaScript files to TypeScript. TypeScript conversions should be done in small + chunks without functional changes to ease the review process. +- Use full type definitions for function parameters and return values. +- Avoid `any` types and `any` casts React ----- @@ -200,6 +202,8 @@ React this.state = { counter: 0 }; } ``` +- Prefer class components over function components and hooks (not a strict rule though) + - Think about whether your component really needs state: are you duplicating information in component state that could be derived from the model? diff --git a/docs/ciderEditor.md b/docs/ciderEditor.md index 00033b5b8c..379b6f5b51 100644 --- a/docs/ciderEditor.md +++ b/docs/ciderEditor.md @@ -1,6 +1,6 @@ # The CIDER (Contenteditable-Input-Diff-Error-Reconcile) editor -The CIDER editor is a custom editor written for Riot. +The CIDER editor is a custom editor written for Element. Most of the code can be found in the `/editor/` directory of the `matrix-react-sdk` project. It is used to power the composer main composer (both to send and edit messages), and might be used for other usecases where autocomplete is desired (invite box, ...). @@ -21,14 +21,14 @@ caret nodes (more on that later). For these reasons it doesn't use `innerText`, `textContent` or anything similar. The model addresses any content in the editor within as an offset within this string. The caret position is thus also converted from a position in the DOM tree -to an offset in the content string. This happens in `getCaretOffsetAndText` in `dom.js`. +to an offset in the content string. This happens in `getCaretOffsetAndText` in `dom.ts`. Once the content string and caret offset is calculated, it is passed to the `update()` method of the model. The model first calculates the same content string of its current parts, basically just concatenating their text. It then looks for differences between the current and the new content string. The diffing algorithm is very basic, and assumes there is only one change around the caret offset, -so this should be very inexpensive. See `diff.js` for details. +so this should be very inexpensive. See `diff.ts` for details. The result of the diffing is the strings that were added and/or removed from the current content. These differences are then applied to the parts, @@ -51,7 +51,7 @@ which relate poorly to text input or changes, and don't need the `beforeinput` e which isn't broadly supported yet. Once the parts of the model are updated, the DOM of the editor is then reconciled -with the new model state, see `renderModel` in `render.js` for this. +with the new model state, see `renderModel` in `render.ts` for this. If the model didn't reject the input and didn't make any additional changes, this won't make any changes to the DOM at all, and should thus be fairly efficient. diff --git a/docs/img/RoomListStore2.png b/docs/img/RoomListStore2.png new file mode 100644 index 0000000000..9952d1c910 Binary files /dev/null and b/docs/img/RoomListStore2.png differ diff --git a/docs/jitsi.md b/docs/jitsi.md index 779ef79d3a..2b63ce0f72 100644 --- a/docs/jitsi.md +++ b/docs/jitsi.md @@ -25,7 +25,7 @@ which takes several parameters: be null. The react-sdk will assume that `jitsi.html` is at the path of wherever it is currently -being served. For example, `https://riot.im/develop/jitsi.html` or `vector://webapp/jitsi.html`. +being served. For example, `https://develop.element.io/jitsi.html` or `vector://webapp/jitsi.html`. The `jitsi.html` wrapper can use the react-sdk's `WidgetApi` to communicate, making it easier to actually implement the feature. diff --git a/docs/local-echo-dev.md b/docs/local-echo-dev.md new file mode 100644 index 0000000000..e4725a9b07 --- /dev/null +++ b/docs/local-echo-dev.md @@ -0,0 +1,39 @@ +# Local echo (developer docs) + +The React SDK provides some local echo functionality to allow for components to do something +quickly and fall back when it fails. This is all available in the `local-echo` directory within +`stores`. + +Echo is handled in EchoChambers, with `GenericEchoChamber` being the base implementation for all +chambers. The `EchoChamber` class is provided as semantic access to a `GenericEchoChamber` +implementation, such as the `RoomEchoChamber` (which handles echoable details of a room). + +Anything that can be locally echoed will be provided by the `GenericEchoChamber` implementation. +The echo chamber will also need to deal with external changes, and has full control over whether +or not something has successfully been echoed. + +An `EchoContext` is provided to echo chambers (usually with a matching type: `RoomEchoContext` +gets provided to a `RoomEchoChamber` for example) with details about their intended area of +effect, as well as manage `EchoTransaction`s. An `EchoTransaction` is simply a unit of work that +needs to be locally echoed. + +The `EchoStore` manages echo chamber instances, builds contexts, and is generally less semantically +accessible than the `EchoChamber` class. For separation of concerns, and to try and keep things +tidy, this is an intentional design decision. + +**Note**: The local echo stack uses a "whenable" pattern, which is similar to thenables and +`EventEmitter`. Whenables are ways of actioning a changing condition without having to deal +with listeners being torn down. Once the reference count of the Whenable causes garbage collection, +the Whenable's listeners will also be torn down. This is accelerated by the `IDestroyable` interface +usage. + +## Audit functionality + +The UI supports a "Server isn't responding" dialog which includes a partial audit log-like +structure to it. This is partially the reason for added complexity of `EchoTransaction`s +and `EchoContext`s - this information feeds the UI states which then provide direct retry +mechanisms. + +The `EchoStore` is responsible for ensuring that the appropriate non-urgent toast (lower left) +is set up, where the dialog then drives through the contexts and transactions. + diff --git a/docs/media-handling.md b/docs/media-handling.md new file mode 100644 index 0000000000..a4307fb7d4 --- /dev/null +++ b/docs/media-handling.md @@ -0,0 +1,19 @@ +# Media handling + +Surely media should be as easy as just putting a URL into an `img` and calling it good, right? +Not quite. Matrix uses something called a Matrix Content URI (better known as MXC URI) to identify +content, which is then converted to a regular HTTPS URL on the homeserver. However, sometimes that +URL can change depending on deployment considerations. + +The react-sdk features a [customisation endpoint](https://github.com/vector-im/element-web/blob/develop/docs/customisations.md) +for media handling where all conversions from MXC URI to HTTPS URL happen. This is to ensure that +those obscure deployments can route all their media to the right place. + +For development, there are currently two functions available: `mediaFromMxc` and `mediaFromContent`. +The `mediaFromMxc` function should be self-explanatory. `mediaFromContent` takes an event content as +a parameter and will automatically parse out the source media and thumbnail. Both functions return +a `Media` object with a number of options on it, such as getting various common HTTPS URLs for the +media. + +**It is extremely important that all media calls are put through this customisation endpoint.** So +much so it's a lint rule to avoid accidental use of the wrong functions. diff --git a/docs/room-list-store.md b/docs/room-list-store.md new file mode 100644 index 0000000000..6fc5f71124 --- /dev/null +++ b/docs/room-list-store.md @@ -0,0 +1,165 @@ +# Room list sorting + +It's so complicated it needs its own README. + +![](img/RoomListStore2.png) + +Legend: +* Orange = External event. +* Purple = Deterministic flow. +* Green = Algorithm definition. +* Red = Exit condition/point. +* Blue = Process definition. + +## Algorithms involved + +There's two main kinds of algorithms involved in the room list store: list ordering and tag sorting. +Throughout the code an intentional decision has been made to call them the List Algorithm and Sorting +Algorithm respectively. The list algorithm determines the primary ordering of a given tag whereas the +tag sorting defines how rooms within that tag get sorted, at the discretion of the list ordering. + +Behaviour of the overall room list (sticky rooms, etc) are determined by the generically-named Algorithm +class. Here is where much of the coordination from the room list store is done to figure out which list +algorithm to call, instead of having all the logic in the room list store itself. + + +Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm +the power to decide when and how to apply the tag sorting, if at all. For example, the importance algorithm, +later described in this document, heavily uses the list ordering behaviour to break the tag into categories. +Each category then gets sorted by the appropriate tag sorting algorithm. + +### Tag sorting algorithm: Alphabetical + +When used, rooms in a given tag will be sorted alphabetically, where the alphabet's order is a problem +for the browser. All we do is a simple string comparison and expect the browser to return something +useful. + +### Tag sorting algorithm: Manual + +Manual sorting makes use of the `order` property present on all tags for a room, per the +[Matrix specification](https://matrix.org/docs/spec/client_server/r0.6.0#room-tagging). Smaller values +of `order` cause rooms to appear closer to the top of the list. + +### Tag sorting algorithm: Recent + +Rooms get ordered by the timestamp of the most recent useful message. Usefulness is yet another algorithm +in the room list system which determines whether an event type is capable of bubbling up in the room list. +Normally events like room messages, stickers, and room security changes will be considered useful enough +to cause a shift in time. + +Note that this is reliant on the event timestamps of the most recent message. Because Matrix is eventually +consistent this means that from time to time a room might plummet or skyrocket across the tag due to the +timestamp contained within the event (generated server-side by the sender's server). + +### List ordering algorithm: Natural + +This is the easiest of the algorithms to understand because it does essentially nothing. It imposes no +behavioural changes over the tag sorting algorithm and is by far the simplest way to order a room list. +Historically, it's been the only option in Element and extremely common in most chat applications due to +its relative deterministic behaviour. + +### List ordering algorithm: Importance + +On the other end of the spectrum, this is the most complicated algorithm which exists. There's major +behavioural changes, and the tag sorting algorithm gets selectively applied depending on circumstances. + +Each tag which is not manually ordered gets split into 4 sections or "categories". Manually ordered tags +simply get the manual sorting algorithm applied to them with no further involvement from the importance +algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off +relative (perceived) importance to the user: + +* **Red**: The room has unread mentions waiting for the user. +* **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread + messages which cause a push notification or badge count. Typically, this is the default as rooms get + set to 'All Messages'. +* **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without + a badge/notification count (or 'Mentions Only'/'Muted'). +* **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user + last read it. + +Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey +above bold, etc. + +Once the algorithm has determined which rooms belong in which categories, the tag sorting algorithm +gets applied to each category in a sub-list fashion. This should result in the red rooms (for example) +being sorted alphabetically amongst each other as well as the grey rooms sorted amongst each other, but +collectively the tag will be sorted into categories with red being at the top. + +## Sticky rooms + +When the user visits a room, that room becomes 'sticky' in the list, regardless of ordering algorithm. +From a code perspective, the underlying algorithm is not aware of a sticky room and instead the base class +manages which room is sticky. This is to ensure that all algorithms handle it the same. + +The sticky flag is simply to say it will not move higher or lower down the list while it is active. For +example, if using the importance algorithm, the room would naturally become idle once viewed and thus +would normally fly down the list out of sight. The sticky room concept instead holds it in place, never +letting it fly down until the user moves to another room. + +Only one room can be sticky at a time. Room updates around the sticky room will still hold the sticky +room in place. The best example of this is the importance algorithm: if the user has 3 red rooms and +selects the middle room, they will see exactly one room above their selection at all times. If they +receive another notification which causes the room to move into the topmost position, the room that was +above the sticky room will move underneath to allow for the new room to take the top slot, maintaining +the sticky room's position. + +Though only applicable to the importance algorithm, the sticky room is not aware of category boundaries +and thus the user can see a shift in what kinds of rooms move around their selection. An example would +be the user having 4 red rooms, the user selecting the third room (leaving 2 above it), and then having +the rooms above it read on another device. This would result in 1 red room and 1 other kind of room +above the sticky room as it will try to maintain 2 rooms above the sticky room. + +An exception for the sticky room placement is when there's suddenly not enough rooms to maintain the placement +exactly. This typically happens if the user selects a room and leaves enough rooms where it cannot maintain +the N required rooms above the sticky room. In this case, the sticky room will simply decrease N as needed. +The N value will never increase while selection remains unchanged: adding a bunch of rooms after having +put the sticky room in a position where it's had to decrease N will not increase N. + +## Responsibilities of the store + +The store is responsible for the ordering, upkeep, and tracking of all rooms. The room list component simply gets +an object containing the tags it needs to worry about and the rooms within. The room list component will +decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with +all kinds of filtering. + +## Filtering + +Filters are provided to the store as condition classes and have two major kinds: Prefilters and Runtime. + +Prefilters flush out rooms which shouldn't appear to the algorithm implementations. Typically this is +due to some higher order room list filtering (such as spaces or tags) deliberately exposing a subset of +rooms to the user. The algorithm implementations will not see a room being prefiltered out. + +Runtime filters are used for more dynamic filtering, such as the user filtering by room name. These +filters are passed along to the algorithm implementations where those implementations decide how and +when to apply the filter. In practice, the base `Algorithm` class ends up doing the heavy lifting for +optimization reasons. + +The results of runtime filters get cached to avoid needlessly iterating over potentially thousands of +rooms, as the old room list store does. When a filter condition changes, it emits an update which (in this +case) the `Algorithm` class will pick up and act accordingly. Typically, this also means filtering a +minor subset where possible to avoid over-iterating rooms. + +All filter conditions are considered "stable" by the consumers, meaning that the consumer does not +expect a change in the condition unless the condition says it has changed. This is intentional to +maintain the caching behaviour described above. + +One might ask why we don't just use prefilter conditions for everything, and the answer is one of slight +subtlety: in the cases of prefilters we are knowingly exposing the user to a workspace-style UX where +room notifications are self-contained within that workspace. Runtime filters tend to not want to affect +visible notification counts (as it doesn't want the room header to suddenly be confusing to the user as +they type), and occasionally UX like "found 2/12 rooms" is desirable. If prefiltering were used instead, +the notification counts would vary while the user was typing and "found 2/12" UX would not be possible. + +## Class breakdowns + +The `RoomListStore` is the major coordinator of various algorithm implementations, which take care +of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` class is responsible +for figuring out which tags get which rooms, as Matrix specifies them as a reverse map: tags get +defined on rooms and are not defined as a collection of rooms (unlike how they are presented to the +user). Various list-specific utilities are also included, though they are expected to move somewhere +more general when needed. For example, the `membership` utilities could easily be moved elsewhere +as needed. + +The various bits throughout the room list store should also have jsdoc of some kind to help describe +what they do and how they work. diff --git a/docs/scrolling.md b/docs/scrolling.md index 71329e5c32..a5232359a7 100644 --- a/docs/scrolling.md +++ b/docs/scrolling.md @@ -13,7 +13,7 @@ ScrollPanel supports a mode to prevent it shrinking. This is used to prevent a j BACAT scrolling implements a different way of restoring the scroll position in the timeline while tiles out of view are changing height or tiles are being added or removed. It was added in https://github.com/matrix-org/matrix-react-sdk/pull/2842. -The motivation for the changes is having noticed that setting scrollTop while scrolling tends to not work well, with it interrupting ongoing scrolling and also querying scrollTop reporting outdated values and consecutive scroll adjustments cancelling each out previous ones. This seems to be worse on macOS than other platforms, presumably because of a higher resolution in scroll events there. Also see https://github.com/vector-im/riot-web/issues/528. The BACAT approach allows to only have to change the scroll offset when adding or removing tiles. +The motivation for the changes is having noticed that setting scrollTop while scrolling tends to not work well, with it interrupting ongoing scrolling and also querying scrollTop reporting outdated values and consecutive scroll adjustments cancelling each out previous ones. This seems to be worse on macOS than other platforms, presumably because of a higher resolution in scroll events there. Also see https://github.com/vector-im/element-web/issues/528. The BACAT approach allows to only have to change the scroll offset when adding or removing tiles. The approach taken instead is to vertically align the timeline tiles to the bottom of the scroll container (using flexbox) and give the timeline inside the scroll container an explicit height, initially set to a multiple of the PAGE_SIZE (400px at time of writing) as needed by the content. When scrolled up, we can compensate for anything that grew below the viewport by changing the height of the timeline to maintain what's currently visible in the viewport without adjusting the scrollTop and hence without jumping. diff --git a/docs/settings.md b/docs/settings.md index 46e4a68fdb..891877a57a 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -9,7 +9,7 @@ of dealing with the different levels and exposes easy to use getters and setters ## Levels Granular Settings rely on a series of known levels in order to use the correct value for the scenario. These levels, in -order of prioirty, are: +order of priority, are: * `device` - The current user's device * `room-device` - The current user's device, but only when in a specific room * `room-account` - The current user's account, but only when in a specific room @@ -25,33 +25,10 @@ that room administrators cannot force account-only settings upon participants. ## Settings Settings are the different options a user may set or experience in the application. These are pre-defined in -`src/settings/Settings.js` under the `SETTINGS` constant and have the following minimum requirements: -``` -// The ID is used to reference the setting throughout the application. This must be unique. -"theSettingId": { - // The levels this setting supports is required. In `src/settings/Settings.js` there are various pre-set arrays - // for this option - they should be used where possible to avoid copy/pasting arrays across settings. - supportedLevels: [...], +`src/settings/Settings.ts` under the `SETTINGS` constant, and match the `ISetting` interface as defined there. - // The default for this setting serves two purposes: It provides a value if the setting is not defined at other - // levels, and it serves to demonstrate the expected type to other developers. The value isn't enforced, but it - // should be respected throughout the code. The default may be any data type. - default: false, - - // The display name has two notations: string and object. The object notation allows for different translatable - // strings to be used for different levels, while the string notation represents the string for all levels. - - displayName: _td("Change something"), // effectively `displayName: { "default": _td("Change something") }` - displayName: { - "room": _td("Change something for participants of this room"), - - // Note: the default will be used if the level requested (such as `device`) does not have a string defined here. - "default": _td("Change something"), - } -} -``` - -Settings that support the config level can be set in the config file under the `settingDefaults` key (note that some settings, like the "theme" setting, are special cased in the config file): +Settings that support the config level can be set in the config file under the `settingDefaults` key (note that some +settings, like the "theme" setting, are special cased in the config file): ```json { ... @@ -119,39 +96,42 @@ for you. If a display name cannot be found, it will return `null`. ## Features -Occasionally some parts of the application may be undergoing testing and are not quite production ready. These are -commonly known to be behind a "labs flag". Features behind lab flags must go through the granular settings system, and -look and act very much normal settings. The exception is that they must supply `isFeature: true` as part of the setting -definition and should go through the helper functions on `SettingsStore`. +Feature flags are just like regular settings with some underlying semantics for how they are meant to be used. Usually +a feature flag is used when a portion of the application is under development or not ready for full release yet, such +as new functionality or experimental ideas. In these cases, the feature name *should* be named with the `feature_*` +convention and must be tagged with `isFeature: true` in the setting definition. By doing so, the feature will automatically +appear in the "labs" section of the user's settings. -Although features have levels and a default value, the calculation of those options is blocked by the feature's state. -A feature's state is determined from the `SdkConfig` and is a little complex. If `enableLabs` (a legacy flag) is `true` -then the feature's state is `labs`, if it is `false`, the state is `disable`. If `enableLabs` is not set then the state -is determined from the `features` config, such as in the following: +Features can be controlled at the config level using the following structure: ```json "features": { - "feature_lazyloading": "labs" + "feature_lazyloading": true } ``` -In this example, `feature_lazyloading` is in the `labs` state. It may also be in the `enable` or `disable` state with a -similar approach. If the state is invalid, the feature is in the `disable` state. A feature's levels are only calculated -if it is in the `labs` state, therefore the default only applies in that scenario. If the state is `enable`, the feature -is always-on. -Once a feature flag has served its purpose, it is generally recommended to remove it and the associated feature flag -checks. This would enable the feature implicitly as it is part of the application now. +When `true`, the user will see the feature as enabled. Similarly, when `false` the user will see the feature as disabled. +The user will only be able to change/see these states if `showLabsSettings: true` is in the config. ### Determining if a feature is enabled -A simple call to `SettingsStore.isFeatureEnabled` will tell you if the feature is enabled. This will perform all the -required calculations to determine if the feature is enabled based upon the configuration and user selection. +Call `SettingsStore.getValue()` as you would for any other setting. ### Enabling a feature -Features can only be enabled if the feature is in the `labs` state, otherwise this is a no-op. To find the current set -of features in the `labs` state, call `SettingsStore.getLabsFeatures`. To set the value, call -`SettingsStore.setFeatureEnabled`. +Call `SettingsStore.setValue("feature_name", null, SettingLevel.DEVICE, true)`. +### A note on UI features + +UI features are a different concept to plain features. Instead of being representative of unstable or +unpredicatable behaviour, they are logical chunks of UI which can be disabled by deployments for ease +of understanding with users. They are simply represented as boring settings with a convention of being +named as `UIFeature.$location` where `$location` is a rough descriptor of what is toggled, such as +`URLPreviews` or `Communities`. + +UI features also tend to have their own setting controller (see below) to manipulate settings which might +be affected by the UI feature being disabled. For example, if URL previews are disabled as a UI feature +then the URL preview options will use the `UIFeatureController` to ensure they remain disabled while the +UI feature is disabled. ## Setting controllers @@ -162,7 +142,7 @@ kept up to date with the setting where it is otherwise not possible. An example they can only be considered enabled if the platform supports notifications, and enabling notifications requires additional steps to actually enable notifications. -For more information, see `src/settings/controllers/SettingController.js`. +For more information, see `src/settings/controllers/SettingController.ts`. ## Local echo @@ -222,7 +202,7 @@ The `SettingsStore` uses the hardcoded `LEVEL_ORDER` constant to ensure that it The array is checked from left to right, simulating the behaviour of overriding values from the higher levels. Each level should be defined in this array, including `default`. -Handlers (`src/settings/handlers/SettingsHandler.js`) represent a single level and are responsible for getting and +Handlers (`src/settings/handlers/SettingsHandler.ts`) represent a single level and are responsible for getting and setting values at that level. Handlers also provide additional information to the `SettingsStore` such as if the level is supported or if the current user may set values at the level. The `SettingsStore` will use the handler to enforce checks and manipulate settings. Handlers are also responsible for dealing with migration patterns or legacy settings for @@ -230,7 +210,7 @@ their level (for example, a setting being renamed or using a different key from Handlers are provided to the `SettingsStore` via the `LEVEL_HANDLERS` constant. `SettingsStore` will optimize lookups by only considering handlers that are supported on the platform. -Local echo is achieved through `src/settings/handlers/LocalEchoWrapper.js` which acts as a wrapper around a given +Local echo is achieved through `src/settings/handlers/LocalEchoWrapper.ts` which acts as a wrapper around a given handler. This is automatically applied to all defined `LEVEL_HANDLERS` and proxies the calls to the wrapped handler where possible. The echo is achieved by a simple object cache stored within the class itself. The cache is invalidated immediately upon the proxied save call succeeding or failing. @@ -240,20 +220,7 @@ Controllers are notified of changes by the `SettingsStore`, and are given the op ### Features -Features automatically get considered as `disabled` if they are not listed in the `SdkConfig` or `enableLabs` is -false/not set. Features are always checked against the configuration before going through the level order as they have -the option of being forced-on or forced-off for the application. This is done by the `features` section and looks -something like this: - -``` -"features": { - "feature_groups": "enable", - "feature_pinning": "disable", // the default - "feature_presence": "labs" -} -``` - -If `enableLabs` is true in the configuration, the default for features becomes `"labs"`. +See above for feature reference. ### Watchers @@ -271,4 +238,3 @@ In practice, handlers which rely on remote changes (account data, room events, e generalized `WatchManager` - a class specifically designed to deduplicate the logic of managing watchers. The handlers which are localized to the local client (device) generally just trigger the `WatchManager` when they manipulate the setting themselves as there's nothing to really 'watch'. - diff --git a/docs/usercontent.md b/docs/usercontent.md index e54851dd0d..db0e34e5fa 100644 --- a/docs/usercontent.md +++ b/docs/usercontent.md @@ -5,9 +5,9 @@ letting the browser and user interact with the resulting data may be dangerous, previously `usercontent.riot.im` was used to act as a sandbox on a different origin to close the attack surface, it is now possible to do by using a combination of a sandboxed iframe and some code written into the app which consumes this SDK. -Usercontent is an iframe sandbox target for allowing a user to safely download a decrypted attachment from a sandboxed origin where it cannot be used to XSS your riot session out from under you. +Usercontent is an iframe sandbox target for allowing a user to safely download a decrypted attachment from a sandboxed origin where it cannot be used to XSS your Element session out from under you. -Its function is to create an Object URL for the user/browser to use but bound to an origin different to that of the riot instance to protect against XSS. +Its function is to create an Object URL for the user/browser to use but bound to an origin different to that of the Element instance to protect against XSS. It exposes a function over a postMessage API, when sent an object with the matching fields to render a download link with the Object URL: @@ -24,4 +24,4 @@ It exposes a function over a postMessage API, when sent an object with the match If only imgSrc, imgStyle and style are passed then just update the existing link without overwriting other things about it. -It is expected that this target be available at `usercontent/` relative to the root of the app, this can be seen in riot-web's webpack config. +It is expected that this target be available at `usercontent/` relative to the root of the app, this can be seen in element-web's webpack config. diff --git a/docs/widget-layouts.md b/docs/widget-layouts.md new file mode 100644 index 0000000000..e7f72e2001 --- /dev/null +++ b/docs/widget-layouts.md @@ -0,0 +1,60 @@ +# Widget layout support + +Rooms can have a default widget layout to auto-pin certain widgets, make the container different +sizes, etc. These are defined through the `io.element.widgets.layout` state event (empty state key). + +Full example content: +```json5 +{ + "widgets": { + "first-widget-id": { + "container": "top", + "index": 0, + "width": 60, + "height": 40 + }, + "second-widget-id": { + "container": "right" + } + } +} +``` + +As shown, there are two containers possible for widgets. These containers have different behaviour +and interpret the other options differently. + +## `top` container + +This is the "App Drawer" or any pinned widgets in a room. This is by far the most versatile container +though does introduce potential usability issues upon members of the room (widgets take up space and +therefore fewer messages can be shown). + +The `index` for a widget determines which order the widgets show up in from left to right. Widgets +without an `index` will show up as the rightmost widgets. Tiebreaks (same `index` or multiple defined +without an `index`) are resolved by comparing widget IDs. A maximum of 3 widgets can be in the top +container - any which exceed this will be ignored (placed into the `right` container). Smaller numbers +represent leftmost widgets. + +The `width` is relative width within the container in percentage points. This will be clamped to a +range of 0-100 (inclusive). The widgets will attempt to scale to relative proportions when more than +100% space is allocated. For example, if 3 widgets are defined at 40% width each then the client will +attempt to show them at 33% width each. + +Note that the client may impose minimum widths on the widgets, such as a 10% minimum to avoid pinning +hidden widgets. In general, widgets defined in the 30-70% range each will be free of these restrictions. + +The `height` is not in fact applied per-widget but is recorded per-widget for potential future +capabilities in future containers. The top container will take the tallest `height` and use that for +the height of the whole container, and thus all widgets in that container. The `height` is relative +to the container, like with `width`, meaning that 100% will consume as much space as the client is +willing to sacrifice to the widget container. Like with `width`, the client may impose minimums to avoid +the container being uselessly small. Heights in the 30-100% range are generally acceptable. The height +is also clamped to be within 0-100, inclusive. + +## `right` container + +This is the default container and has no special configuration. Widgets which overflow from the top +container will be put in this container instead. Putting a widget in the right container does not +automatically show it - it only mentions that widgets should not be in another container. + +The behaviour of this container may change in the future. diff --git a/package.json b/package.json index 7b66c95d28..e80ed8dd5a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.3.1", + "version": "3.25.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -23,18 +23,17 @@ "package.json" ], "bin": { - "reskindex": "scripts/reskindex.js", - "matrix-gen-i18n": "scripts/gen-i18n.js", - "matrix-prune-i18n": "scripts/prune-i18n.js" + "reskindex": "scripts/reskindex.js" }, - "main": "./lib/index.js", - "typings": "./lib/index.d.ts", + "main": "./src/index.js", "matrix_src_main": "./src/index.js", + "matrix_lib_main": "./lib/index.js", + "matrix_lib_typings": "./lib/index.d.ts", "scripts": { - "prepare": "yarn build", + "prepublishOnly": "yarn build", "i18n": "matrix-gen-i18n", "prunei18n": "matrix-prune-i18n", - "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", + "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && matrix-gen-i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", "reskindex": "node scripts/reskindex.js -h header", "reskindex:watch": "node scripts/reskindex.js -h header -w", "rethemendex": "res/css/rethemendex.sh", @@ -45,126 +44,160 @@ "start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:all", "start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn start:build\" \"yarn reskindex:watch\"", "start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"", - "lint": "yarn lint:types && yarn lint:ts && yarn lint:js && yarn lint:style", - "lint:js": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test", - "lint:ts": "tslint --project ./tsconfig.json -t stylish", + "lint": "yarn lint:types && yarn lint:js && yarn lint:style", + "lint:js": "eslint --max-warnings 0 src test", "lint:types": "tsc --noEmit --jsx react", "lint:style": "stylelint 'res/css/**/*.scss'", "test": "jest", - "test:e2e": "./test/end-to-end-tests/run.sh --riot-url http://localhost:8080" + "test:e2e": "./test/end-to-end-tests/run.sh --app-url http://localhost:8080", + "coverage": "yarn test --coverage" }, "dependencies": { - "@babel/runtime": "^7.8.3", - "blueimp-canvas-to-blob": "^3.5.0", + "@babel/runtime": "^7.12.5", + "await-lock": "^2.1.0", + "blurhash": "^1.1.3", "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", - "classnames": "^2.1.2", - "commonmark": "^0.28.1", - "counterpart": "^0.18.0", - "create-react-class": "^15.6.0", - "diff-dom": "^4.1.3", - "diff-match-patch": "^1.0.4", - "emojibase-data": "^5.0.1", - "emojibase-regex": "^4.0.1", + "cheerio": "^1.0.0-rc.9", + "classnames": "^2.2.6", + "commonmark": "^0.29.3", + "counterpart": "^0.18.6", + "diff-dom": "^4.2.2", + "diff-match-patch": "^1.0.5", + "emojibase-data": "^5.1.1", + "emojibase-regex": "^4.1.1", "escape-html": "^1.0.3", - "file-saver": "^1.3.3", - "filesize": "3.5.6", + "file-saver": "^2.0.5", + "filesize": "6.1.0", "flux": "2.1.1", - "focus-visible": "^5.0.2", - "fuse.js": "^2.2.0", - "gfm.css": "^1.1.1", + "focus-visible": "^5.2.0", + "gfm.css": "^1.1.2", "glob-to-regexp": "^0.4.1", - "highlight.js": "^9.15.8", - "html-entities": "^1.2.1", - "is-ip": "^2.0.0", - "linkifyjs": "^2.1.6", - "lodash": "^4.17.14", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", - "minimist": "^1.2.0", - "pako": "^1.0.5", + "highlight.js": "^10.5.0", + "html-entities": "^1.4.0", + "is-ip": "^3.1.0", + "katex": "^0.12.0", + "linkifyjs": "^2.1.9", + "lodash": "^4.17.20", + "matrix-js-sdk": "12.0.1", + "matrix-widget-api": "^0.1.0-beta.15", + "minimist": "^1.2.5", + "opus-recorder": "^8.0.3", + "pako": "^2.0.3", + "parse5": "^6.0.1", "png-chunks-extract": "^1.0.0", - "project-name-generator": "^2.1.7", - "prop-types": "^15.5.8", + "prop-types": "^15.7.2", "qrcode": "^1.4.4", - "qrcode-react": "^0.1.16", - "react": "^16.9.0", - "react-addons-css-transition-group": "15.6.2", - "react-beautiful-dnd": "^4.0.1", - "react-dom": "^16.9.0", - "react-focus-lock": "^2.2.1", - "resize-observer-polyfill": "^1.5.0", - "sanitize-html": "^1.18.4", - "text-encoding-utf-8": "^1.0.1", + "re-resizable": "^6.9.0", + "react": "^17.0.2", + "react-beautiful-dnd": "^13.1.0", + "react-blurhash": "^0.1.3", + "react-dom": "^17.0.2", + "react-focus-lock": "^2.5.0", + "react-transition-group": "^4.4.1", + "resize-observer-polyfill": "^1.5.1", + "rfc4648": "^1.4.0", + "sanitize-html": "^2.3.2", + "tar-js": "^0.3.0", "url": "^0.11.0", - "velocity-animate": "^1.5.2", - "what-input": "^5.2.6", + "what-input": "^5.2.10", "zxcvbn": "^4.4.2" }, "devDependencies": { - "@babel/cli": "^7.7.5", - "@babel/core": "^7.7.5", - "@babel/plugin-proposal-class-properties": "^7.7.4", - "@babel/plugin-proposal-decorators": "^7.7.4", - "@babel/plugin-proposal-export-default-from": "^7.7.4", - "@babel/plugin-proposal-numeric-separator": "^7.7.4", - "@babel/plugin-proposal-object-rest-spread": "^7.7.4", - "@babel/plugin-transform-flow-comments": "^7.7.4", - "@babel/plugin-transform-runtime": "^7.8.3", - "@babel/preset-env": "^7.7.6", - "@babel/preset-flow": "^7.7.4", - "@babel/preset-react": "^7.7.4", - "@babel/preset-typescript": "^7.7.4", - "@babel/register": "^7.7.4", - "@peculiar/webcrypto": "^1.0.22", - "@types/classnames": "^2.2.10", + "@babel/cli": "^7.12.10", + "@babel/core": "^7.12.10", + "@babel/eslint-parser": "^7.12.10", + "@babel/eslint-plugin": "^7.12.10", + "@babel/parser": "^7.12.11", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-proposal-decorators": "^7.12.12", + "@babel/plugin-proposal-export-default-from": "^7.12.1", + "@babel/plugin-proposal-numeric-separator": "^7.12.7", + "@babel/plugin-proposal-object-rest-spread": "^7.12.1", + "@babel/plugin-transform-runtime": "^7.12.10", + "@babel/preset-env": "^7.12.11", + "@babel/preset-react": "^7.12.10", + "@babel/preset-typescript": "^7.12.7", + "@babel/register": "^7.12.10", + "@babel/traverse": "^7.12.12", + "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", + "@peculiar/webcrypto": "^1.1.4", + "@sinonjs/fake-timers": "^7.0.2", + "@types/classnames": "^2.2.11", + "@types/commonmark": "^0.27.4", + "@types/counterpart": "^0.18.1", + "@types/css-font-loading-module": "^0.0.6", + "@types/diff-match-patch": "^1.0.32", + "@types/flux": "^3.1.9", + "@types/jest": "^26.0.20", + "@types/linkifyjs": "^2.1.3", + "@types/lodash": "^4.14.168", "@types/modernizr": "^3.5.3", - "@types/react": "16.9", - "babel-eslint": "^10.0.3", - "babel-jest": "^24.9.0", - "chokidar": "^3.3.1", - "concurrently": "^4.0.1", - "enzyme": "^3.10.0", - "enzyme-adapter-react-16": "^1.15.1", - "eslint": "^5.12.0", - "eslint-config-google": "^0.7.1", - "eslint-plugin-babel": "^5.2.1", - "eslint-plugin-flowtype": "^2.30.0", - "eslint-plugin-jest": "^23.0.4", - "eslint-plugin-react": "^7.7.0", - "eslint-plugin-react-hooks": "^2.0.1", - "estree-walker": "^0.5.0", - "file-loader": "^3.0.1", - "flow-parser": "^0.57.3", - "glob": "^5.0.14", - "jest": "^24.9.0", - "lolex": "^5.1.2", + "@types/node": "^14.14.22", + "@types/pako": "^1.0.1", + "@types/parse5": "^6.0.0", + "@types/qrcode": "^1.3.5", + "@types/react": "^17.0.2", + "@types/react-beautiful-dnd": "^13.0.0", + "@types/react-dom": "^17.0.2", + "@types/react-transition-group": "^4.4.0", + "@types/sanitize-html": "^2.3.1", + "@types/zxcvbn": "^4.4.0", + "@typescript-eslint/eslint-plugin": "^4.17.0", + "@typescript-eslint/parser": "^4.17.0", + "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", + "babel-jest": "^26.6.3", + "chokidar": "^3.5.1", + "concurrently": "^5.3.0", + "enzyme": "^3.11.0", + "eslint": "7.18.0", + "eslint-config-google": "^0.14.0", + "eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#main", + "eslint-plugin-react": "^7.22.0", + "eslint-plugin-react-hooks": "^4.2.0", + "glob": "^7.1.6", + "jest": "^26.6.3", + "jest-canvas-mock": "^2.3.0", + "jest-environment-jsdom-sixteen": "^1.0.3", + "jest-fetch-mock": "^3.0.3", "matrix-mock-request": "^1.2.3", - "matrix-react-test-utils": "^0.2.2", - "react-test-renderer": "^16.9.0", - "rimraf": "^2.4.3", - "source-map-loader": "^0.2.3", - "stylelint": "^9.10.1", - "stylelint-config-standard": "^18.2.0", - "stylelint-scss": "^3.9.0", - "tslint": "^5.20.1", - "typescript": "^3.7.3", - "walk": "^2.3.9", - "webpack": "^4.20.2", - "webpack-cli": "^3.1.1" + "matrix-react-test-utils": "^0.2.3", + "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", + "react-test-renderer": "^17.0.2", + "rimraf": "^3.0.2", + "stylelint": "^13.9.0", + "stylelint-config-standard": "^20.0.0", + "stylelint-scss": "^3.18.0", + "typescript": "^4.1.3", + "walk": "^2.3.14" }, "jest": { + "testEnvironment": "./__test-utils__/environment.js", "testMatch": [ - "/test/**/*-test.js" + "/test/**/*-test.[jt]s?(x)" + ], + "setupFiles": [ + "jest-canvas-mock" ], "setupFilesAfterEnv": [ "/test/setupTests.js" ], "moduleNameMapper": { "\\.(gif|png|svg|ttf|woff2)$": "/__mocks__/imageMock.js", - "\\$webapp/i18n/languages.json": "/__mocks__/languages.json" + "\\$webapp/i18n/languages.json": "/__mocks__/languages.json", + "decoderWorker\\.min\\.js": "/__mocks__/empty.js", + "decoderWorker\\.min\\.wasm": "/__mocks__/empty.js", + "waveWorker\\.min\\.js": "/__mocks__/empty.js", + "workers/(.+)\\.worker\\.ts": "/__mocks__/workerMock.js" }, "transformIgnorePatterns": [ "/node_modules/(?!matrix-js-sdk).+$" + ], + "collectCoverageFrom": [ + "/src/**/*.{js,ts,tsx}" + ], + "coverageReporters": [ + "text" ] } } diff --git a/release.sh b/release.sh index 23b8822041..4742f00dea 100755 --- a/release.sh +++ b/release.sh @@ -9,6 +9,9 @@ set -e cd `dirname $0` +# This link seems to get eaten by the release process, so ensure it exists. +yarn link matrix-js-sdk + for i in matrix-js-sdk do echo "Checking version of $i..." @@ -29,9 +32,7 @@ do echo "Upgrading $i to $latestver..." yarn add -E $i@$latestver git add -u - # The `-e` flag opens the editor and gives you a chance to check - # the upgrade for correctness. - git commit -m "Upgrade $i to $latestver" -e + git commit -m "Upgrade $i to $latestver" fi fi done diff --git a/res/css/_common.scss b/res/css/_common.scss index 03442ca510..b128a82442 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -17,9 +17,27 @@ limitations under the License. */ @import "./_font-sizes.scss"; +@import "./_font-weights.scss"; + +$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic + +$EventTile_e2e_state_indicator_width: 4px; + +$MessageTimestamp_width: 46px; /* 8 + 30 (avatar) + 8 */ +$MessageTimestamp_width_hover: calc($MessageTimestamp_width - 2 * $EventTile_e2e_state_indicator_width); :root { - font-size: 15px; + font-size: 10px; + + --transition-short: .1s; + --transition-standard: .3s; +} + +@media (prefers-reduced-motion) { + :root { + --transition-short: 0; + --transition-standard: 0; + } } html { @@ -27,6 +45,8 @@ html { N.B. Breaks things when we have legitimate horizontal overscroll */ height: 100%; overflow: hidden; + // Stop similar overscroll bounce in Firefox Nightly for macOS + overscroll-behavior: none; } body { @@ -38,7 +58,7 @@ body { margin: 0px; // needed to match the designs correctly on macOS - // see https://github.com/vector-im/riot-web/issues/11425 + // see https://github.com/vector-im/element-web/issues/11425 -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -57,6 +77,10 @@ pre, code { color: $accent-color; } +.text-muted { + color: $muted-fg-color; +} + b { // On Firefox, the default weight for `` is `bolder` which results in no bold // effect since we only have specific weights of our fonts available. @@ -160,11 +184,9 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { background-color: transparent; color: $input-darker-fg-color; border-radius: 4px; - border: 1px solid $dialog-close-fg-color; - // these things should probably not be defined - // globally + border: 1px solid rgba($primary-fg-color, .1); + // these things should probably not be defined globally margin: 9px; - flex: 0 0 auto; } .mx_textinput { @@ -175,7 +197,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { :not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type=text]::placeholder, :not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type=search]::placeholder, .mx_textinput input::placeholder { - color: $roomsublist-label-fg-color; + color: rgba($input-darker-fg-color, .75); } } @@ -207,12 +229,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { border: 0; } -/* applied to side-panels and messagepanel when in RoomSettings */ -.mx_fadable { - opacity: 1; - transition: opacity 0.2s ease-in-out; -} - // These are magic constants which are excluded from tinting, to let themes // (which only have CSS, unlike skins) tell the app what their non-tinted // colourscheme is by inspecting the stylesheet DOM. @@ -227,7 +243,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { } #mx_theme_tertiaryAccentColor { - color: $roomsublist-label-bg-color; + color: $tertiary-accent-color; } /* Expected z-indexes for dialogs: @@ -261,10 +277,10 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { font-weight: 300; font-size: $font-15px; position: relative; - padding: 25px 30px 30px 30px; + padding: 24px; max-height: 80%; box-shadow: 2px 15px 30px 0 $dialog-shadow-color; - border-radius: 4px; + border-radius: 8px; overflow-y: auto; } @@ -275,6 +291,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { .mx_Dialog_staticWrapper .mx_Dialog { z-index: 4010; + contain: content; } .mx_Dialog_background { @@ -299,7 +316,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { } .mx_Dialog_lightbox .mx_Dialog_background { - opacity: 0.85; + opacity: $lightbox-background-bg-opacity; background-color: $lightbox-background-bg-color; } @@ -311,6 +328,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { max-width: 100%; max-height: 100%; pointer-events: none; + padding: 0; } .mx_Dialog_header { @@ -319,7 +337,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { } .mx_Dialog_titleImage { - vertical-align: middle; + vertical-align: sub; width: 25px; height: 25px; margin-left: -2px; @@ -328,6 +346,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { .mx_Dialog_title { font-size: $font-22px; + font-weight: $font-semi-bold; line-height: $font-36px; color: $dialog-title-fg-color; } @@ -335,6 +354,9 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { .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_title.danger { color: $warning-color; @@ -350,8 +372,8 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { background-color: $dialog-close-fg-color; cursor: pointer; position: absolute; - top: 4px; - right: 0px; + top: 10px; + right: 0; } .mx_Dialog_content { @@ -364,6 +386,11 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { .mx_Dialog_buttons { margin-top: 20px; text-align: right; + + .mx_Dialog_buttons_additive { + // The consumer is responsible for positioning their elements. + float: left; + } } /* XXX: Our button style are a mess: buttons that happen to appear in dialogs get special styles applied @@ -382,6 +409,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { border: 1px solid $accent-color; color: $accent-color; background-color: $button-secondary-bg-color; + font-family: inherit; } .mx_Dialog button:last-child { @@ -425,12 +453,16 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { border-radius: 8px; padding: 0px; box-shadow: none; + + /* Don't show scroll-bars on spinner dialogs */ + overflow-x: hidden; + overflow-y: hidden; } // TODO: Review mx_GeneralButton usage to see if it can use a different class // These classes were brought in from the old UserSettings and are included here to avoid // breaking the app. -// Ref: https://github.com/vector-im/riot-web/issues/8420 +// Ref: https://github.com/vector-im/element-web/issues/8420 .mx_GeneralButton { @mixin mx_DialogButton; display: inline; @@ -472,54 +504,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { margin-top: 69px; } -.mx_Beta { - color: red; - margin-right: 10px; - position: relative; - top: -3px; - background-color: white; - padding: 0 4px; - border-radius: 3px; - border: 1px solid darkred; - cursor: help; - transition-duration: 200ms; - font-size: smaller; - filter: opacity(0.5); -} - -.mx_Beta:hover { - color: white; - border: 1px solid gray; - background-color: darkred; -} - -.mx_TintableSvgButton { - position: relative; - display: flex; - flex-direction: row; - justify-content: center; - align-content: center; -} - -.mx_TintableSvgButton object { - margin: 0; - padding: 0; - width: 100%; - height: 100%; - max-width: 100%; - max-height: 100%; -} - -.mx_TintableSvgButton span { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - opacity: 0; - cursor: pointer; -} - // username colors // used by SenderProfile & RoomPreviewBar .mx_Username_color1 { @@ -578,3 +562,43 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { // So it fits in the space provided by the page max-width: 120px; } + +@define-mixin ProgressBarColour $colour { + color: $colour; + &::-moz-progress-bar { + background-color: $colour; + } + &::-webkit-progress-value { + background-color: $colour; + } +} + +@define-mixin ProgressBarBgColour $colour { + background-color: $colour; + &::-webkit-progress-bar { + background-color: $colour; + } +} + +@define-mixin ProgressBarBorderRadius $radius { + border-radius: $radius; + &::-moz-progress-bar { + border-radius: $radius; + } + &::-webkit-progress-bar, + &::-webkit-progress-value { + border-radius: $radius; + } +} + +@define-mixin unreal-focus { + outline-width: 2px; + outline-style: solid; + outline-color: Highlight; + + /* WebKit gets its native focus styles. */ + @media (-webkit-min-device-pixel-ratio: 0) { + outline-color: -webkit-focus-ring-color; + outline-style: auto; + } +} diff --git a/res/css/_components.scss b/res/css/_components.scss index 8706772ad9..4efc3f2316 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -1,6 +1,7 @@ // autogenerated by rethemendex.sh @import "./_common.scss"; @import "./_font-sizes.scss"; +@import "./_font-weights.scss"; @import "./structures/_AutoHideScrollbar.scss"; @import "./structures/_CompatibilityPage.scss"; @import "./structures/_ContextualMenu.scss"; @@ -8,29 +9,39 @@ @import "./structures/_CustomRoomTagPanel.scss"; @import "./structures/_FilePanel.scss"; @import "./structures/_GenericErrorPage.scss"; +@import "./structures/_GroupFilterPanel.scss"; @import "./structures/_GroupView.scss"; @import "./structures/_HeaderButtons.scss"; @import "./structures/_HomePage.scss"; @import "./structures/_LeftPanel.scss"; +@import "./structures/_LeftPanelWidget.scss"; @import "./structures/_MainSplit.scss"; @import "./structures/_MatrixChat.scss"; @import "./structures/_MyGroups.scss"; +@import "./structures/_NonUrgentToastContainer.scss"; @import "./structures/_NotificationPanel.scss"; @import "./structures/_RightPanel.scss"; @import "./structures/_RoomDirectory.scss"; +@import "./structures/_RoomSearch.scss"; @import "./structures/_RoomStatusBar.scss"; -@import "./structures/_RoomSubList.scss"; @import "./structures/_RoomView.scss"; @import "./structures/_ScrollPanel.scss"; @import "./structures/_SearchBox.scss"; +@import "./structures/_SpacePanel.scss"; +@import "./structures/_SpaceRoomDirectory.scss"; +@import "./structures/_SpaceRoomView.scss"; @import "./structures/_TabbedView.scss"; -@import "./structures/_TagPanel.scss"; @import "./structures/_ToastContainer.scss"; -@import "./structures/_TopLeftMenuButton.scss"; @import "./structures/_UploadBar.scss"; +@import "./structures/_UserMenu.scss"; @import "./structures/_ViewSource.scss"; @import "./structures/auth/_CompleteSecurity.scss"; @import "./structures/auth/_Login.scss"; +@import "./views/audio_messages/_AudioPlayer.scss"; +@import "./views/audio_messages/_PlayPauseButton.scss"; +@import "./views/audio_messages/_PlaybackContainer.scss"; +@import "./views/audio_messages/_SeekBar.scss"; +@import "./views/audio_messages/_Waveform.scss"; @import "./views/auth/_AuthBody.scss"; @import "./views/auth/_AuthButtons.scss"; @import "./views/auth/_AuthFooter.scss"; @@ -41,79 +52,102 @@ @import "./views/auth/_CountryDropdown.scss"; @import "./views/auth/_InteractiveAuthEntryComponents.scss"; @import "./views/auth/_LanguageSelector.scss"; -@import "./views/auth/_ServerConfig.scss"; -@import "./views/auth/_ServerTypeSelector.scss"; +@import "./views/auth/_PassphraseField.scss"; @import "./views/auth/_Welcome.scss"; @import "./views/avatars/_BaseAvatar.scss"; +@import "./views/avatars/_DecoratedRoomAvatar.scss"; @import "./views/avatars/_MemberStatusMessageAvatar.scss"; +@import "./views/avatars/_WidgetAvatar.scss"; +@import "./views/beta/_BetaCard.scss"; +@import "./views/context_menus/_CallContextMenu.scss"; +@import "./views/context_menus/_IconizedContextMenu.scss"; @import "./views/context_menus/_MessageContextMenu.scss"; -@import "./views/context_menus/_RoomTileContextMenu.scss"; @import "./views/context_menus/_StatusMessageContextMenu.scss"; @import "./views/context_menus/_TagTileContextMenu.scss"; -@import "./views/context_menus/_TopLeftMenu.scss"; -@import "./views/context_menus/_WidgetContextMenu.scss"; +@import "./views/dialogs/_AddExistingToSpaceDialog.scss"; @import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_Analytics.scss"; +@import "./views/dialogs/_BetaFeedbackDialog.scss"; +@import "./views/dialogs/_BugReportDialog.scss"; @import "./views/dialogs/_ChangelogDialog.scss"; @import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss"; +@import "./views/dialogs/_CommunityPrototypeInviteDialog.scss"; @import "./views/dialogs/_ConfirmUserActionDialog.scss"; +@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss"; @import "./views/dialogs/_CreateGroupDialog.scss"; @import "./views/dialogs/_CreateRoomDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss"; -@import "./views/dialogs/_DeviceVerifyDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss"; -@import "./views/dialogs/_EncryptedEventDialog.scss"; +@import "./views/dialogs/_EditCommunityPrototypeDialog.scss"; +@import "./views/dialogs/_FeedbackDialog.scss"; +@import "./views/dialogs/_ForwardDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; +@import "./views/dialogs/_HostSignupDialog.scss"; @import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_InviteDialog.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; +@import "./views/dialogs/_ModalWidgetDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss"; +@import "./views/dialogs/_RegistrationEmailPromptDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomSettingsDialogBridges.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; @import "./views/dialogs/_RoomUpgradeWarningDialog.scss"; +@import "./views/dialogs/_ServerOfflineDialog.scss"; +@import "./views/dialogs/_ServerPickerDialog.scss"; @import "./views/dialogs/_SetEmailDialog.scss"; -@import "./views/dialogs/_SetMxIdDialog.scss"; -@import "./views/dialogs/_SetPasswordDialog.scss"; @import "./views/dialogs/_SettingsDialog.scss"; @import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_SlashCommandHelpDialog.scss"; +@import "./views/dialogs/_SpaceSettingsDialog.scss"; @import "./views/dialogs/_TabbedIntegrationManagerDialog.scss"; @import "./views/dialogs/_TermsDialog.scss"; -@import "./views/dialogs/_UnknownDeviceDialog.scss"; +@import "./views/dialogs/_UntrustedDeviceDialog.scss"; @import "./views/dialogs/_UploadConfirmDialog.scss"; @import "./views/dialogs/_UserSettingsDialog.scss"; +@import "./views/dialogs/_WidgetCapabilitiesPromptDialog.scss"; @import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss"; -@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss"; -@import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss"; -@import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss"; -@import "./views/dialogs/secretstorage/_AccessSecretStorageDialog.scss"; -@import "./views/dialogs/secretstorage/_CreateSecretStorageDialog.scss"; +@import "./views/dialogs/security/_AccessSecretStorageDialog.scss"; +@import "./views/dialogs/security/_CreateCrossSigningDialog.scss"; +@import "./views/dialogs/security/_CreateKeyBackupDialog.scss"; +@import "./views/dialogs/security/_CreateSecretStorageDialog.scss"; +@import "./views/dialogs/security/_KeyBackupFailedDialog.scss"; +@import "./views/dialogs/security/_RestoreKeyBackupDialog.scss"; @import "./views/directory/_NetworkDropdown.scss"; @import "./views/elements/_AccessibleButton.scss"; @import "./views/elements/_AddressSelector.scss"; @import "./views/elements/_AddressTile.scss"; +@import "./views/elements/_DesktopBuildsNotice.scss"; +@import "./views/elements/_DesktopCapturerSourcePicker.scss"; +@import "./views/elements/_DialPadBackspaceButton.scss"; @import "./views/elements/_DirectorySearchBox.scss"; @import "./views/elements/_Dropdown.scss"; @import "./views/elements/_EditableItemList.scss"; @import "./views/elements/_ErrorBoundary.scss"; @import "./views/elements/_EventListSummary.scss"; +@import "./views/elements/_FacePile.scss"; @import "./views/elements/_Field.scss"; -@import "./views/elements/_FormButton.scss"; -@import "./views/elements/_IconButton.scss"; @import "./views/elements/_ImageView.scss"; +@import "./views/elements/_InfoTooltip.scss"; @import "./views/elements/_InlineSpinner.scss"; -@import "./views/elements/_InteractiveTooltip.scss"; +@import "./views/elements/_InviteReason.scss"; @import "./views/elements/_ManageIntegsButton.scss"; +@import "./views/elements/_MiniAvatarUploader.scss"; @import "./views/elements/_PowerSelector.scss"; @import "./views/elements/_ProgressBar.scss"; +@import "./views/elements/_QRCode.scss"; @import "./views/elements/_ReplyThread.scss"; @import "./views/elements/_ResizeHandle.scss"; @import "./views/elements/_RichText.scss"; @import "./views/elements/_RoleButton.scss"; @import "./views/elements/_RoomAliasField.scss"; +@import "./views/elements/_SSOButtons.scss"; +@import "./views/elements/_ServerPicker.scss"; +@import "./views/elements/_Slider.scss"; @import "./views/elements/_Spinner.scss"; +@import "./views/elements/_StyledCheckbox.scss"; +@import "./views/elements/_StyledRadioButton.scss"; @import "./views/elements/_SyntaxHighlight.scss"; @import "./views/elements/_TextWithTooltip.scss"; @import "./views/elements/_ToggleSwitch.scss"; @@ -121,35 +155,43 @@ @import "./views/elements/_TooltipButton.scss"; @import "./views/elements/_Validation.scss"; @import "./views/emojipicker/_EmojiPicker.scss"; -@import "./views/globals/_MatrixToolbar.scss"; @import "./views/groups/_GroupPublicityToggle.scss"; @import "./views/groups/_GroupRoomList.scss"; @import "./views/groups/_GroupUserSettings.scss"; @import "./views/messages/_CreateEvent.scss"; @import "./views/messages/_DateSeparator.scss"; +@import "./views/messages/_EventTileBubble.scss"; @import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MFileBody.scss"; @import "./views/messages/_MImageBody.scss"; +@import "./views/messages/_MImageReplyBody.scss"; +@import "./views/messages/_MJitsiWidgetEvent.scss"; @import "./views/messages/_MNoticeBody.scss"; @import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MTextBody.scss"; +@import "./views/messages/_MVideoBody.scss"; +@import "./views/messages/_MVoiceMessageBody.scss"; +@import "./views/messages/_MediaBody.scss"; @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; @import "./views/messages/_MjolnirBody.scss"; @import "./views/messages/_ReactionsRow.scss"; @import "./views/messages/_ReactionsRowButton.scss"; -@import "./views/messages/_ReactionsRowButtonTooltip.scss"; +@import "./views/messages/_RedactedBody.scss"; @import "./views/messages/_RoomAvatarEvent.scss"; @import "./views/messages/_SenderProfile.scss"; @import "./views/messages/_TextualEvent.scss"; @import "./views/messages/_UnknownBody.scss"; @import "./views/messages/_ViewSourceEvent.scss"; @import "./views/messages/_common_CryptoEvent.scss"; +@import "./views/right_panel/_BaseCard.scss"; @import "./views/right_panel/_EncryptionInfo.scss"; +@import "./views/right_panel/_PinnedMessagesCard.scss"; +@import "./views/right_panel/_RoomSummaryCard.scss"; @import "./views/right_panel/_UserInfo.scss"; @import "./views/right_panel/_VerificationPanel.scss"; +@import "./views/right_panel/_WidgetCard.scss"; @import "./views/room_settings/_AliasSettings.scss"; -@import "./views/room_settings/_ColorSettings.scss"; @import "./views/rooms/_AppsDrawer.scss"; @import "./views/rooms/_Autocomplete.scss"; @import "./views/rooms/_AuxPanel.scss"; @@ -158,31 +200,33 @@ @import "./views/rooms/_EditMessageComposer.scss"; @import "./views/rooms/_EntityTile.scss"; @import "./views/rooms/_EventTile.scss"; -@import "./views/rooms/_InviteOnlyIcon.scss"; +@import "./views/rooms/_GroupLayout.scss"; +@import "./views/rooms/_IRCLayout.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; +@import "./views/rooms/_LinkPreviewGroup.scss"; @import "./views/rooms/_LinkPreviewWidget.scss"; -@import "./views/rooms/_MemberDeviceInfo.scss"; @import "./views/rooms/_MemberInfo.scss"; @import "./views/rooms/_MemberList.scss"; @import "./views/rooms/_MessageComposer.scss"; @import "./views/rooms/_MessageComposerFormatBar.scss"; +@import "./views/rooms/_NewRoomIntro.scss"; +@import "./views/rooms/_NotificationBadge.scss"; @import "./views/rooms/_PinnedEventTile.scss"; -@import "./views/rooms/_PinnedEventsPanel.scss"; @import "./views/rooms/_PresenceLabel.scss"; @import "./views/rooms/_ReplyPreview.scss"; +@import "./views/rooms/_ReplyTile.scss"; @import "./views/rooms/_RoomBreadcrumbs.scss"; -@import "./views/rooms/_RoomDropTarget.scss"; @import "./views/rooms/_RoomHeader.scss"; @import "./views/rooms/_RoomList.scss"; @import "./views/rooms/_RoomPreviewBar.scss"; -@import "./views/rooms/_RoomRecoveryReminder.scss"; +@import "./views/rooms/_RoomSublist.scss"; @import "./views/rooms/_RoomTile.scss"; @import "./views/rooms/_RoomUpgradeWarningBar.scss"; @import "./views/rooms/_SearchBar.scss"; @import "./views/rooms/_SendMessageComposer.scss"; @import "./views/rooms/_Stickers.scss"; @import "./views/rooms/_TopUnreadMessagesBar.scss"; -@import "./views/rooms/_UserOnlineDot.scss"; +@import "./views/rooms/_VoiceRecordComposerTile.scss"; @import "./views/rooms/_WhoIsTypingTile.scss"; @import "./views/settings/_AvatarSetting.scss"; @import "./views/settings/_CrossSigningPanel.scss"; @@ -190,25 +234,39 @@ @import "./views/settings/_E2eAdvancedPanel.scss"; @import "./views/settings/_EmailAddresses.scss"; @import "./views/settings/_IntegrationManager.scss"; -@import "./views/settings/_KeyBackupPanel.scss"; @import "./views/settings/_Notifications.scss"; @import "./views/settings/_PhoneNumbers.scss"; @import "./views/settings/_ProfileSettings.scss"; +@import "./views/settings/_SecureBackupPanel.scss"; @import "./views/settings/_SetIdServer.scss"; @import "./views/settings/_SetIntegrationManager.scss"; +@import "./views/settings/_SpellCheckLanguages.scss"; +@import "./views/settings/_UpdateCheckButton.scss"; @import "./views/settings/tabs/_SettingsTab.scss"; @import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss"; @import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss"; @import "./views/settings/tabs/room/_SecurityRoomSettingsTab.scss"; +@import "./views/settings/tabs/user/_AppearanceUserSettingsTab.scss"; @import "./views/settings/tabs/user/_GeneralUserSettingsTab.scss"; @import "./views/settings/tabs/user/_HelpUserSettingsTab.scss"; +@import "./views/settings/tabs/user/_LabsUserSettingsTab.scss"; @import "./views/settings/tabs/user/_MjolnirUserSettingsTab.scss"; @import "./views/settings/tabs/user/_NotificationUserSettingsTab.scss"; @import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss"; @import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss"; @import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss"; +@import "./views/spaces/_SpaceBasicSettings.scss"; +@import "./views/spaces/_SpaceCreateMenu.scss"; +@import "./views/spaces/_SpacePublicShare.scss"; @import "./views/terms/_InlineTermsAgreement.scss"; +@import "./views/toasts/_AnalyticsToast.scss"; +@import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/verification/_VerificationShowSas.scss"; +@import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallView.scss"; -@import "./views/voip/_IncomingCallbox.scss"; -@import "./views/voip/_VideoView.scss"; +@import "./views/voip/_CallViewForRoom.scss"; +@import "./views/voip/_CallPreview.scss"; +@import "./views/voip/_DialPad.scss"; +@import "./views/voip/_DialPadContextMenu.scss"; +@import "./views/voip/_DialPadModal.scss"; +@import "./views/voip/_VideoFeed.scss"; diff --git a/res/css/_font-sizes.scss b/res/css/_font-sizes.scss index ad9e2e7103..caa3a452b0 100644 --- a/res/css/_font-sizes.scss +++ b/res/css/_font-sizes.scss @@ -14,50 +14,60 @@ See the License for the specific language governing permissions and limitations under the License. */ -$font-8px: 0.533rem; -$font-9px: 0.600rem; -$font-10px: 0.667rem; -$font-10-4px: 0.693rem; -$font-11px: 0.733rem; -$font-12px: 0.800rem; -$font-13px: 0.867rem; -$font-14px: 0.933rem; -$font-15px: 1.000rem; -$font-16px: 1.067rem; -$font-17px: 1.133rem; -$font-18px: 1.200rem; -$font-19px: 1.267rem; -$font-20px: 1.333rem; -$font-21px: 1.400rem; -$font-22px: 1.467rem; -$font-23px: 1.533rem; -$font-24px: 1.600rem; -$font-25px: 1.667rem; -$font-26px: 1.733rem; -$font-27px: 1.800rem; -$font-28px: 1.867rem; -$font-29px: 1.933rem; -$font-30px: 2.000rem; -$font-31px: 2.067rem; -$font-32px: 2.133rem; -$font-33px: 2.200rem; -$font-34px: 2.267rem; -$font-35px: 2.333rem; -$font-36px: 2.400rem; -$font-37px: 2.467rem; -$font-38px: 2.533rem; -$font-39px: 2.600rem; -$font-40px: 2.667rem; -$font-41px: 2.733rem; -$font-42px: 2.800rem; -$font-43px: 2.867rem; -$font-44px: 2.933rem; -$font-45px: 3.000rem; -$font-46px: 3.067rem; -$font-47px: 3.133rem; -$font-48px: 3.200rem; -$font-49px: 3.267rem; -$font-50px: 3.333rem; -$font-51px: 3.400rem; -$font-52px: 3.467rem; -$font-400px: 26.667rem; +$font-1px: 0.1rem; +$font-1-5px: 0.15rem; +$font-2px: 0.2rem; +$font-3px: 0.3rem; +$font-4px: 0.4rem; +$font-5px: 0.5rem; +$font-6px: 0.6rem; +$font-7px: 0.7rem; +$font-8px: 0.8rem; +$font-9px: 0.9rem; +$font-10px: 1.0rem; +$font-10-4px: 1.04rem; +$font-11px: 1.1rem; +$font-12px: 1.2rem; +$font-13px: 1.3rem; +$font-14px: 1.4rem; +$font-15px: 1.5rem; +$font-16px: 1.6rem; +$font-17px: 1.7rem; +$font-18px: 1.8rem; +$font-19px: 1.9rem; +$font-20px: 2.0rem; +$font-21px: 2.1rem; +$font-22px: 2.2rem; +$font-23px: 2.3rem; +$font-24px: 2.4rem; +$font-25px: 2.5rem; +$font-26px: 2.6rem; +$font-27px: 2.7rem; +$font-28px: 2.8rem; +$font-29px: 2.9rem; +$font-30px: 3.0rem; +$font-31px: 3.1rem; +$font-32px: 3.2rem; +$font-33px: 3.3rem; +$font-34px: 3.4rem; +$font-35px: 3.5rem; +$font-36px: 3.6rem; +$font-37px: 3.7rem; +$font-38px: 3.8rem; +$font-39px: 3.9rem; +$font-40px: 4.0rem; +$font-41px: 4.1rem; +$font-42px: 4.2rem; +$font-43px: 4.3rem; +$font-44px: 4.4rem; +$font-45px: 4.5rem; +$font-46px: 4.6rem; +$font-47px: 4.7rem; +$font-48px: 4.8rem; +$font-49px: 4.9rem; +$font-50px: 5.0rem; +$font-51px: 5.1rem; +$font-52px: 5.2rem; +$font-78px: 7.8rem; +$font-88px: 8.8rem; +$font-400px: 40rem; diff --git a/res/css/_font-weights.scss b/res/css/_font-weights.scss new file mode 100644 index 0000000000..3e2b19d516 --- /dev/null +++ b/res/css/_font-weights.scss @@ -0,0 +1,17 @@ +/* +Copyright 2020 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. +*/ + +$font-semi-bold: 600; diff --git a/res/css/structures/_ContextualMenu.scss b/res/css/structures/_ContextualMenu.scss index 61070a0541..d7f2cb76e8 100644 --- a/res/css/structures/_ContextualMenu.scss +++ b/res/css/structures/_ContextualMenu.scss @@ -31,13 +31,14 @@ limitations under the License. } .mx_ContextualMenu { - border-radius: 4px; + border-radius: 8px; box-shadow: 4px 4px 12px 0 $menu-box-shadow-color; background-color: $menu-bg-color; color: $primary-fg-color; position: absolute; font-size: $font-14px; z-index: 5001; + contain: content; } .mx_ContextualMenu_right { @@ -115,8 +116,3 @@ limitations under the License. border-top: 8px solid $menu-bg-color; border-right: 8px solid transparent; } - -.mx_ContextualMenu_spinner { - display: block; - margin: 0 auto; -} diff --git a/res/css/structures/_CustomRoomTagPanel.scss b/res/css/structures/_CustomRoomTagPanel.scss index 1fb18ec41e..be1138cf5b 100644 --- a/res/css/structures/_CustomRoomTagPanel.scss +++ b/res/css/structures/_CustomRoomTagPanel.scss @@ -14,13 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_LeftPanel_tagPanelContainer { - display: flex; - flex-direction: column; -} +// TODO: Update design for custom tags to match new designs .mx_CustomRoomTagPanel { - background-color: $tagpanel-bg-color; + background-color: $groupFilterPanel-bg-color; max-height: 40vh; } @@ -50,7 +47,7 @@ limitations under the License. background-color: $accent-color-alt; width: 5px; position: absolute; - left: -15px; + left: -9px; border-radius: 0 3px 3px 0; - top: 2px; // 10 [padding-top] - (56 - 40)/2 + top: 5px; // just feels right (see comment above about designs needing to be updated) } diff --git a/res/css/structures/_FilePanel.scss b/res/css/structures/_FilePanel.scss index 859ee28035..7b975110e1 100644 --- a/res/css/structures/_FilePanel.scss +++ b/res/css/structures/_FilePanel.scss @@ -22,7 +22,13 @@ limitations under the License. } .mx_FilePanel .mx_RoomView_messageListWrapper { - margin-right: 20px; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.mx_FilePanel .mx_RoomView_MessageList { + width: 100%; } .mx_FilePanel .mx_RoomView_MessageList h2 { @@ -41,13 +47,19 @@ limitations under the License. .mx_FilePanel .mx_EventTile { word-break: break-word; + margin-top: 32px; } .mx_FilePanel .mx_EventTile .mx_MImageBody { margin-right: 0px; } +.mx_FilePanel .mx_EventTile .mx_MFileBody { + line-height: 2.4rem; +} + .mx_FilePanel .mx_EventTile .mx_MFileBody_download { + padding-top: 8px; display: flex; font-size: $font-14px; color: $event-timestamp-color; @@ -60,7 +72,7 @@ limitations under the License. .mx_FilePanel .mx_EventTile .mx_MImageBody_size { flex: 1 0 0; - font-size: $font-11px; + font-size: $font-14px; text-align: right; white-space: nowrap; } @@ -80,7 +92,7 @@ limitations under the License. flex: 1 1 auto; line-height: initial; padding: 0px; - font-size: $font-11px; + font-size: $font-14px; opacity: 1.0; color: $event-timestamp-color; } @@ -90,7 +102,7 @@ limitations under the License. text-align: right; visibility: visible; position: initial; - font-size: $font-11px; + font-size: $font-14px; opacity: 1.0; color: $event-timestamp-color; } @@ -109,3 +121,7 @@ limitations under the License. .mx_FilePanel .mx_EventTile:hover .mx_EventTile_line { background-color: $primary-bg-color; } + +.mx_FilePanel_empty::before { + mask-image: url('$(res)/img/element-icons/room/files.svg'); +} diff --git a/res/css/structures/_GroupFilterPanel.scss b/res/css/structures/_GroupFilterPanel.scss new file mode 100644 index 0000000000..444435dd57 --- /dev/null +++ b/res/css/structures/_GroupFilterPanel.scss @@ -0,0 +1,196 @@ +/* +Copyright 2017 New Vector Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_GroupFilterPanel { + flex: 1; + background-color: $groupFilterPanel-bg-color; + cursor: pointer; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + min-height: 0; +} + +.mx_GroupFilterPanel_items_selected { + cursor: pointer; +} + +.mx_GroupFilterPanel .mx_GroupFilterPanel_divider { + height: 0px; + width: 90%; + border: none; + border-bottom: 1px solid $groupFilterPanel-divider-color; +} + +.mx_GroupFilterPanel .mx_GroupFilterPanel_scroller { + flex-grow: 1; + width: 100%; +} + +.mx_GroupFilterPanel .mx_GroupFilterPanel_tagTileContainer { + display: flex; + flex-direction: column; + align-items: center; + + padding-top: 6px; +} +.mx_GroupFilterPanel .mx_GroupFilterPanel_tagTileContainer > div { + margin: 6px 0; +} + +.mx_GroupFilterPanel .mx_TagTile { + // opacity: 0.5; + position: relative; + + .mx_BetaDot { + position: absolute; + right: -13px; + top: -11px; + } +} + +.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype { + padding: 3px; +} + +.mx_GroupFilterPanel .mx_TagTile:focus, +.mx_GroupFilterPanel .mx_TagTile:hover, +.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected { + // opacity: 1; +} + +.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected_prototype { + background-color: $primary-bg-color; + border-radius: 6px; +} + +.mx_TagTile_selected_prototype { + .mx_TagTile_homeIcon::before { + background-color: $primary-fg-color; // dark-on-light + } +} + +.mx_TagTile:not(.mx_TagTile_selected_prototype) .mx_TagTile_homeIcon { + background-color: $roomheader-addroom-bg-color; + border-radius: 48px; + + &::before { + background-color: $roomheader-addroom-fg-color; + } +} + +.mx_TagTile_homeIcon { + width: 32px; + height: 32px; + position: relative; + + &::before { + mask-image: url('$(res)/img/element-icons/home.svg'); + mask-position: center; + mask-repeat: no-repeat; + mask-size: 21px; + content: ''; + display: inline-block; + width: 32px; + height: 32px; + position: absolute; + top: calc(50% - 16px); + left: calc(50% - 16px); + } +} + +.mx_GroupFilterPanel .mx_TagTile_plus { + margin-bottom: 12px; + height: 32px; + width: 32px; + border-radius: 20px; + background-color: $roomheader-addroom-bg-color; + position: relative; + /* overwrite mx_RoleButton inline-block */ + display: block !important; + + &::before { + background-color: $roomheader-addroom-fg-color; + mask-image: url('$(res)/img/feather-customised/plus.svg'); + mask-position: center; + mask-repeat: no-repeat; + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } +} + +.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected::before { + content: ''; + height: 100%; + background-color: $accent-color; + width: 4px; + position: absolute; + left: -12px; + border-radius: 0 3px 3px 0; +} + +.mx_GroupFilterPanel .mx_TagTile.mx_AccessibleButton:focus { + filter: none; +} + +.mx_TagTile_tooltip { + position: relative; + top: -30px; + left: 5px; +} + +.mx_TagTile_context_button { + min-width: 15px; + height: 15px; + position: absolute; + right: -5px; + top: -8px; + border-radius: 8px; + background-color: $neutral-badge-color; + color: #000; + font-weight: 600; + font-size: $font-10px; + text-align: center; + padding-top: 1px; + padding-left: 4px; + padding-right: 4px; +} + +.mx_TagTile_avatar { + position: relative; +} + +.mx_TagTile_badge { + position: absolute; + right: -4px; + top: -2px; + border-radius: 8px; + color: $accent-fg-color; + font-weight: 600; + font-size: $font-14px; + padding: 0 5px; + background-color: $muted-fg-color; +} + +.mx_TagTile_badgeHighlight { + background-color: $warning-color; +} diff --git a/res/css/structures/_GroupView.scss b/res/css/structures/_GroupView.scss index ed0cf121a4..60f9ebdd08 100644 --- a/res/css/structures/_GroupView.scss +++ b/res/css/structures/_GroupView.scss @@ -29,12 +29,12 @@ limitations under the License. align-items: center; display: flex; padding-bottom: 10px; + padding-left: 19px; } .mx_GroupView_header_view { border-bottom: 1px solid $primary-hairline-color; padding-bottom: 0px; - padding-left: 19px; padding-right: 8px; } @@ -63,11 +63,11 @@ limitations under the License. } .mx_GroupHeader_editButton::before { - mask-image: url('$(res)/img/feather-customised/settings.svg'); + mask-image: url('$(res)/img/element-icons/settings.svg'); } .mx_GroupHeader_shareButton::before { - mask-image: url('$(res)/img/icons-share.svg'); + mask-image: url('$(res)/img/element-icons/room/share.svg'); } .mx_GroupView_hostingSignup img { @@ -182,6 +182,7 @@ limitations under the License. .mx_GroupView_body { flex-grow: 1; + margin: 0 24px; } .mx_GroupView_rooms { @@ -250,6 +251,7 @@ limitations under the License. .mx_GroupView_membershipSubSection { justify-content: space-between; display: flex; + padding-bottom: 8px; } .mx_GroupView_membershipSubSection .mx_Spinner { @@ -321,7 +323,7 @@ limitations under the License. } .mx_GroupView_featuredThing .mx_BaseAvatar { - /* To prevent misalignment with mx_TintableSvg (in addButton) */ + /* To prevent misalignment with img (in addButton) */ vertical-align: initial; } diff --git a/res/css/structures/_HeaderButtons.scss b/res/css/structures/_HeaderButtons.scss index eef7653b24..72b663ef0e 100644 --- a/res/css/structures/_HeaderButtons.scss +++ b/res/css/structures/_HeaderButtons.scss @@ -18,11 +18,19 @@ limitations under the License. display: flex; } +.mx_RoomHeader_buttons + .mx_HeaderButtons { + // remove the | separator line for when next to RoomHeaderButtons + // TODO: remove this once when we redo communities and make the right panel similar to the new rooms one + &::before { + content: unset; + } +} + .mx_HeaderButtons::before { content: ""; background-color: $header-divider-color; opacity: 0.5; - margin: 0 15px; + margin: 6px 8px; border-radius: 1px; width: 1px; } diff --git a/res/css/structures/_HomePage.scss b/res/css/structures/_HomePage.scss index 0160cf368b..9f72213d1a 100644 --- a/res/css/structures/_HomePage.scss +++ b/res/css/structures/_HomePage.scss @@ -26,9 +26,10 @@ limitations under the License. .mx_HomePage_default { text-align: center; + display: flex; .mx_HomePage_default_wrapper { - padding: 25vh 0 12px; + margin: auto; } img { @@ -50,56 +51,54 @@ limitations under the License. color: $muted-fg-color; } + .mx_MiniAvatarUploader { + margin: 0 auto; + } + .mx_HomePage_default_buttons { - margin: 80px auto 0; + margin: 60px auto 0; width: fit-content; .mx_AccessibleButton { padding: 73px 8px 15px; // top: 20px top padding + 40px icon + 13px margin - width: 104px; // 120px - 2* 8px - margin: 0 39px; // 55px - 2* 8px + width: 160px; + height: 132px; + margin: 20px; position: relative; display: inline-block; border-radius: 8px; vertical-align: top; word-break: break-word; + box-sizing: border-box; font-weight: 600; font-size: $font-15px; line-height: $font-20px; - color: $muted-fg-color; - - &:hover { - color: $accent-color; - background: rgba(#03b381, 0.06); - - &::before { - background-color: $accent-color; - } - } + color: #fff; // on all themes + background-color: $accent-color; &::before { top: 20px; - left: 40px; // (120px-40px)/2 + left: 60px; // (160px-40px)/2 width: 40px; height: 40px; content: ''; position: absolute; - background-color: $muted-fg-color; + background-color: #fff; // on all themes mask-repeat: no-repeat; mask-size: contain; } &.mx_HomePage_button_sendDm::before { - mask-image: url('$(res)/img/feather-customised/message-circle.svg'); + mask-image: url('$(res)/img/element-icons/feedback.svg'); } &.mx_HomePage_button_explore::before { - mask-image: url('$(res)/img/feather-customised/explore.svg'); + mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); } &.mx_HomePage_button_createGroup::before { - mask-image: url('$(res)/img/feather-customised/group.svg'); + mask-image: url('$(res)/img/element-icons/community-members.svg'); } } } diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 7d57425f6f..f254ca3226 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -1,6 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,163 +14,212 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_LeftPanel_container { - display: flex; - /* LeftPanel 260px */ - min-width: 260px; - flex: 0 0 auto; -} - -.mx_LeftPanel_container.collapsed { - min-width: unset; - /* Collapsed LeftPanel 50px */ - flex: 0 0 50px; -} - -.mx_LeftPanel_container.collapsed.mx_LeftPanel_container_hasTagPanel { - /* TagPanel 70px + Collapsed LeftPanel 50px */ - flex: 0 0 120px; -} - -.mx_LeftPanel_tagPanelContainer { - flex: 0 0 70px; - height: 100%; -} - -.mx_LeftPanel_hideButton { - position: absolute; - top: 10px; - right: 0px; - padding: 8px; - cursor: pointer; -} +$groupFilterPanelWidth: 56px; // only applies in this file, used for calculations +$roomListCollapsedWidth: 68px; .mx_LeftPanel { - flex: 1; - overflow-x: hidden; + background-color: $roomlist-bg-color; + // TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel + min-width: 206px; + max-width: 50%; + + // Create a row-based flexbox for the GroupFilterPanel and the room list display: flex; - flex-direction: column; - min-height: 0; -} + contain: content; -.mx_LeftPanel .mx_AppTile_mini { - height: 132px; -} + .mx_LeftPanel_GroupFilterPanelContainer { + flex-grow: 0; + flex-shrink: 0; + flex-basis: $groupFilterPanelWidth; + height: 100%; -.mx_LeftPanel .mx_RoomList_scrollbar { - order: 1; + // Create another flexbox so the GroupFilterPanel fills the container + display: flex; + flex-direction: column; - flex: 1 1 0; - - overflow-y: auto; - z-index: 6; -} - -.mx_LeftPanel .mx_BottomLeftMenu { - order: 3; - - border-top: 1px solid $panel-divider-color; - margin-left: 16px; /* gutter */ - margin-right: 16px; /* gutter */ - flex: 0 0 60px; - z-index: 1; -} - -.mx_LeftPanel_container.collapsed .mx_BottomLeftMenu { - flex: 0 0 160px; - margin-bottom: 9px; -} - -.mx_LeftPanel .mx_BottomLeftMenu_options { - margin-top: 18px; -} - -.mx_BottomLeftMenu_options object { - pointer-events: none; -} - -.mx_BottomLeftMenu_options > div { - display: inline-block; -} - -.mx_BottomLeftMenu_options .mx_RoleButton { - margin-left: 0px; - margin-right: 10px; - height: 30px; -} - -.mx_BottomLeftMenu_options .mx_BottomLeftMenu_settings { - float: right; -} - -.mx_BottomLeftMenu_options .mx_BottomLeftMenu_settings .mx_RoleButton { - margin-right: 0px; -} - -.mx_LeftPanel_container.collapsed .mx_BottomLeftMenu_settings { - float: none; -} - -.mx_MatrixChat_useCompactLayout { - .mx_LeftPanel .mx_BottomLeftMenu { - flex: 0 0 50px; + // GroupFilterPanel handles its own CSS } - .mx_LeftPanel_container.collapsed .mx_BottomLeftMenu { - flex: 0 0 160px; - } - - .mx_LeftPanel .mx_BottomLeftMenu_options { - margin-top: 12px; - } -} - -.mx_LeftPanel_exploreAndFilterRow { - display: flex; - - .mx_SearchBox { - flex: 1 1 0; + // Note: The 'room list' in this context is actually everything that isn't the tag + // panel, such as the menu options, breadcrumbs, filtering, etc + .mx_LeftPanel_roomListContainer { + background-color: $roomlist-bg-color; + flex: 1 0 0; min-width: 0; - margin: 4px 9px 1px 9px; - } -} + // Create another flexbox (this time a column) for the room list components + display: flex; + flex-direction: column; -.mx_LeftPanel_explore { - flex: 0 0 50%; - overflow: hidden; - transition: flex-basis 0.2s; - box-sizing: border-box; + .mx_LeftPanel_userHeader { + /* 12px top, 12px sides, 20px bottom (using 13px bottom to account + * for internal whitespace in the breadcrumbs) + */ + padding: 12px; + flex-shrink: 0; // to convince safari's layout engine the flexbox is fine - &.mx_LeftPanel_explore_hidden { - flex-basis: 0; - } - - .mx_AccessibleButton { - font-size: $font-14px; - margin: 4px 0 1px 9px; - padding: 9px; - padding-left: 42px; - font-weight: 600; - color: $notice-secondary-color; - position: relative; - border-radius: 4px; - - &:hover { - background-color: $primary-bg-color; + // Create another flexbox column for the rows to stack within + display: flex; + flex-direction: column; } - &::before { - cursor: pointer; - mask: url('$(res)/img/explore.svg'); - mask-repeat: no-repeat; - mask-position: center center; - content: ""; - left: 14px; - top: 10px; - width: 16px; - height: 16px; - background-color: $notice-secondary-color; - position: absolute; + .mx_LeftPanel_breadcrumbsContainer { + overflow-y: hidden; + overflow-x: scroll; + margin: 12px 12px 0 12px; + flex: 0 0 auto; + // Create yet another flexbox, this time within the row, to ensure items stay + // aligned correctly. This is also a row-based flexbox. + display: flex; + align-items: center; + contain: content; + + &.mx_IndicatorScrollbar_leftOverflow { + mask-image: linear-gradient(90deg, transparent, black 5%); + } + + &.mx_IndicatorScrollbar_rightOverflow { + mask-image: linear-gradient(90deg, black, black 95%, transparent); + } + + &.mx_IndicatorScrollbar_rightOverflow.mx_IndicatorScrollbar_leftOverflow { + mask-image: linear-gradient(90deg, transparent, black 5%, black 95%, transparent); + } + } + + .mx_LeftPanel_filterContainer { + margin-left: 12px; + margin-right: 12px; + + flex-shrink: 0; // to convince safari's layout engine the flexbox is fine + + // Create a flexbox to organize the inputs + display: flex; + align-items: center; + + .mx_RoomSearch_focused, .mx_RoomSearch_hasQuery { + & + .mx_LeftPanel_exploreButton { + // Cheaty way to return the occupied space to the filter input + flex-basis: 0; + margin: 0; + width: 0; + + // Don't forget to hide the masked ::before icon, + // using display:none or visibility:hidden would break accessibility + &::before { + content: none; + } + } + } + + .mx_LeftPanel_dialPadButton { + width: 32px; + height: 32px; + border-radius: 8px; + background-color: $roomlist-button-bg-color; + position: relative; + margin-left: 8px; + + &::before { + content: ''; + position: absolute; + top: 8px; + left: 8px; + width: 16px; + height: 16px; + mask-image: url('$(res)/img/element-icons/call/dialpad.svg'); + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $secondary-fg-color; + } + } + + .mx_LeftPanel_exploreButton { + width: 32px; + height: 32px; + border-radius: 8px; + background-color: $roomlist-button-bg-color; + position: relative; + margin-left: 8px; + + &::before { + content: ''; + position: absolute; + top: 8px; + left: 8px; + width: 16px; + height: 16px; + mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $secondary-fg-color; + } + + &.mx_LeftPanel_exploreButton_space::before { + mask-image: url('$(res)/img/element-icons/roomlist/browse.svg'); + } + } + } + + .mx_LeftPanel_roomListFilterCount { + font-size: $font-13px; + font-weight: $font-semi-bold; + margin-left: 12px; + margin-top: 14px; + margin-bottom: -4px; // to counteract the normal roomListWrapper margin-top + } + + .mx_LeftPanel_roomListWrapper { + overflow: hidden; + margin-top: 10px; // so we're not up against the search/filter + flex: 1 0 0; // needed in Safari to properly set flex-basis + + &.mx_LeftPanel_roomListWrapper_stickyBottom { + padding-bottom: 32px; + } + + &.mx_LeftPanel_roomListWrapper_stickyTop { + padding-top: 32px; + } + } + + .mx_LeftPanel_actualRoomListContainer { + position: relative; // for sticky headers + height: 100%; // ensure scrolling still works + } + } + + // These styles override the defaults for the minimized (66px) layout + &.mx_LeftPanel_minimized { + min-width: unset; + width: unset !important; + + .mx_LeftPanel_roomListContainer { + width: $roomListCollapsedWidth; + + .mx_LeftPanel_userHeader { + flex-direction: row; + justify-content: center; + } + + .mx_LeftPanel_filterContainer { + // Organize the flexbox into a centered column layout + flex-direction: column; + justify-content: center; + + .mx_LeftPanel_dialPadButton { + margin-left: 0; + margin-top: 8px; + background-color: transparent; + } + + .mx_LeftPanel_exploreButton { + margin-left: 0; + margin-top: 8px; + background-color: transparent; + } + } } } } diff --git a/res/css/structures/_LeftPanelWidget.scss b/res/css/structures/_LeftPanelWidget.scss new file mode 100644 index 0000000000..6e2d99bb37 --- /dev/null +++ b/res/css/structures/_LeftPanelWidget.scss @@ -0,0 +1,145 @@ +/* +Copyright 2020 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_LeftPanelWidget { + // largely based on RoomSublist + margin-left: 8px; + margin-bottom: 4px; + + .mx_LeftPanelWidget_headerContainer { + display: flex; + align-items: center; + + height: 24px; + color: $roomlist-header-color; + margin-top: 4px; + + .mx_LeftPanelWidget_stickable { + flex: 1; + max-width: 100%; + + display: flex; + align-items: center; + } + + .mx_LeftPanelWidget_headerText { + flex: 1; + max-width: calc(100% - 16px); + line-height: $font-16px; + font-size: $font-13px; + font-weight: 600; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + + .mx_LeftPanelWidget_collapseBtn { + display: inline-block; + position: relative; + width: 14px; + height: 14px; + margin-right: 6px; + + &::before { + content: ''; + width: 18px; + height: 18px; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background-color: $roomlist-header-color; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + } + + &.mx_LeftPanelWidget_collapseBtn_collapsed::before { + transform: rotate(-90deg); + } + } + } + } + + .mx_LeftPanelWidget_resizeBox { + position: relative; + + display: flex; + flex-direction: column; + overflow: visible; // let the resize handle out + } + + .mx_AppTileFullWidth { + flex: 1 0 0; + overflow: hidden; + // need this to be flex otherwise the overflow hidden from above + // sometimes vertically centers the clipped list ... no idea why it would do this + // as the box model should be top aligned. Happens in both FF and Chromium + display: flex; + flex-direction: column; + box-sizing: border-box; + + mask-image: linear-gradient(0deg, transparent, black 4px); + } + + .mx_LeftPanelWidget_resizerHandle { + cursor: ns-resize; + border-radius: 3px; + + // Override styles from library + width: unset !important; + height: 4px !important; + + position: absolute; + top: -24px !important; // override from library - puts it in the margin-top of the headerContainer + + // Together, these make the bar 64px wide + // These are also overridden from the library + left: calc(50% - 32px) !important; + right: calc(50% - 32px) !important; + } + + &:hover .mx_LeftPanelWidget_resizerHandle { + opacity: 0.8; + background-color: $primary-fg-color; + } + + .mx_LeftPanelWidget_maximizeButton { + margin-left: 8px; + margin-right: 7px; + position: relative; + width: 24px; + height: 24px; + border-radius: 32px; + + &::before { + content: ''; + width: 16px; + height: 16px; + position: absolute; + top: 4px; + left: 4px; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/feather-customised/maximise.svg'); + background: $muted-fg-color; + } + } +} + +.mx_LeftPanelWidget_maximizeButtonTooltip { + margin-top: -3px; +} diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss index 25e1153fce..8199121420 100644 --- a/res/css/structures/_MainSplit.scss +++ b/res/css/structures/_MainSplit.scss @@ -18,11 +18,27 @@ limitations under the License. display: flex; flex-direction: row; min-width: 0; + min-height: 0; height: 100%; } -// move hit area 5px to the right so it doesn't overlap with the timeline scrollbar -.mx_MainSplit > .mx_ResizeHandle.mx_ResizeHandle_horizontal { - margin: 0 -10px 0 0; - padding: 0 10px 0 0; +.mx_MainSplit > .mx_RightPanel_ResizeWrapper { + padding: 5px; + // margin left to not allow the handle to not encroach on the space for the scrollbar + margin-left: 8px; + height: calc(100vh - 51px); // height of .mx_RoomHeader.light-panel + + &:hover .mx_RightPanel_ResizeHandle { + // Need to use important to override element style attributes + // set by re-resizable + top: 50% !important; + transform: translate(0, -50%); + + height: 64px !important; // to match width of the ones on roomlist + width: 4px !important; + border-radius: 4px !important; + + background-color: $primary-fg-color; + opacity: 0.8; + } } diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index c5a5d50068..a220c5d505 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -41,10 +41,6 @@ limitations under the License. height: 40px; } -.mx_MatrixChat_toolbarShowing { - height: auto; -} - .mx_MatrixChat { width: 100%; height: 100%; @@ -70,15 +66,35 @@ limitations under the License. } /* not the left panel, and not the resize handle, so the roomview/groupview/... */ -.mx_MatrixChat > :not(.mx_LeftPanel_container):not(.mx_ResizeHandle) { +.mx_MatrixChat > :not(.mx_LeftPanel):not(.mx_SpacePanel):not(.mx_ResizeHandle) { background-color: $primary-bg-color; flex: 1 1 0; min-width: 0; - /* To fix https://github.com/vector-im/riot-web/issues/3298 where Safari + /* To fix https://github.com/vector-im/element-web/issues/3298 where Safari needed height 100% all the way down to the HomePage. Height does not have to be auto, empirically. */ height: 100%; } + +.mx_MatrixChat > .mx_ResizeHandle_horizontal:hover { + position: relative; + + &::before { + position: absolute; + left: 6px; + top: 50%; + transform: translate(0, -50%); + + height: 64px; // to match width of the ones on roomlist + width: 4px; + border-radius: 4px; + + content: ' '; + + background-color: $primary-fg-color; + opacity: 0.8; + } +} diff --git a/res/css/structures/_MyGroups.scss b/res/css/structures/_MyGroups.scss index 73f1332cd0..9c0062b72d 100644 --- a/res/css/structures/_MyGroups.scss +++ b/res/css/structures/_MyGroups.scss @@ -17,6 +17,11 @@ limitations under the License. .mx_MyGroups { display: flex; flex-direction: column; + + .mx_BetaCard { + margin: 0 72px; + max-width: 760px; + } } .mx_MyGroups .mx_RoomHeader_simpleHeader { @@ -30,7 +35,7 @@ limitations under the License. flex-wrap: wrap; } -.mx_MyGroups > :not(.mx_RoomHeader) { +.mx_MyGroups > :not(.mx_RoomHeader):not(.mx_BetaCard) { max-width: 960px; margin: 40px; } diff --git a/res/css/structures/_NonUrgentToastContainer.scss b/res/css/structures/_NonUrgentToastContainer.scss new file mode 100644 index 0000000000..826a812406 --- /dev/null +++ b/res/css/structures/_NonUrgentToastContainer.scss @@ -0,0 +1,35 @@ +/* +Copyright 2020 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_NonUrgentToastContainer { + position: absolute; + bottom: 30px; + left: 28px; + z-index: 101; // same level as other toasts + + .mx_NonUrgentToastContainer_toast { + padding: 10px 12px; + border-radius: 8px; + width: 320px; + font-size: $font-13px; + margin-top: 8px; + + // We don't use variables on the colours because we want it to be the same + // in all themes. + background-color: #17191c; + color: #fff; + } +} diff --git a/res/css/structures/_NotificationPanel.scss b/res/css/structures/_NotificationPanel.scss index 44205b1f01..e54feca175 100644 --- a/res/css/structures/_NotificationPanel.scss +++ b/res/css/structures/_NotificationPanel.scss @@ -22,7 +22,13 @@ limitations under the License. } .mx_NotificationPanel .mx_RoomView_messageListWrapper { - margin-right: 20px; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.mx_NotificationPanel .mx_RoomView_MessageList { + width: 100%; } .mx_NotificationPanel .mx_RoomView_MessageList h2 { @@ -35,11 +41,32 @@ limitations under the License. .mx_NotificationPanel .mx_EventTile { word-break: break-word; + position: relative; + padding-bottom: 18px; + + &:not(.mx_EventTile_last):not(.mx_EventTile_lastInSection)::after { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background-color: $tertiary-fg-color; + height: 1px; + opacity: 0.4; + content: ''; + } } .mx_NotificationPanel .mx_EventTile_roomName { font-weight: bold; font-size: $font-14px; + + > * { + vertical-align: middle; + } + + > .mx_BaseAvatar { + margin-right: 8px; + } } .mx_NotificationPanel .mx_EventTile_roomName a { @@ -47,8 +74,7 @@ limitations under the License. } .mx_NotificationPanel .mx_EventTile_avatar { - top: 8px; - left: 0px; + display: none; // we don't need this in this view } .mx_NotificationPanel .mx_EventTile .mx_SenderProfile, @@ -56,13 +82,15 @@ limitations under the License. color: $primary-fg-color; font-size: $font-12px; display: inline; - padding-left: 0px; } .mx_NotificationPanel .mx_EventTile_senderDetails { - padding-left: 32px; - padding-top: 8px; + padding-left: 36px; // align with the room name position: relative; + + a { + display: flex; + } } .mx_NotificationPanel .mx_EventTile_roomName a, @@ -74,11 +102,12 @@ limitations under the License. visibility: visible; position: initial; display: inline; + padding-left: 5px; } .mx_NotificationPanel .mx_EventTile_line { margin-right: 0px; - padding-left: 32px; + padding-left: 36px; // align with the room name padding-top: 0px; padding-bottom: 0px; padding-right: 0px; @@ -95,3 +124,7 @@ limitations under the License. .mx_NotificationPanel .mx_EventTile_content { margin-right: 0px; } + +.mx_NotificationPanel_empty::before { + mask-image: url('$(res)/img/element-icons/notifications.svg'); +} diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 10878322e3..3222fe936c 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -19,9 +19,17 @@ limitations under the License. overflow-x: hidden; flex: 0 0 auto; position: relative; - min-width: 264px; display: flex; flex-direction: column; + border-radius: 8px; + padding: 4px 0; + box-sizing: border-box; + height: 100%; + contain: strict; + + .mx_RoomView_MessageList { + padding: 14px 18px; // top and bottom is 4px smaller to balance with the padding set above + } } .mx_RightPanel_header { @@ -43,56 +51,128 @@ limitations under the License. .mx_RightPanel_headerButton { cursor: pointer; flex: 0 0 auto; - vertical-align: top; - margin-left: 5px; - margin-right: 5px; - text-align: center; - border-bottom: 2px solid transparent; - height: 20px; - width: 20px; + margin-left: 1px; + margin-right: 1px; + height: 32px; + width: 32px; position: relative; + border-radius: 100%; &::before { content: ''; position: absolute; - top: 0; - left: 0; - height: 20px; - width: 20px; - background-color: $rightpanel-button-color; + top: 4px; // center with parent of 32px + left: 4px; // center with parent of 32px + height: 24px; + width: 24px; + background-color: $icon-button-color; mask-repeat: no-repeat; mask-size: contain; } -} -.mx_RightPanel_membersButton::before { - mask-image: url('$(res)/img/feather-customised/user.svg'); -} + &:hover { + background: rgba($accent-color, 0.1); -.mx_RightPanel_filesButton::before { - mask-image: url('$(res)/img/feather-customised/files.svg'); + &::before { + background-color: $accent-color; + } + } } .mx_RightPanel_notifsButton::before { - mask-image: url('$(res)/img/feather-customised/notifications.svg'); + mask-image: url('$(res)/img/element-icons/notifications.svg'); + mask-position: center; +} + +.mx_RightPanel_roomSummaryButton::before { + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; } .mx_RightPanel_groupMembersButton::before { - mask-image: url('$(res)/img/icons-people.svg'); + mask-image: url('$(res)/img/element-icons/community-members.svg'); + mask-position: center; } .mx_RightPanel_roomsButton::before { - mask-image: url('$(res)/img/icons-room-nobg.svg'); + mask-image: url('$(res)/img/element-icons/community-rooms.svg'); + mask-position: center; } -.mx_RightPanel_headerButton_highlight::after { - content: ''; - position: absolute; - bottom: -6px; - left: 0; - right: 0; - height: 2px; - background-color: $button-bg-color; +$dot-size: 8px; +$pulse-color: $pinned-unread-color; + +.mx_RightPanel_pinnedMessagesButton { + &::before { + mask-image: url('$(res)/img/element-icons/room/pin.svg'); + mask-position: center; + } + + .mx_RightPanel_pinnedMessagesButton_unreadIndicator { + position: absolute; + right: 0; + top: 0; + margin: 4px; + width: $dot-size; + height: $dot-size; + border-radius: 50%; + transform: scale(1); + background: rgba($pulse-color, 1); + box-shadow: 0 0 0 0 rgba($pulse-color, 1); + animation: mx_RightPanel_indicator_pulse 2s infinite; + animation-iteration-count: 1; + + &::after { + content: ""; + position: absolute; + width: inherit; + height: inherit; + top: 0; + left: 0; + transform: scale(1); + transform-origin: center center; + animation-name: mx_RightPanel_indicator_pulse_shadow; + animation-duration: inherit; + animation-iteration-count: inherit; + border-radius: 50%; + background: rgba($pulse-color, 1); + } + } +} + +@keyframes mx_RightPanel_indicator_pulse { + 0% { + transform: scale(0.95); + } + + 70% { + transform: scale(1); + } + + 100% { + transform: scale(0.95); + } +} + +@keyframes mx_RightPanel_indicator_pulse_shadow { + 0% { + opacity: 0.7; + } + + 70% { + transform: scale(2.2); + opacity: 0; + } + + 100% { + opacity: 0; + } +} + +.mx_RightPanel_headerButton_highlight { + &::before { + background-color: $accent-color !important; + } } .mx_RightPanel_headerButton_badge { @@ -126,3 +206,45 @@ limitations under the License. order: 2; margin: auto; } + +.mx_RightPanel_empty { + margin-right: -28px; + + h2 { + font-weight: 700; + margin: 16px 0; + } + + h2, p { + font-size: $font-14px; + } + + &::before { + content: ''; + display: block; + margin: 11px auto 29px auto; + height: 42px; + width: 42px; + background-color: $rightpanel-button-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } +} + +.mx_RightPanel_scopeHeader { + margin: 24px; + text-align: center; + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + + .mx_BaseAvatar { + margin-right: 8px; + vertical-align: middle; + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } +} diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss index e0814182f5..ec07500af5 100644 --- a/res/css/structures/_RoomDirectory.scss +++ b/res/css/structures/_RoomDirectory.scss @@ -61,31 +61,59 @@ limitations under the License. .mx_RoomDirectory_tableWrapper { overflow-y: auto; flex: 1 1 0; + + .mx_RoomDirectory_footer { + margin-top: 24px; + text-align: center; + + > h5 { + margin: 0; + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + color: $primary-fg-color; + } + + > p { + margin: 40px auto 60px; + font-size: $font-14px; + line-height: $font-20px; + color: $secondary-fg-color; + max-width: 464px; // easier reading + } + + > hr { + margin: 0; + border: none; + height: 1px; + background-color: $header-panel-bg-color; + } + + .mx_RoomDirectory_newRoom { + margin: 24px auto 0; + width: max-content; + } + } } .mx_RoomDirectory_table { - font-size: $font-12px; color: $primary-fg-color; - width: 100%; + display: grid; + font-size: $font-12px; + grid-template-columns: max-content auto max-content max-content max-content; + row-gap: 24px; text-align: left; - table-layout: fixed; + width: 100%; } .mx_RoomDirectory_roomAvatar { - width: 32px; - padding-right: 14px; - vertical-align: top; -} - -.mx_RoomDirectory_roomDescription { - padding-bottom: 16px; + padding: 2px 14px 0 0; } .mx_RoomDirectory_roomMemberCount { + align-self: center; color: $light-fg-color; - width: 60px; - padding: 0 10px; - text-align: center; + padding: 3px 10px 0; &::before { background-color: $light-fg-color; @@ -105,8 +133,7 @@ limitations under the License. } .mx_RoomDirectory_join, .mx_RoomDirectory_preview { - width: 80px; - text-align: center; + align-self: center; white-space: nowrap; } @@ -133,6 +160,10 @@ limitations under the License. .mx_RoomDirectory_topic { cursor: initial; color: $light-fg-color; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; } .mx_RoomDirectory_alias { @@ -140,11 +171,6 @@ limitations under the License. color: $settings-grey-fg-color; } -.mx_RoomDirectory_table tr { - padding-bottom: 10px; - cursor: pointer; -} - .mx_RoomDirectory .mx_RoomView_MessageList { padding: 0; } diff --git a/res/css/structures/_RoomSearch.scss b/res/css/structures/_RoomSearch.scss new file mode 100644 index 0000000000..7fdafab5a6 --- /dev/null +++ b/res/css/structures/_RoomSearch.scss @@ -0,0 +1,94 @@ +/* +Copyright 2020 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. +*/ + +// Note: this component expects to be contained within a flexbox +.mx_RoomSearch { + flex: 1; + border-radius: 8px; + background-color: $roomlist-button-bg-color; + // keep border thickness consistent to prevent movement + border: 1px solid transparent; + height: 28px; + padding: 1px; + + // Create a flexbox for the icons (easier to manage) + display: flex; + align-items: center; + + .mx_RoomSearch_icon { + width: 16px; + height: 16px; + mask: url('$(res)/img/element-icons/roomlist/search.svg'); + mask-repeat: no-repeat; + background-color: $secondary-fg-color; + margin-left: 7px; + } + + .mx_RoomSearch_input { + border: none !important; // !important to override default app-wide styles + flex: 1 !important; // !important to override default app-wide styles + color: $primary-fg-color !important; // !important to override default app-wide styles + padding: 0; + height: 100%; + width: 100%; + font-size: $font-12px; + line-height: $font-16px; + + &:not(.mx_RoomSearch_inputExpanded)::placeholder { + color: $tertiary-fg-color !important; // !important to override default app-wide styles + } + } + + &.mx_RoomSearch_hasQuery { + border-color: $secondary-fg-color; + } + + &.mx_RoomSearch_focused { + box-shadow: 0 0 4px 4px rgba(0, 132, 255, 0.5); + border-color: transparent; + } + + &.mx_RoomSearch_focused, &.mx_RoomSearch_hasQuery { + background-color: $roomlist-filter-active-bg-color; + + .mx_RoomSearch_clearButton { + width: 16px; + height: 16px; + mask-image: url('$(res)/img/element-icons/roomlist/search-clear.svg'); + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background-color: $secondary-fg-color; + margin-right: 8px; + } + } + + .mx_RoomSearch_clearButton { + width: 0; + height: 0; + } + + &.mx_RoomSearch_minimized { + border-radius: 32px; + height: auto; + width: auto; + padding: 8px; + + .mx_RoomSearch_icon { + margin-left: 0; + } + } +} diff --git a/res/css/structures/_RoomStatusBar.scss b/res/css/structures/_RoomStatusBar.scss index cd4390ee5c..de9e049165 100644 --- a/res/css/structures/_RoomStatusBar.scss +++ b/res/css/structures/_RoomStatusBar.scss @@ -14,62 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_RoomStatusBar { +.mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) { margin-left: 65px; min-height: 50px; } -/* position the indicator in the same place horizontally as .mx_EventTile_avatar. */ -.mx_RoomStatusBar_indicator { - padding-left: 17px; - padding-right: 12px; - margin-left: -73px; - margin-top: 15px; - float: left; - width: 24px; - text-align: center; -} - -.mx_RoomStatusBar_callBar { - height: 50px; - line-height: $font-50px; -} - -.mx_RoomStatusBar_placeholderIndicator span { - color: $primary-fg-color; - opacity: 0.5; - position: relative; - top: -4px; - /* - animation-duration: 1s; - animation-name: bounce; - animation-direction: alternate; - animation-iteration-count: infinite; - */ -} - -.mx_RoomStatusBar_placeholderIndicator span:nth-child(1) { - animation-delay: 0.3s; -} -.mx_RoomStatusBar_placeholderIndicator span:nth-child(2) { - animation-delay: 0.6s; -} -.mx_RoomStatusBar_placeholderIndicator span:nth-child(3) { - animation-delay: 0.9s; -} - -@keyframes bounce { - from { - opacity: 0.5; - top: 0; - } - - to { - opacity: 0.2; - top: -3px; - } -} - .mx_RoomStatusBar_typingIndicatorAvatars { width: 52px; margin-top: -1px; @@ -119,6 +68,97 @@ limitations under the License. min-height: 58px; } +.mx_RoomStatusBar_unsentMessages { + > div[role="alert"] { + // cheat some basic alignment + display: flex; + align-items: center; + min-height: 70px; + margin: 12px; + padding-left: 16px; + background-color: $header-panel-bg-color; + border-radius: 4px; + } + + .mx_RoomStatusBar_unsentBadge { + margin-right: 12px; + + .mx_NotificationBadge { + // Override sizing from the default badge + width: 24px !important; + height: 24px !important; + border-radius: 24px !important; + + .mx_NotificationBadge_count { + font-size: $font-16px !important; // override default + } + } + } + + .mx_RoomStatusBar_unsentTitle { + color: $warning-color; + font-size: $font-15px; + } + + .mx_RoomStatusBar_unsentDescription { + font-size: $font-12px; + } + + .mx_RoomStatusBar_unsentButtonBar { + flex-grow: 1; + text-align: right; + margin-right: 22px; + color: $muted-fg-color; + + .mx_AccessibleButton { + padding: 5px 10px; + padding-left: 30px; // 18px for the icon, 2px margin to text, 10px regular padding + display: inline-block; + position: relative; + + &:nth-child(2) { + border-left: 1px solid $resend-button-divider-color; + } + + &::before { + content: ''; + position: absolute; + left: 10px; // inset for regular button padding + background-color: $muted-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + width: 18px; + height: 18px; + top: 50%; // text sizes are dynamic + transform: translateY(-50%); + } + + &.mx_RoomStatusBar_unsentCancelAllBtn::before { + mask-image: url('$(res)/img/element-icons/trashcan.svg'); + } + + &.mx_RoomStatusBar_unsentResendAllBtn { + padding-left: 34px; // 28px from above, but +6px to account for the wider icon + + &::before { + mask-image: url('$(res)/img/element-icons/retry.svg'); + } + } + } + + .mx_InlineSpinner { + vertical-align: middle; + margin-right: 5px; + top: 1px; // just to help the vertical alignment be slightly better + + & + span { + margin-right: 10px; // same margin/padding as the rightmost button + } + } + } +} + .mx_RoomStatusBar_connectionLostBar img { padding-left: 10px; padding-right: 10px; @@ -153,18 +193,8 @@ limitations under the License. display: block; } -.mx_RoomStatusBar_isAlone { - height: 50px; - line-height: $font-50px; - - color: $primary-fg-color; - opacity: 0.5; - overflow-y: hidden; - display: block; -} - .mx_MatrixChat_useCompactLayout { - .mx_RoomStatusBar { + .mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) { min-height: 40px; } @@ -172,11 +202,6 @@ limitations under the License. margin-top: 10px; } - .mx_RoomStatusBar_callBar { - height: 40px; - line-height: $font-40px; - } - .mx_RoomStatusBar_typingBar { height: 40px; line-height: $font-40px; diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss deleted file mode 100644 index 2e0c94263e..0000000000 --- a/res/css/structures/_RoomSubList.scss +++ /dev/null @@ -1,187 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/* a word of explanation about the flex-shrink values employed here: - there are 3 priotized categories of screen real-estate grabbing, - each with a flex-shrink difference of 4 order of magnitude, - so they ideally wouldn't affect each other. - lowest category: .mx_RoomSubList - flex-shrink: 10000000 - distribute size of items within the same category by their size - middle category: .mx_RoomSubList.resized-sized - flex-shrink: 1000 - applied when using the resizer, will have a max-height set to it, - to limit the size - highest category: .mx_RoomSubList.resized-all - flex-shrink: 1 - small flex-shrink value (1), is only added if you can drag the resizer so far - so in practice you can only assign this category if there is enough space. -*/ - -.mx_RoomSubList { - display: flex; - flex-direction: column; -} - - -.mx_RoomSubList_nonEmpty .mx_AutoHideScrollbar_offset { - padding-bottom: 4px; -} - -.mx_RoomSubList_labelContainer { - display: flex; - flex-direction: row; - align-items: center; - flex: 0 0 auto; - margin: 0 8px; - padding: 0 8px; - height: 36px; -} - -.mx_RoomSubList_labelContainer.focus-visible:focus-within { - background-color: $roomtile-focused-bg-color; -} - -.mx_RoomSubList_label { - flex: 1; - cursor: pointer; - display: flex; - align-items: center; - padding: 0 6px; -} - -.mx_RoomSubList_label > span { - flex: 1 1 auto; - text-transform: uppercase; - color: $roomsublist-label-fg-color; - font-weight: 700; - font-size: $font-12px; - margin-left: 8px; -} - -.mx_RoomSubList_badge > div { - flex: 0 0 auto; - border-radius: 8px; - font-weight: 600; - font-size: $font-12px; - padding: 0 5px; - color: $roomtile-badge-fg-color; - background-color: $roomtile-name-color; - cursor: pointer; -} - -.mx_RoomSubList_addRoom, .mx_RoomSubList_badge { - margin-left: 7px; -} - -.mx_RoomSubList_addRoom { - background-color: $roomheader-addroom-bg-color; - border-radius: 10px; // 16/2 + 2 padding - height: 16px; - flex: 0 0 16px; - position: relative; - - &::before { - background-color: $roomheader-addroom-fg-color; - mask: url('$(res)/img/icons-room-add.svg'); - mask-repeat: no-repeat; - mask-position: center; - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - } -} - -.mx_RoomSubList_badgeHighlight > div { - color: $accent-fg-color; - background-color: $warning-color; -} - -.mx_RoomSubList_chevron { - pointer-events: none; - mask: url('$(res)/img/feather-customised/dropdown-arrow.svg'); - mask-repeat: no-repeat; - transition: transform 0.2s ease-in; - width: 10px; - height: 6px; - margin-left: 2px; - background-color: $roomsublist-label-fg-color; -} - -.mx_RoomSubList_chevronDown { - transform: rotateZ(0deg); -} - -.mx_RoomSubList_chevronUp { - transform: rotateZ(180deg); -} - -.mx_RoomSubList_chevronRight { - transform: rotateZ(-90deg); -} - -.mx_RoomSubList_scroll { - /* let rooms list grab as much space as it needs (auto), - potentially overflowing and showing a scrollbar */ - flex: 0 1 auto; - padding: 0 8px; -} - -.collapsed { - .mx_RoomSubList_scroll { - padding: 0; - } - - .mx_RoomSubList_labelContainer { - margin-right: 8px; - margin-left: 2px; - padding: 0; - } - - .mx_RoomSubList_addRoom { - margin-left: 3px; - margin-right: 10px; - } - - .mx_RoomSubList_label > span { - display: none; - } -} - -// overflow indicators -.mx_RoomSubList:not(.resized-all) > .mx_RoomSubList_scroll { - &.mx_IndicatorScrollbar_topOverflow::before { - position: sticky; - content: ""; - top: 0; - left: 0; - right: 0; - height: 8px; - z-index: 100; - display: block; - pointer-events: none; - transition: background-image 0.1s ease-in; - background: linear-gradient(to top, $panel-gradient); - } - - - &.mx_IndicatorScrollbar_topOverflow { - margin-top: -8px; - } -} diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index f2154ef448..831f186ed4 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -20,35 +20,55 @@ limitations under the License. flex-direction: column; } + +@keyframes mx_RoomView_fileDropTarget_animation { + from { + opacity: 0; + } + to { + opacity: 0.95; + } +} + .mx_RoomView_fileDropTarget { min-width: 0px; width: 100%; + height: 100%; + font-size: $font-18px; text-align: center; pointer-events: none; - padding-left: 12px; - padding-right: 12px; - margin-left: -12px; + background-color: $primary-bg-color; + opacity: 0.95; - border-top-left-radius: 10px; - border-top-right-radius: 10px; - - background-color: $droptarget-bg-color; - border: 2px #e1dddd solid; - border-bottom: none; position: absolute; - top: 52px; - bottom: 0px; z-index: 3000; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + animation: mx_RoomView_fileDropTarget_animation; + animation-duration: 0.5s; } -.mx_RoomView_fileDropTargetLabel { - top: 50%; - width: 100%; - margin-top: -50px; - position: absolute; +@keyframes mx_RoomView_fileDropTarget_image_animation { + from { + transform: scaleX(0); + } + to { + transform: scaleX(1); + } +} + +.mx_RoomView_fileDropTarget_image { + width: 32px; + animation: mx_RoomView_fileDropTarget_image_animation; + animation-duration: 0.5s; + margin-bottom: 16px; } .mx_RoomView_auxPanel { @@ -117,7 +137,6 @@ limitations under the License. } .mx_RoomView_body { - position: relative; //for .mx_RoomView_auxPanel_fullHeight display: flex; flex-direction: column; flex: 1; @@ -134,6 +153,7 @@ limitations under the License. flex: 1; display: flex; flex-direction: column; + contain: content; } .mx_RoomView_statusArea { @@ -185,13 +205,11 @@ limitations under the License. } .mx_RoomView_empty { - flex: 1 1 auto; font-size: $font-13px; - padding-left: 3em; - padding-right: 3em; - margin-right: 20px; - margin-top: 33%; + padding: 0 24px; + margin-right: 30px; text-align: center; + margin-bottom: 80px; // visually center the content (intentional offset) } .mx_RoomView_MessageList { @@ -221,7 +239,8 @@ hr.mx_RoomView_myReadMarker { position: relative; top: -1px; z-index: 1; - transition: width 400ms easeInSine 1s, opacity 400ms easeInSine 1s; + will-change: width; + transition: width 400ms easeinsine 1s, opacity 400ms easeinsine 1s; width: 99%; opacity: 1; } @@ -246,12 +265,6 @@ hr.mx_RoomView_myReadMarker { padding-top: 1px; } -.mx_RoomView_inCall .mx_RoomView_statusAreaBox { - background-color: $accent-color; - color: $accent-fg-color; - position: relative; -} - .mx_RoomView_voipChevron { position: absolute; bottom: -11px; @@ -261,7 +274,7 @@ hr.mx_RoomView_myReadMarker { .mx_RoomView_voipButton { float: right; margin-right: 13px; - margin-top: 10px; + margin-top: 13px; cursor: pointer; } diff --git a/res/css/structures/_ScrollPanel.scss b/res/css/structures/_ScrollPanel.scss index 699224949b..7b75c69e86 100644 --- a/res/css/structures/_ScrollPanel.scss +++ b/res/css/structures/_ScrollPanel.scss @@ -21,6 +21,8 @@ limitations under the License. display: flex; flex-direction: column; justify-content: flex-end; - overflow-y: hidden; + + content-visibility: auto; + contain-intrinsic-size: 50px; } } diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss new file mode 100644 index 0000000000..e64057d16c --- /dev/null +++ b/res/css/structures/_SpacePanel.scss @@ -0,0 +1,376 @@ +/* +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. +*/ + +$topLevelHeight: 32px; +$nestedHeight: 24px; +$gutterSize: 16px; +$activeBorderTransparentGap: 1px; + +$activeBackgroundColor: $roomtile-selected-bg-color; +$activeBorderColor: $secondary-fg-color; + +.mx_SpacePanel { + flex: 0 0 auto; + background-color: $groupFilterPanel-bg-color; + padding: 0; + margin: 0; + + // Create another flexbox so the Panel fills the container + display: flex; + flex-direction: column; + + .mx_SpacePanel_spaceTreeWrapper { + flex: 1; + padding: 8px 8px 16px 0; + } + + .mx_SpacePanel_toggleCollapse { + flex: 0 0 auto; + width: 40px; + height: 40px; + mask-position: center; + mask-size: 32px; + mask-repeat: no-repeat; + margin-left: $gutterSize; + margin-bottom: 12px; + background-color: $roomlist-header-color; + mask-image: url('$(res)/img/element-icons/expand-space-panel.svg'); + + &.expanded { + transform: scaleX(-1); + } + } + + ul { + margin: 0; + list-style: none; + padding: 0; + + > .mx_SpaceItem { + padding-left: 16px; + } + } + + .mx_SpaceButton_toggleCollapse { + cursor: pointer; + } + + .mx_SpaceItem_dragging { + .mx_SpaceButton_toggleCollapse { + visibility: hidden; + } + } + + .mx_SpaceTreeLevel { + display: flex; + flex-direction: column; + max-width: 250px; + flex-grow: 1; + } + + .mx_SpaceItem { + display: inline-flex; + flex-flow: wrap; + + &.mx_SpaceItem_narrow { + align-self: baseline; + } + } + + .mx_SpaceItem.collapsed { + & > .mx_SpaceButton > .mx_SpaceButton_toggleCollapse { + transform: rotate(-90deg); + } + + & > .mx_SpaceTreeLevel { + display: none; + } + } + + .mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton { + margin-left: $gutterSize; + min-width: 40px; + } + + .mx_SpaceButton { + border-radius: 8px; + display: flex; + align-items: center; + padding: 4px 4px 4px 0; + width: 100%; + + &.mx_SpaceButton_active { + &:not(.mx_SpaceButton_narrow) .mx_SpaceButton_selectionWrapper { + background-color: $activeBackgroundColor; + } + + &.mx_SpaceButton_narrow .mx_SpaceButton_selectionWrapper { + padding: $activeBorderTransparentGap; + border: 3px $activeBorderColor solid; + } + } + + .mx_SpaceButton_selectionWrapper { + position: relative; + display: flex; + flex: 1; + align-items: center; + border-radius: 12px; + padding: 4px; + } + + &:not(.mx_SpaceButton_narrow) { + .mx_SpaceButton_selectionWrapper { + width: 100%; + padding-right: 16px; + overflow: hidden; + } + } + + .mx_SpaceButton_name { + flex: 1; + margin-left: 8px; + white-space: nowrap; + display: block; + text-overflow: ellipsis; + overflow: hidden; + padding-right: 8px; + font-size: $font-14px; + line-height: $font-18px; + } + + .mx_SpaceButton_toggleCollapse { + width: $gutterSize; + height: 20px; + mask-position: center; + mask-size: 20px; + mask-repeat: no-repeat; + background-color: $roomlist-header-color; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + } + + .mx_SpaceButton_icon { + width: $topLevelHeight; + min-width: $topLevelHeight; + height: $topLevelHeight; + border-radius: 8px; + position: relative; + + &::before { + position: absolute; + content: ''; + width: $topLevelHeight; + height: $topLevelHeight; + top: 0; + left: 0; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 18px; + } + } + + &.mx_SpaceButton_home .mx_SpaceButton_icon { + background-color: #ffffff; + + &::before { + background-color: #3f3d3d; + mask-image: url('$(res)/img/element-icons/home.svg'); + } + } + + &.mx_SpaceButton_new .mx_SpaceButton_icon { + background-color: $accent-color; + transition: all .1s ease-in-out; // TODO transition + + &::before { + background-color: #ffffff; + mask-image: url('$(res)/img/element-icons/plus.svg'); + transition: all .2s ease-in-out; // TODO transition + } + } + + &.mx_SpaceButton_newCancel .mx_SpaceButton_icon { + background-color: $icon-button-color; + + &::before { + transform: rotate(45deg); + } + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } + + .mx_SpaceButton_menuButton { + width: 20px; + min-width: 20px; // yay flex + height: 20px; + margin-top: auto; + margin-bottom: auto; + display: none; + position: absolute; + right: 4px; + + &::before { + top: 2px; + left: 2px; + content: ''; + width: 16px; + height: 16px; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/context-menu.svg'); + background: $primary-fg-color; + } + } + } + + .mx_SpacePanel_badgeContainer { + position: absolute; + + // Create a flexbox to make aligning dot badges easier + display: flex; + align-items: center; + + .mx_NotificationBadge { + margin: 0 2px; // centering + } + + .mx_NotificationBadge_dot { + // make the smaller dot occupy the same width for centering + margin: 0 7px; + } + } + + &.collapsed { + .mx_SpaceButton { + .mx_SpacePanel_badgeContainer { + right: 0; + top: 0; + + .mx_NotificationBadge { + background-clip: padding-box; + } + + .mx_NotificationBadge_dot { + margin: 0 -1px 0 0; + border: 3px solid $groupFilterPanel-bg-color; + } + + .mx_NotificationBadge_2char, + .mx_NotificationBadge_3char { + margin: -5px -5px 0 0; + border: 2px solid $groupFilterPanel-bg-color; + } + } + + &.mx_SpaceButton_active .mx_SpacePanel_badgeContainer { + // when we draw the selection border we move the relative bounds of our parent + // so update our position within the bounds of the parent to maintain position overall + right: -3px; + top: -3px; + } + } + } + + &:not(.collapsed) { + .mx_SpacePanel_badgeContainer { + position: absolute; + right: 4px; + } + + .mx_SpaceButton:hover, + .mx_SpaceButton:focus-within, + .mx_SpaceButton_hasMenuOpen { + &:not(.mx_SpaceButton_home):not(.mx_SpaceButton_invite) { + // Hide the badge container on hover because it'll be a menu button + .mx_SpacePanel_badgeContainer { + width: 0; + height: 0; + display: none; + } + + .mx_SpaceButton_menuButton { + display: block; + } + } + } + } + + /* root space buttons are bigger and not indented */ + & > .mx_AutoHideScrollbar { + & > .mx_SpaceButton { + height: $topLevelHeight; + + &.mx_SpaceButton_active::before { + height: $topLevelHeight; + } + } + + & > ul { + padding-left: 0; + } + } +} + +.mx_SpacePanel_contextMenu { + .mx_SpacePanel_contextMenu_header { + margin: 12px 16px 12px; + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + } + + .mx_IconizedContextMenu_optionList .mx_AccessibleButton.mx_SpacePanel_contextMenu_inviteButton { + color: $accent-color; + + .mx_SpacePanel_iconInvite::before { + background-color: $accent-color; + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } + } + + .mx_SpacePanel_iconSettings::before { + mask-image: url('$(res)/img/element-icons/settings.svg'); + } + + .mx_SpacePanel_iconLeave::before { + mask-image: url('$(res)/img/element-icons/leave.svg'); + } + + .mx_SpacePanel_iconMembers::before { + mask-image: url('$(res)/img/element-icons/room/members.svg'); + } + + .mx_SpacePanel_iconPlus::before { + mask-image: url('$(res)/img/element-icons/roomlist/plus-circle.svg'); + } + + .mx_SpacePanel_iconHash::before { + mask-image: url('$(res)/img/element-icons/roomlist/hash-circle.svg'); + } + + .mx_SpacePanel_iconExplore::before { + mask-image: url('$(res)/img/element-icons/roomlist/browse.svg'); + } +} + + +.mx_SpacePanel_sharePublicSpace { + margin: 0; +} diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss new file mode 100644 index 0000000000..7925686bf1 --- /dev/null +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -0,0 +1,315 @@ +/* +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_SpaceRoomDirectory_dialogWrapper > .mx_Dialog { + max-width: 960px; + height: 100%; +} + +.mx_SpaceRoomDirectory { + height: 100%; + margin-bottom: 12px; + color: $primary-fg-color; + word-break: break-word; + display: flex; + flex-direction: column; +} + +.mx_SpaceRoomDirectory, +.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-fg-color; + font-size: $font-15px; + line-height: $font-24px; + } + } + } + + .mx_AccessibleButton_kind_link { + padding: 0; + } + + .mx_SearchBox { + margin: 24px 0 16px; + } + + .mx_SpaceRoomDirectory_noResults { + text-align: center; + + > div { + font-size: $font-15px; + line-height: $font-24px; + color: $secondary-fg-color; + } + } + + .mx_SpaceRoomDirectory_listHeader { + display: flex; + min-height: 32px; + align-items: center; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; + + .mx_AccessibleButton { + padding: 4px 12px; + font-weight: normal; + + & + .mx_AccessibleButton { + margin-left: 16px; + } + } + + .mx_AccessibleButton_kind_danger_outline, + .mx_AccessibleButton_kind_primary_outline { + padding: 3px 12px; // to account for the 1px border + } + + > span { + margin-left: auto; + } + } + + .mx_SpaceRoomDirectory_error { + position: relative; + font-weight: $font-semi-bold; + color: $notice-primary-color; + font-size: $font-15px; + line-height: $font-18px; + margin: 20px auto 12px; + padding-left: 24px; + width: max-content; + + &::before { + content: ""; + position: absolute; + height: 16px; + width: 16px; + left: 0; + background-image: url("$(res)/img/element-icons/warning-badge.svg"); + } + } +} + +.mx_SpaceRoomDirectory_list { + margin-top: 16px; + padding-bottom: 40px; + + .mx_SpaceRoomDirectory_roomCount { + > h3 { + display: inline; + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + color: $primary-fg-color; + } + + > span { + margin-left: 8px; + font-size: $font-15px; + line-height: $font-24px; + color: $secondary-fg-color; + } + } + + .mx_SpaceRoomDirectory_subspace { + .mx_BaseAvatar_image { + border-radius: 8px; + } + } + + .mx_SpaceRoomDirectory_subspace_toggle { + position: absolute; + left: -1px; + top: 10px; + height: 16px; + width: 16px; + border-radius: 4px; + background-color: $primary-bg-color; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + height: 16px; + width: 16px; + mask-repeat: no-repeat; + mask-position: center; + background-color: $tertiary-fg-color; + mask-size: 16px; + transform: rotate(270deg); + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + } + + &.mx_SpaceRoomDirectory_subspace_toggle_shown::before { + transform: rotate(0deg); + } + } + + .mx_SpaceRoomDirectory_subspace_children { + position: relative; + padding-left: 12px; + } + + .mx_SpaceRoomDirectory_roomTile { + position: relative; + padding: 8px 16px; + border-radius: 8px; + min-height: 56px; + box-sizing: border-box; + + display: grid; + grid-template-columns: 20px auto max-content; + grid-column-gap: 8px; + grid-row-gap: 6px; + align-items: center; + + .mx_BaseAvatar { + grid-row: 1; + grid-column: 1; + } + + .mx_SpaceRoomDirectory_roomTile_name { + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + grid-row: 1; + grid-column: 2; + + .mx_InfoTooltip { + display: inline; + margin-left: 12px; + color: $tertiary-fg-color; + font-size: $font-12px; + line-height: $font-15px; + + .mx_InfoTooltip_icon { + margin-right: 4px; + position: relative; + vertical-align: text-top; + + &::before { + position: absolute; + top: 0; + left: 0; + } + } + } + } + + .mx_SpaceRoomDirectory_roomTile_info { + font-size: $font-14px; + line-height: $font-18px; + color: $secondary-fg-color; + grid-row: 2; + grid-column: 1/3; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + } + + .mx_SpaceRoomDirectory_actions { + text-align: right; + margin-left: 20px; + grid-column: 3; + grid-row: 1/3; + + .mx_AccessibleButton { + line-height: $font-24px; + padding: 4px 16px; + display: inline-block; + visibility: hidden; + } + + .mx_AccessibleButton_kind_danger_outline, + .mx_AccessibleButton_kind_primary_outline { + padding: 3px 16px; // to account for the 1px border + } + + .mx_Checkbox { + display: inline-flex; + vertical-align: middle; + margin-left: 12px; + } + } + + &:hover { + background-color: $groupFilterPanel-bg-color; + + .mx_AccessibleButton { + visibility: visible; + } + } + } + + .mx_SpaceRoomDirectory_roomTile, + .mx_SpaceRoomDirectory_subspace_children { + &::before { + content: ""; + position: absolute; + background-color: $groupFilterPanel-bg-color; + width: 1px; + height: 100%; + left: 6px; + top: 0; + } + } + + .mx_SpaceRoomDirectory_actions { + .mx_SpaceRoomDirectory_actionsText { + font-weight: normal; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + } + } + + > hr { + border: none; + height: 1px; + background-color: rgba(141, 151, 165, 0.2); + margin: 20px 0; + } + + .mx_SpaceRoomDirectory_createRoom { + display: block; + margin: 16px auto 0; + width: max-content; + } +} diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss new file mode 100644 index 0000000000..48b565be7f --- /dev/null +++ b/res/css/structures/_SpaceRoomView.scss @@ -0,0 +1,569 @@ +/* +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. +*/ + +$SpaceRoomViewInnerWidth: 428px; + +@define-mixin SpacePillButton { + position: relative; + padding: 16px 32px 16px 72px; + width: 432px; + box-sizing: border-box; + border-radius: 8px; + border: 1px solid $input-border-color; + font-size: $font-15px; + margin: 20px 0; + + > h3 { + font-weight: $font-semi-bold; + margin: 0 0 4px; + } + + > span { + color: $secondary-fg-color; + } + + &::before { + position: absolute; + content: ''; + width: 32px; + height: 32px; + top: 24px; + left: 20px; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 24px; + background-color: $tertiary-fg-color; + } + + &:hover { + border-color: $accent-color; + + &::before { + background-color: $accent-color; + } + + > span { + color: $primary-fg-color; + } + } +} + +.mx_SpaceRoomView { + .mx_MainSplit > div:first-child { + padding: 80px 60px; + flex-grow: 1; + max-height: 100%; + overflow-y: auto; + + h1 { + margin: 0; + font-size: $font-24px; + font-weight: $font-semi-bold; + color: $primary-fg-color; + width: max-content; + } + + .mx_SpaceRoomView_description { + font-size: $font-15px; + color: $secondary-fg-color; + margin-top: 12px; + margin-bottom: 24px; + max-width: $SpaceRoomViewInnerWidth; + } + + .mx_AddExistingToSpace { + max-width: $SpaceRoomViewInnerWidth; + + .mx_AddExistingToSpace_content { + height: calc(100vh - 360px); + max-height: 400px; + } + } + + &:not(.mx_SpaceRoomView_landing) .mx_SpaceFeedbackPrompt { + width: $SpaceRoomViewInnerWidth; + } + + .mx_SpaceRoomView_buttons { + display: block; + margin-top: 44px; + width: $SpaceRoomViewInnerWidth; + text-align: right; // button alignment right + + .mx_AccessibleButton_hasKind { + padding: 8px 22px; + margin-left: 16px; + } + + input.mx_AccessibleButton { + border: none; // override default styles + } + } + + .mx_Field { + max-width: $SpaceRoomViewInnerWidth; + + & + .mx_Field { + margin-top: 28px; + } + } + + .mx_SpaceRoomView_errorText { + font-weight: $font-semi-bold; + font-size: $font-12px; + line-height: $font-15px; + color: $notice-primary-color; + margin-bottom: 28px; + } + + .mx_AccessibleButton_disabled { + cursor: not-allowed; + } + } + + .mx_SpaceRoomView_preview { + padding: 32px 24px !important; // override default padding from above + margin: auto; + max-width: 480px; + box-sizing: border-box; + box-shadow: 2px 15px 30px $dialog-shadow-color; + border-radius: 8px; + position: relative; + + // XXX remove this when spaces leaves Beta + .mx_BetaCard_betaPill { + position: absolute; + right: 24px; + top: 32px; + } + // XXX remove this when spaces leaves Beta + .mx_SpaceRoomView_preview_spaceBetaPrompt { + font-weight: $font-semi-bold; + font-size: $font-14px; + line-height: $font-24px; + color: $primary-fg-color; + margin-top: 24px; + position: relative; + padding-left: 24px; + + .mx_AccessibleButton_kind_link { + display: inline; + padding: 0; + font-size: inherit; + line-height: inherit; + } + + &::before { + content: ""; + position: absolute; + height: $font-24px; + width: 20px; + left: 0; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + background-color: $secondary-fg-color; + } + } + + .mx_SpaceRoomView_preview_inviter { + display: flex; + align-items: center; + margin-bottom: 20px; + font-size: $font-15px; + + > div { + margin-left: 8px; + + .mx_SpaceRoomView_preview_inviter_name { + line-height: $font-18px; + } + + .mx_SpaceRoomView_preview_inviter_mxid { + line-height: $font-24px; + color: $secondary-fg-color; + } + } + } + + > .mx_BaseAvatar_image, + > .mx_BaseAvatar > .mx_BaseAvatar_image { + border-radius: 12px; + } + + h1.mx_SpaceRoomView_preview_name { + margin: 20px 0 !important; // override default margin from above + } + + .mx_SpaceRoomView_preview_topic { + font-size: $font-14px; + line-height: $font-22px; + color: $secondary-fg-color; + margin: 20px 0; + max-height: 160px; + overflow-y: auto; + } + + .mx_SpaceRoomView_preview_joinButtons { + margin-top: 20px; + + .mx_AccessibleButton { + width: 200px; + box-sizing: border-box; + padding: 14px 0; + + & + .mx_AccessibleButton { + margin-left: 20px; + } + } + } + } + + .mx_SpaceRoomView_landing { + > .mx_BaseAvatar_image, + > .mx_BaseAvatar > .mx_BaseAvatar_image { + border-radius: 12px; + } + + .mx_SpaceRoomView_landing_name { + margin: 24px 0 16px; + font-size: $font-15px; + color: $secondary-fg-color; + + > span { + display: inline-block; + } + + .mx_SpaceRoomView_landing_nameRow { + margin-top: 12px; + + > h1 { + display: inline-block; + } + } + + .mx_SpaceRoomView_landing_inviter { + .mx_BaseAvatar { + margin-right: 4px; + vertical-align: middle; + } + } + } + + .mx_SpaceRoomView_landing_info { + display: flex; + align-items: center; + + .mx_SpaceRoomView_info { + display: inline-block; + margin: 0 auto 0 0; + } + + .mx_FacePile { + display: inline-block; + margin-right: 12px; + + .mx_FacePile_faces { + cursor: pointer; + } + } + + .mx_SpaceRoomView_landing_inviteButton { + position: relative; + padding: 4px 18px 4px 40px; + line-height: $font-24px; + height: min-content; + + &::before { + position: absolute; + content: ""; + left: 8px; + height: 16px; + width: 16px; + background: #ffffff; // white icon fill + mask-position: center; + mask-size: 16px; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } + } + + .mx_SpaceRoomView_landing_settingsButton { + position: relative; + margin-left: 16px; + width: 24px; + height: 24px; + + &::before { + position: absolute; + content: ""; + left: 0; + top: 0; + height: 24px; + width: 24px; + background: $tertiary-fg-color; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/settings.svg'); + } + } + } + + .mx_SpaceRoomView_landing_topic { + font-size: $font-15px; + margin-top: 12px; + margin-bottom: 16px; + white-space: pre-wrap; + word-wrap: break-word; + } + + > hr { + border: none; + height: 1px; + background-color: $groupFilterPanel-bg-color; + } + + .mx_SearchBox { + margin: 0 0 20px; + } + + .mx_SpaceFeedbackPrompt { + margin-bottom: 16px; + + // hide the HR as we have our own + & + hr { + display: none; + } + } + } + + .mx_SpaceRoomView_privateScope { + > .mx_AccessibleButton { + @mixin SpacePillButton; + } + + .mx_SpaceRoomView_privateScope_justMeButton::before { + mask-image: url('$(res)/img/element-icons/room/members.svg'); + } + + .mx_SpaceRoomView_privateScope_meAndMyTeammatesButton::before { + mask-image: url('$(res)/img/element-icons/community-members.svg'); + } + } + + .mx_SpaceRoomView_betaWarning { + padding: 12px 12px 12px 54px; + position: relative; + font-size: $font-15px; + line-height: $font-24px; + width: 432px; + border-radius: 8px; + background-color: $info-plinth-bg-color; + color: $secondary-fg-color; + box-sizing: border-box; + + > h3 { + font-weight: $font-semi-bold; + font-size: inherit; + line-height: inherit; + margin: 0; + } + + > p { + font-size: inherit; + line-height: inherit; + margin: 0; + } + + &::before { + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + content: ''; + width: 20px; + height: 20px; + position: absolute; + top: 14px; + left: 14px; + background-color: $secondary-fg-color; + } + } + + .mx_SpaceRoomView_inviteTeammates { + // XXX remove this when spaces leaves Beta + .mx_SpaceRoomView_inviteTeammates_betaDisclaimer { + padding: 58px 16px 16px; + position: relative; + border-radius: 8px; + background-color: $header-panel-bg-color; + max-width: $SpaceRoomViewInnerWidth; + margin: 20px 0 30px; + box-sizing: border-box; + + .mx_BetaCard_betaPill { + position: absolute; + left: 16px; + top: 16px; + } + } + + .mx_SpaceRoomView_inviteTeammates_buttons { + color: $secondary-fg-color; + margin-top: 28px; + + .mx_AccessibleButton { + position: relative; + display: inline-block; + padding-left: 32px; + line-height: 24px; // to center icons + + &::before { + content: ""; + position: absolute; + height: 24px; + width: 24px; + top: 0; + left: 0; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + } + + & + .mx_AccessibleButton { + margin-left: 32px; + } + } + + .mx_SpaceRoomView_inviteTeammates_inviteDialogButton::before { + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } + } + } +} + +.mx_SpaceRoomView_info { + color: $secondary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + margin: 20px 0; + + .mx_SpaceRoomView_info_public, + .mx_SpaceRoomView_info_private { + padding-left: 20px; + position: relative; + + &::before { + position: absolute; + content: ""; + width: 20px; + height: 20px; + top: 0; + left: -2px; + mask-position: center; + mask-repeat: no-repeat; + background-color: $tertiary-fg-color; + } + } + + .mx_SpaceRoomView_info_public::before { + mask-size: 12px; + mask-image: url("$(res)/img/globe.svg"); + } + + .mx_SpaceRoomView_info_private::before { + mask-size: 14px; + mask-image: url("$(res)/img/element-icons/lock.svg"); + } + + .mx_AccessibleButton_kind_link { + color: inherit; + position: relative; + padding-left: 16px; + + &::before { + content: "·"; // visual separator + position: absolute; + left: 6px; + } + } +} + +.mx_SpaceFeedbackPrompt { + margin-top: 18px; + margin-bottom: 12px; + + > hr { + border: none; + border-top: 1px solid $input-border-color; + margin-bottom: 12px; + } + + > div { + display: flex; + flex-direction: row; + font-size: $font-15px; + line-height: $font-24px; + + > span { + color: $secondary-fg-color; + position: relative; + padding-left: 32px; + font-size: inherit; + line-height: inherit; + margin-right: auto; + + &::before { + content: ''; + position: absolute; + left: 0; + top: 2px; + height: 20px; + width: 20px; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; + } + } + + .mx_AccessibleButton_kind_link { + color: $accent-color; + position: relative; + padding: 0 0 0 24px; + margin-left: 8px; + font-size: inherit; + line-height: inherit; + + &::before { + content: ''; + position: absolute; + left: 0; + height: 16px; + width: 16px; + background-color: $accent-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/chat-bubbles.svg'); + mask-position: center; + } + } + } +} diff --git a/res/css/structures/_TabbedView.scss b/res/css/structures/_TabbedView.scss index 4a4bb125a3..833450a25b 100644 --- a/res/css/structures/_TabbedView.scss +++ b/res/css/structures/_TabbedView.scss @@ -1,6 +1,7 @@ /* Copyright 2017 Travis Ralston Copyright 2019 New Vector Ltd +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. @@ -17,62 +18,126 @@ limitations under the License. .mx_TabbedView { margin: 0; - padding: 0 0 0 58px; + padding: 0 0 0 16px; display: flex; flex-direction: column; - position: absolute; top: 0; bottom: 0; left: 0; right: 0; + margin-top: 8px; +} + +.mx_TabbedView_tabsOnLeft { + flex-direction: column; + position: absolute; + + .mx_TabbedView_tabLabels { + width: 170px; + max-width: 170px; + position: fixed; + } + + .mx_TabbedView_tabPanel { + margin-left: 240px; // 170px sidebar + 70px padding + flex-direction: column; + } + + .mx_TabbedView_tabLabel_active { + background-color: $tab-label-active-bg-color; + color: $tab-label-active-fg-color; + } + + .mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before { + background-color: $tab-label-active-icon-bg-color; + } + + .mx_TabbedView_maskedIcon { + width: 16px; + height: 16px; + margin-left: 8px; + margin-right: 16px; + } + + .mx_TabbedView_maskedIcon::before { + mask-size: 16px; + width: 16px; + height: 16px; + } +} + +.mx_TabbedView_tabsOnTop { + flex-direction: column; + + .mx_TabbedView_tabLabels { + display: flex; + margin-bottom: 8px; + } + + .mx_TabbedView_tabLabel { + padding-left: 0px; + padding-right: 52px; + + .mx_TabbedView_tabLabel_text { + font-size: 15px; + color: $tertiary-fg-color; + } + } + + .mx_TabbedView_tabPanel { + flex-direction: row; + } + + .mx_TabbedView_tabLabel_active { + color: $accent-color; + .mx_TabbedView_tabLabel_text { + color: $accent-color; + } + } + + .mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before { + background-color: $accent-color; + } + + .mx_TabbedView_maskedIcon { + width: 22px; + height: 22px; + margin-left: 0px; + margin-right: 8px; + } + + .mx_TabbedView_maskedIcon::before { + mask-size: 22px; + width: inherit; + height: inherit; + } } .mx_TabbedView_tabLabels { - width: 170px; - max-width: 170px; color: $tab-label-fg-color; - position: fixed; } .mx_TabbedView_tabLabel { + display: flex; + align-items: center; vertical-align: text-top; cursor: pointer; - display: block; - border-radius: 3px; - font-size: $font-14px; - min-height: 24px; // use min-height instead of height to allow the label to overflow a bit - margin-bottom: 6px; + padding: 8px 0; + border-radius: 8px; + font-size: $font-13px; position: relative; } -.mx_TabbedView_tabLabel_active { - background-color: $tab-label-active-bg-color; - color: $tab-label-active-fg-color; -} - .mx_TabbedView_maskedIcon { - margin-left: 6px; - margin-right: 9px; - margin-top: 1px; - width: 16px; - height: 16px; display: inline-block; } .mx_TabbedView_maskedIcon::before { display: inline-block; - background-color: $tab-label-icon-bg-color; + background-color: $icon-button-color; mask-repeat: no-repeat; - mask-size: 16px; - width: 16px; - height: 22px; mask-position: center; content: ''; - vertical-align: middle; -} - -.mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before { - background-color: $tab-label-active-icon-bg-color; } .mx_TabbedView_tabLabel_text { @@ -80,10 +145,8 @@ limitations under the License. } .mx_TabbedView_tabPanel { - margin-left: 240px; // 170px sidebar + 70px padding flex-grow: 1; display: flex; - flex-direction: column; min-height: 0; // firefox } diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss deleted file mode 100644 index 0065ffa502..0000000000 --- a/res/css/structures/_TagPanel.scss +++ /dev/null @@ -1,167 +0,0 @@ -/* -Copyright 2017 New Vector Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_TagPanel { - flex: 1; - background-color: $tagpanel-bg-color; - cursor: pointer; - - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; - min-height: 0; -} - -.mx_TagPanel_items_selected { - cursor: pointer; -} - -.mx_TagPanel .mx_TagPanel_clearButton_container { - /* Constant height within flex mx_TagPanel */ - height: 70px; - width: 60px; - - flex: none; - - justify-content: center; - align-items: flex-start; - - display: none; -} - -.mx_TagPanel .mx_TagPanel_clearButton object { - /* Same as .mx_SearchBox padding-top */ - margin-top: 24px; - pointer-events: none; -} - -.mx_TagPanel .mx_TagPanel_divider { - height: 0px; - width: 42px; - border-bottom: 1px solid $panel-divider-color; - display: none; -} - -.mx_TagPanel .mx_TagPanel_scroller { - flex-grow: 1; - width: 100%; -} - -.mx_TagPanel .mx_TagPanel_tagTileContainer { - display: flex; - flex-direction: column; - align-items: center; - - height: 100%; -} -.mx_TagPanel .mx_TagPanel_tagTileContainer > div { - height: 40px; - padding: 10px 0 9px 0; -} - -.mx_TagPanel .mx_TagTile { - margin: 9px 0; - // opacity: 0.5; - position: relative; -} -.mx_TagPanel .mx_TagTile:focus, -.mx_TagPanel .mx_TagTile:hover, -.mx_TagPanel .mx_TagTile.mx_TagTile_selected { - // opacity: 1; -} - -.mx_TagPanel .mx_TagTile_plus { - margin-bottom: 12px; - height: 40px; - width: 40px; - border-radius: 20px; - background-color: $roomheader-addroom-bg-color; - position: relative; - /* overwrite mx_RoleButton inline-block */ - display: block !important; - - &::before { - background-color: $roomheader-addroom-fg-color; - mask-image: url('$(res)/img/feather-customised/plus.svg'); - mask-position: center; - mask-repeat: no-repeat; - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - } -} - -.mx_TagPanel .mx_TagTile.mx_TagTile_selected::before { - content: ''; - height: 56px; - background-color: $accent-color; - width: 5px; - position: absolute; - left: -15px; - border-radius: 0 3px 3px 0; - top: -8px; // (56 - 40)/2 -} - -.mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus { - filter: none; -} - -.mx_TagTile_tooltip { - position: relative; - top: -30px; - left: 5px; -} - -.mx_TagTile_context_button { - min-width: 15px; - height: 15px; - position: absolute; - right: -5px; - top: -8px; - border-radius: 8px; - background-color: $neutral-badge-color; - color: #000; - font-weight: 600; - font-size: $font-10px; - text-align: center; - padding-top: 1px; - padding-left: 4px; - padding-right: 4px; -} - -.mx_TagTile_avatar { - position: relative; -} - -.mx_TagTile_badge { - position: absolute; - right: -4px; - top: -2px; - border-radius: 8px; - color: $accent-fg-color; - font-weight: 600; - font-size: $font-14px; - padding: 0 5px; - background-color: $roomtile-name-color; -} - -.mx_TagTile_badgeHighlight { - background-color: $warning-color; -} diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index af595aaeee..d248568740 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019-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. @@ -28,8 +28,8 @@ limitations under the License. margin: 0 4px; grid-row: 2 / 4; grid-column: 1; - background-color: white; - box-shadow: 0px 4px 12px $menu-box-shadow-color; + background-color: $dark-panel-bg-color; + box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); border-radius: 8px; } @@ -37,26 +37,28 @@ limitations under the License. grid-row: 1 / 3; grid-column: 1; color: $primary-fg-color; - background-color: $primary-bg-color; - box-shadow: 0px 4px 12px $menu-box-shadow-color; + background-color: $dark-panel-bg-color; + box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); border-radius: 8px; overflow: hidden; display: grid; - grid-template-columns: 20px 1fr; - column-gap: 10px; + grid-template-columns: 22px 1fr; + column-gap: 8px; row-gap: 4px; padding: 8px; - padding-right: 16px; &.mx_Toast_hasIcon { - &::after { + &::before, &::after { content: ""; width: 22px; height: 22px; grid-column: 1; grid-row: 1; mask-size: 100%; + mask-position: center; mask-repeat: no-repeat; + background-size: 100%; + background-repeat: no-repeat; } &.mx_Toast_icon_verification::after { @@ -64,21 +66,64 @@ limitations under the License. background-color: $primary-fg-color; } - &.mx_Toast_icon_verification_warning::after { - background-image: url("$(res)/img/e2e/warning.svg"); + &.mx_Toast_icon_verification_warning { + // white infill for the hollow svg mask + &::before { + background-color: #ffffff; + mask-image: url('$(res)/img/e2e/normal.svg'); + mask-size: 80%; + } + + &::after { + mask-image: url("$(res)/img/e2e/warning.svg"); + background-color: $notice-primary-color; + } } - h2, .mx_Toast_body { + &.mx_Toast_icon_secure_backup::after { + mask-image: url('$(res)/img/feather-customised/secure-backup.svg'); + background-color: $primary-fg-color; + } + + .mx_Toast_title, .mx_Toast_body { grid-column: 2; } } + &:not(.mx_Toast_hasIcon) { + padding-left: 12px; - h2 { - grid-column: 1 / 3; - grid-row: 1; - margin: 0; - font-size: $font-15px; - font-weight: 600; + .mx_Toast_title { + grid-column: 1 / -1; + } + } + + .mx_Toast_title, + .mx_Toast_description { + padding-right: 8px; + } + + .mx_Toast_title { + width: 100%; + box-sizing: border-box; + + h2 { + grid-column: 1 / 3; + grid-row: 1; + margin: 0; + font-size: $font-15px; + font-weight: 600; + display: inline; + width: auto; + vertical-align: middle; + } + + span { + padding-left: 8px; + float: right; + font-size: $font-12px; + line-height: $font-22px; + color: $muted-fg-color; + } } .mx_Toast_body { @@ -87,16 +132,38 @@ limitations under the License. } .mx_Toast_buttons { + float: right; display: flex; + + .mx_AccessibleButton { + min-width: 96px; + box-sizing: border-box; + } + + .mx_AccessibleButton + .mx_AccessibleButton { + margin-left: 5px; + } } .mx_Toast_description { - max-width: 400px; + max-width: 272px; overflow: hidden; - white-space: nowrap; text-overflow: ellipsis; margin: 4px 0 11px 0; font-size: $font-12px; + + .mx_AccessibleButton_kind_link { + font-size: inherit; + padding: 0; + } + + a { + text-decoration: none; + } + } + + .mx_Toast_detail { + color: $secondary-fg-color; } .mx_Toast_deviceID { diff --git a/res/css/structures/_TopLeftMenuButton.scss b/res/css/structures/_TopLeftMenuButton.scss deleted file mode 100644 index 53d44e7c24..0000000000 --- a/res/css/structures/_TopLeftMenuButton.scss +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_TopLeftMenuButton { - flex: 0 0 52px; - border-bottom: 1px solid $panel-divider-color; - color: $topleftmenu-color; - background-color: $primary-bg-color; - display: flex; - align-items: center; - min-width: 0; - padding: 0 4px; - overflow: hidden; -} - -.mx_TopLeftMenuButton .mx_BaseAvatar { - margin: 0 7px; -} - -.mx_TopLeftMenuButton_name { - margin: 0 7px; - font-size: $font-18px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - font-weight: 600; -} - -.mx_TopLeftMenuButton_chevron { - margin: 0 7px; - mask: url('$(res)/img/feather-customised/dropdown-arrow.svg'); - mask-repeat: no-repeat; - width: 10px; - height: 6px; - background-color: $roomsublist-label-fg-color; -} diff --git a/res/css/structures/_UploadBar.scss b/res/css/structures/_UploadBar.scss index d76c81668c..7c62516b47 100644 --- a/res/css/structures/_UploadBar.scss +++ b/res/css/structures/_UploadBar.scss @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2015, 2016, 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. @@ -15,47 +15,45 @@ limitations under the License. */ .mx_UploadBar { + padding-left: 65px; // line up with the shield area in the composer position: relative; + + .mx_ProgressBar { + width: calc(100% - 40px); // cheating at a right margin + } } -.mx_UploadBar_uploadProgressOuter { - height: 5px; - margin-left: 63px; - margin-top: -1px; - padding-bottom: 5px; -} - -.mx_UploadBar_uploadProgressInner { - background-color: $accent-color; - height: 5px; -} - -.mx_UploadBar_uploadFilename { +.mx_UploadBar_filename { margin-top: 5px; - margin-left: 65px; - opacity: 0.5; - color: $primary-fg-color; -} - -.mx_UploadBar_uploadIcon { - float: left; - margin-top: 5px; - margin-left: 14px; -} - -.mx_UploadBar_uploadCancel { - float: right; - margin-top: 5px; - margin-right: 10px; + color: $muted-fg-color; position: relative; - opacity: 0.6; - cursor: pointer; - z-index: 1; + padding-left: 22px; // 18px for icon, 4px for padding + font-size: $font-15px; + vertical-align: middle; + + &::before { + content: ""; + height: 18px; + width: 18px; + position: absolute; + top: 0; + left: 0; + mask-repeat: no-repeat; + mask-position: center; + background-color: $muted-fg-color; + mask-image: url('$(res)/img/element-icons/upload.svg'); + } } -.mx_UploadBar_uploadBytes { - float: right; - margin-top: 5px; - margin-right: 30px; - color: $accent-color; +.mx_UploadBar_cancel { + position: absolute; + top: 0; + right: 0; + height: 16px; + width: 16px; + margin-right: 16px; // align over rightmost button in composer + mask-repeat: no-repeat; + mask-position: center; + background-color: $muted-fg-color; + mask-image: url('$(res)/img/icons-close.svg'); } diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss new file mode 100644 index 0000000000..17e6ad75df --- /dev/null +++ b/res/css/structures/_UserMenu.scss @@ -0,0 +1,337 @@ +/* +Copyright 2020 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_UserMenu { + // to make the menu button sort of aligned with the explore button below + padding-right: 6px; + + &.mx_UserMenu_prototype { + // The margin & padding combination between here and the ::after is to + // align the border line with the tag panel. + margin-bottom: 6px; + + padding-right: 0; // make the right edge line up with the explore button + + .mx_UserMenu_headerButtons { + // considering we've eliminated right padding on the menu itself, we need to + // push the chevron in slightly (roughly lining up with the center of the + // plus buttons) + margin-right: 2px; + } + + // we cheat opacity on the theme colour with an after selector here + &::after { + content: ''; + border-bottom: 1px solid $primary-fg-color; // XXX: Variable abuse + opacity: 0.2; + display: block; + padding-top: 8px; + } + } + + .mx_UserMenu_headerButtons { + width: 16px; + height: 16px; + position: relative; + display: block; + + &::before { + content: ''; + width: 16px; + height: 16px; + position: absolute; + top: 0; + left: 0; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $tertiary-fg-color; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + } + } + + .mx_UserMenu_row { + // Create a row-based flexbox to ensure items stay aligned correctly. + display: flex; + align-items: center; + + .mx_UserMenu_userAvatarContainer { + position: relative; // to make default avatars work + margin-right: 8px; + height: 32px; // to remove the unknown 4px gap the browser puts below it + padding: 3px 0; // to align with and without using doubleName + + .mx_UserMenu_userAvatar { + border-radius: 32px; // should match avatar size + object-fit: cover; + } + } + + .mx_UserMenu_doubleName { + flex: 1; + min-width: 0; // make flexbox aware that it can crush this to a tiny width + + .mx_UserMenu_userName, + .mx_UserMenu_subUserName { + display: block; + } + + .mx_UserMenu_subUserName { + color: $muted-fg-color; + font-size: $font-13px; + line-height: $font-18px; + flex: 1; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + + .mx_UserMenu_userName { + font-weight: 600; + font-size: $font-15px; + line-height: $font-20px; + flex: 1; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .mx_UserMenu_headerButtons { + // No special styles: the rest of the layout happens to make it work. + } + + .mx_UserMenu_dnd { + width: 24px; + height: 24px; + margin-right: 8px; + position: relative; + + &::before { + content: ''; + position: absolute; + width: 24px; + height: 24px; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $muted-fg-color; + } + + &.mx_UserMenu_dnd_noisy::before { + mask-image: url('$(res)/img/element-icons/notifications.svg'); + } + + &.mx_UserMenu_dnd_muted::before { + mask-image: url('$(res)/img/element-icons/roomlist/notifications-off.svg'); + } + } + } + + &.mx_UserMenu_minimized { + padding-right: 0px; + + .mx_UserMenu_userAvatarContainer { + margin-right: 0px; + } + } +} + +.mx_UserMenu_contextMenu { + width: 258px; + + // These override the styles already present on the user menu rather than try to + // define a new menu. They are specifically for the stacked menu when a community + // is being represented as a prototype. + &.mx_UserMenu_contextMenu_prototype { + padding-bottom: 16px; + + .mx_UserMenu_contextMenu_header { + padding-bottom: 0; + padding-top: 16px; + + &:nth-child(n + 2) { + padding-top: 8px; + } + } + + hr { + width: 85%; + opacity: 0.2; + border: none; + border-bottom: 1px solid $primary-fg-color; // XXX: Variable abuse + } + + &.mx_IconizedContextMenu { + > .mx_IconizedContextMenu_optionList { + margin-top: 4px; + + &::before { + border: none; + } + + > .mx_AccessibleButton { + padding-top: 2px; + padding-bottom: 2px; + } + } + } + } + + &.mx_IconizedContextMenu .mx_IconizedContextMenu_optionList_red { + .mx_AccessibleButton { + padding-top: 16px; + padding-bottom: 16px; + } + } + + .mx_UserMenu_contextMenu_header { + padding: 20px; + + // Create a flexbox to organize the header a bit easier + display: flex; + align-items: center; + + .mx_UserMenu_contextMenu_name { + // Create another flexbox of columns to handle large user IDs + display: flex; + flex-direction: column; + width: calc(100% - 40px); // 40px = 32px theme button + 8px margin to theme button + + * { + // Automatically grow all subelements to fit the container + flex: 1; + width: 100%; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .mx_UserMenu_contextMenu_displayName { + font-weight: bold; + font-size: $font-15px; + line-height: $font-20px; + } + + .mx_UserMenu_contextMenu_userId { + font-size: $font-15px; + line-height: $font-24px; + } + } + + .mx_UserMenu_contextMenu_themeButton { + min-width: 32px; + max-width: 32px; + width: 32px; + height: 32px; + margin-left: 8px; + border-radius: 32px; + background-color: $theme-button-bg-color; + cursor: pointer; + + // to make alignment easier, create flexbox for the image + display: flex; + align-items: center; + justify-content: center; + } + + &.mx_UserMenu_contextMenu_guestPrompts, + &.mx_UserMenu_contextMenu_hostingLink { + padding-top: 0; + } + + &.mx_UserMenu_contextMenu_guestPrompts { + display: inline-block; + + > span { + font-weight: 600; + display: block; + + & + span { + margin-top: 8px; + } + } + + .mx_AccessibleButton_kind_link { + font-weight: normal; + font-size: inherit; + padding: 0; + } + } + } + + .mx_IconizedContextMenu_icon { + width: 16px; + height: 16px; + display: block; + + &::before { + content: ''; + width: 16px; + height: 16px; + display: block; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + } + } + + .mx_UserMenu_iconHome::before { + mask-image: url('$(res)/img/element-icons/roomlist/home.svg'); + } + .mx_UserMenu_iconHosting::before { + mask-image: url('$(res)/img/element-icons/brands/element.svg'); + } + + .mx_UserMenu_iconBell::before { + mask-image: url('$(res)/img/element-icons/notifications.svg'); + } + + .mx_UserMenu_iconLock::before { + mask-image: url('$(res)/img/element-icons/security.svg'); + } + + .mx_UserMenu_iconSettings::before { + mask-image: url('$(res)/img/element-icons/settings.svg'); + } + + .mx_UserMenu_iconArchive::before { + mask-image: url('$(res)/img/element-icons/roomlist/archived.svg'); + } + + .mx_UserMenu_iconMessage::before { + mask-image: url('$(res)/img/element-icons/roomlist/feedback.svg'); + } + + .mx_UserMenu_iconSignOut::before { + mask-image: url('$(res)/img/element-icons/leave.svg'); + } + + .mx_UserMenu_iconMembers::before { + mask-image: url('$(res)/img/element-icons/room/members.svg'); + } + + .mx_UserMenu_iconInvite::before { + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } +} diff --git a/res/css/structures/_ViewSource.scss b/res/css/structures/_ViewSource.scss index 421d1f03cd..248eab5d88 100644 --- a/res/css/structures/_ViewSource.scss +++ b/res/css/structures/_ViewSource.scss @@ -14,17 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_ViewSource_label_left { - float: left; -} - -.mx_ViewSource_label_right { - float: right; -} - -.mx_ViewSource_label_bottom { +.mx_ViewSource_separator { clear: both; border-bottom: 1px solid #e5e5e5; + padding-top: 0.7em; + padding-bottom: 0.7em; +} + +.mx_ViewSource_heading { + font-size: $font-17px; + font-weight: 400; + color: $primary-fg-color; + margin-top: 0.7em; } .mx_ViewSource pre { @@ -34,3 +35,7 @@ limitations under the License. word-wrap: break-word; white-space: pre-wrap; } + +.mx_ViewSource_details { + margin-top: 0.8em; +} diff --git a/res/css/structures/auth/_CompleteSecurity.scss b/res/css/structures/auth/_CompleteSecurity.scss index 3050840fe8..80e7aaada0 100644 --- a/res/css/structures/auth/_CompleteSecurity.scss +++ b/res/css/structures/auth/_CompleteSecurity.scss @@ -44,6 +44,7 @@ limitations under the License. .mx_CompleteSecurity_actionRow { display: flex; justify-content: flex-end; + margin-top: $font-28px; .mx_AccessibleButton { margin-inline-start: 18px; diff --git a/res/css/structures/auth/_Login.scss b/res/css/structures/auth/_Login.scss index 4ce90cc6bd..9c98ca3a1c 100644 --- a/res/css/structures/auth/_Login.scss +++ b/res/css/structures/auth/_Login.scss @@ -18,7 +18,7 @@ limitations under the License. .mx_Login_submit { @mixin mx_DialogButton; width: 100%; - margin-top: 35px; + margin-top: 24px; margin-bottom: 24px; box-sizing: border-box; text-align: center; @@ -33,12 +33,6 @@ limitations under the License. cursor: default; } -.mx_AuthBody a.mx_Login_sso_link:link, -.mx_AuthBody a.mx_Login_sso_link:hover, -.mx_AuthBody a.mx_Login_sso_link:visited { - color: $button-primary-fg-color; -} - .mx_Login_loader { display: inline; position: relative; @@ -87,5 +81,18 @@ limitations under the License. } .mx_Login_underlinedServerName { + width: max-content; border-bottom: 1px dashed $accent-color; } + +div.mx_AccessibleButton_kind_link.mx_Login_forgot { + display: block; + margin: 0 auto; + // style it as a link + font-size: inherit; + padding: 0; + + &.mx_AccessibleButton_disabled { + cursor: not-allowed; + } +} diff --git a/res/css/views/audio_messages/_AudioPlayer.scss b/res/css/views/audio_messages/_AudioPlayer.scss new file mode 100644 index 0000000000..9a65ad008f --- /dev/null +++ b/res/css/views/audio_messages/_AudioPlayer.scss @@ -0,0 +1,68 @@ +/* +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_AudioPlayer_container { + padding: 16px 12px 12px 12px; + max-width: 267px; // use max to make the control fit in the files/pinned panels + + .mx_AudioPlayer_primaryContainer { + display: flex; + + .mx_PlayPauseButton { + margin-right: 8px; + } + + .mx_AudioPlayer_mediaInfo { + flex: 1; + overflow: hidden; // makes the ellipsis on the file name work + + & > * { + display: block; + } + + .mx_AudioPlayer_mediaName { + color: $primary-fg-color; + font-size: $font-15px; + line-height: $font-15px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + padding-bottom: 4px; // mimics the line-height differences in the Figma + } + + .mx_AudioPlayer_byline { + font-size: $font-12px; + line-height: $font-12px; + } + } + } + + .mx_AudioPlayer_seek { + display: flex; + align-items: center; + + .mx_SeekBar { + flex: 1; + } + + .mx_Clock { + width: $font-42px; // we're not using a monospace font, so fake it + min-width: $font-42px; // for flexbox + padding-left: 4px; // isolate from seek bar + text-align: right; + } + } +} diff --git a/res/css/views/audio_messages/_PlayPauseButton.scss b/res/css/views/audio_messages/_PlayPauseButton.scss new file mode 100644 index 0000000000..714da3e605 --- /dev/null +++ b/res/css/views/audio_messages/_PlayPauseButton.scss @@ -0,0 +1,53 @@ +/* +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_PlayPauseButton { + position: relative; + width: 32px; + height: 32px; + min-width: 32px; // for when the button is used in a flexbox + min-height: 32px; // for when the button is used in a flexbox + border-radius: 32px; + background-color: $voice-playback-button-bg-color; + + &::before { + content: ''; + position: absolute; // sizing varies by icon + background-color: $voice-playback-button-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + } + + &.mx_PlayPauseButton_disabled::before { + opacity: 0.5; + } + + &.mx_PlayPauseButton_play::before { + width: 13px; + height: 16px; + top: 8px; // center + left: 12px; // center + mask-image: url('$(res)/img/element-icons/play.svg'); + } + + &.mx_PlayPauseButton_pause::before { + width: 10px; + height: 12px; + top: 10px; // center + left: 11px; // center + mask-image: url('$(res)/img/element-icons/pause.svg'); + } +} diff --git a/res/css/views/audio_messages/_PlaybackContainer.scss b/res/css/views/audio_messages/_PlaybackContainer.scss new file mode 100644 index 0000000000..5548f6198e --- /dev/null +++ b/res/css/views/audio_messages/_PlaybackContainer.scss @@ -0,0 +1,56 @@ +/* +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. +*/ + +// Dev note: there's no actual component called . These classes +// are shared amongst multiple voice message components. + +// Container for live recording and playback controls +.mx_VoiceMessagePrimaryContainer { + // 7px top and bottom for visual design. 12px left & right, but the waveform (right) + // has a 1px padding on it that we want to account for. + padding: 7px 12px 7px 11px; + + // Cheat at alignment a bit + display: flex; + align-items: center; + + contain: content; + + .mx_Waveform { + .mx_Waveform_bar { + background-color: $voice-record-waveform-incomplete-fg-color; + height: 100%; + /* Variable set by a JS component */ + transform: scaleY(max(0.05, var(--barHeight))); + + &.mx_Waveform_bar_100pct { + // Small animation to remove the mechanical feel of progress + transition: background-color 250ms ease; + background-color: $message-body-panel-fg-color; + } + } + } + + .mx_Clock { + width: $font-42px; // we're not using a monospace font, so fake it + padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended. + padding-left: 8px; // isolate from recording circle / play control + } + + &.mx_VoiceMessagePrimaryContainer_noWaveform { + max-width: 162px; // with all the padding this results in 185px wide + } +} diff --git a/res/css/views/audio_messages/_SeekBar.scss b/res/css/views/audio_messages/_SeekBar.scss new file mode 100644 index 0000000000..d13fe4ac6a --- /dev/null +++ b/res/css/views/audio_messages/_SeekBar.scss @@ -0,0 +1,103 @@ +/* +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. +*/ + +// CSS inspiration from: +// * https://www.w3schools.com/howto/howto_js_rangeslider.asp +// * https://stackoverflow.com/a/28283806 +// * https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/ + +.mx_SeekBar { + // Dev note: we deliberately do not have the -ms-track (and friends) selectors because we don't + // need to support IE. + + appearance: none; // default style override + + width: 100%; + height: 1px; + background: $quaternary-fg-color; + outline: none; // remove blue selection border + position: relative; // for before+after pseudo elements later on + + cursor: pointer; + + &::-webkit-slider-thumb { + appearance: none; // default style override + + // Dev note: This needs to be duplicated with the -moz-range-thumb selector + // because otherwise Edge (webkit) will fail to see the styles and just refuse + // to apply them. + width: 8px; + height: 8px; + border-radius: 8px; + background-color: $tertiary-fg-color; + cursor: pointer; + } + + &::-moz-range-thumb { + width: 8px; + height: 8px; + border-radius: 8px; + background-color: $tertiary-fg-color; + cursor: pointer; + + // Firefox adds a border on the thumb + border: none; + } + + // This is for webkit support, but we can't limit the functionality of it to just webkit + // browsers. Firefox responds to webkit-prefixed values now, which means we can't use media + // or support queries to selectively apply the rule. An upside is that this CSS doesn't work + // in firefox, so it's just wasted CPU/GPU time. + &::before { // ::before to ensure it ends up under the thumb + content: ''; + background-color: $tertiary-fg-color; + + // Absolute positioning to ensure it overlaps with the existing bar + position: absolute; + top: 0; + left: 0; + + // Sizing to match the bar + width: 100%; + height: 1px; + + // And finally dynamic width without overly hurting the rendering engine. + transform-origin: 0 100%; + transform: scaleX(var(--fillTo)); + } + + // This is firefox's built-in support for the above, with 100% less hacks. + &::-moz-range-progress { + background-color: $tertiary-fg-color; + height: 1px; + } + + &:disabled { + opacity: 0.5; + } + + // Increase clickable area for the slider (approximately same size as browser default) + // We do it this way to keep the same padding and margins of the element, avoiding margin math. + // Source: https://front-back.com/expand-clickable-areas-for-a-better-touch-experience/ + &::after { + content: ''; + position: absolute; + top: -6px; + bottom: -6px; + left: 0; + right: 0; + } +} diff --git a/res/css/views/audio_messages/_Waveform.scss b/res/css/views/audio_messages/_Waveform.scss new file mode 100644 index 0000000000..cf03c84601 --- /dev/null +++ b/res/css/views/audio_messages/_Waveform.scss @@ -0,0 +1,40 @@ +/* +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_Waveform { + position: relative; + height: 30px; // tallest bar can only be 30px + top: 1px; // because of our border trick (see below), we're off by 1px of aligntment + + display: flex; + align-items: center; // so the bars grow from the middle + + overflow: hidden; // this is cheaper than a `max-height: calc(100% - 4px)` in the bar's CSS. + + // A bar is meant to be a 2x2 circle when at zero height, and otherwise a 2px wide line + // with rounded caps. + .mx_Waveform_bar { + width: 0; // 0px width means we'll end up using the border as our width + border: 1px solid transparent; // transparent means we'll use the background colour + border-radius: 2px; // rounded end caps, based on the border + min-height: 0; // like the width, we'll rely on the border to give us height + max-height: 100%; // this makes the `height: 42%` work on the element + margin-left: 1px; // we want 2px between each bar, so 1px on either side for balance + margin-right: 1px; + + // background color is handled by the parent components + } +} diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index 468a4b3d62..90dca32e48 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -34,7 +34,11 @@ limitations under the License. h3 { font-size: $font-14px; font-weight: 600; - color: $authpage-primary-color; + color: $authpage-secondary-color; + } + + h3.mx_AuthBody_centered { + text-align: center; } a:link, @@ -96,12 +100,6 @@ limitations under the License. } } -.mx_AuthBody_editServerDetails { - padding-left: 1em; - font-size: $font-12px; - font-weight: normal; -} - .mx_AuthBody_fieldRow { display: flex; margin-bottom: 10px; @@ -119,36 +117,52 @@ limitations under the License. margin-right: 0; } +.mx_AuthBody_paddedFooter { + height: 80px; // height of the submit button + register link + padding-top: 28px; + text-align: center; + + .mx_AuthBody_paddedFooter_title { + margin-top: 16px; + font-size: $font-15px; + line-height: $font-24px; + + .mx_InlineSpinner img { + vertical-align: sub; + margin-right: 5px; + } + } + + .mx_AuthBody_paddedFooter_subtitle { + margin-top: 8px; + font-size: $font-10px; + line-height: $font-14px; + } +} + .mx_AuthBody_changeFlow { display: block; text-align: center; width: 100%; + + > a { + font-weight: $font-semi-bold; + } +} + +.mx_SSOButtons + .mx_AuthBody_changeFlow { + margin-top: 24px; } .mx_AuthBody_spinner { margin: 1em 0; } -.mx_AuthBody_passwordScore { - width: 100%; - appearance: none; - height: 4px; - border: 0; - border-radius: 2px; - position: absolute; - top: -12px; - - &::-moz-progress-bar { - border-radius: 2px; - background-color: $accent-color; - } - - &::-webkit-progress-bar, - &::-webkit-progress-value { - border-radius: 2px; - } - - &::-webkit-progress-value { - background-color: $accent-color; +@media only screen and (max-width: 480px) { + .mx_AuthBody { + border-radius: 4px; + width: auto; + max-width: 500px; + padding: 10px; } } diff --git a/res/css/views/auth/_AuthHeader.scss b/res/css/views/auth/_AuthHeader.scss index b3d07b1925..13d5195160 100644 --- a/res/css/views/auth/_AuthHeader.scss +++ b/res/css/views/auth/_AuthHeader.scss @@ -18,6 +18,12 @@ limitations under the License. display: flex; flex-direction: column; width: 206px; - padding: 25px 40px; + padding: 25px 25px; box-sizing: border-box; } + +@media only screen and (max-width: 480px) { + .mx_AuthHeader { + display: none; + } +} diff --git a/res/css/views/auth/_AuthHeaderLogo.scss b/res/css/views/auth/_AuthHeaderLogo.scss index 091fb0197b..86f0313b68 100644 --- a/res/css/views/auth/_AuthHeaderLogo.scss +++ b/res/css/views/auth/_AuthHeaderLogo.scss @@ -17,9 +17,15 @@ limitations under the License. .mx_AuthHeaderLogo { margin-top: 15px; flex: 1; - padding: 0 10px; + padding: 0 25px; } .mx_AuthHeaderLogo img { width: 100%; } + +@media only screen and (max-width: 480px) { + .mx_AuthHeaderLogo { + display: none; + } +} diff --git a/res/css/views/auth/_AuthPage.scss b/res/css/views/auth/_AuthPage.scss index 8ef48b6265..e3409792f0 100644 --- a/res/css/views/auth/_AuthPage.scss +++ b/res/css/views/auth/_AuthPage.scss @@ -29,3 +29,9 @@ limitations under the License. box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.33); background-color: $authpage-modal-bg-color; } + +@media only screen and (max-width: 480px) { + .mx_AuthPage_modal { + margin-top: 0; + } +} diff --git a/res/css/views/auth/_InteractiveAuthEntryComponents.scss b/res/css/views/auth/_InteractiveAuthEntryComponents.scss index 05cddf2c48..ffaad3cd7a 100644 --- a/res/css/views/auth/_InteractiveAuthEntryComponents.scss +++ b/res/css/views/auth/_InteractiveAuthEntryComponents.scss @@ -14,6 +14,35 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_InteractiveAuthEntryComponents_emailWrapper { + padding-right: 100px; + position: relative; + margin-top: 32px; + margin-bottom: 32px; + + &::before, &::after { + position: absolute; + width: 116px; + height: 116px; + content: ""; + right: -10px; + } + + &::before { + background-color: rgba(244, 246, 250, 0.91); + border-radius: 50%; + top: -20px; + } + + &::after { + background-image: url('$(res)/img/element-icons/email-prompt.svg'); + background-repeat: no-repeat; + background-position: center; + background-size: contain; + top: -25px; + } +} + .mx_InteractiveAuthEntryComponents_msisdnWrapper { text-align: center; } @@ -54,7 +83,10 @@ limitations under the License. } .mx_InteractiveAuthEntryComponents_termsPolicy { - display: block; + display: flex; + flex-direction: row; + justify-content: start; + align-items: center; } .mx_InteractiveAuthEntryComponents_passwordSection { diff --git a/res/css/views/auth/_LanguageSelector.scss b/res/css/views/auth/_LanguageSelector.scss index 781561f876..885ee7f30d 100644 --- a/res/css/views/auth/_LanguageSelector.scss +++ b/res/css/views/auth/_LanguageSelector.scss @@ -23,6 +23,7 @@ limitations under the License. font-size: $font-14px; font-weight: 600; color: $authpage-lang-color; + width: auto; } .mx_AuthBody_language .mx_Dropdown_arrow { diff --git a/res/css/views/auth/_PassphraseField.scss b/res/css/views/auth/_PassphraseField.scss new file mode 100644 index 0000000000..bf8e7f4438 --- /dev/null +++ b/res/css/views/auth/_PassphraseField.scss @@ -0,0 +1,37 @@ +/* +Copyright 2020 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. +*/ + +$PassphraseStrengthHigh: $accent-color; +$PassphraseStrengthMedium: $username-variant5-color; +$PassphraseStrengthLow: $notice-primary-color; + +progress.mx_PassphraseField_progress { + appearance: none; + width: 100%; + border: 0; + height: 4px; + position: absolute; + top: -12px; + + @mixin ProgressBarBorderRadius "2px"; + @mixin ProgressBarColour $PassphraseStrengthLow; + &[value="2"], &[value="3"] { + @mixin ProgressBarColour $PassphraseStrengthMedium; + } + &[value="4"] { + @mixin ProgressBarColour $PassphraseStrengthHigh; + } +} diff --git a/res/css/views/auth/_ServerConfig.scss b/res/css/views/auth/_ServerConfig.scss deleted file mode 100644 index a7e0057ab3..0000000000 --- a/res/css/views/auth/_ServerConfig.scss +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 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_ServerConfig_help:link { - opacity: 0.8; -} - -.mx_ServerConfig_error { - display: block; - color: $warning-color; -} - -.mx_ServerConfig_identityServer { - transform: scaleY(0); - transform-origin: top; - transition: transform 0.25s; - - &.mx_ServerConfig_identityServer_shown { - transform: scaleY(1); - } -} diff --git a/res/css/views/auth/_ServerTypeSelector.scss b/res/css/views/auth/_ServerTypeSelector.scss deleted file mode 100644 index fbd3d2655d..0000000000 --- a/res/css/views/auth/_ServerTypeSelector.scss +++ /dev/null @@ -1,69 +0,0 @@ -/* -Copyright 2019 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_ServerTypeSelector { - display: flex; - margin-bottom: 28px; -} - -.mx_ServerTypeSelector_type { - margin: 0 5px; -} - -.mx_ServerTypeSelector_type:first-child { - margin-left: 0; -} - -.mx_ServerTypeSelector_type:last-child { - margin-right: 0; -} - -.mx_ServerTypeSelector_label { - text-align: center; - font-weight: 600; - color: $authpage-primary-color; - margin: 8px 0; -} - -.mx_ServerTypeSelector_type .mx_AccessibleButton { - padding: 10px; - border: 1px solid $input-border-color; - border-radius: 4px; -} - -.mx_ServerTypeSelector_type.mx_ServerTypeSelector_type_selected .mx_AccessibleButton { - border-color: $input-valid-border-color; -} - -.mx_ServerTypeSelector_logo { - display: flex; - justify-content: center; - height: 18px; - margin-bottom: 12px; - font-weight: 600; - color: $authpage-primary-color; -} - -.mx_ServerTypeSelector_logo > div { - display: flex; - width: 70%; - align-items: center; - justify-content: space-evenly; -} - -.mx_ServerTypeSelector_description { - font-size: $font-10px; -} diff --git a/res/css/views/auth/_Welcome.scss b/res/css/views/auth/_Welcome.scss index 9043289184..894174d6e2 100644 --- a/res/css/views/auth/_Welcome.scss +++ b/res/css/views/auth/_Welcome.scss @@ -18,9 +18,14 @@ limitations under the License. display: flex; flex-direction: column; align-items: center; + &.mx_WelcomePage_registrationDisabled { + .mx_ButtonCreateAccount { + display: none; + } + } } .mx_Welcome .mx_AuthBody_language { - width: 120px; + width: 160px; margin-bottom: 10px; } diff --git a/res/css/views/avatars/_BaseAvatar.scss b/res/css/views/avatars/_BaseAvatar.scss index e59598278f..cbddd97e18 100644 --- a/res/css/views/avatars/_BaseAvatar.scss +++ b/res/css/views/avatars/_BaseAvatar.scss @@ -22,7 +22,7 @@ limitations under the License. // different results during full reflow of the page vs. incremental reflow // of small portions. While that's surely a browser bug, we can avoid it by // using `inline-block` instead of the default `inline`. - // https://github.com/vector-im/riot-web/issues/5594 + // https://github.com/vector-im/element-web/issues/5594 // https://bugzilla.mozilla.org/show_bug.cgi?id=1535053 // https://bugzilla.mozilla.org/show_bug.cgi?id=255139 display: inline-block; @@ -41,7 +41,7 @@ limitations under the License. .mx_BaseAvatar_image { object-fit: cover; - border-radius: 40px; + border-radius: 125px; vertical-align: top; background-color: $avatar-bg-color; } diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.scss b/res/css/views/avatars/_DecoratedRoomAvatar.scss new file mode 100644 index 0000000000..257b512579 --- /dev/null +++ b/res/css/views/avatars/_DecoratedRoomAvatar.scss @@ -0,0 +1,73 @@ +/* +Copyright 2020 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_DecoratedRoomAvatar, .mx_ExtraTile { + position: relative; + contain: content; + + &.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar { + mask-image: url('$(res)/img/element-icons/roomlist/decorated-avatar-mask.svg'); + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + } + + .mx_DecoratedRoomAvatar_icon { + position: absolute; + bottom: -2px; + right: -2px; + margin: 4px; + width: 8px; + height: 8px; + border-radius: 50%; + } + + .mx_DecoratedRoomAvatar_icon::before { + content: ''; + width: 8px; + height: 8px; + position: absolute; + border-radius: 8px; + } + + .mx_DecoratedRoomAvatar_icon_globe::before { + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $secondary-fg-color; + mask-image: url('$(res)/img/globe.svg'); + } + + .mx_DecoratedRoomAvatar_icon_offline::before { + background-color: $presence-offline; + } + + .mx_DecoratedRoomAvatar_icon_online::before { + background-color: $presence-online; + } + + .mx_DecoratedRoomAvatar_icon_away::before { + background-color: $presence-away; + } + + .mx_NotificationBadge, .mx_RoomTile_badgeContainer { + position: absolute; + top: 0; + right: 0; + height: 18px; + width: 18px; + } +} diff --git a/res/css/views/avatars/_MemberStatusMessageAvatar.scss b/res/css/views/avatars/_MemberStatusMessageAvatar.scss index c101a5d8a8..975b4e5ce9 100644 --- a/res/css/views/avatars/_MemberStatusMessageAvatar.scss +++ b/res/css/views/avatars/_MemberStatusMessageAvatar.scss @@ -17,7 +17,7 @@ limitations under the License. .mx_MessageComposer_avatar .mx_BaseAvatar { padding: 2px; border: 1px solid transparent; - border-radius: 15px; + border-radius: 100%; } .mx_MessageComposer_avatar .mx_BaseAvatar_initial { diff --git a/res/css/views/avatars/_WidgetAvatar.scss b/res/css/views/avatars/_WidgetAvatar.scss new file mode 100644 index 0000000000..8e5cfb54d8 --- /dev/null +++ b/res/css/views/avatars/_WidgetAvatar.scss @@ -0,0 +1,19 @@ +/* +Copyright 2020 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_WidgetAvatar { + border-radius: 4px; +} diff --git a/res/css/views/beta/_BetaCard.scss b/res/css/views/beta/_BetaCard.scss new file mode 100644 index 0000000000..2af4e79ecd --- /dev/null +++ b/res/css/views/beta/_BetaCard.scss @@ -0,0 +1,161 @@ +/* +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_BetaCard { + margin-bottom: 20px; + padding: 24px; + background-color: $settings-profile-placeholder-bg-color; + border-radius: 8px; + box-sizing: border-box; + + .mx_BetaCard_columns { + display: flex; + + > div { + .mx_BetaCard_title { + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + color: $primary-fg-color; + margin: 4px 0 14px; + + .mx_BetaCard_betaPill { + margin-left: 12px; + } + } + + .mx_BetaCard_caption { + font-size: $font-15px; + line-height: $font-20px; + color: $secondary-fg-color; + margin-bottom: 20px; + } + + .mx_BetaCard_buttons .mx_AccessibleButton { + display: block; + margin: 12px 0; + padding: 7px 40px; + width: auto; + } + + .mx_BetaCard_disclaimer { + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + margin-top: 20px; + } + } + + > img { + margin: auto 0 auto 20px; + width: 300px; + object-fit: contain; + height: 100%; + } + } + + .mx_BetaCard_relatedSettings { + .mx_SettingsFlag { + margin: 16px 0 0; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; + + .mx_SettingsFlag_microcopy { + margin-top: 4px; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + } + } + } +} + +.mx_BetaCard_betaPill { + background-color: $accent-color-alt; + padding: 4px 10px; + border-radius: 8px; + text-transform: uppercase; + font-size: 12px; + line-height: 15px; + color: #FFFFFF; + display: inline-block; + vertical-align: text-bottom; + + &.mx_BetaCard_betaPill_clickable { + cursor: pointer; + } +} + +$pulse-color: $accent-color-alt; +$dot-size: 12px; + +.mx_BetaDot { + border-radius: 50%; + margin: 10px; + height: $dot-size; + width: $dot-size; + transform: scale(1); + background: rgba($pulse-color, 1); + animation: mx_Beta_bluePulse 2s infinite; + animation-iteration-count: 20; + position: relative; + + &::after { + content: ""; + position: absolute; + width: inherit; + height: inherit; + top: 0; + left: 0; + transform: scale(1); + transform-origin: center center; + animation-name: mx_Beta_bluePulse_shadow; + animation-duration: inherit; + animation-iteration-count: inherit; + border-radius: 50%; + background: rgba($pulse-color, 1); + } +} + +@keyframes mx_Beta_bluePulse { + 0% { + transform: scale(0.95); + } + + 70% { + transform: scale(1); + } + + 100% { + transform: scale(0.95); + } +} + +@keyframes mx_Beta_bluePulse_shadow { + 0% { + opacity: 0.7; + } + + 70% { + transform: scale(2.2); + opacity: 0; + } + + 100% { + opacity: 0; + } +} diff --git a/res/css/views/context_menus/_CallContextMenu.scss b/res/css/views/context_menus/_CallContextMenu.scss new file mode 100644 index 0000000000..55b73b0344 --- /dev/null +++ b/res/css/views/context_menus/_CallContextMenu.scss @@ -0,0 +1,23 @@ +/* +Copyright 2020 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_CallContextMenu_item { + width: 205px; + height: 40px; + padding-left: 16px; + line-height: 40px; + vertical-align: center; +} diff --git a/res/css/views/context_menus/_IconizedContextMenu.scss b/res/css/views/context_menus/_IconizedContextMenu.scss new file mode 100644 index 0000000000..204435995f --- /dev/null +++ b/res/css/views/context_menus/_IconizedContextMenu.scss @@ -0,0 +1,156 @@ +/* +Copyright 2020 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. +*/ + +// A context menu that largely fits the | [icon] [label] | format. +.mx_IconizedContextMenu { + min-width: 146px; + + .mx_IconizedContextMenu_optionList { + & > * { + padding-left: 20px; + padding-right: 20px; + } + + // the notFirst class is for cases where the optionList might be under a header of sorts. + &:nth-child(n + 2), .mx_IconizedContextMenu_optionList_notFirst { + // This is a bit of a hack when we could just use a simple border-top property, + // however we have a (kinda) good reason for doing it this way: we need opacity. + // To get the right color, we need an opacity modifier which means we have to work + // around the problem. PostCSS doesn't support the opacity() function, and if we + // use something like postcss-functions we quickly run into an issue where the + // function we would define gets passed a CSS variable for custom themes, which + // can't be converted easily even when considering https://stackoverflow.com/a/41265350/7037379 + // + // Therefore, we just hack in a line and border the thing ourselves + &::before { + border-top: 1px solid $primary-fg-color; + opacity: 0.1; + content: ''; + + // Counteract the padding problems (width: 100% ignores the 40px padding, + // unless we position it absolutely then it does the right thing). + width: 100%; + position: absolute; + left: 0; + } + } + + // round the top corners of the top button for the hover effect to be bounded + &:first-child .mx_AccessibleButton:first-child { + border-radius: 8px 8px 0 0; // radius matches .mx_ContextualMenu + } + + // round the bottom corners of the bottom button for the hover effect to be bounded + &:last-child .mx_AccessibleButton:last-child { + border-radius: 0 0 8px 8px; // radius matches .mx_ContextualMenu + } + + .mx_AccessibleButton { + // pad the inside of the button so that the hover background is padded too + padding-top: 12px; + padding-bottom: 12px; + text-decoration: none; + color: $primary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + + // Create a flexbox to more easily define the list items + display: flex; + align-items: center; + + &:hover { + background-color: $menu-selected-color; + } + + &.mx_AccessibleButton_disabled { + opacity: 0.5; + cursor: not-allowed; + } + + img, .mx_IconizedContextMenu_icon { // icons + width: 16px; + min-width: 16px; + max-width: 16px; + } + + span.mx_IconizedContextMenu_label { // labels + width: 100%; + flex: 1; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .mx_IconizedContextMenu_icon + .mx_IconizedContextMenu_label { + padding-left: 14px; + } + } + } + + .mx_IconizedContextMenu_icon { + position: relative; + width: 16px; + height: 16px; + + &::before { + content: ''; + width: 16px; + height: 16px; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + } + } + + .mx_IconizedContextMenu_optionList_red { + .mx_AccessibleButton { + color: $warning-color !important; + } + + .mx_IconizedContextMenu_icon::before { + background-color: $warning-color; + } + } + + .mx_IconizedContextMenu_active { + &.mx_AccessibleButton, .mx_AccessibleButton { + color: $accent-color !important; + } + + .mx_IconizedContextMenu_icon::before { + background-color: $accent-color; + } + } + + &.mx_IconizedContextMenu_compact { + .mx_IconizedContextMenu_optionList > * { + padding: 8px 16px 8px 11px; + } + } + + .mx_IconizedContextMenu_checked { + margin-left: 16px; + margin-right: -5px; + + &::before { + mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); + } + } +} diff --git a/res/css/views/context_menus/_MessageContextMenu.scss b/res/css/views/context_menus/_MessageContextMenu.scss index d15d566bdb..338841cce4 100644 --- a/res/css/views/context_menus/_MessageContextMenu.scss +++ b/res/css/views/context_menus/_MessageContextMenu.scss @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2021 Michael Weimann Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,15 +16,69 @@ limitations under the License. */ .mx_MessageContextMenu { - padding: 6px; -} -.mx_MessageContextMenu_field { - padding: 3px 6px 3px 6px; - cursor: pointer; - white-space: nowrap; -} + .mx_IconizedContextMenu_icon { + width: 16px; + height: 16px; + display: block; -.mx_MessageContextMenu_field.mx_MessageContextMenu_fieldSet { - font-weight: bold; + &::before { + content: ''; + width: 16px; + height: 16px; + display: block; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + } + } + + .mx_MessageContextMenu_iconCollapse::before { + mask-image: url('$(res)/img/element-icons/message/chevron-up.svg'); + } + + .mx_MessageContextMenu_iconReport::before { + mask-image: url('$(res)/img/element-icons/warning-badge.svg'); + } + + .mx_MessageContextMenu_iconLink::before { + mask-image: url('$(res)/img/element-icons/link.svg'); + } + + .mx_MessageContextMenu_iconPermalink::before { + mask-image: url('$(res)/img/element-icons/room/share.svg'); + } + + .mx_MessageContextMenu_iconUnhidePreview::before { + mask-image: url('$(res)/img/element-icons/settings/appearance.svg'); + } + + .mx_MessageContextMenu_iconForward::before { + mask-image: url('$(res)/img/element-icons/message/fwd.svg'); + } + + .mx_MessageContextMenu_iconRedact::before { + mask-image: url('$(res)/img/element-icons/trashcan.svg'); + } + + .mx_MessageContextMenu_iconResend::before { + mask-image: url('$(res)/img/element-icons/retry.svg'); + } + + .mx_MessageContextMenu_iconSource::before { + mask-image: url('$(res)/img/element-icons/room/format-bar/code.svg'); + } + + .mx_MessageContextMenu_iconQuote::before { + mask-image: url('$(res)/img/element-icons/room/format-bar/quote.svg'); + } + + .mx_MessageContextMenu_iconPin::before { + mask-image: url('$(res)/img/element-icons/room/pin-upright.svg'); + } + + .mx_MessageContextMenu_iconUnpin::before { + mask-image: url('$(res)/img/element-icons/room/pin.svg'); + } } diff --git a/res/css/views/context_menus/_RoomTileContextMenu.scss b/res/css/views/context_menus/_RoomTileContextMenu.scss deleted file mode 100644 index 9697ac9bef..0000000000 --- a/res/css/views/context_menus/_RoomTileContextMenu.scss +++ /dev/null @@ -1,114 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_RoomTileContextMenu { - padding: 6px; -} - -.mx_RoomTileContextMenu_tag_icon { - padding-right: 8px; - padding-left: 4px; - display: inline-block; -} - -.mx_RoomTileContextMenu_tag_icon_set { - padding-right: 8px; - padding-left: 4px; - display: none; -} - -.mx_RoomTileContextMenu_tag_field, .mx_RoomTileContextMenu_leave { - padding-top: 8px; - padding-right: 20px; - padding-bottom: 8px; - cursor: pointer; - white-space: nowrap; - display: flex; - align-items: center; - line-height: $font-16px; -} - -.mx_RoomTileContextMenu_tag_field.mx_RoomTileContextMenu_tag_fieldSet { - font-weight: bold; -} - -.mx_RoomTileContextMenu_tag_field.mx_RoomTileContextMenu_tag_fieldSet .mx_RoomTileContextMenu_tag_icon { - display: none; -} - -.mx_RoomTileContextMenu_tag_field.mx_RoomTileContextMenu_tag_fieldSet .mx_RoomTileContextMenu_tag_icon_set { - display: inline-block; -} - -.mx_RoomTileContextMenu_tag_field.mx_RoomTileContextMenu_tag_fieldDisabled { - color: rgba(0, 0, 0, 0.2); -} - -.mx_RoomTileContextMenu_separator { - margin-top: 0; - margin-bottom: 0; - border-bottom-style: none; - border-left-style: none; - border-right-style: none; - border-top-style: solid; - border-top-width: 1px; - border-color: $menu-border-color; -} - -.mx_RoomTileContextMenu_leave { - color: $warning-color; -} - -.mx_RoomTileContextMenu_notif_picker { - position: absolute; - top: 16px; - left: 5px; -} - -.mx_RoomTileContextMenu_notif_field { - padding-top: 4px; - padding-right: 6px; - padding-bottom: 10px; - padding-left: 8px; /* 20px */ - cursor: pointer; - white-space: nowrap; - display: flex; - align-items: center; -} - -.mx_RoomTileContextMenu_notif_field.mx_RoomTileContextMenu_notif_fieldSet { - font-weight: bold; -} - -.mx_RoomTileContextMenu_notif_field.mx_RoomTileContextMenu_notif_fieldDisabled { - color: rgba(0, 0, 0, 0.2); -} - -.mx_RoomTileContextMenu_notif_icon { - padding-right: 4px; - padding-left: 4px; -} - -.mx_RoomTileContextMenu_notif_activeIcon { - display: inline-block; - opacity: 0; - position: relative; - left: -5px; -} - -.mx_RoomTileContextMenu_notif_fieldSet .mx_RoomTileContextMenu_notif_activeIcon { - opacity: 1; -} diff --git a/res/css/views/context_menus/_TagTileContextMenu.scss b/res/css/views/context_menus/_TagTileContextMenu.scss index e4ccc030a2..d707f4ce7c 100644 --- a/res/css/views/context_menus/_TagTileContextMenu.scss +++ b/res/css/views/context_menus/_TagTileContextMenu.scss @@ -15,9 +15,8 @@ limitations under the License. */ .mx_TagTileContextMenu_item { - padding-top: 8px; + padding: 8px; padding-right: 20px; - padding-bottom: 8px; cursor: pointer; white-space: nowrap; display: flex; @@ -25,15 +24,31 @@ limitations under the License. line-height: $font-16px; } -.mx_TagTileContextMenu_item object { - pointer-events: none; +.mx_TagTileContextMenu_item::before { + content: ''; + height: 15px; + width: 15px; + background-color: currentColor; + mask-repeat: no-repeat; + mask-size: contain; + margin-right: 8px; } +.mx_TagTileContextMenu_viewCommunity::before { + mask-image: url('$(res)/img/element-icons/view-community.svg'); +} -.mx_TagTileContextMenu_item_icon { - padding-right: 8px; - padding-left: 4px; - display: inline-block; +.mx_TagTileContextMenu_moveUp::before { + transform: rotate(180deg); + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); +} + +.mx_TagTileContextMenu_moveDown::before { + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); +} + +.mx_TagTileContextMenu_hideCommunity::before { + mask-image: url('$(res)/img/element-icons/hide.svg'); } .mx_TagTileContextMenu_separator { diff --git a/res/css/views/context_menus/_TopLeftMenu.scss b/res/css/views/context_menus/_TopLeftMenu.scss deleted file mode 100644 index 973c306695..0000000000 --- a/res/css/views/context_menus/_TopLeftMenu.scss +++ /dev/null @@ -1,96 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_TopLeftMenu { - min-width: 210px; - border-radius: 4px; - - .mx_TopLeftMenu_greyedText { - font-size: $font-12px; - opacity: 0.5; - } - - .mx_TopLeftMenu_upgradeLink { - font-size: $font-12px; - - img { - margin-left: 5px; - } - } - - .mx_TopLeftMenu_section:not(:last-child) { - border-bottom: 1px solid $menu-border-color; - } - - .mx_TopLeftMenu_section_noIcon { - margin: 5px 0; - padding: 5px 20px 5px 15px; - - div:not(:first-child) { - margin-top: 5px; - } - } - - .mx_TopLeftMenu_section_withIcon { - margin: 5px 0; - padding: 0; - list-style: none; - - .mx_TopLeftMenu_icon_home::after { - mask-image: url('$(res)/img/feather-customised/home.svg'); - } - - .mx_TopLeftMenu_icon_help::after { - mask-image: url('$(res)/img/feather-customised/life-buoy.svg'); - } - - .mx_TopLeftMenu_icon_settings::after { - mask-image: url('$(res)/img/feather-customised/settings.svg'); - } - - .mx_TopLeftMenu_icon_signin::after { - mask-image: url('$(res)/img/feather-customised/sign-in.svg'); - } - - .mx_TopLeftMenu_icon_signout::after { - mask-image: url('$(res)/img/feather-customised/sign-out.svg'); - } - - .mx_AccessibleButton::after { - mask-repeat: no-repeat; - mask-position: 0 center; - mask-size: 16px; - position: absolute; - width: 16px; - height: 16px; - content: ""; - top: 5px; - left: 14px; - background-color: $primary-fg-color; - } - - .mx_AccessibleButton { - position: relative; - cursor: pointer; - white-space: nowrap; - padding: 5px 20px 5px 43px; - } - - .mx_AccessibleButton:hover { - background-color: $menu-selected-color; - } - } -} diff --git a/res/css/views/context_menus/_WidgetContextMenu.scss b/res/css/views/context_menus/_WidgetContextMenu.scss deleted file mode 100644 index 60b7b93f99..0000000000 --- a/res/css/views/context_menus/_WidgetContextMenu.scss +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundaction 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_WidgetContextMenu { - padding: 6px; - - .mx_WidgetContextMenu_option { - padding: 3px 6px 3px 6px; - cursor: pointer; - white-space: nowrap; - } - - .mx_WidgetContextMenu_separator { - margin-top: 0; - margin-bottom: 0; - border-bottom-style: none; - border-left-style: none; - border-right-style: none; - border-top-style: solid; - border-top-width: 1px; - border-color: $menu-border-color; - } -} diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss new file mode 100644 index 0000000000..2776c477fc --- /dev/null +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -0,0 +1,281 @@ +/* +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_AddExistingToSpaceDialog_wrapper { + .mx_Dialog { + display: flex; + flex-direction: column; + } +} + +.mx_AddExistingToSpace { + .mx_SearchBox { + // To match the space around the title + margin: 0 0 15px 0; + flex-grow: 0; + } + + .mx_AddExistingToSpace_content { + flex-grow: 1; + } + + .mx_AddExistingToSpace_noResults { + display: block; + margin-top: 24px; + } + + .mx_AddExistingToSpace_section { + &:not(:first-child) { + margin-top: 24px; + } + + > h3 { + margin: 0; + color: $secondary-fg-color; + font-size: $font-12px; + font-weight: $font-semi-bold; + line-height: $font-15px; + } + + .mx_AddExistingToSpace_entry { + display: flex; + margin-top: 12px; + + // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling + .mx_DecoratedRoomAvatar { + margin-right: 12px; + } + + .mx_AddExistingToSpace_entry_name { + font-size: $font-15px; + line-height: 30px; + flex-grow: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin-right: 12px; + } + + .mx_Checkbox { + align-items: center; + } + } + } + + .mx_AddExistingToSpace_section_spaces { + .mx_BaseAvatar { + margin-right: 12px; + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } + } + + .mx_AddExistingToSpace_section_experimental { + position: relative; + border-radius: 8px; + margin: 12px 0; + padding: 8px 8px 8px 42px; + background-color: $header-panel-bg-color; + + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + + &::before { + content: ''; + position: absolute; + left: 10px; + top: calc(50% - 8px); // vertical centering + height: 16px; + width: 16px; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; + } + } + + .mx_AddExistingToSpace_footer { + display: flex; + margin-top: 20px; + + > span { + flex-grow: 1; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + + .mx_ProgressBar { + height: 8px; + width: 100%; + + @mixin ProgressBarBorderRadius 8px; + } + + .mx_AddExistingToSpace_progressText { + margin-top: 8px; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; + } + + > * { + vertical-align: middle; + } + } + + .mx_AddExistingToSpace_error { + padding-left: 12px; + + > img { + align-self: center; + } + + .mx_AddExistingToSpace_errorHeading { + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + color: $notice-primary-color; + } + + .mx_AddExistingToSpace_errorCaption { + margin-top: 4px; + font-size: $font-12px; + line-height: $font-15px; + color: $primary-fg-color; + } + } + + .mx_AccessibleButton { + display: inline-block; + align-self: center; + } + + .mx_AccessibleButton_kind_primary { + padding: 8px 36px; + } + + .mx_AddExistingToSpace_retryButton { + margin-left: 12px; + padding-left: 24px; + position: relative; + + &::before { + content: ''; + position: absolute; + background-color: $primary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/retry.svg'); + width: 18px; + height: 18px; + left: 0; + } + } + + .mx_AccessibleButton_kind_link { + padding: 0; + } + } +} + +.mx_AddExistingToSpaceDialog { + width: 480px; + color: $primary-fg-color; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-height: 0; + height: 80vh; + + .mx_Dialog_title { + display: flex; + + .mx_BaseAvatar_image { + border-radius: 8px; + margin: 0; + vertical-align: unset; + } + + .mx_BaseAvatar { + display: inline-flex; + margin: auto 16px auto 5px; + vertical-align: middle; + } + + > div { + > h1 { + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + margin: 0; + } + + .mx_AddExistingToSpaceDialog_onlySpace { + color: $secondary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + } + } + + .mx_Dropdown_input { + border: none; + + > .mx_Dropdown_option { + padding-left: 0; + flex: unset; + height: unset; + color: $secondary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + + .mx_BaseAvatar { + display: none; + } + } + + .mx_Dropdown_menu { + .mx_AddExistingToSpaceDialog_dropdownOptionActive { + color: $accent-color; + padding-right: 32px; + position: relative; + + &::before { + content: ''; + width: 20px; + height: 20px; + top: 8px; + right: 0; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background-color: $accent-color; + mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); + } + } + } + } + } + + .mx_AddExistingToSpace { + display: contents; + } +} diff --git a/res/css/views/dialogs/_BetaFeedbackDialog.scss b/res/css/views/dialogs/_BetaFeedbackDialog.scss new file mode 100644 index 0000000000..9f5f6b512e --- /dev/null +++ b/res/css/views/dialogs/_BetaFeedbackDialog.scss @@ -0,0 +1,30 @@ +/* +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_BetaFeedbackDialog { + .mx_BetaFeedbackDialog_subheading { + color: $primary-fg-color; + font-size: $font-14px; + line-height: $font-20px; + margin-bottom: 24px; + } + + .mx_AccessibleButton_kind_link { + padding: 0; + font-size: inherit; + line-height: inherit; + } +} diff --git a/res/css/views/dialogs/_BugReportDialog.scss b/res/css/views/dialogs/_BugReportDialog.scss new file mode 100644 index 0000000000..1920ac33ea --- /dev/null +++ b/res/css/views/dialogs/_BugReportDialog.scss @@ -0,0 +1,23 @@ +/* +Copyright 2020 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_BugReportDialog { + .mx_BugReportDialog_download { + .mx_AccessibleButton_kind_link { + padding-left: 0; + } + } +} diff --git a/res/css/views/dialogs/_CommunityPrototypeInviteDialog.scss b/res/css/views/dialogs/_CommunityPrototypeInviteDialog.scss new file mode 100644 index 0000000000..beae03f00f --- /dev/null +++ b/res/css/views/dialogs/_CommunityPrototypeInviteDialog.scss @@ -0,0 +1,88 @@ +/* +Copyright 2020 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_CommunityPrototypeInviteDialog { + &.mx_Dialog_fixedWidth { + width: 360px; + } + + .mx_Dialog_content { + margin-bottom: 0; + + .mx_CommunityPrototypeInviteDialog_people { + position: relative; + margin-bottom: 4px; + + .mx_AccessibleButton { + display: inline-block; + background-color: $focus-bg-color; // XXX: Abuse of variables + border-radius: 4px; + padding: 3px 5px; + font-size: $font-12px; + float: right; + } + } + + .mx_CommunityPrototypeInviteDialog_morePeople { + margin-top: 8px; + } + + .mx_CommunityPrototypeInviteDialog_person { + position: relative; + margin-top: 4px; + + & > * { + vertical-align: middle; + } + + .mx_Checkbox { + position: absolute; + right: 0; + top: calc(50% - 8px); // checkbox is 16px high + width: 16px; // to force a square + } + + .mx_CommunityPrototypeInviteDialog_personIdentifiers { + display: inline-block; + + & > * { + display: block; + } + + .mx_CommunityPrototypeInviteDialog_personName { + font-weight: 600; + font-size: $font-14px; + color: $primary-fg-color; + margin-left: 7px; + } + + .mx_CommunityPrototypeInviteDialog_personId { + font-size: $font-12px; + color: $muted-fg-color; + margin-left: 7px; + } + } + } + + .mx_CommunityPrototypeInviteDialog_primaryButton { + display: block; + font-size: $font-13px; + line-height: 20px; + height: 20px; + margin-top: 24px; + } + } +} diff --git a/res/css/views/dialogs/_CreateCommunityPrototypeDialog.scss b/res/css/views/dialogs/_CreateCommunityPrototypeDialog.scss new file mode 100644 index 0000000000..81babc4c38 --- /dev/null +++ b/res/css/views/dialogs/_CreateCommunityPrototypeDialog.scss @@ -0,0 +1,102 @@ +/* +Copyright 2020 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_CreateCommunityPrototypeDialog { + .mx_Dialog_content { + display: flex; + flex-direction: row; + margin-bottom: 12px; + + .mx_CreateCommunityPrototypeDialog_colName { + flex-basis: 66.66%; + padding-right: 100px; + + .mx_Field input { + font-size: $font-16px; + line-height: $font-20px; + } + + .mx_CreateCommunityPrototypeDialog_subtext { + display: block; + color: $muted-fg-color; + margin-bottom: 16px; + + &:last-child { + margin-top: 16px; + } + + &.mx_CreateCommunityPrototypeDialog_subtext_error { + color: $warning-color; + } + } + + .mx_CreateCommunityPrototypeDialog_communityId { + position: relative; + + .mx_InfoTooltip { + float: right; + } + } + + .mx_AccessibleButton { + display: block; + height: 32px; + font-size: $font-16px; + line-height: 32px; + } + } + + .mx_CreateCommunityPrototypeDialog_colAvatar { + flex-basis: 33.33%; + + .mx_CreateCommunityPrototypeDialog_avatarContainer { + margin-top: 12px; + margin-bottom: 20px; + + .mx_CreateCommunityPrototypeDialog_avatar, + .mx_CreateCommunityPrototypeDialog_placeholderAvatar { + width: 96px; + height: 96px; + border-radius: 96px; + } + + .mx_CreateCommunityPrototypeDialog_placeholderAvatar { + background-color: #368bd6; // hardcoded for both themes + + &::before { + display: inline-block; + background-color: #fff; // hardcoded because the background is + mask-repeat: no-repeat; + mask-size: 96px; + width: 96px; + height: 96px; + mask-position: center; + content: ''; + vertical-align: middle; + mask-image: url('$(res)/img/element-icons/add-photo.svg'); + } + } + } + + .mx_CreateCommunityPrototypeDialog_tip { + & > b, & > span { + display: block; + color: $muted-fg-color; + } + } + } + } +} diff --git a/res/css/views/dialogs/_DeviceVerifyDialog.scss b/res/css/views/dialogs/_DeviceVerifyDialog.scss deleted file mode 100644 index 1997e0c21d..0000000000 --- a/res/css/views/dialogs/_DeviceVerifyDialog.scss +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2019 New Vector Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_DeviceVerifyDialog_cryptoSection ul { - display: table; -} - -.mx_DeviceVerifyDialog_cryptoSection li { - display: table-row; -} - -.mx_DeviceVerifyDialog_cryptoSection label, -.mx_DeviceVerifyDialog_cryptoSection span { - display: table-cell; - padding-right: 1em; -} diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss index 35cb6bc7ab..8fee740016 100644 --- a/res/css/views/dialogs/_DevtoolsDialog.scss +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -223,3 +223,54 @@ limitations under the License. content: ":"; } } + +.mx_DevTools_SettingsExplorer { + table { + width: 100%; + table-layout: fixed; + border-collapse: collapse; + + th { + // Colour choice: first one autocomplete gave me. + border-bottom: 1px solid $accent-color; + text-align: left; + } + + td, th { + width: 360px; // "feels right" number + + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + td + td, th + th { + width: auto; + } + + tr:hover { + // Colour choice: first one autocomplete gave me. + background-color: $accent-color-50pct; + } + } + + .mx_DevTools_SettingsExplorer_mutable { + background-color: $accent-color; + } + + .mx_DevTools_SettingsExplorer_immutable { + background-color: $warning-color; + } + + .mx_DevTools_SettingsExplorer_edit { + float: right; + margin-right: 16px; + } + + .mx_DevTools_SettingsExplorer_warning { + border: 2px solid $warning-color; + border-radius: 4px; + padding: 4px; + margin-bottom: 8px; + } +} diff --git a/res/css/views/dialogs/_EditCommunityPrototypeDialog.scss b/res/css/views/dialogs/_EditCommunityPrototypeDialog.scss new file mode 100644 index 0000000000..75a56bf6b3 --- /dev/null +++ b/res/css/views/dialogs/_EditCommunityPrototypeDialog.scss @@ -0,0 +1,77 @@ +/* +Copyright 2020 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. +*/ + +// XXX: many of these styles are shared with the create dialog +.mx_EditCommunityPrototypeDialog { + &.mx_Dialog_fixedWidth { + width: 360px; + } + + .mx_Dialog_content { + margin-bottom: 12px; + + .mx_AccessibleButton.mx_AccessibleButton_kind_primary { + display: block; + height: 32px; + font-size: $font-16px; + line-height: 32px; + } + + .mx_EditCommunityPrototypeDialog_rowAvatar { + display: flex; + flex-direction: row; + align-items: center; + } + + .mx_EditCommunityPrototypeDialog_avatarContainer { + margin-top: 20px; + margin-bottom: 20px; + + .mx_EditCommunityPrototypeDialog_avatar, + .mx_EditCommunityPrototypeDialog_placeholderAvatar { + width: 96px; + height: 96px; + border-radius: 96px; + } + + .mx_EditCommunityPrototypeDialog_placeholderAvatar { + background-color: #368bd6; // hardcoded for both themes + + &::before { + display: inline-block; + background-color: #fff; // hardcoded because the background is + mask-repeat: no-repeat; + mask-size: 96px; + width: 96px; + height: 96px; + mask-position: center; + content: ''; + vertical-align: middle; + mask-image: url('$(res)/img/element-icons/add-photo.svg'); + } + } + } + + .mx_EditCommunityPrototypeDialog_tip { + margin-left: 20px; + + & > b, & > span { + display: block; + color: $muted-fg-color; + } + } + } +} diff --git a/res/css/views/dialogs/_EncryptedEventDialog.scss b/res/css/views/dialogs/_EncryptedEventDialog.scss deleted file mode 100644 index ff73df509d..0000000000 --- a/res/css/views/dialogs/_EncryptedEventDialog.scss +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_EncryptedEventDialog .mx_DeviceVerifyButtons { - float: right; - padding: 0px; - margin-right: 42px; - display: flex; - flex-wrap: wrap; - justify-content: space-between; -} - -.mx_EncryptedEventDialog .mx_MemberDeviceInfo_textButton { - @mixin mx_DialogButton; - background-color: $primary-bg-color; - color: $accent-color; -} - -.mx_EncryptedEventDialog button { - margin-top: 0px; -} diff --git a/res/css/views/dialogs/_FeedbackDialog.scss b/res/css/views/dialogs/_FeedbackDialog.scss new file mode 100644 index 0000000000..fd225dd882 --- /dev/null +++ b/res/css/views/dialogs/_FeedbackDialog.scss @@ -0,0 +1,121 @@ +/* +Copyright 2020 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_FeedbackDialog { + hr { + margin: 24px 0; + border-color: $input-border-color; + } + + .mx_Dialog_content { + margin-bottom: 24px; + + > h2 { + margin-bottom: 32px; + } + } + + .mx_FeedbackDialog_section { + position: relative; + padding-left: 52px; + + > p { + color: $tertiary-fg-color; + } + + .mx_AccessibleButton_kind_link { + padding: 0; + font-size: inherit; + } + + a, .mx_AccessibleButton_kind_link { + color: $accent-color; + text-decoration: underline; + } + + &::before, &::after { + content: ""; + position: absolute; + width: 40px; + height: 40px; + left: 0; + top: 0; + } + + &::before { + background-color: $icon-button-color; + border-radius: 20px; + } + + &::after { + background: $avatar-initial-color; // TODO + mask-position: center; + mask-size: 24px; + mask-repeat: no-repeat; + } + } + + .mx_FeedbackDialog_reportBug { + &::after { + mask-image: url('$(res)/img/feather-customised/bug.svg'); + } + } + + .mx_FeedbackDialog_rateApp { + .mx_RadioButton { + display: inline-flex; + font-size: 20px; + transition: font-size 1s, border .5s; + border-radius: 50%; + border: 2px solid transparent; + margin-top: 12px; + margin-bottom: 24px; + vertical-align: top; + cursor: pointer; + + input[type="radio"] + div { + display: none; + } + + .mx_RadioButton_content { + background: $icon-button-color; + width: 40px; + height: 40px; + text-align: center; + line-height: 40px; + border-radius: 20px; + margin: 5px; + } + + .mx_RadioButton_spacer { + display: none; + } + + & + .mx_RadioButton { + margin-left: 16px; + } + } + + .mx_RadioButton_checked { + font-size: 24px; + border-color: $accent-color; + } + + &::after { + mask-image: url('$(res)/img/element-icons/feedback.svg'); + } + } +} diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss new file mode 100644 index 0000000000..95d7ce74c4 --- /dev/null +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -0,0 +1,159 @@ +/* +Copyright 2021 Robin Townsend + +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_ForwardDialog { + width: 520px; + color: $primary-fg-color; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-height: 0; + height: 80vh; + + > h3 { + margin: 0 0 6px; + color: $secondary-fg-color; + font-size: $font-12px; + font-weight: $font-semi-bold; + line-height: $font-15px; + } + + > .mx_ForwardDialog_preview { + max-height: 30%; + flex-shrink: 0; + overflow-y: auto; + + div { + 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_MFileBody_download { + display: none; + } + } + + > hr { + width: 100%; + border: none; + border-top: 1px solid $input-border-color; + margin: 12px 0; + } + + > .mx_ForwardList { + display: contents; + + .mx_SearchBox { + // To match the space around the title + margin: 0 0 15px 0; + flex-grow: 0; + } + + .mx_ForwardList_content { + flex-grow: 1; + } + + .mx_ForwardList_noResults { + display: block; + margin-top: 24px; + } + + .mx_ForwardList_results { + &:not(:first-child) { + margin-top: 24px; + } + + .mx_ForwardList_entry { + display: flex; + justify-content: space-between; + height: 32px; + padding: 6px; + border-radius: 8px; + + &:hover { + background-color: $groupFilterPanel-bg-color; + } + + .mx_ForwardList_roomButton { + display: flex; + margin-right: 12px; + min-width: 0; + + .mx_DecoratedRoomAvatar { + margin-right: 12px; + } + + .mx_ForwardList_entry_name { + font-size: $font-15px; + line-height: 30px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin-right: 12px; + } + } + + .mx_ForwardList_sendButton { + position: relative; + + &:not(.mx_ForwardList_canSend) .mx_ForwardList_sendLabel { + // Hide the "Send" label while preserving button size + visibility: hidden; + } + + .mx_ForwardList_sendIcon, .mx_NotificationBadge { + position: absolute; + } + + .mx_NotificationBadge { + // Match the failed to send indicator's color with the disabled button + background-color: $button-danger-disabled-fg-color; + } + + &.mx_ForwardList_sending .mx_ForwardList_sendIcon { + background-color: $button-primary-bg-color; + mask-image: url('$(res)/img/element-icons/circle-sending.svg'); + mask-position: center; + mask-repeat: no-repeat; + mask-size: 14px; + width: 14px; + height: 14px; + } + + &.mx_ForwardList_sent .mx_ForwardList_sendIcon { + background-color: $button-primary-bg-color; + mask-image: url('$(res)/img/element-icons/circle-sent.svg'); + mask-position: center; + mask-repeat: no-repeat; + mask-size: 14px; + width: 14px; + height: 14px; + } + } + } + } + } +} diff --git a/res/css/views/dialogs/_GroupAddressPicker.scss b/res/css/views/dialogs/_GroupAddressPicker.scss index 20a7cc1047..5fa18931f0 100644 --- a/res/css/views/dialogs/_GroupAddressPicker.scss +++ b/res/css/views/dialogs/_GroupAddressPicker.scss @@ -18,8 +18,3 @@ limitations under the License. margin-top: 10px; display: flex; } - -.mx_GroupAddressPicker_checkboxContainer input[type="checkbox"] { - /* Stop flex from shrinking the checkbox */ - width: 20px; -} diff --git a/res/css/views/dialogs/_HostSignupDialog.scss b/res/css/views/dialogs/_HostSignupDialog.scss new file mode 100644 index 0000000000..ac4bc41951 --- /dev/null +++ b/res/css/views/dialogs/_HostSignupDialog.scss @@ -0,0 +1,143 @@ +/* +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_HostSignupDialog { + width: 90vw; + max-width: 580px; + height: 80vh; + max-height: 600px; + // Ensure dialog borders are always white as the HostSignupDialog + // does not yet support dark mode or theming in general. + // In the future we might want to pass the theme to the called + // iframe, should some hosting provider have that need. + background-color: #ffffff; + + .mx_HostSignupDialog_info { + text-align: center; + + .mx_HostSignupDialog_content_top { + margin-bottom: 24px; + } + + .mx_HostSignupDialog_paragraphs { + text-align: left; + padding-left: 25%; + padding-right: 25%; + } + + .mx_HostSignupDialog_buttons { + margin-bottom: 24px; + display: flex; + justify-content: center; + + button { + padding: 12px; + margin: 0 16px; + } + } + + .mx_HostSignupDialog_footer { + display: flex; + justify-content: center; + align-items: baseline; + + img { + padding-right: 5px; + } + } + } + + iframe { + width: 100%; + height: 100%; + border: none; + background-color: #fff; + min-height: 540px; + } +} + +.mx_HostSignupDialog_text_dark { + color: $primary-fg-color; +} + +.mx_HostSignupDialog_text_light { + color: $secondary-fg-color; +} + +.mx_HostSignup_maximize_button { + mask: url('$(res)/img/feather-customised/maximise.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: cover; + width: 14px; + height: 14px; + background-color: $dialog-close-fg-color; + cursor: pointer; + position: absolute; + top: 10px; + right: 10px; +} + +.mx_HostSignup_minimize_button { + mask: url('$(res)/img/feather-customised/minimise.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: cover; + width: 14px; + height: 14px; + background-color: $dialog-close-fg-color; + cursor: pointer; + position: absolute; + top: 10px; + right: 25px; +} + +.mx_HostSignup_persisted { + width: 90vw; + max-width: 580px; + height: 80vh; + max-height: 600px; + top: 0; + left: 0; + position: fixed; + display: none; +} + +.mx_HostSignupDialog_minimized { + position: fixed; + bottom: 80px; + right: 26px; + width: 314px; + height: 217px; + overflow: hidden; + + &.mx_Dialog { + padding: 12px; + } + + .mx_Dialog_title { + text-align: left !important; + padding-left: 20px; + font-size: $font-15px; + } + + iframe { + width: 100%; + height: 100%; + border: none; + background-color: #fff; + } +} diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index a77d0bfbba..9fc4b7a15c 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -14,9 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_InviteDialog_transferWrapper .mx_Dialog { + padding-bottom: 16px; +} + .mx_InviteDialog_addressBar { display: flex; flex-direction: row; + // Right margin for the design. We could apply this to the whole dialog, but then the scrollbar + // for the user section gets weird. + margin: 8px 45px 0 0; .mx_InviteDialog_editor { flex: 1; @@ -27,37 +34,29 @@ limitations under the License. padding-left: 8px; overflow-x: hidden; overflow-y: auto; + display: flex; + flex-wrap: wrap; .mx_InviteDialog_userTile { + margin: 6px 6px 0 0; display: inline-block; - float: left; - position: relative; - top: 7px; + min-width: max-content; // prevent manipulation by flexbox } - // Using a textarea for this element, to circumvent autofill - // Mostly copied from AddressPickerDialog - textarea, - textarea:focus { - height: 34px; - line-height: $font-34px; + // Mostly copied from AddressPickerDialog; overrides bunch of our default text input styles + > input[type="text"] { + margin: 6px 0 !important; + height: 24px; + line-height: $font-24px; font-size: $font-14px; padding-left: 12px; - margin: 0 !important; border: 0 !important; outline: 0 !important; resize: none; - overflow: hidden; box-sizing: border-box; - word-wrap: nowrap; - - // Roughly fill about 2/5ths of the available space. This is to try and 'fill' the - // remaining space after a bunch of pills, but is a bit hacky. Ideally we'd have - // support for "fill remaining width", but traditional tricks don't work with what - // we're pushing into this "field". Flexbox just makes things worse. The theory is - // that users won't need more than about 2/5ths of the input to find the person - // they're looking for. - width: 40%; + min-width: 40%; + flex: 1 !important; + color: $primary-fg-color !important; } } @@ -81,7 +80,7 @@ limitations under the License. } .mx_InviteDialog_section { - padding-bottom: 10px; + padding-bottom: 4px; h3 { font-size: $font-12px; @@ -89,6 +88,78 @@ limitations under the License. font-weight: bold; text-transform: uppercase; } + + > p { + margin: 0; + } + + > span { + color: $primary-fg-color; + } + + .mx_InviteDialog_subname { + margin-bottom: 10px; + margin-top: -10px; // HACK: Positioning with margins is bad + font-size: $font-12px; + color: $muted-fg-color; + } +} + +.mx_InviteDialog_section_hidden_suggestions_disclaimer { + padding: 8px 0 16px 0; + font-size: $font-14px; + + > span { + color: $primary-fg-color; + font-weight: 600; + } + + > p { + margin: 0; + } +} + +.mx_InviteDialog_footer { + border-top: 1px solid $input-border-color; + + > h3 { + margin: 12px 0; + font-size: $font-12px; + color: $muted-fg-color; + font-weight: bold; + text-transform: uppercase; + } + + .mx_InviteDialog_footer_link { + display: flex; + justify-content: space-between; + border-radius: 4px; + border: solid 1px $light-fg-color; + padding: 8px; + + > a { + text-decoration: none; + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .mx_InviteDialog_footer_link_copy { + flex-shrink: 0; + cursor: pointer; + margin-left: 20px; + display: inherit; + + > div { + mask-image: url($copy-button-url); + background-color: $message-action-bar-fg-color; + margin-left: 5px; + width: 20px; + height: 20px; + background-repeat: no-repeat; + } + } } .mx_InviteDialog_roomTile { @@ -141,6 +212,11 @@ limitations under the License. } } + .mx_InviteDialog_roomTile_nameStack { + display: inline-block; + overflow: hidden; + } + .mx_InviteDialog_roomTile_name { font-weight: 600; font-size: $font-14px; @@ -154,6 +230,13 @@ limitations under the License. margin-left: 7px; } + .mx_InviteDialog_roomTile_name, + .mx_InviteDialog_roomTile_userId { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .mx_InviteDialog_roomTile_time { text-align: right; font-size: $font-12px; @@ -207,22 +290,165 @@ limitations under the License. } } -.mx_InviteDialog { +.mx_InviteDialog_other { // Prevent the dialog from jumping around randomly when elements change. - height: 590px; + height: 600px; padding-left: 20px; // the design wants some padding on the left + + .mx_InviteDialog_userSections { + height: calc(100% - 115px); // mx_InviteDialog's height minus some for the upper and lower elements + } +} + +.mx_InviteDialog_content { + height: calc(100% - 36px); // full height minus the size of the header + overflow: hidden; +} + +.mx_InviteDialog_transfer { + width: 496px; + height: 466px; + flex-direction: column; + + .mx_InviteDialog_content { + flex-direction: column; + + .mx_TabbedView { + height: calc(100% - 60px); + } + overflow: visible; + } + + .mx_InviteDialog_addressBar { + margin-top: 8px; + } + + input[type="checkbox"] { + margin-right: 8px; + } } .mx_InviteDialog_userSections { - margin-top: 10px; + margin-top: 4px; overflow-y: auto; - padding-right: 45px; - height: 455px; // mx_InviteDialog's height minus some for the upper elements + padding: 0 45px 4px 0; } -// Right margin for the design. We could apply this to the whole dialog, but then the scrollbar -// for the user section gets weird. -.mx_InviteDialog_helpText, -.mx_InviteDialog_addressBar { - margin-right: 45px; +.mx_InviteDialog_hasFooter .mx_InviteDialog_userSections { + height: calc(100% - 175px); +} + +.mx_InviteDialog_helpText { + margin: 0; +} + +.mx_InviteDialog_helpText .mx_AccessibleButton_kind_link { + padding: 0; +} + +.mx_InviteDialog_dialPad .mx_InviteDialog_dialPadField { + border-top: 0; + border-left: 0; + border-right: 0; + border-radius: 0; + margin-top: 0; + border-color: $quaternary-fg-color; + + input { + font-size: 18px; + font-weight: 600; + padding-top: 0; + } +} + +.mx_InviteDialog_dialPad .mx_InviteDialog_dialPadField:focus-within { + border-color: $accent-color; +} + +.mx_InviteDialog_dialPadField .mx_Field_postfix { + /* Remove border separator between postfix and field content */ + border-left: none; +} + +.mx_InviteDialog_dialPad { + width: 224px; + margin-top: 16px; + margin-left: auto; + margin-right: auto; +} + +.mx_InviteDialog_dialPad .mx_DialPad { + row-gap: 16px; + column-gap: 48px; + + margin-left: auto; + margin-right: auto; +} + +.mx_InviteDialog_transferConsultConnect { + padding-top: 16px; + /* This wants a drop shadow the full width of the dialog, so relative-position it + * and make it wider, then compensate with padding + */ + position: relative; + width: 496px; + left: -24px; + padding-left: 24px; + padding-right: 24px; + border-top: 1px solid $message-body-panel-bg-color; + + display: flex; + flex-direction: row; + align-items: center; +} + +.mx_InviteDialog_transferConsultConnect_pushRight { + margin-left: auto; +} + +.mx_InviteDialog_userDirectoryIcon::before { + mask-image: url('$(res)/img/voip/tab-userdirectory.svg'); +} + +.mx_InviteDialog_dialPadIcon::before { + mask-image: url('$(res)/img/voip/tab-dialpad.svg'); +} + +.mx_InviteDialog_multiInviterError { + > h4 { + font-size: $font-15px; + line-height: $font-24px; + color: $secondary-fg-color; + font-weight: normal; + } + + > div { + .mx_InviteDialog_multiInviterError_entry { + margin-bottom: 24px; + + .mx_InviteDialog_multiInviterError_entry_userProfile { + .mx_InviteDialog_multiInviterError_entry_name { + margin-left: 6px; + font-size: $font-15px; + line-height: $font-24px; + font-weight: $font-semi-bold; + color: $primary-fg-color; + } + + .mx_InviteDialog_multiInviterError_entry_userId { + margin-left: 6px; + font-size: $font-12px; + line-height: $font-15px; + color: $tertiary-fg-color; + } + } + + .mx_InviteDialog_multiInviterError_entry_error { + margin-left: 32px; + font-size: $font-15px; + line-height: $font-24px; + color: $notice-primary-color; + } + } + } } diff --git a/res/css/views/dialogs/_ModalWidgetDialog.scss b/res/css/views/dialogs/_ModalWidgetDialog.scss new file mode 100644 index 0000000000..aa2dd0d395 --- /dev/null +++ b/res/css/views/dialogs/_ModalWidgetDialog.scss @@ -0,0 +1,42 @@ +/* +Copyright 2020 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_ModalWidgetDialog { + .mx_ModalWidgetDialog_warning { + margin-bottom: 24px; + + > img { + vertical-align: middle; + margin-right: 8px; + } + } + + .mx_ModalWidgetDialog_buttons { + float: right; + margin-top: 24px; + + .mx_AccessibleButton + .mx_AccessibleButton { + margin-left: 8px; + } + } + + iframe { + width: 100%; + height: 450px; + border: 0; + border-radius: 8px; + } +} diff --git a/res/css/views/dialogs/_RegistrationEmailPromptDialog.scss b/res/css/views/dialogs/_RegistrationEmailPromptDialog.scss new file mode 100644 index 0000000000..31fc6d7a04 --- /dev/null +++ b/res/css/views/dialogs/_RegistrationEmailPromptDialog.scss @@ -0,0 +1,28 @@ +/* +Copyright 2020 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_RegistrationEmailPromptDialog { + width: 417px; + + .mx_Dialog_content { + margin-bottom: 24px; + color: $tertiary-fg-color; + } + + .mx_Dialog_primary { + width: 100%; + } +} diff --git a/res/css/views/dialogs/_RoomSettingsDialog.scss b/res/css/views/dialogs/_RoomSettingsDialog.scss index 3751c15643..9bcde6e1e0 100644 --- a/res/css/views/dialogs/_RoomSettingsDialog.scss +++ b/res/css/views/dialogs/_RoomSettingsDialog.scss @@ -18,19 +18,19 @@ limitations under the License. // ========================================================== .mx_RoomSettingsDialog_settingsIcon::before { - mask-image: url('$(res)/img/feather-customised/settings.svg'); + mask-image: url('$(res)/img/element-icons/settings.svg'); } .mx_RoomSettingsDialog_securityIcon::before { - mask-image: url('$(res)/img/feather-customised/lock.svg'); + mask-image: url('$(res)/img/element-icons/security.svg'); } .mx_RoomSettingsDialog_rolesIcon::before { - mask-image: url('$(res)/img/feather-customised/users-sm.svg'); + mask-image: url('$(res)/img/element-icons/room/settings/roles.svg'); } .mx_RoomSettingsDialog_notificationsIcon::before { - mask-image: url('$(res)/img/feather-customised/notifications.svg'); + mask-image: url('$(res)/img/element-icons/notifications.svg'); } .mx_RoomSettingsDialog_bridgesIcon::before { @@ -39,7 +39,7 @@ limitations under the License. } .mx_RoomSettingsDialog_warningIcon::before { - mask-image: url('$(res)/img/feather-customised/warning-triangle.svg'); + mask-image: url('$(res)/img/element-icons/room/settings/advanced.svg'); } .mx_RoomSettingsDialog .mx_Dialog_title { @@ -48,7 +48,6 @@ limitations under the License. white-space: nowrap; overflow: hidden; margin: 0 auto; - padding-left: 40px; padding-right: 80px; } diff --git a/res/css/views/dialogs/_RoomSettingsDialogBridges.scss b/res/css/views/dialogs/_RoomSettingsDialogBridges.scss index a1793cc75e..c97a3b69b7 100644 --- a/res/css/views/dialogs/_RoomSettingsDialogBridges.scss +++ b/res/css/views/dialogs/_RoomSettingsDialogBridges.scss @@ -89,24 +89,18 @@ limitations under the License. } } - .mx_showMore { - display: block; - text-align: left; - margin-top: 10px; - } - .metadata { color: $muted-fg-color; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; margin-bottom: 0; - } - - .metadata.visible { overflow-y: visible; text-overflow: ellipsis; white-space: normal; + padding: 0; + + > li { + padding: 0; + border: 0; + } } } } diff --git a/res/css/views/dialogs/_ServerOfflineDialog.scss b/res/css/views/dialogs/_ServerOfflineDialog.scss new file mode 100644 index 0000000000..ae4b70beb3 --- /dev/null +++ b/res/css/views/dialogs/_ServerOfflineDialog.scss @@ -0,0 +1,72 @@ +/* +Copyright 2020 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_ServerOfflineDialog { + .mx_ServerOfflineDialog_content { + padding-right: 85px; + color: $primary-fg-color; + + hr { + border-color: $primary-fg-color; + opacity: 0.1; + border-bottom: none; + } + + ul { + padding: 16px; + + li:nth-child(n + 2) { + margin-top: 16px; + } + } + + .mx_ServerOfflineDialog_content_context { + .mx_ServerOfflineDialog_content_context_timestamp { + display: inline-block; + width: 115px; + color: $muted-fg-color; + line-height: 24px; // same as avatar + vertical-align: top; + } + + .mx_ServerOfflineDialog_content_context_timeline { + display: inline-block; + width: calc(100% - 155px); // 115px timestamp width + 40px right margin + + .mx_ServerOfflineDialog_content_context_timeline_header { + span { + margin-left: 8px; + vertical-align: middle; + } + } + + .mx_ServerOfflineDialog_content_context_txn { + position: relative; + margin-top: 8px; + + .mx_ServerOfflineDialog_content_context_txn_desc { + width: calc(100% - 100px); // 100px is an arbitrary margin for the button + } + + .mx_AccessibleButton { + float: right; + padding: 0; + } + } + } + } + } +} diff --git a/res/css/views/dialogs/_ServerPickerDialog.scss b/res/css/views/dialogs/_ServerPickerDialog.scss new file mode 100644 index 0000000000..b01b49d7af --- /dev/null +++ b/res/css/views/dialogs/_ServerPickerDialog.scss @@ -0,0 +1,78 @@ +/* +Copyright 2020 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_ServerPickerDialog { + width: 468px; + box-sizing: border-box; + + .mx_Dialog_content { + margin-bottom: 0; + + > p { + color: $secondary-fg-color; + font-size: $font-14px; + margin: 16px 0; + + &:first-of-type { + margin-bottom: 40px; + } + + &:last-of-type { + margin: 0 24px 24px; + } + } + + > h4 { + font-size: $font-15px; + font-weight: $font-semi-bold; + color: $secondary-fg-color; + margin-left: 8px; + } + + > a { + color: $accent-color; + margin-left: 8px; + } + } + + .mx_ServerPickerDialog_otherHomeserverRadio { + input[type="radio"] + div { + margin-top: auto; + margin-bottom: auto; + } + } + + .mx_ServerPickerDialog_otherHomeserver { + border-top: none; + border-left: none; + border-right: none; + border-radius: unset; + + > input { + padding-left: 0; + } + + > label { + margin-left: 0; + } + } + + .mx_AccessibleButton_kind_primary { + width: calc(100% - 64px); + margin: 0 8px; + padding: 15px 18px; + } +} diff --git a/res/css/views/dialogs/_SetMxIdDialog.scss b/res/css/views/dialogs/_SetMxIdDialog.scss deleted file mode 100644 index 1df34f3408..0000000000 --- a/res/css/views/dialogs/_SetMxIdDialog.scss +++ /dev/null @@ -1,50 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_SetMxIdDialog .mx_Dialog_title { - padding-right: 40px; -} - -.mx_SetMxIdDialog_input_group { - display: flex; -} - -.mx_SetMxIdDialog_input { - border-radius: 3px; - border: 1px solid $input-border-color; - padding: 9px; - color: $primary-fg-color; - background-color: $primary-bg-color; - font-size: $font-15px; - width: 100%; - max-width: 280px; -} - -.mx_SetMxIdDialog_input.error, -.mx_SetMxIdDialog_input.error:focus { - border: 1px solid $warning-color; -} - -.mx_SetMxIdDialog_input_group .mx_Spinner { - height: 37px; - padding-left: 10px; - justify-content: flex-start; -} - -.mx_SetMxIdDialog .success { - color: $accent-color; -} diff --git a/res/css/views/dialogs/_SetPasswordDialog.scss b/res/css/views/dialogs/_SetPasswordDialog.scss deleted file mode 100644 index 1f99353298..0000000000 --- a/res/css/views/dialogs/_SetPasswordDialog.scss +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_SetPasswordDialog_change_password input { - border-radius: 3px; - border: 1px solid $input-border-color; - padding: 9px; - color: $primary-fg-color; - background-color: $primary-bg-color; - font-size: $font-15px; - max-width: 280px; - margin-bottom: 10px; -} - -.mx_SetPasswordDialog_change_password_button { - margin-top: 68px; -} - -.mx_SetPasswordDialog .mx_Dialog_content { - margin-bottom: 0px; -} diff --git a/res/css/views/dialogs/_SettingsDialog.scss b/res/css/views/dialogs/_SettingsDialog.scss index ec813a1a07..b3b6802c3d 100644 --- a/res/css/views/dialogs/_SettingsDialog.scss +++ b/res/css/views/dialogs/_SettingsDialog.scss @@ -15,7 +15,7 @@ limitations under the License. */ // Not actually a component but things shared by settings components -.mx_UserSettingsDialog, .mx_RoomSettingsDialog { +.mx_UserSettingsDialog, .mx_RoomSettingsDialog, .mx_SpaceSettingsDialog { width: 90vw; max-width: 1000px; // set the height too since tabbed view scrolls itself. @@ -36,7 +36,6 @@ limitations under the License. } .mx_Dialog_title { - text-align: center; margin-bottom: 24px; } } diff --git a/res/css/views/dialogs/_ShareDialog.scss b/res/css/views/dialogs/_ShareDialog.scss index 9a2f67dea3..4d5e1409db 100644 --- a/res/css/views/dialogs/_ShareDialog.scss +++ b/res/css/views/dialogs/_ShareDialog.scss @@ -50,11 +50,14 @@ limitations under the License. margin-left: 20px; display: inherit; } -.mx_ShareDialog_matrixto_copy > div { - background-image: url($copy-button-url); +.mx_ShareDialog_matrixto_copy::after { + content: ""; + mask-image: url($copy-button-url); + background-color: $message-action-bar-fg-color; margin-left: 5px; width: 20px; height: 20px; + background-repeat: no-repeat; } .mx_ShareDialog_split { @@ -64,17 +67,17 @@ limitations under the License. .mx_ShareDialog_qrcode_container { float: left; - background-color: #ffffff; - padding: 5px; // makes qr code more readable in dark theme - border-radius: 5px; height: 256px; width: 256px; margin-right: 64px; } +.mx_ShareDialog_qrcode_container + .mx_ShareDialog_social_container { + width: 299px; +} + .mx_ShareDialog_social_container { display: inline-block; - width: 299px; } .mx_ShareDialog_social_icon { diff --git a/res/css/views/dialogs/_SpaceSettingsDialog.scss b/res/css/views/dialogs/_SpaceSettingsDialog.scss new file mode 100644 index 0000000000..fa074fdbe8 --- /dev/null +++ b/res/css/views/dialogs/_SpaceSettingsDialog.scss @@ -0,0 +1,100 @@ +/* +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_SpaceSettingsDialog { + color: $primary-fg-color; + + .mx_SpaceSettings_errorText { + font-weight: $font-semi-bold; + font-size: $font-12px; + line-height: $font-15px; + color: $notice-primary-color; + margin-bottom: 28px; + } + + .mx_ToggleSwitch { + display: inline-block; + vertical-align: middle; + margin-left: 16px; + } + + .mx_SettingsTab_section { + .mx_SettingsTab_section_caption { + margin-top: 12px; + margin-bottom: 20px; + } + + & + .mx_SettingsTab_subheading { + border-top: 1px solid $message-body-panel-bg-color; + margin-top: 0; + padding-top: 24px; + } + + .mx_RadioButton { + margin-top: 8px; + margin-bottom: 4px; + + .mx_RadioButton_content { + font-weight: $font-semi-bold; + line-height: $font-18px; + color: $primary-fg-color; + } + + & + span { + font-size: $font-15px; + line-height: $font-18px; + color: $secondary-fg-color; + margin-left: 26px; + } + } + + .mx_SettingsTab_showAdvanced { + margin: 16px 0; + padding: 0; + } + + .mx_SettingsFlag { + margin-top: 24px; + } + } + + .mx_SpaceSettingsDialog_buttons { + display: flex; + margin-top: 64px; + + .mx_AccessibleButton { + display: inline-block; + } + + .mx_AccessibleButton_kind_link { + margin-left: auto; + } + } + + .mx_AccessibleButton_hasKind { + padding: 8px 22px; + } + + .mx_TabbedView_tabLabel { + .mx_SpaceSettingsDialog_generalIcon::before { + mask-image: url('$(res)/img/element-icons/settings.svg'); + } + + .mx_SpaceSettingsDialog_visibilityIcon::before { + mask-image: url('$(res)/img/element-icons/eye.svg'); + } + } +} diff --git a/res/css/views/dialogs/_UnknownDeviceDialog.scss b/res/css/views/dialogs/_UnknownDeviceDialog.scss deleted file mode 100644 index daa6bd2352..0000000000 --- a/res/css/views/dialogs/_UnknownDeviceDialog.scss +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_UnknownDeviceDialog { - height: 100%; - display: flex; - flex-direction: column; -} - -.mx_UnknownDeviceDialog ul { - list-style: none; - padding: 0; -} -// userid -.mx_UnknownDeviceDialog p { - font-weight: bold; - font-size: $font-16px; -} - -.mx_UnknownDeviceDialog .mx_DeviceVerifyButtons { - flex-direction: row !important; -} - -.mx_UnknownDeviceDialog .mx_Dialog_content { - margin-bottom: 24px; - overflow-y: scroll; -} - -.mx_UnknownDeviceDialog_deviceList > li { - padding: 4px; -} - -.mx_UnknownDeviceDialog_deviceList > li > * { - padding-bottom: 0; -} diff --git a/res/css/views/dialogs/_UntrustedDeviceDialog.scss b/res/css/views/dialogs/_UntrustedDeviceDialog.scss new file mode 100644 index 0000000000..0ecd9d4f71 --- /dev/null +++ b/res/css/views/dialogs/_UntrustedDeviceDialog.scss @@ -0,0 +1,26 @@ +/* +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_UntrustedDeviceDialog { + .mx_Dialog_title { + display: flex; + align-items: center; + + .mx_E2EIcon { + margin-left: 0; + } + } +} diff --git a/res/css/views/dialogs/_UserSettingsDialog.scss b/res/css/views/dialogs/_UserSettingsDialog.scss index 4d831d7858..bd472710ea 100644 --- a/res/css/views/dialogs/_UserSettingsDialog.scss +++ b/res/css/views/dialogs/_UserSettingsDialog.scss @@ -18,37 +18,41 @@ limitations under the License. // ========================================================== .mx_UserSettingsDialog_settingsIcon::before { - mask-image: url('$(res)/img/feather-customised/settings.svg'); + mask-image: url('$(res)/img/element-icons/settings.svg'); +} + +.mx_UserSettingsDialog_appearanceIcon::before { + mask-image: url('$(res)/img/element-icons/settings/appearance.svg'); } .mx_UserSettingsDialog_voiceIcon::before { - mask-image: url('$(res)/img/feather-customised/phone.svg'); + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); } .mx_UserSettingsDialog_bellIcon::before { - mask-image: url('$(res)/img/feather-customised/notifications.svg'); + mask-image: url('$(res)/img/element-icons/notifications.svg'); } .mx_UserSettingsDialog_preferencesIcon::before { - mask-image: url('$(res)/img/feather-customised/sliders.svg'); + mask-image: url('$(res)/img/element-icons/settings/preference.svg'); } .mx_UserSettingsDialog_securityIcon::before { - mask-image: url('$(res)/img/feather-customised/lock.svg'); + mask-image: url('$(res)/img/element-icons/security.svg'); } .mx_UserSettingsDialog_helpIcon::before { - mask-image: url('$(res)/img/feather-customised/help-circle.svg'); + mask-image: url('$(res)/img/element-icons/settings/help.svg'); } .mx_UserSettingsDialog_labsIcon::before { - mask-image: url('$(res)/img/feather-customised/flag.svg'); + mask-image: url('$(res)/img/element-icons/settings/lab-flags.svg'); } .mx_UserSettingsDialog_mjolnirIcon::before { - mask-image: url('$(res)/img/feather-customised/face.svg'); + mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg'); } .mx_UserSettingsDialog_flairIcon::before { - mask-image: url('$(res)/img/feather-customised/flair.svg'); + mask-image: url('$(res)/img/element-icons/settings/flair.svg'); } diff --git a/res/css/views/dialogs/_WidgetCapabilitiesPromptDialog.scss b/res/css/views/dialogs/_WidgetCapabilitiesPromptDialog.scss new file mode 100644 index 0000000000..176919b84c --- /dev/null +++ b/res/css/views/dialogs/_WidgetCapabilitiesPromptDialog.scss @@ -0,0 +1,75 @@ +/* +Copyright 2020 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_WidgetCapabilitiesPromptDialog { + .text-muted { + font-size: $font-12px; + } + + .mx_Dialog_content { + margin-bottom: 16px; + } + + .mx_WidgetCapabilitiesPromptDialog_cap { + margin-top: 20px; + font-size: $font-15px; + line-height: $font-15px; + + .mx_WidgetCapabilitiesPromptDialog_byline { + color: $muted-fg-color; + margin-left: 26px; + font-size: $font-12px; + line-height: $font-12px; + } + } + + .mx_Dialog_buttons { + margin-top: 40px; // double normal + } + + .mx_SettingsFlag { + line-height: calc($font-14px + 7px + 7px); // 7px top & bottom padding + color: $muted-fg-color; + font-size: $font-12px; + + .mx_ToggleSwitch { + display: inline-block; + vertical-align: middle; + margin-right: 8px; + + // downsize the switch + ball + width: $font-32px; + height: $font-15px; + + + &.mx_ToggleSwitch_on > .mx_ToggleSwitch_ball { + left: calc(100% - $font-15px); + } + + .mx_ToggleSwitch_ball { + width: $font-15px; + height: $font-15px; + border-radius: $font-15px; + } + } + + .mx_SettingsFlag_label { + display: inline-block; + vertical-align: middle; + } + } +} diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss deleted file mode 100644 index b9babd05f5..0000000000 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ /dev/null @@ -1,93 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_CreateKeyBackupDialog .mx_Dialog_title { - /* TODO: Consider setting this for all dialog titles. */ - margin-bottom: 1em; -} - -.mx_CreateKeyBackupDialog_primaryContainer { - /* FIXME: plinth colour in new theme(s). background-color: $accent-color; */ - padding: 20px; -} - -.mx_CreateKeyBackupDialog_primaryContainer::after { - content: ""; - clear: both; - display: block; -} - -.mx_CreateKeyBackupDialog_passPhraseContainer { - display: flex; - align-items: flex-start; -} - -.mx_CreateKeyBackupDialog_passPhraseHelp { - flex: 1; - height: 85px; - margin-left: 20px; - font-size: 80%; -} - -.mx_CreateKeyBackupDialog_passPhraseHelp progress { - width: 100%; -} - -.mx_CreateKeyBackupDialog_passPhraseInput { - flex: none; - width: 250px; - border: 1px solid $accent-color; - border-radius: 5px; - padding: 10px; - margin-bottom: 1em; -} - -.mx_CreateKeyBackupDialog_passPhraseMatch { - margin-left: 20px; -} - -.mx_CreateKeyBackupDialog_recoveryKeyHeader { - margin-bottom: 1em; -} - -.mx_CreateKeyBackupDialog_recoveryKeyContainer { - display: flex; -} - -.mx_CreateKeyBackupDialog_recoveryKey { - width: 262px; - padding: 20px; - color: $info-plinth-fg-color; - background-color: $info-plinth-bg-color; - margin-right: 12px; -} - -.mx_CreateKeyBackupDialog_recoveryKeyButtons { - flex: 1; - display: flex; - align-items: center; -} - -.mx_CreateKeyBackupDialog_recoveryKeyButtons button { - flex: 1; - white-space: nowrap; -} - -.mx_CreateKeyBackupDialog { - details .mx_AccessibleButton { - margin: 1em 0; // emulate paragraph spacing because we can't put this button in a paragraph due to HTML rules - } -} diff --git a/res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss deleted file mode 100644 index 9cba8e0da9..0000000000 --- a/res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2018 New Vector Ltd -Copyright 2019 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_RestoreKeyBackupDialog_keyStatus { - height: 30px; -} - -.mx_RestoreKeyBackupDialog_primaryContainer { - /* FIXME: plinth colour in new theme(s). background-color: $accent-color; */ - padding: 20px; -} - -.mx_RestoreKeyBackupDialog_passPhraseInput, -.mx_RestoreKeyBackupDialog_recoveryKeyInput { - width: 300px; - border: 1px solid $accent-color; - border-radius: 5px; - padding: 10px; -} - diff --git a/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss deleted file mode 100644 index db11e91bdb..0000000000 --- a/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2018 New Vector Ltd -Copyright 2019 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_AccessSecretStorageDialog_keyStatus { - height: 30px; -} - -.mx_AccessSecretStorageDialog_primaryContainer { - /* FIXME: plinth colour in new theme(s). background-color: $accent-color; */ - padding: 20px; -} - -.mx_AccessSecretStorageDialog_passPhraseInput, -.mx_AccessSecretStorageDialog_recoveryKeyInput { - width: 300px; - border: 1px solid $accent-color; - border-radius: 5px; - padding: 10px; -} - diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss deleted file mode 100644 index a9ebd54b31..0000000000 --- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss +++ /dev/null @@ -1,116 +0,0 @@ -/* -Copyright 2018 New Vector Ltd -Copyright 2019, 2020 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_CreateSecretStorageDialog { - // Why you ask? Because CompleteSecurityBody is 600px so this is the width - // we end up when in there, but when in our own dialog we set our own width - // so need to fix it to something sensible as otherwise we'd end up either - // really wide or really narrow depending on the phase. I bet you wish you - // never asked. - width: 560px; - - .mx_SettingsFlag { - display: flex; - } - - .mx_SettingsFlag_label { - flex: 1 1 0; - min-width: 0; - font-weight: 600; - } - - .mx_ToggleSwitch { - flex: 0 0 auto; - margin-left: 30px; - } - - details .mx_AccessibleButton { - margin: 1em 0; // emulate paragraph spacing because we can't put this button in a paragraph due to HTML rules - } -} - -.mx_CreateSecretStorageDialog .mx_Dialog_title { - /* TODO: Consider setting this for all dialog titles. */ - margin-bottom: 1em; -} - -.mx_CreateSecretStorageDialog_primaryContainer { - /* FIXME: plinth colour in new theme(s). background-color: $accent-color; */ - padding-top: 20px; -} - -.mx_CreateSecretStorageDialog_primaryContainer::after { - content: ""; - clear: both; - display: block; -} - -.mx_CreateSecretStorageDialog_passPhraseContainer { - display: flex; - align-items: flex-start; -} - -.mx_Field.mx_CreateSecretStorageDialog_passPhraseField { - margin-top: 0px; -} - -.mx_CreateSecretStorageDialog_passPhraseHelp { - flex: 1; - height: 64px; - margin-left: 20px; - font-size: 80%; -} - -.mx_CreateSecretStorageDialog_passPhraseHelp progress { - width: 100%; -} - -.mx_CreateSecretStorageDialog_passPhraseMatch { - width: 200px; - margin-left: 20px; -} - -.mx_CreateSecretStorageDialog_recoveryKeyHeader { - margin-bottom: 1em; -} - -.mx_CreateSecretStorageDialog_recoveryKeyContainer { - display: flex; -} - -.mx_CreateSecretStorageDialog_recoveryKey { - width: 262px; - padding: 20px; - color: $info-plinth-fg-color; - background-color: $info-plinth-bg-color; - margin-right: 12px; -} - -.mx_CreateSecretStorageDialog_recoveryKeyButtons { - flex: 1; - display: flex; - align-items: center; -} - -.mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton { - margin-right: 10px; -} - -.mx_CreateSecretStorageDialog_recoveryKeyButtons button { - flex: 1; - white-space: nowrap; -} diff --git a/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss b/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss new file mode 100644 index 0000000000..ec3bea0ef7 --- /dev/null +++ b/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss @@ -0,0 +1,121 @@ +/* +Copyright 2018, 2019, 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_AccessSecretStorageDialog_reset { + position: relative; + padding-left: 24px; // 16px icon + 8px padding + margin-top: 7px; // vertical alignment to buttons + + &::before { + content: ""; + display: inline-block; + position: absolute; + height: 16px; + width: 16px; + left: 0; + top: 2px; // alignment + background-image: url("$(res)/img/element-icons/warning-badge.svg"); + background-size: contain; + } + + .mx_AccessSecretStorageDialog_reset_link { + color: $warning-color; + } +} + +.mx_AccessSecretStorageDialog_titleWithIcon::before { + content: ''; + display: inline-block; + width: 24px; + height: 24px; + margin-right: 8px; + position: relative; + top: 5px; + background-color: $primary-fg-color; +} + +.mx_AccessSecretStorageDialog_resetBadge::before { + // The image isn't capable of masking, so we use a background instead. + background-image: url("$(res)/img/element-icons/warning-badge.svg"); + background-size: 24px; + background-color: transparent; +} + +.mx_AccessSecretStorageDialog_secureBackupTitle::before { + mask-image: url('$(res)/img/feather-customised/secure-backup.svg'); +} + +.mx_AccessSecretStorageDialog_securePhraseTitle::before { + mask-image: url('$(res)/img/feather-customised/secure-phrase.svg'); +} + +.mx_AccessSecretStorageDialog_keyStatus { + height: 30px; +} + +.mx_AccessSecretStorageDialog_passPhraseInput { + width: 300px; + border: 1px solid $accent-color; + border-radius: 5px; + padding: 10px; +} + +.mx_AccessSecretStorageDialog_recoveryKeyEntry { + display: flex; + align-items: center; +} + +.mx_AccessSecretStorageDialog_recoveryKeyEntry_textInput { + flex-grow: 1; +} + +.mx_AccessSecretStorageDialog_recoveryKeyEntry_entryControlSeparatorText { + margin: 16px; +} + +.mx_AccessSecretStorageDialog_recoveryKeyFeedback { + &::before { + content: ""; + display: inline-block; + vertical-align: bottom; + width: 20px; + height: 20px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 20px; + margin-right: 5px; + } +} + +.mx_AccessSecretStorageDialog_recoveryKeyFeedback_valid { + color: $input-valid-border-color; + &::before { + mask-image: url('$(res)/img/feather-customised/check.svg'); + background-color: $input-valid-border-color; + } +} + +.mx_AccessSecretStorageDialog_recoveryKeyFeedback_invalid { + color: $input-invalid-border-color; + &::before { + mask-image: url('$(res)/img/feather-customised/x.svg'); + background-color: $input-invalid-border-color; + } +} + +.mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput { + display: none; +} diff --git a/res/css/views/dialogs/security/_CreateCrossSigningDialog.scss b/res/css/views/dialogs/security/_CreateCrossSigningDialog.scss new file mode 100644 index 0000000000..8303e02b9e --- /dev/null +++ b/res/css/views/dialogs/security/_CreateCrossSigningDialog.scss @@ -0,0 +1,33 @@ +/* +Copyright 2020 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_CreateCrossSigningDialog { + // Why you ask? Because CompleteSecurityBody is 600px so this is the width + // we end up when in there, but when in our own dialog we set our own width + // so need to fix it to something sensible as otherwise we'd end up either + // really wide or really narrow depending on the phase. I bet you wish you + // never asked. + width: 560px; + + details .mx_AccessibleButton { + margin: 1em 0; // emulate paragraph spacing because we can't put this button in a paragraph due to HTML rules + } +} + +.mx_CreateCrossSigningDialog .mx_Dialog_title { + /* TODO: Consider setting this for all dialog titles. */ + margin-bottom: 1em; +} diff --git a/res/css/views/dialogs/security/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/security/_CreateKeyBackupDialog.scss new file mode 100644 index 0000000000..9be98e25b2 --- /dev/null +++ b/res/css/views/dialogs/security/_CreateKeyBackupDialog.scss @@ -0,0 +1,82 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_CreateKeyBackupDialog .mx_Dialog_title { + /* TODO: Consider setting this for all dialog titles. */ + margin-bottom: 1em; +} + +.mx_CreateKeyBackupDialog_primaryContainer { + /* FIXME: plinth colour in new theme(s). background-color: $accent-color; */ + padding: 20px; +} + +.mx_CreateKeyBackupDialog_primaryContainer::after { + content: ""; + clear: both; + display: block; +} + +.mx_CreateKeyBackupDialog_passPhraseContainer { + display: flex; + align-items: flex-start; +} + +.mx_CreateKeyBackupDialog_passPhraseInput { + flex: none; + width: 250px; + border: 1px solid $accent-color; + border-radius: 5px; + padding: 10px; + margin-bottom: 1em; +} + +.mx_CreateKeyBackupDialog_passPhraseMatch { + margin-left: 20px; +} + +.mx_CreateKeyBackupDialog_recoveryKeyHeader { + margin-bottom: 1em; +} + +.mx_CreateKeyBackupDialog_recoveryKeyContainer { + display: flex; +} + +.mx_CreateKeyBackupDialog_recoveryKey { + width: 262px; + padding: 20px; + color: $info-plinth-fg-color; + background-color: $info-plinth-bg-color; + margin-right: 12px; +} + +.mx_CreateKeyBackupDialog_recoveryKeyButtons { + flex: 1; + display: flex; + align-items: center; +} + +.mx_CreateKeyBackupDialog_recoveryKeyButtons button { + flex: 1; + white-space: nowrap; +} + +.mx_CreateKeyBackupDialog { + details .mx_AccessibleButton { + margin: 1em 0; // emulate paragraph spacing because we can't put this button in a paragraph due to HTML rules + } +} diff --git a/res/css/views/dialogs/security/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/security/_CreateSecretStorageDialog.scss new file mode 100644 index 0000000000..d30803b1f0 --- /dev/null +++ b/res/css/views/dialogs/security/_CreateSecretStorageDialog.scss @@ -0,0 +1,167 @@ +/* +Copyright 2018 New Vector Ltd +Copyright 2019, 2020 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_CreateSecretStorageDialog { + // Why you ask? Because CompleteSecurityBody is 600px so this is the width + // we end up when in there, but when in our own dialog we set our own width + // so need to fix it to something sensible as otherwise we'd end up either + // really wide or really narrow depending on the phase. I bet you wish you + // never asked. + width: 560px; + + .mx_SettingsFlag { + display: flex; + } + + .mx_SettingsFlag_label { + flex: 1 1 0; + min-width: 0; + font-weight: 600; + } + + .mx_ToggleSwitch { + flex: 0 0 auto; + margin-left: 30px; + } + + details .mx_AccessibleButton { + margin: 1em 0; // emulate paragraph spacing because we can't put this button in a paragraph due to HTML rules + } +} + +.mx_CreateSecretStorageDialog .mx_Dialog_title { + /* TODO: Consider setting this for all dialog titles. */ + margin-bottom: 1em; +} + +.mx_CreateSecretStorageDialog_titleWithIcon::before { + content: ''; + display: inline-block; + width: 24px; + height: 24px; + margin-right: 8px; + position: relative; + top: 5px; + background-color: $primary-fg-color; +} + +.mx_CreateSecretStorageDialog_secureBackupTitle::before { + mask-image: url('$(res)/img/feather-customised/secure-backup.svg'); +} + +.mx_CreateSecretStorageDialog_securePhraseTitle::before { + mask-image: url('$(res)/img/feather-customised/secure-phrase.svg'); +} + +.mx_CreateSecretStorageDialog_centeredTitle, .mx_CreateSecretStorageDialog_centeredBody { + text-align: center; +} + +.mx_CreateSecretStorageDialog_primaryContainer { + /* FIXME: plinth colour in new theme(s). background-color: $accent-color; */ + padding-top: 20px; +} + +.mx_CreateSecretStorageDialog_primaryContainer::after { + content: ""; + clear: both; + display: block; +} + +.mx_CreateSecretStorageDialog_primaryContainer .mx_RadioButton { + margin-bottom: 16px; + padding: 11px; +} + +.mx_CreateSecretStorageDialog_optionTitle { + color: $dialog-title-fg-color; + font-weight: 600; + font-size: $font-18px; + padding-bottom: 10px; +} + +.mx_CreateSecretStorageDialog_optionIcon { + display: inline-block; + width: 24px; + height: 24px; + margin-right: 8px; + position: relative; + top: 5px; + background-color: $primary-fg-color; +} + +.mx_CreateSecretStorageDialog_optionIcon_securePhrase { + mask-image: url('$(res)/img/feather-customised/secure-phrase.svg'); +} + +.mx_CreateSecretStorageDialog_optionIcon_secureBackup { + mask-image: url('$(res)/img/feather-customised/secure-backup.svg'); +} + +.mx_CreateSecretStorageDialog_passPhraseContainer { + display: flex; + align-items: flex-start; +} + +.mx_Field.mx_CreateSecretStorageDialog_passPhraseField { + margin-top: 0px; +} + +.mx_CreateSecretStorageDialog_passPhraseMatch { + width: 200px; + margin-left: 20px; +} + +.mx_CreateSecretStorageDialog_recoveryKeyContainer { + width: 380px; + margin-left: auto; + margin-right: auto; +} + +.mx_CreateSecretStorageDialog_recoveryKey { + font-weight: bold; + text-align: center; + padding: 20px; + color: $info-plinth-fg-color; + background-color: $info-plinth-bg-color; + border-radius: 6px; + word-spacing: 1em; + margin-bottom: 20px; +} + +.mx_CreateSecretStorageDialog_recoveryKeyButtons { + display: flex; + justify-content: space-between; + align-items: center; +} + +.mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton { + width: 160px; + padding-left: 0px; + padding-right: 0px; + white-space: nowrap; +} + +.mx_CreateSecretStorageDialog_continueSpinner { + margin-top: 33px; + text-align: right; +} + +.mx_CreateSecretStorageDialog_continueSpinner img { + width: 20px; + height: 20px; +} diff --git a/res/css/views/dialogs/keybackup/_KeyBackupFailedDialog.scss b/res/css/views/dialogs/security/_KeyBackupFailedDialog.scss similarity index 100% rename from res/css/views/dialogs/keybackup/_KeyBackupFailedDialog.scss rename to res/css/views/dialogs/security/_KeyBackupFailedDialog.scss diff --git a/res/css/views/dialogs/security/_RestoreKeyBackupDialog.scss b/res/css/views/dialogs/security/_RestoreKeyBackupDialog.scss new file mode 100644 index 0000000000..5689d84bc5 --- /dev/null +++ b/res/css/views/dialogs/security/_RestoreKeyBackupDialog.scss @@ -0,0 +1,40 @@ +/* +Copyright 2018 New Vector Ltd +Copyright 2019 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_RestoreKeyBackupDialog_keyStatus { + height: 30px; +} + +.mx_RestoreKeyBackupDialog_primaryContainer { + /* FIXME: plinth colour in new theme(s). background-color: $accent-color; */ + padding: 20px; +} + +.mx_RestoreKeyBackupDialog_passPhraseInput, +.mx_RestoreKeyBackupDialog_recoveryKeyInput { + width: 300px; + border: 1px solid $accent-color; + border-radius: 5px; + padding: 10px; +} + +.mx_RestoreKeyBackupDialog_content > div { + display: flex; + flex-direction: column; + justify-content: space-between; + min-height: 110px; /* Empirically measured */ +} diff --git a/res/css/views/directory/_NetworkDropdown.scss b/res/css/views/directory/_NetworkDropdown.scss index 269b507e3c..ae0927386a 100644 --- a/res/css/views/directory/_NetworkDropdown.scss +++ b/res/css/views/directory/_NetworkDropdown.scss @@ -35,6 +35,8 @@ limitations under the License. border-radius: 4px; border: 1px solid $dialog-close-fg-color; background-color: $primary-bg-color; + max-height: calc(100vh - 20px); // allow 10px padding on both top and bottom + overflow-y: auto; } .mx_NetworkDropdown_menu_network { @@ -51,15 +53,16 @@ limitations under the License. font-weight: 600; line-height: $font-20px; margin-bottom: 4px; + position: relative; // remove server button .mx_AccessibleButton { position: absolute; display: inline; - right: 12px; + right: 10px; height: 16px; width: 16px; - margin-top: 4px; + margin-top: 2px; &::after { content: ""; @@ -142,13 +145,14 @@ limitations under the License. &::after { content: ""; position: absolute; - width: 24px; - height: 24px; - right: -28px; // - (24 + 4) + width: 26px; + height: 26px; + right: -27.5px; // - (width: 26 + spacing to align with X above: 1.5) + top: -3px; mask-repeat: no-repeat; mask-position: center; mask-size: contain; - mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + mask-image: url('$(res)/img/feather-customised/chevron-down-thin.svg'); background-color: $primary-fg-color; } diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 96269cea43..7bc47a3c98 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -25,8 +25,10 @@ limitations under the License. .mx_AccessibleButton_hasKind { padding: 7px 18px; text-align: center; - border-radius: 4px; - display: inline-block; + border-radius: 8px; + display: inline-flex; + align-items: center; + justify-content: center; font-size: $font-14px; } @@ -70,16 +72,20 @@ limitations under the License. .mx_AccessibleButton_kind_danger_outline { color: $button-danger-bg-color; - background-color: $button-secondary-bg-color; + background-color: transparent; border: 1px solid $button-danger-bg-color; } -.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled, -.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled { +.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled { color: $button-danger-disabled-fg-color; background-color: $button-danger-disabled-bg-color; } +.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled { + color: $button-danger-disabled-bg-color; + border-color: $button-danger-disabled-bg-color; +} + .mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_danger_sm { padding: 5px 12px; color: $button-danger-fg-color; diff --git a/res/css/views/elements/_AddressSelector.scss b/res/css/views/elements/_AddressSelector.scss index dd78fcc0f0..087504390c 100644 --- a/res/css/views/elements/_AddressSelector.scss +++ b/res/css/views/elements/_AddressSelector.scss @@ -23,6 +23,7 @@ limitations under the License. border-radius: 3px; border: solid 1px $accent-color; cursor: pointer; + z-index: 1; } .mx_AddressSelector.mx_AddressSelector_empty { diff --git a/res/css/views/elements/_DesktopBuildsNotice.scss b/res/css/views/elements/_DesktopBuildsNotice.scss new file mode 100644 index 0000000000..3672595bf1 --- /dev/null +++ b/res/css/views/elements/_DesktopBuildsNotice.scss @@ -0,0 +1,28 @@ +/* +Copyright 2020 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_DesktopBuildsNotice { + text-align: center; + padding: 0 16px; + + > * { + vertical-align: middle; + } + + > img { + margin-right: 8px; + } +} diff --git a/res/css/views/elements/_DesktopCapturerSourcePicker.scss b/res/css/views/elements/_DesktopCapturerSourcePicker.scss new file mode 100644 index 0000000000..69dde5925e --- /dev/null +++ b/res/css/views/elements/_DesktopCapturerSourcePicker.scss @@ -0,0 +1,72 @@ +/* +Copyright 2021 Šimon Brandner + +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_desktopCapturerSourcePicker { + overflow: hidden; +} + +.mx_desktopCapturerSourcePicker_tabLabels { + display: flex; + padding: 0 0 8px 0; +} + +.mx_desktopCapturerSourcePicker_tabLabel, +.mx_desktopCapturerSourcePicker_tabLabel_selected { + width: 100%; + text-align: center; + border-radius: 8px; + padding: 8px 0; + font-size: $font-13px; +} + +.mx_desktopCapturerSourcePicker_tabLabel_selected { + background-color: $tab-label-active-bg-color; + color: $tab-label-active-fg-color; +} + +.mx_desktopCapturerSourcePicker_panel { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: flex-start; + height: 500px; + overflow: overlay; +} + +.mx_desktopCapturerSourcePicker_stream_button { + display: flex; + flex-direction: column; + margin: 8px; + border-radius: 4px; +} + +.mx_desktopCapturerSourcePicker_stream_button:hover, +.mx_desktopCapturerSourcePicker_stream_button:focus { + background: $roomtile-selected-bg-color; +} + +.mx_desktopCapturerSourcePicker_stream_thumbnail { + margin: 4px; + width: 312px; +} + +.mx_desktopCapturerSourcePicker_stream_name { + margin: 0 4px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + width: 312px; +} diff --git a/res/css/views/elements/_DialPadBackspaceButton.scss b/res/css/views/elements/_DialPadBackspaceButton.scss new file mode 100644 index 0000000000..40e4af7025 --- /dev/null +++ b/res/css/views/elements/_DialPadBackspaceButton.scss @@ -0,0 +1,40 @@ +/* +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_DialPadBackspaceButton { + position: relative; + height: 28px; + width: 28px; + + &::before { + /* force this element to appear on the DOM */ + content: ""; + + background-color: #8D97A5; + width: inherit; + height: inherit; + top: 0px; + left: 0px; + position: absolute; + display: inline-block; + vertical-align: middle; + + mask-image: url('$(res)/img/element-icons/call/delete.svg'); + mask-position: 8px; + mask-size: 20px; + mask-repeat: no-repeat; + } +} diff --git a/res/css/views/elements/_Dropdown.scss b/res/css/views/elements/_Dropdown.scss index 0dd9656c9c..2a2508c17c 100644 --- a/res/css/views/elements/_Dropdown.scss +++ b/res/css/views/elements/_Dropdown.scss @@ -33,6 +33,10 @@ limitations under the License. user-select: none; } +.mx_Dropdown_input.mx_AccessibleButton_disabled { + cursor: not-allowed; +} + .mx_Dropdown_input:focus { border-color: $input-focused-border-color; } @@ -63,6 +67,8 @@ limitations under the License. text-overflow: ellipsis; white-space: nowrap; flex: 1; + display: inline-flex; + align-items: center; } .mx_Dropdown_option div { @@ -71,12 +77,18 @@ limitations under the License. white-space: nowrap; } -.mx_Dropdown_option img { +.mx_Dropdown_option img, +.mx_Dropdown_option .mx_Dropdown_option_emoji { margin: 5px; width: 16px; vertical-align: middle; } +.mx_Dropdown_option_emoji { + font-size: $font-16px; + line-height: $font-16px; +} + input.mx_Dropdown_option, input.mx_Dropdown_option:focus { font-weight: normal; diff --git a/res/css/views/elements/_EditableItemList.scss b/res/css/views/elements/_EditableItemList.scss index ef60f006cc..f089fa3dc2 100644 --- a/res/css/views/elements/_EditableItemList.scss +++ b/res/css/views/elements/_EditableItemList.scss @@ -53,6 +53,9 @@ limitations under the License. .mx_EditableItem_item { flex: auto 1 0; order: 1; + width: calc(100% - 14px); // leave space for the remove button + overflow-x: hidden; + text-overflow: ellipsis; } .mx_EditableItemList_label { diff --git a/res/css/views/elements/_FacePile.scss b/res/css/views/elements/_FacePile.scss new file mode 100644 index 0000000000..c691baffb5 --- /dev/null +++ b/res/css/views/elements/_FacePile.scss @@ -0,0 +1,65 @@ +/* +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_FacePile { + .mx_FacePile_faces { + display: inline-flex; + flex-direction: row-reverse; + vertical-align: middle; + + > .mx_FacePile_face + .mx_FacePile_face { + margin-right: -8px; + } + + .mx_BaseAvatar_image { + border: 1px solid $primary-bg-color; + } + + .mx_BaseAvatar_initial { + margin: 1px; // to offset the border on the image + } + + .mx_FacePile_more { + position: relative; + border-radius: 100%; + width: 30px; + height: 30px; + background-color: $groupFilterPanel-bg-color; + + &::before { + content: ""; + z-index: 1; + position: absolute; + top: 0; + left: 0; + height: inherit; + width: inherit; + background: $tertiary-fg-color; + mask-position: center; + mask-size: 20px; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); + } + } + } + + .mx_FacePile_summary { + margin-left: 12px; + font-size: $font-14px; + line-height: $font-24px; + color: $tertiary-fg-color; + } +} diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index cf5bc7ab41..f67da6477b 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -191,5 +191,5 @@ limitations under the License. } .mx_Field .mx_CountryDropdown { - width: 67px; + width: $font-78px; } diff --git a/res/css/views/elements/_FormButton.scss b/res/css/views/elements/_FormButton.scss deleted file mode 100644 index 7ec01f17e6..0000000000 --- a/res/css/views/elements/_FormButton.scss +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright 2019 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_FormButton { - line-height: $font-16px; - padding: 5px 15px; - font-size: $font-12px; - height: min-content; - - &:not(:last-child) { - margin-right: 8px; - } - - &.mx_AccessibleButton_kind_primary { - color: $accent-color; - background-color: $accent-bg-color; - } - - &.mx_AccessibleButton_kind_danger { - color: $notice-primary-color; - background-color: $notice-primary-bg-color; - } -} diff --git a/res/css/views/elements/_IconButton.scss b/res/css/views/elements/_IconButton.scss deleted file mode 100644 index d8ebbeb65e..0000000000 --- a/res/css/views/elements/_IconButton.scss +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright 2019 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_IconButton { - width: 32px; - height: 32px; - border-radius: 100%; - background-color: $accent-bg-color; - // don't shrink or grow if in a flex container - flex: 0 0 auto; - - &.mx_AccessibleButton_disabled { - background-color: none; - - &::before { - background-color: lightgrey; - } - } - - &:hover { - opacity: 90%; - } - - &::before { - content: ""; - display: block; - width: 100%; - height: 100%; - mask-repeat: no-repeat; - mask-position: center; - mask-size: 55%; - background-color: $accent-color; - } - - &.mx_IconButton_icon_check::before { - mask-image: url('$(res)/img/feather-customised/check.svg'); - } - - &.mx_IconButton_icon_edit::before { - mask-image: url('$(res)/img/feather-customised/edit.svg'); - } -} diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss index 0a4ed2a194..cf92ffec64 100644 --- a/res/css/views/elements/_ImageView.scss +++ b/res/css/views/elements/_ImageView.scss @@ -14,139 +14,113 @@ See the License for the specific language governing permissions and limitations under the License. */ -/* This has got to be the most fragile piece of CSS ever written. - But empirically it works on Chrome/FF/Safari - */ +$button-size: 32px; +$icon-size: 22px; +$button-gap: 24px; .mx_ImageView { display: flex; width: 100%; height: 100%; - align-items: center; -} - -.mx_ImageView_lhs { - order: 1; - flex: 1 1 10%; - min-width: 60px; - // background-color: #080; - // height: 20px; -} - -.mx_ImageView_content { - order: 2; - /* min-width hack needed for FF */ - min-width: 0px; - height: 90%; - flex: 15 15 0; - display: flex; - align-items: center; - justify-content: center; -} - -.mx_ImageView_content img { - max-width: 100%; - /* XXX: max-height interacts badly with flex on Chrome and doesn't relayout properly until you refresh */ - max-height: 100%; - /* object-fit hack needed for Chrome due to Chrome not re-laying-out until you refresh */ - object-fit: contain; - /* background-image: url('$(res)/img/trans.png'); */ - pointer-events: all; -} - -.mx_ImageView_labelWrapper { - position: absolute; - top: 0px; - right: 0px; - height: 100%; - overflow: auto; - pointer-events: all; -} - -.mx_ImageView_label { - text-align: left; - display: flex; - justify-content: center; flex-direction: column; - padding-left: 30px; - padding-right: 30px; - min-height: 100%; - max-width: 240px; +} + +.mx_ImageView_image_wrapper { + pointer-events: initial; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + overflow: hidden; +} + +.mx_ImageView_image { + flex-shrink: 0; +} + +.mx_ImageView_panel { + width: 100%; + height: 68px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.mx_ImageView_info_wrapper { + pointer-events: initial; + padding-left: 32px; + display: flex; + flex-direction: row; + align-items: center; color: $lightbox-fg-color; } -.mx_ImageView_cancel { - position: absolute; - // hack for mx_Dialog having a top padding of 40px - top: 40px; - right: 0px; - padding-top: 35px; - padding-right: 35px; - cursor: pointer; +.mx_ImageView_info { + padding-left: 12px; + display: flex; + flex-direction: column; } -.mx_ImageView_rotateClockwise { - position: absolute; - top: 40px; - right: 70px; - padding-top: 35px; - cursor: pointer; +.mx_ImageView_info_sender { + font-weight: bold; } -.mx_ImageView_rotateCounterClockwise { - position: absolute; - top: 40px; - right: 105px; - padding-top: 35px; - cursor: pointer; -} - -.mx_ImageView_name { - font-size: $font-18px; - margin-bottom: 6px; - word-wrap: break-word; -} - -.mx_ImageView_metadata { - font-size: $font-15px; - opacity: 0.5; -} - -.mx_ImageView_download { - display: table; - margin-top: 24px; - margin-bottom: 6px; - border-radius: 5px; - background-color: $lightbox-bg-color; - font-size: $font-14px; - padding: 9px; - border: 1px solid $lightbox-border-color; -} - -.mx_ImageView_size { - font-size: $font-11px; -} - -.mx_ImageView_link { - color: $lightbox-fg-color !important; - text-decoration: none !important; +.mx_ImageView_toolbar { + padding-right: 16px; + pointer-events: initial; + display: flex; + align-items: center; + gap: calc($button-gap - ($button-size - $icon-size)); } .mx_ImageView_button { - font-size: $font-15px; - opacity: 0.5; - margin-top: 18px; - cursor: pointer; + padding: calc(($button-size - $icon-size) / 2); + display: block; + + &::before { + content: ''; + height: $icon-size; + width: $icon-size; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + display: block; + background-color: $icon-button-color; + } } -.mx_ImageView_shim { - height: 30px; +.mx_ImageView_button_rotateCW::before { + mask-image: url('$(res)/img/image-view/rotate-cw.svg'); } -.mx_ImageView_rhs { - order: 3; - flex: 1 1 10%; - min-width: 300px; - // background-color: #800; - // height: 20px; +.mx_ImageView_button_rotateCCW::before { + mask-image: url('$(res)/img/image-view/rotate-ccw.svg'); +} + +.mx_ImageView_button_zoomOut::before { + mask-image: url('$(res)/img/image-view/zoom-out.svg'); +} + +.mx_ImageView_button_zoomIn::before { + mask-image: url('$(res)/img/image-view/zoom-in.svg'); +} + +.mx_ImageView_button_download::before { + mask-image: url('$(res)/img/image-view/download.svg'); +} + +.mx_ImageView_button_more::before { + mask-image: url('$(res)/img/image-view/more.svg'); +} + +.mx_ImageView_button_close { + padding: calc($button-size - $button-size); + border-radius: 100%; + background: #21262c; // same on all themes + &::before { + width: $button-size; + height: $button-size; + mask-image: url('$(res)/img/image-view/close.svg'); + mask-size: 40%; + } } diff --git a/res/css/views/elements/_InfoTooltip.scss b/res/css/views/elements/_InfoTooltip.scss new file mode 100644 index 0000000000..5858a60629 --- /dev/null +++ b/res/css/views/elements/_InfoTooltip.scss @@ -0,0 +1,34 @@ +/* +Copyright 2020 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_InfoTooltip_icon { + width: 16px; + height: 16px; + display: inline-block; +} + +.mx_InfoTooltip_icon::before { + display: inline-block; + background-color: $muted-fg-color; + mask-repeat: no-repeat; + mask-size: 16px; + width: 16px; + height: 16px; + mask-position: center; + content: ''; + vertical-align: middle; + mask-image: url('$(res)/img/element-icons/info.svg'); +} diff --git a/res/css/views/elements/_InlineSpinner.scss b/res/css/views/elements/_InlineSpinner.scss index 612b6209c6..ca5cb5d3a8 100644 --- a/res/css/views/elements/_InlineSpinner.scss +++ b/res/css/views/elements/_InlineSpinner.scss @@ -18,7 +18,11 @@ limitations under the License. display: inline; } -.mx_InlineSpinner img { +.mx_InlineSpinner img, .mx_InlineSpinner_icon { margin: 0px 6px; vertical-align: -3px; } + +.mx_InlineSpinner_icon { + display: inline-block; +} diff --git a/res/css/views/elements/_InteractiveTooltip.scss b/res/css/views/elements/_InteractiveTooltip.scss deleted file mode 100644 index db98d95709..0000000000 --- a/res/css/views/elements/_InteractiveTooltip.scss +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright 2019 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_InteractiveTooltip_wrapper { - position: fixed; - z-index: 5000; -} - -.mx_InteractiveTooltip { - border-radius: 3px; - background-color: $interactive-tooltip-bg-color; - color: $interactive-tooltip-fg-color; - position: absolute; - font-size: $font-10px; - font-weight: 600; - padding: 6px; - z-index: 5001; -} - -.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_top { - top: 10px; // 8px chevron + 2px spacing -} - -.mx_InteractiveTooltip_chevron_top { - position: absolute; - left: calc(50% - 8px); - top: -8px; - width: 0; - height: 0; - border-left: 8px solid transparent; - border-bottom: 8px solid $interactive-tooltip-bg-color; - border-right: 8px solid transparent; -} - -// Adapted from https://codyhouse.co/blog/post/css-rounded-triangles-with-clip-path -// by Sebastiano Guerriero (@guerriero_se) -@supports (clip-path: polygon(0% 0%, 100% 100%, 0% 100%)) { - .mx_InteractiveTooltip_chevron_top { - height: 16px; - width: 16px; - background-color: inherit; - border: none; - clip-path: polygon(0% 0%, 100% 100%, 0% 100%); - transform: rotate(135deg); - border-radius: 0 0 0 3px; - top: calc(-8px / 1.414); // sqrt(2) because of rotation - } -} - -.mx_InteractiveTooltip.mx_InteractiveTooltip_withChevron_bottom { - bottom: 10px; // 8px chevron + 2px spacing -} - -.mx_InteractiveTooltip_chevron_bottom { - position: absolute; - left: calc(50% - 8px); - bottom: -8px; - width: 0; - height: 0; - border-left: 8px solid transparent; - border-top: 8px solid $interactive-tooltip-bg-color; - border-right: 8px solid transparent; -} - -// Adapted from https://codyhouse.co/blog/post/css-rounded-triangles-with-clip-path -// by Sebastiano Guerriero (@guerriero_se) -@supports (clip-path: polygon(0% 0%, 100% 100%, 0% 100%)) { - .mx_InteractiveTooltip_chevron_bottom { - height: 16px; - width: 16px; - background-color: inherit; - border: none; - clip-path: polygon(0% 0%, 100% 100%, 0% 100%); - transform: rotate(-45deg); - border-radius: 0 0 0 3px; - bottom: calc(-8px / 1.414); // sqrt(2) because of rotation - } -} diff --git a/res/css/views/elements/_InviteReason.scss b/res/css/views/elements/_InviteReason.scss new file mode 100644 index 0000000000..2c2e5687e6 --- /dev/null +++ b/res/css/views/elements/_InviteReason.scss @@ -0,0 +1,57 @@ +/* +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_InviteReason { + position: relative; + margin-bottom: 1em; + + .mx_InviteReason_reason { + visibility: visible; + } + + .mx_InviteReason_view { + display: none; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + justify-content: center; + align-items: center; + cursor: pointer; + color: $secondary-fg-color; + + &::before { + content: ""; + margin-right: 8px; + background-color: $secondary-fg-color; + mask-image: url('$(res)/img/feather-customised/eye.svg'); + display: inline-block; + width: 18px; + height: 14px; + } + } +} + +.mx_InviteReason_hidden { + .mx_InviteReason_reason { + visibility: hidden; + } + + .mx_InviteReason_view { + display: flex; + } +} diff --git a/res/css/views/elements/_MiniAvatarUploader.scss b/res/css/views/elements/_MiniAvatarUploader.scss new file mode 100644 index 0000000000..df4676ab56 --- /dev/null +++ b/res/css/views/elements/_MiniAvatarUploader.scss @@ -0,0 +1,60 @@ +/* +Copyright 2020 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_MiniAvatarUploader { + position: relative; + width: min-content; + + // this isn't a floating tooltip so override some things to not need to bother with z-index and floating + .mx_Tooltip { + display: inline-block; + position: absolute; + z-index: unset; + width: max-content; + left: 72px; + top: 0; + } + + .mx_MiniAvatarUploader_indicator { + position: absolute; + + height: 26px; + width: 26px; + + right: -6px; + bottom: -6px; + + background-color: $primary-bg-color; + border-radius: 50%; + z-index: 1; + + .mx_MiniAvatarUploader_cameraIcon { + height: 100%; + width: 100%; + + background-color: $secondary-fg-color; + mask-position: center; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/camera.svg'); + mask-size: 16px; + z-index: 2; + } + } +} + +.mx_MiniAvatarUploader_input { + display: none; +} diff --git a/res/css/views/elements/_ProgressBar.scss b/res/css/views/elements/_ProgressBar.scss index a3fee232d0..c075ac74ff 100644 --- a/res/css/views/elements/_ProgressBar.scss +++ b/res/css/views/elements/_ProgressBar.scss @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020, 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. @@ -14,12 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_ProgressBar { - height: 5px; - border: 1px solid $progressbar-color; -} +progress.mx_ProgressBar { + height: 6px; + width: 60px; + overflow: hidden; + appearance: none; + border: none; -.mx_ProgressBar_fill { - height: 100%; - background-color: $progressbar-color; + @mixin ProgressBarBorderRadius 6px; + @mixin ProgressBarColour $progressbar-fg-color; + @mixin ProgressBarBgColour $progressbar-bg-color; + ::-webkit-progress-value { + transition: width 1s; + } + ::-moz-progress-bar { + transition: padding-bottom 1s; + padding-bottom: var(--value); + transform-origin: 0 0; + transform: rotate(-90deg) translateX(-15px); + padding-left: 15px; + + height: 0; + } } diff --git a/res/css/views/elements/_QRCode.scss b/res/css/views/elements/_QRCode.scss new file mode 100644 index 0000000000..96d9114b54 --- /dev/null +++ b/res/css/views/elements/_QRCode.scss @@ -0,0 +1,21 @@ +/* +Copyright 2020 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_QRCode { + img { + border-radius: 8px; + } +} diff --git a/res/css/views/elements/_ReplyThread.scss b/res/css/views/elements/_ReplyThread.scss index bf44a11728..af8ca956ba 100644 --- a/res/css/views/elements/_ReplyThread.scss +++ b/res/css/views/elements/_ReplyThread.scss @@ -18,20 +18,46 @@ limitations under the License. margin-top: 0; } -.mx_ReplyThread .mx_DateSeparator { - font-size: 1em !important; - margin-top: 0; - margin-bottom: 0; - padding-bottom: 1px; - bottom: -5px; -} - .mx_ReplyThread_show { cursor: pointer; } blockquote.mx_ReplyThread { margin-left: 0; + margin-right: 0; + margin-bottom: 8px; padding-left: 10px; - border-left: 4px solid $blockquote-bar-color; + border-left: 4px solid $button-bg-color; + + &.mx_ReplyThread_color1 { + border-left-color: $username-variant1-color; + } + + &.mx_ReplyThread_color2 { + border-left-color: $username-variant2-color; + } + + &.mx_ReplyThread_color3 { + border-left-color: $username-variant3-color; + } + + &.mx_ReplyThread_color4 { + border-left-color: $username-variant4-color; + } + + &.mx_ReplyThread_color5 { + border-left-color: $username-variant5-color; + } + + &.mx_ReplyThread_color6 { + border-left-color: $username-variant6-color; + } + + &.mx_ReplyThread_color7 { + border-left-color: $username-variant7-color; + } + + &.mx_ReplyThread_color8 { + border-left-color: $username-variant8-color; + } } diff --git a/res/css/views/elements/_ResizeHandle.scss b/res/css/views/elements/_ResizeHandle.scss index 5544799a34..5189f80b30 100644 --- a/res/css/views/elements/_ResizeHandle.scss +++ b/res/css/views/elements/_ResizeHandle.scss @@ -34,7 +34,7 @@ limitations under the License. .mx_MatrixChat > .mx_ResizeHandle.mx_ResizeHandle_horizontal { margin: 0 -10px 0 0; - padding: 0 10px 0 0; + padding: 0 8px 0 0; } .mx_ResizeHandle > div { diff --git a/res/css/views/elements/_RichText.scss b/res/css/views/elements/_RichText.scss index e3f88cc779..d60282695c 100644 --- a/res/css/views/elements/_RichText.scss +++ b/res/css/views/elements/_RichText.scss @@ -6,16 +6,33 @@ .mx_RoomPill, .mx_GroupPill, .mx_AtRoomPill { - border-radius: 16px; - display: inline-block; - height: 20px; - line-height: $font-20px; - padding-left: 5px; + display: inline-flex; + align-items: center; + vertical-align: middle; + border-radius: $font-16px; + line-height: $font-15px; + padding-left: 0; } a.mx_Pill { - word-break: break-all; - display: inline; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + max-width: calc(100% - 1ch); +} + +.mx_Pill { + padding: $font-1px; + padding-right: 0.4em; + vertical-align: text-top; + line-height: $font-17px; +} + +/* More specific to override `.markdown-body a` color */ +.mx_EventTile_content .markdown-body a.mx_GroupPill, +.mx_GroupPill { + color: $accent-fg-color; + background-color: $rte-group-pill-color; } /* More specific to override `.markdown-body a` text-decoration */ @@ -28,7 +45,6 @@ a.mx_Pill { .mx_UserPill { color: $primary-fg-color; background-color: $other-user-pill-bg-color; - padding-right: 5px; } .mx_UserPill_selected { @@ -42,7 +58,6 @@ a.mx_Pill { .mx_MessageComposer_input .mx_AtRoomPill { color: $accent-fg-color; background-color: $mention-user-pill-bg-color; - padding-right: 5px; } /* More specific to override `.markdown-body a` color */ @@ -52,15 +67,6 @@ a.mx_Pill { .mx_GroupPill { color: $accent-fg-color; background-color: $rte-room-pill-color; - padding-right: 5px; -} - -/* More specific to override `.markdown-body a` color */ -.mx_EventTile_content .markdown-body a.mx_GroupPill, -.mx_GroupPill { - color: $accent-fg-color; - background-color: $rte-group-pill-color; - padding-right: 5px; } .mx_EventTile_body .mx_UserPill, @@ -74,8 +80,10 @@ a.mx_Pill { .mx_GroupPill .mx_BaseAvatar, .mx_AtRoomPill .mx_BaseAvatar { position: relative; - left: -3px; - top: 2px; + display: inline-flex; + align-items: center; + border-radius: 10rem; + margin-right: 0.24rem; } .mx_Markdown_BOLD { diff --git a/res/css/views/elements/_SSOButtons.scss b/res/css/views/elements/_SSOButtons.scss new file mode 100644 index 0000000000..e02816780f --- /dev/null +++ b/res/css/views/elements/_SSOButtons.scss @@ -0,0 +1,74 @@ +/* +Copyright 2020 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_SSOButtons { + display: flex; + flex-wrap: wrap; + justify-content: center; + + .mx_SSOButtons_row { + & + .mx_SSOButtons_row { + margin-top: 16px; + } + } + + .mx_SSOButton { + position: relative; + width: 100%; + padding: 7px 32px; + text-align: center; + border-radius: 8px; + display: inline-block; + font-size: $font-14px; + font-weight: $font-semi-bold; + border: 1px solid $input-border-color; + color: $primary-fg-color; + + > img { + object-fit: contain; + position: absolute; + left: 8px; + top: 4px; + } + } + + .mx_SSOButton_default { + color: $button-primary-bg-color; + background-color: $button-secondary-bg-color; + border-color: $button-primary-bg-color; + } + .mx_SSOButton_default.mx_SSOButton_primary { + color: $button-primary-fg-color; + background-color: $button-primary-bg-color; + } + + .mx_SSOButton_mini { + box-sizing: border-box; + width: 50px; // 48px + 1px border on all sides + height: 50px; // 48px + 1px border on all sides + min-width: 50px; // prevent crushing by the flexbox + padding: 12px; + + > img { + left: 12px; + top: 12px; + } + + & + .mx_SSOButton_mini { + margin-left: 16px; + } + } +} diff --git a/res/css/views/elements/_ServerPicker.scss b/res/css/views/elements/_ServerPicker.scss new file mode 100644 index 0000000000..188eb5d655 --- /dev/null +++ b/res/css/views/elements/_ServerPicker.scss @@ -0,0 +1,88 @@ +/* +Copyright 2020 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_ServerPicker { + margin-bottom: 14px; + border-bottom: 1px solid rgba(141, 151, 165, 0.2); + display: grid; + grid-template-columns: auto min-content; + grid-template-rows: auto auto auto; + font-size: $font-14px; + line-height: $font-20px; + + > h3 { + font-weight: $font-semi-bold; + margin: 0 0 20px; + grid-column: 1; + grid-row: 1; + } + + .mx_ServerPicker_help { + width: 20px; + height: 20px; + background-color: $icon-button-color; + border-radius: 10px; + grid-column: 2; + grid-row: 1; + margin-left: auto; + text-align: center; + color: #ffffff; + font-size: 16px; + position: relative; + + &::before { + content: ''; + width: 24px; + height: 24px; + position: absolute; + top: -2px; + left: -2px; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/i.svg'); + background: #ffffff; + } + } + + .mx_ServerPicker_server { + color: $authpage-primary-color; + grid-column: 1; + grid-row: 2; + margin-bottom: 16px; + } + + .mx_ServerPicker_change { + padding: 0; + font-size: inherit; + grid-column: 2; + grid-row: 2; + } + + .mx_ServerPicker_desc { + margin-top: -12px; + color: $tertiary-fg-color; + grid-column: 1 / 2; + grid-row: 3; + margin-bottom: 16px; + } +} + +.mx_ServerPicker_helpDialog { + .mx_Dialog_content { + width: 456px; + } +} diff --git a/res/css/views/elements/_Slider.scss b/res/css/views/elements/_Slider.scss new file mode 100644 index 0000000000..58ba2813b4 --- /dev/null +++ b/res/css/views/elements/_Slider.scss @@ -0,0 +1,99 @@ +/* +Copyright 2020 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_Slider { + position: relative; + margin: 0px; + flex-grow: 1; +} + +.mx_Slider_dotContainer { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.mx_Slider_bar { + display: flex; + box-sizing: border-box; + position: absolute; + height: 1em; + width: 100%; + padding: 0 0.5em; // half the width of a dot. + align-items: center; +} + +.mx_Slider_bar > hr { + width: 100%; + height: 0.4em; + background-color: $slider-background-color; + border: 0; +} + +.mx_Slider_selection { + display: flex; + align-items: center; + width: calc(100% - 1em); // 2 * half the width of a dot + height: 1em; + position: absolute; + pointer-events: none; +} + +.mx_Slider_selectionDot { + position: absolute; + width: 1.1em; + height: 1.1em; + background-color: $slider-selection-color; + border-radius: 50%; + box-shadow: 0 0 6px lightgrey; + z-index: 10; +} + +.mx_Slider_selection > hr { + margin: 0; + border: 0.2em solid $slider-selection-color; +} + +.mx_Slider_dot { + height: 1em; + width: 1em; + border-radius: 50%; + background-color: $slider-background-color; + z-index: 0; +} + +.mx_Slider_dotActive { + background-color: $slider-selection-color; +} + +.mx_Slider_dotValue { + display: flex; + flex-direction: column; + align-items: center; + color: $slider-background-color; +} + +// The following is a hack to center the labels without adding +// any width to the slider's dots. +.mx_Slider_labelContainer { + width: 1em; +} + +.mx_Slider_label { + position: relative; + width: fit-content; + left: -50%; +} diff --git a/res/css/views/elements/_Spinner.scss b/res/css/views/elements/_Spinner.scss index 01b4f23c2c..93d5e2d96c 100644 --- a/res/css/views/elements/_Spinner.scss +++ b/res/css/views/elements/_Spinner.scss @@ -26,3 +26,19 @@ limitations under the License. .mx_MatrixChat_middlePanel .mx_Spinner { height: auto; } + +@keyframes spin { + from { + transform: rotateZ(0deg); + } + to { + transform: rotateZ(360deg); + } +} + +.mx_Spinner_icon { + background-color: $primary-fg-color; + mask: url('$(res)/img/spinner.svg'); + mask-size: contain; + animation: 1.1s steps(12, end) infinite spin; +} diff --git a/res/css/views/elements/_StyledCheckbox.scss b/res/css/views/elements/_StyledCheckbox.scss new file mode 100644 index 0000000000..e2d61c033b --- /dev/null +++ b/res/css/views/elements/_StyledCheckbox.scss @@ -0,0 +1,90 @@ +/* +Copyright 2020 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_Checkbox { + $size: $font-16px; + $border-size: $font-1-5px; + $border-radius: $font-4px; + + display: flex; + align-items: flex-start; + + input[type=checkbox] { + appearance: none; + margin: 0; + padding: 0; + + & + label { + display: flex; + align-items: center; + + flex-grow: 1; + } + + & + label > .mx_Checkbox_background { + display: inline-flex; + position: relative; + + flex-shrink: 0; + + height: $size; + width: $size; + size: 0.5rem; + + border: $border-size solid rgba($muted-fg-color, 0.5); + box-sizing: border-box; + border-radius: $border-radius; + + img { + display: none; + + height: 100%; + width: 100%; + filter: invert(100%); + } + } + + &:checked + label > .mx_Checkbox_background { + background: $accent-color; + border-color: $accent-color; + + img { + display: block; + } + } + + & + label > *:not(.mx_Checkbox_background) { + margin-left: 10px; + } + + &:disabled + label { + opacity: 0.5; + cursor: not-allowed; + } + + &:checked:disabled + label > .mx_Checkbox_background { + background-color: $accent-color; + border-color: $accent-color; + } + + &.focus-visible { + & + label .mx_Checkbox_background { + @mixin unreal-focus; + } + } + } +} diff --git a/res/css/views/elements/_StyledRadioButton.scss b/res/css/views/elements/_StyledRadioButton.scss new file mode 100644 index 0000000000..62fb5c5512 --- /dev/null +++ b/res/css/views/elements/_StyledRadioButton.scss @@ -0,0 +1,124 @@ +/* +Copyright 2020 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. +*/ + +/** +* This component expects the parent to specify a positive padding and +* width +*/ + +.mx_RadioButton { + $radio-circle-color: $muted-fg-color; + $active-radio-circle-color: $accent-color; + position: relative; + + display: flex; + align-items: baseline; + flex-grow: 1; + + > .mx_RadioButton_content { + flex-grow: 1; + + display: flex; + flex-direction: column; + + margin-left: 8px; + margin-right: 8px; + } + + .mx_RadioButton_spacer { + flex-shrink: 0; + flex-grow: 0; + + height: $font-16px; + width: $font-16px; + } + + > input[type=radio] { + // Remove the OS's representation + margin: 0; + padding: 0; + appearance: none; + + + div { + flex-shrink: 0; + flex-grow: 0; + + display: flex; + align-items: center; + justify-content: center; + + box-sizing: border-box; + height: $font-16px; + width: $font-16px; + margin-left: 2px; // For the highlight on focus + + border: $font-1-5px solid $radio-circle-color; + border-radius: $font-16px; + + > div { + box-sizing: border-box; + + height: $font-8px; + width: $font-8px; + + border-radius: $font-8px; + } + } + + &.focus-visible { + & + div { + @mixin unreal-focus; + } + } + + &:checked { + & + div { + border-color: $active-radio-circle-color; + + & > div { + background: $active-radio-circle-color; + } + } + } + + &:disabled { + & + div, + & + div + span { + opacity: 0.5; + cursor: not-allowed; + } + + & + div { + border-color: $radio-circle-color; + } + } + + &:checked:disabled { + & + div > div { + background-color: $radio-circle-color; + } + } + } +} + +.mx_RadioButton_outlined { + border: 1px solid $input-darker-bg-color; + border-radius: 8px; +} + +.mx_RadioButton_checked { + border-color: $accent-color; +} diff --git a/res/css/views/elements/_ToggleSwitch.scss b/res/css/views/elements/_ToggleSwitch.scss index 1f4445b88c..62669889ee 100644 --- a/res/css/views/elements/_ToggleSwitch.scss +++ b/res/css/views/elements/_ToggleSwitch.scss @@ -16,11 +16,13 @@ limitations under the License. .mx_ToggleSwitch { transition: background-color 0.20s ease-out 0.1s; - width: 48px; - height: 24px; - border-radius: 14px; + + width: $font-44px; + height: $font-20px; + border-radius: 1.5rem; + padding: 2px; + background-color: $togglesw-off-color; - position: relative; opacity: 0.5; } @@ -31,23 +33,18 @@ limitations under the License. .mx_ToggleSwitch.mx_ToggleSwitch_on { background-color: $togglesw-on-color; + + > .mx_ToggleSwitch_ball { + left: calc(100% - $font-20px); + } } .mx_ToggleSwitch_ball { - transition: left 0.15s ease-out 0.1s; - margin: 2px; - width: 20px; - height: 20px; - border-radius: 20px; + position: relative; + width: $font-20px; + height: $font-20px; + border-radius: $font-20px; background-color: $togglesw-ball-color; - position: absolute; - top: 0; -} - -.mx_ToggleSwitch_on > .mx_ToggleSwitch_ball { - left: 23px; // 48px switch - 20px ball - 5px padding = 23px -} - -.mx_ToggleSwitch:not(.mx_ToggleSwitch_on) > .mx_ToggleSwitch_ball { - left: 2px; + transition: left 0.15s ease-out 0.1s; + left: 0; } diff --git a/res/css/views/elements/_Tooltip.scss b/res/css/views/elements/_Tooltip.scss index 73ac9b3558..d90c818f94 100644 --- a/res/css/views/elements/_Tooltip.scss +++ b/res/css/views/elements/_Tooltip.scss @@ -51,21 +51,27 @@ limitations under the License. .mx_Tooltip { display: none; position: fixed; - border: 1px solid $menu-border-color; - border-radius: 4px; + border-radius: 8px; box-shadow: 4px 4px 12px 0 $menu-box-shadow-color; - background-color: $menu-bg-color; - z-index: 4000; // Higher than dialogs so tooltips can be used in dialogs + z-index: 6000; // Higher than context menu so tooltips can be used everywhere padding: 10px; pointer-events: none; line-height: $font-14px; font-size: $font-12px; - font-weight: 600; - color: $primary-fg-color; + font-weight: 500; max-width: 200px; word-break: break-word; margin-right: 50px; + background-color: $inverted-bg-color; + color: $accent-fg-color; + border: 0; + text-align: center; + + .mx_Tooltip_chevron { + display: none; + } + &.mx_Tooltip_visible { animation: mx_fadein 0.2s forwards; } @@ -75,18 +81,23 @@ limitations under the License. } } -.mx_Tooltip_timeline { - box-shadow: none; - background-color: $tooltip-timeline-bg-color; - color: $tooltip-timeline-fg-color; - text-align: center; - border: none; - border-radius: 3px; - font-size: $font-14px; - line-height: 1.2; - padding: 6px 8px; +// These tooltips use an older style with a chevron +.mx_Field_tooltip { + background-color: $menu-bg-color; + color: $primary-fg-color; + border: 1px solid $menu-border-color; + text-align: unset; - .mx_Tooltip_chevron::after { - border-right-color: $tooltip-timeline-bg-color; + .mx_Tooltip_chevron { + display: unset; } } + +.mx_Tooltip_title { + font-weight: 600; +} + +.mx_Tooltip_sub { + opacity: 0.7; + margin-top: 4px; +} diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss index 24561eeeb9..400e40e233 100644 --- a/res/css/views/emojipicker/_EmojiPicker.scss +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -190,7 +190,7 @@ limitations under the License. .mx_EmojiPicker_footer { border-top: 1px solid $message-action-bar-border-color; - height: 72px; + min-height: 72px; display: flex; align-items: center; diff --git a/res/css/views/globals/_MatrixToolbar.scss b/res/css/views/globals/_MatrixToolbar.scss deleted file mode 100644 index 5fdf572f99..0000000000 --- a/res/css/views/globals/_MatrixToolbar.scss +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_MatrixToolbar { - background-color: $accent-color; - color: $accent-fg-color; - - display: flex; - align-items: center; -} - -.mx_MatrixToolbar_warning { - margin-left: 16px; - margin-right: 8px; - margin-top: -2px; -} - -.mx_MatrixToolbar_info { - padding-left: 16px; - padding-right: 8px; - background-color: $info-bg-color; -} - -.mx_MatrixToolbar_error { - padding-left: 16px; - padding-right: 8px; - background-color: $warning-bg-color; -} - -.mx_MatrixToolbar_content { - flex: 1; -} - -.mx_MatrixToolbar_link { - color: $accent-fg-color !important; - text-decoration: underline !important; - cursor: pointer; -} - -.mx_MatrixToolbar_clickable { - cursor: pointer; -} - -.mx_MatrixToolbar_close { - cursor: pointer; -} - -.mx_MatrixToolbar_close img { - display: block; - float: right; - margin-right: 10px; -} - -.mx_MatrixToolbar_action { - margin-right: 16px; -} - -.mx_MatrixToolbar_changelog { - white-space: pre; -} diff --git a/res/css/views/messages/_CreateEvent.scss b/res/css/views/messages/_CreateEvent.scss index d45645863f..cb2bf841dd 100644 --- a/res/css/views/messages/_CreateEvent.scss +++ b/res/css/views/messages/_CreateEvent.scss @@ -1,5 +1,5 @@ /* -Copyright 2018 New Vector Ltd +Copyright 2018, 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,25 +15,8 @@ limitations under the License. */ .mx_CreateEvent { - background-color: $info-plinth-bg-color; - padding-left: 20px; - padding-right: 20px; - padding-top: 10px; - padding-bottom: 10px; -} - -.mx_CreateEvent_image { - float: left; - margin-right: 20px; - width: 72px; - height: 34px; - - background-color: $primary-fg-color; - mask: url('$(res)/img/room-continuation.svg'); - mask-repeat: no-repeat; - mask-position: center; -} - -.mx_CreateEvent_header { - font-weight: bold; + &::before { + background-color: $composer-e2e-icon-color; + mask-image: url('$(res)/img/element-icons/chat-bubbles.svg'); + } } diff --git a/res/css/views/messages/_EventTileBubble.scss b/res/css/views/messages/_EventTileBubble.scss new file mode 100644 index 0000000000..e0f5d521cb --- /dev/null +++ b/res/css/views/messages/_EventTileBubble.scss @@ -0,0 +1,60 @@ +/* +Copyright 2019, 2020 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_EventTileBubble { + background-color: $dark-panel-bg-color; + padding: 10px; + border-radius: 8px; + margin: 10px auto; + max-width: 75%; + box-sizing: border-box; + display: grid; + grid-template-columns: 24px minmax(0, 1fr) min-content; + + &::before, &::after { + position: relative; + grid-column: 1; + grid-row: 1 / 3; + width: 16px; + height: 16px; + content: ""; + top: 0; + bottom: 0; + left: 0; + right: 0; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + margin-top: 4px; + } + + .mx_EventTileBubble_title, .mx_EventTileBubble_subtitle { + overflow-wrap: break-word; + } + + .mx_EventTileBubble_title { + font-weight: 600; + font-size: $font-15px; + grid-column: 2; + grid-row: 1; + } + + .mx_EventTileBubble_subtitle { + font-size: $font-12px; + grid-column: 2; + grid-row: 2; + } +} diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss index 6cbce68745..b91c461ce5 100644 --- a/res/css/views/messages/_MFileBody.scss +++ b/res/css/views/messages/_MFileBody.scss @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2015, 2016, 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. @@ -16,6 +16,19 @@ limitations under the License. .mx_MFileBody_download { color: $accent-color; + + .mx_MFileBody_download_icon { + // 12px instead of 14px to better match surrounding font size + width: 12px; + height: 12px; + mask-size: 12px; + + mask-position: center; + mask-repeat: no-repeat; + mask-image: url("$(res)/img/download.svg"); + background-color: $accent-color; + display: inline-block; + } } .mx_MFileBody_download a { @@ -45,3 +58,46 @@ limitations under the License. * big the content of the iframe is. */ height: 1.5em; } + +.mx_MFileBody_info { + background-color: $message-body-panel-bg-color; + border-radius: 12px; + width: 243px; // same width as a playable voice message, accounting for padding + padding: 6px 12px; + color: $message-body-panel-fg-color; + + .mx_MFileBody_info_icon { + background-color: $message-body-panel-icon-bg-color; + border-radius: 20px; + display: inline-block; + width: 32px; + height: 32px; + position: relative; + vertical-align: middle; + margin-right: 12px; + + &::before { + content: ''; + mask-repeat: no-repeat; + mask-position: center; + mask-size: cover; + mask-image: url('$(res)/img/element-icons/room/composer/attach.svg'); + background-color: $message-body-panel-icon-fg-color; + width: 15px; + height: 15px; + + position: absolute; + top: 8px; + left: 8px; + } + } + + .mx_MFileBody_info_filename { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: inline-block; + width: calc(100% - 32px - 12px); // 32px icon, 12px margin on the icon + vertical-align: middle; + } +} diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 547b16e9ad..878a4154cd 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +$timelineImageBorderRadius: 4px; + .mx_MImageBody { display: block; margin-right: 34px; @@ -25,6 +27,11 @@ limitations under the License. height: 100%; left: 0; top: 0; + border-radius: $timelineImageBorderRadius; + + > canvas { + border-radius: $timelineImageBorderRadius; + } } .mx_MImageBody_thumbnail_container { @@ -42,7 +49,7 @@ limitations under the License. top: 50%; } -// Inner img and TintableSvg should be centered around 0, 0 +// Inner img should be centered around 0, 0 .mx_MImageBody_thumbnail_spinner > * { transform: translate(-50%, -50%); } diff --git a/res/css/views/messages/_MImageReplyBody.scss b/res/css/views/messages/_MImageReplyBody.scss new file mode 100644 index 0000000000..70c53f8c9c --- /dev/null +++ b/res/css/views/messages/_MImageReplyBody.scss @@ -0,0 +1,37 @@ +/* +Copyright 2020 Tulir Asokan + +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_MImageReplyBody { + display: flex; + + .mx_MImageBody_thumbnail_container { + flex: 1; + margin-right: 4px; + } + + .mx_MImageReplyBody_info { + flex: 1; + + .mx_MImageReplyBody_sender { + grid-area: sender; + } + + .mx_MImageReplyBody_filename { + grid-area: filename; + } + } +} + diff --git a/res/css/views/messages/_MJitsiWidgetEvent.scss b/res/css/views/messages/_MJitsiWidgetEvent.scss new file mode 100644 index 0000000000..bea8651543 --- /dev/null +++ b/res/css/views/messages/_MJitsiWidgetEvent.scss @@ -0,0 +1,22 @@ +/* +Copyright 2020 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_MJitsiWidgetEvent { + &::before { + background-color: $composer-e2e-icon-color; // XXX: Variable abuse + mask-image: url('$(res)/img/element-icons/call/video-call.svg'); + } +} diff --git a/res/css/views/messages/_MVideoBody.scss b/res/css/views/messages/_MVideoBody.scss new file mode 100644 index 0000000000..ac3491bc8f --- /dev/null +++ b/res/css/views/messages/_MVideoBody.scss @@ -0,0 +1,23 @@ +/* +Copyright 2020 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. +*/ + +span.mx_MVideoBody { + video.mx_MVideoBody { + max-width: 100%; + height: auto; + border-radius: 4px; + } +} diff --git a/res/css/views/messages/_MVoiceMessageBody.scss b/res/css/views/messages/_MVoiceMessageBody.scss new file mode 100644 index 0000000000..3dfb98f778 --- /dev/null +++ b/res/css/views/messages/_MVoiceMessageBody.scss @@ -0,0 +1,19 @@ +/* +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_MVoiceMessageBody { + display: inline-block; // makes the playback controls magically line up +} diff --git a/res/css/views/messages/_MediaBody.scss b/res/css/views/messages/_MediaBody.scss new file mode 100644 index 0000000000..12e441750c --- /dev/null +++ b/res/css/views/messages/_MediaBody.scss @@ -0,0 +1,28 @@ +/* +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. +*/ + +// A "media body" is any file upload looking thing, apart from images and videos (they +// have unique styles). + +.mx_MediaBody { + background-color: $message-body-panel-bg-color; + border-radius: 12px; + + color: $message-body-panel-fg-color; + font-size: $font-14px; + line-height: $font-24px; +} + diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 9f3971ecf0..e2fafe6c62 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -20,11 +20,12 @@ limitations under the License. visibility: hidden; cursor: pointer; display: flex; - height: 24px; + height: 32px; line-height: $font-24px; - border-radius: 4px; - background: $message-action-bar-bg-color; - top: -18px; + border-radius: 8px; + background: $primary-bg-color; + border: 1px solid $input-border-color; + top: -32px; right: 8px; user-select: none; // Ensure the action bar appears above over things, like the read marker. @@ -41,7 +42,7 @@ limitations under the License. width: calc(10px + 48px + 100% + 8px); // safe area + action bar height: calc(20px + 100%); - top: -20px; + top: -12px; left: -58px; z-index: -1; cursor: initial; @@ -51,31 +52,19 @@ limitations under the License. white-space: nowrap; display: inline-block; position: relative; - border: 1px solid $message-action-bar-border-color; - margin-left: -1px; + margin: 2px; &:hover { - border-color: $message-action-bar-hover-border-color; + background: $roomlist-button-bg-color; + border-radius: 6px; z-index: 1; } - - &:first-child { - border-radius: 3px 0 0 3px; - } - - &:last-child { - border-radius: 0 3px 3px 0; - } - - &:only-child { - border-radius: 3px; - } } } - .mx_MessageActionBar_maskButton { - width: 27px; + width: 28px; + height: 28px; } .mx_MessageActionBar_maskButton::after { @@ -85,23 +74,36 @@ limitations under the License. left: 0; height: 100%; width: 100%; + mask-size: 18px; mask-repeat: no-repeat; mask-position: center; - background-color: $message-action-bar-fg-color; + background-color: $secondary-fg-color; +} + +.mx_MessageActionBar_maskButton:hover::after { + background-color: $primary-fg-color; } .mx_MessageActionBar_reactButton::after { - mask-image: url('$(res)/img/react.svg'); + mask-image: url('$(res)/img/element-icons/room/message-bar/emoji.svg'); } .mx_MessageActionBar_replyButton::after { - mask-image: url('$(res)/img/reply.svg'); + mask-image: url('$(res)/img/element-icons/room/message-bar/reply.svg'); } .mx_MessageActionBar_editButton::after { - mask-image: url('$(res)/img/edit.svg'); + mask-image: url('$(res)/img/element-icons/room/message-bar/edit.svg'); } .mx_MessageActionBar_optionsButton::after { - mask-image: url('$(res)/img/icon_context.svg'); + mask-image: url('$(res)/img/element-icons/context-menu.svg'); +} + +.mx_MessageActionBar_resendButton::after { + mask-image: url('$(res)/img/element-icons/retry.svg'); +} + +.mx_MessageActionBar_cancelButton::after { + mask-image: url('$(res)/img/element-icons/trashcan.svg'); } diff --git a/res/css/views/messages/_MessageTimestamp.scss b/res/css/views/messages/_MessageTimestamp.scss index f8d91cc083..85c910296a 100644 --- a/res/css/views/messages/_MessageTimestamp.scss +++ b/res/css/views/messages/_MessageTimestamp.scss @@ -17,4 +17,5 @@ limitations under the License. .mx_MessageTimestamp { color: $event-timestamp-color; font-size: $font-10px; + font-variant-numeric: tabular-nums; } diff --git a/res/css/views/messages/_ReactionsRow.scss b/res/css/views/messages/_ReactionsRow.scss index 2f5695e1fb..e05065eb02 100644 --- a/res/css/views/messages/_ReactionsRow.scss +++ b/res/css/views/messages/_ReactionsRow.scss @@ -17,18 +17,56 @@ limitations under the License. .mx_ReactionsRow { margin: 6px 0; color: $primary-fg-color; + + .mx_ReactionsRow_addReactionButton { + position: relative; + display: inline-block; + visibility: hidden; // show on hover of the .mx_EventTile + width: 24px; + height: 24px; + vertical-align: middle; + margin-left: 4px; + + &::before { + content: ''; + position: absolute; + height: 100%; + width: 100%; + mask-size: 16px; + mask-repeat: no-repeat; + mask-position: center; + background-color: $tertiary-fg-color; + mask-image: url('$(res)/img/element-icons/room/message-bar/emoji.svg'); + } + + &.mx_ReactionsRow_addReactionButton_active { + visibility: visible; // keep showing whilst the context menu is shown + } + + &:hover, &.mx_ReactionsRow_addReactionButton_active { + &::before { + background-color: $primary-fg-color; + } + } + } +} + +.mx_EventTile:hover .mx_ReactionsRow_addReactionButton { + visibility: visible; } .mx_ReactionsRow_showAll { text-decoration: none; - font-size: $font-10px; - font-weight: 600; - margin-left: 6px; - vertical-align: top; + font-size: $font-12px; + line-height: $font-20px; + margin-left: 4px; + vertical-align: middle; - &:hover, - &:link, - &:visited { - color: $accent-color; + &:link, &:visited { + color: $tertiary-fg-color; + } + + &:hover { + color: $primary-fg-color; } } diff --git a/res/css/views/messages/_ReactionsRowButton.scss b/res/css/views/messages/_ReactionsRowButton.scss index 941153ca5b..766fea2f8f 100644 --- a/res/css/views/messages/_ReactionsRowButton.scss +++ b/res/css/views/messages/_ReactionsRowButton.scss @@ -16,15 +16,15 @@ limitations under the License. .mx_ReactionsRowButton { display: inline-flex; - height: 20px; - line-height: $font-21px; + line-height: $font-20px; margin-right: 6px; - padding: 0 6px; + padding: 1px 6px; border: 1px solid $reaction-row-button-border-color; border-radius: 10px; background-color: $reaction-row-button-bg-color; cursor: pointer; user-select: none; + vertical-align: middle; &:hover { border-color: $reaction-row-button-hover-border-color; @@ -34,12 +34,16 @@ limitations under the License. background-color: $reaction-row-button-selected-bg-color; border-color: $reaction-row-button-selected-border-color; } -} -.mx_ReactionsRowButton_content { - max-width: 100px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - padding-right: 4px; + &.mx_AccessibleButton_disabled { + cursor: not-allowed; + } + + .mx_ReactionsRowButton_content { + max-width: 100px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + padding-right: 4px; + } } diff --git a/res/css/views/messages/_ReactionsRowButtonTooltip.scss b/res/css/views/messages/_ReactionsRowButtonTooltip.scss deleted file mode 100644 index cf4219fcec..0000000000 --- a/res/css/views/messages/_ReactionsRowButtonTooltip.scss +++ /dev/null @@ -1,19 +0,0 @@ -/* -Copyright 2019 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_ReactionsRowButtonTooltip_reactedWith { - opacity: 0.7; -} diff --git a/res/css/views/messages/_RedactedBody.scss b/res/css/views/messages/_RedactedBody.scss new file mode 100644 index 0000000000..600ac0c6b7 --- /dev/null +++ b/res/css/views/messages/_RedactedBody.scss @@ -0,0 +1,36 @@ +/* +Copyright 2020 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_RedactedBody { + white-space: pre-wrap; + color: $muted-fg-color; + vertical-align: middle; + + padding-left: 20px; + position: relative; + + &::before { + height: 14px; + width: 14px; + background-color: $muted-fg-color; + mask-image: url('$(res)/img/feather-customised/trash.custom.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + content: ''; + position: absolute; + top: 1px; + left: 0; + } +} diff --git a/res/css/views/messages/_SenderProfile.scss b/res/css/views/messages/_SenderProfile.scss index 655cb39489..08644b14e3 100644 --- a/res/css/views/messages/_SenderProfile.scss +++ b/res/css/views/messages/_SenderProfile.scss @@ -14,7 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_SenderProfile_name { +.mx_SenderProfile_displayName { font-weight: 600; } +.mx_SenderProfile_mxid { + font-weight: 600; + font-size: 1.1rem; + margin-left: 5px; + opacity: 0.5; // Match mx_TextualEvent +} diff --git a/res/css/views/messages/_TextualEvent.scss b/res/css/views/messages/_TextualEvent.scss index be7565b3c5..e87fed90de 100644 --- a/res/css/views/messages/_TextualEvent.scss +++ b/res/css/views/messages/_TextualEvent.scss @@ -17,4 +17,9 @@ limitations under the License. .mx_TextualEvent { opacity: 0.5; overflow-y: hidden; + + a { + color: $accent-color; + cursor: pointer; + } } diff --git a/res/css/views/messages/_ViewSourceEvent.scss b/res/css/views/messages/_ViewSourceEvent.scss index 076932ee97..66825030e0 100644 --- a/res/css/views/messages/_ViewSourceEvent.scss +++ b/res/css/views/messages/_ViewSourceEvent.scss @@ -35,13 +35,13 @@ limitations under the License. mask-size: auto 12px; visibility: hidden; background-color: $accent-color; - mask-image: url('$(res)/img/feather-customised/widget/maximise.svg'); + mask-image: url('$(res)/img/feather-customised/maximise.svg'); } &.mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle { mask-position: 0 bottom; margin-bottom: 7px; - mask-image: url('$(res)/img/feather-customised/widget/minimise.svg'); + mask-image: url('$(res)/img/feather-customised/minimise.svg'); } &:hover .mx_ViewSourceEvent_toggle { diff --git a/res/css/views/messages/_common_CryptoEvent.scss b/res/css/views/messages/_common_CryptoEvent.scss index 637d25d7a1..afaed50fa4 100644 --- a/res/css/views/messages/_common_CryptoEvent.scss +++ b/res/css/views/messages/_common_CryptoEvent.scss @@ -15,49 +15,30 @@ limitations under the License. */ .mx_cryptoEvent { - - display: grid; - grid-template-columns: 24px minmax(0, 1fr) min-content; + // white infill for the transparency + &.mx_cryptoEvent_icon::before { + background-color: #ffffff; + mask-image: url('$(res)/img/e2e/normal.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: 80%; + } &.mx_cryptoEvent_icon::after { - grid-column: 1; - grid-row: 1 / 3; - width: 16px; - height: 16px; - content: ""; - background-image: url("$(res)/img/e2e/normal.svg"); - background-repeat: no-repeat; - background-size: 100%; - margin-top: 4px; + mask-image: url('$(res)/img/e2e/normal.svg'); + background-color: $composer-e2e-icon-color; } &.mx_cryptoEvent_icon_verified::after { - background-image: url("$(res)/img/e2e/verified.svg"); + mask-image: url("$(res)/img/e2e/verified.svg"); + background-color: $accent-color; } &.mx_cryptoEvent_icon_warning::after { - background-image: url("$(res)/img/e2e/warning.svg"); + mask-image: url("$(res)/img/e2e/warning.svg"); + background-color: $notice-primary-color; } - .mx_cryptoEvent_title, .mx_cryptoEvent_subtitle, .mx_cryptoEvent_state { - overflow-wrap: break-word; - } - - .mx_cryptoEvent_title { - font-weight: 600; - font-size: $font-15px; - grid-column: 2; - grid-row: 1; - } - - .mx_cryptoEvent_subtitle { - grid-column: 2; - grid-row: 2; - } - - .mx_cryptoEvent_state, .mx_cryptoEvent_subtitle { - font-size: $font-12px; - } .mx_cryptoEvent_state, .mx_cryptoEvent_buttons { grid-column: 3; @@ -67,6 +48,7 @@ limitations under the License. .mx_cryptoEvent_buttons { align-items: center; display: flex; + gap: 5px; } .mx_cryptoEvent_state { @@ -75,5 +57,7 @@ limitations under the License. margin: auto 0; text-align: center; color: $notice-secondary-color; + overflow-wrap: break-word; + font-size: $font-12px; } } diff --git a/res/css/views/right_panel/_BaseCard.scss b/res/css/views/right_panel/_BaseCard.scss new file mode 100644 index 0000000000..9a5a59bda8 --- /dev/null +++ b/res/css/views/right_panel/_BaseCard.scss @@ -0,0 +1,172 @@ +/* +Copyright 2020 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_BaseCard { + padding: 0 8px; + overflow: hidden; + display: flex; + flex-direction: column; + flex: 1; + + .mx_BaseCard_header { + margin: 8px 0; + + > h2 { + margin: 0 44px; + font-size: $font-18px; + font-weight: $font-semi-bold; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .mx_BaseCard_back, .mx_BaseCard_close { + position: absolute; + background-color: rgba(141, 151, 165, 0.2); + height: 20px; + width: 20px; + margin: 12px; + top: 0; + border-radius: 10px; + + &::before { + content: ""; + position: absolute; + height: 20px; + width: 20px; + top: 0; + left: 0; + mask-repeat: no-repeat; + mask-position: center; + background-color: $rightpanel-button-color; + } + } + + .mx_BaseCard_back { + left: 0; + + &::before { + transform: rotate(90deg); + mask-size: 22px; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + } + } + + .mx_BaseCard_close { + right: 0; + + &::before { + mask-image: url('$(res)/img/icons-close.svg'); + mask-size: 8px; + } + } + } + + .mx_AutoHideScrollbar { + // collapse the margin into a padding to move the scrollbar into the right gutter + margin-right: -8px; + padding-right: 8px; + min-height: 0; + width: 100%; + height: 100%; + } + + .mx_BaseCard_Group { + margin: 20px 0 16px; + + & > * { + margin-left: 12px; + margin-right: 12px; + } + + > h1 { + color: $tertiary-fg-color; + font-size: $font-12px; + font-weight: 500; + } + + .mx_BaseCard_Button { + padding: 10px 38px 10px 12px; + margin: 0; + position: relative; + font-size: $font-13px; + height: 20px; + line-height: 20px; + border-radius: 8px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + &:hover { + background-color: rgba(141, 151, 165, 0.1); + } + + &::after { + content: ''; + position: absolute; + top: 10px; + right: 6px; + height: 20px; + width: 20px; + mask-repeat: no-repeat; + mask-position: center; + background-color: $icon-button-color; + transform: rotate(270deg); + mask-size: 20px; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + } + + &.mx_AccessibleButton_disabled { + padding-right: 12px; + &::after { + content: unset; + } + } + } + } + + .mx_BaseCard_footer { + padding-top: 4px; + text-align: center; + display: flex; + justify-content: space-around; + + .mx_AccessibleButton_kind_secondary { + color: $secondary-fg-color; + background-color: rgba(141, 151, 165, 0.2); + font-weight: $font-semi-bold; + font-size: $font-14px; + } + + .mx_AccessibleButton_disabled { + cursor: not-allowed; + } + } +} + +.mx_FilePanel, +.mx_UserInfo, +.mx_NotificationPanel, +.mx_MemberList { + &.mx_BaseCard { + padding: 32px 0 0; + + .mx_AutoHideScrollbar { + margin-right: unset; + padding-right: unset; + } + } +} diff --git a/res/css/views/right_panel/_EncryptionInfo.scss b/res/css/views/right_panel/_EncryptionInfo.scss index e13b1b6802..b3d4275f60 100644 --- a/res/css/views/right_panel/_EncryptionInfo.scss +++ b/res/css/views/right_panel/_EncryptionInfo.scss @@ -14,13 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_UserInfo { - .mx_EncryptionInfo_spinner { - .mx_Spinner { - margin-top: 25px; - margin-bottom: 15px; - } - - text-align: center; +.mx_EncryptionInfo_spinner { + .mx_Spinner { + margin-top: 25px; + margin-bottom: 15px; } + + text-align: center; } diff --git a/res/css/views/right_panel/_PinnedMessagesCard.scss b/res/css/views/right_panel/_PinnedMessagesCard.scss new file mode 100644 index 0000000000..785aee09ca --- /dev/null +++ b/res/css/views/right_panel/_PinnedMessagesCard.scss @@ -0,0 +1,90 @@ +/* +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_PinnedMessagesCard { + padding-top: 0; + + .mx_BaseCard_header { + text-align: center; + margin-top: 0; + border-bottom: 1px solid $menu-border-color; + + > h2 { + font-weight: $font-semi-bold; + font-size: $font-18px; + margin: 8px 0; + } + + .mx_BaseCard_close { + margin-right: 6px; + } + } + + .mx_PinnedMessagesCard_empty { + display: flex; + height: 100%; + + > div { + height: max-content; + text-align: center; + margin: auto 40px; + + .mx_PinnedMessagesCard_MessageActionBar { + pointer-events: none; + display: flex; + height: 32px; + line-height: $font-24px; + border-radius: 8px; + background: $primary-bg-color; + border: 1px solid $input-border-color; + padding: 1px; + width: max-content; + margin: 0 auto; + box-sizing: border-box; + + .mx_MessageActionBar_maskButton { + display: inline-block; + position: relative; + } + + .mx_MessageActionBar_optionsButton { + background: $roomlist-button-bg-color; + border-radius: 6px; + z-index: 1; + + &::after { + background-color: $primary-fg-color; + } + } + } + + > h2 { + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; + margin-top: 24px; + margin-bottom: 20px; + } + + > span { + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + } + } + } +} diff --git a/res/css/views/right_panel/_RoomSummaryCard.scss b/res/css/views/right_panel/_RoomSummaryCard.scss new file mode 100644 index 0000000000..dc7804d072 --- /dev/null +++ b/res/css/views/right_panel/_RoomSummaryCard.scss @@ -0,0 +1,241 @@ +/* +Copyright 2020 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_RoomSummaryCard { + .mx_BaseCard_header { + text-align: center; + margin-top: 20px; + + h2 { + font-weight: $font-semi-bold; + font-size: $font-18px; + margin: 12px 0 4px; + } + + .mx_RoomSummaryCard_alias { + font-size: $font-13px; + color: $secondary-fg-color; + } + + h2, .mx_RoomSummaryCard_alias { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + white-space: pre-wrap; + } + + .mx_RoomSummaryCard_avatar { + display: inline-flex; + + .mx_RoomSummaryCard_e2ee { + display: inline-block; + position: relative; + width: 54px; + height: 54px; + border-radius: 50%; + background-color: #737d8c; + margin-top: -3px; // alignment + margin-left: -10px; // overlap + border: 3px solid $dark-panel-bg-color; + + &::before { + content: ''; + position: absolute; + top: 13px; + left: 13px; + height: 28px; + width: 28px; + mask-size: cover; + mask-repeat: no-repeat; + mask-position: center; + mask-image: url('$(res)/img/e2e/disabled.svg'); + background-color: #ffffff; + } + } + + .mx_RoomSummaryCard_e2ee_normal { + background-color: #424446; + &::before { + mask-image: url('$(res)/img/e2e/normal.svg'); + } + } + + .mx_RoomSummaryCard_e2ee_verified { + background-color: #0dbd8b; + &::before { + mask-image: url('$(res)/img/e2e/verified.svg'); + } + } + + .mx_RoomSummaryCard_e2ee_warning { + background-color: #ff4b55; + &::before { + mask-image: url('$(res)/img/e2e/warning.svg'); + } + } + } + } + + .mx_RoomSummaryCard_aboutGroup { + .mx_RoomSummaryCard_Button { + padding-left: 44px; + + &::before { + content: ''; + position: absolute; + top: 8px; + left: 10px; + height: 24px; + width: 24px; + mask-repeat: no-repeat; + mask-position: center; + background-color: $icon-button-color; + } + } + } + + .mx_RoomSummaryCard_appsGroup { + .mx_RoomSummaryCard_Button { + // this button is special so we have to override some of the original styling + // as we will be applying it in its children + padding: 0; + height: auto; + color: $tertiary-fg-color; + + .mx_RoomSummaryCard_icon_app { + padding: 10px 48px 10px 12px; // based on typical mx_RoomSummaryCard_Button padding + text-overflow: ellipsis; + overflow: hidden; + + .mx_BaseAvatar_image { + vertical-align: top; + margin-right: 12px; + } + + span { + color: $primary-fg-color; + } + } + + .mx_RoomSummaryCard_app_pinToggle, + .mx_RoomSummaryCard_app_options { + position: absolute; + top: 0; + height: 100%; // to give bigger interactive zone + width: 24px; + padding: 12px 4px; + box-sizing: border-box; + min-width: 24px; // prevent flexbox crushing + + &:hover { + &::after { + content: ''; + position: absolute; + height: 24px; + width: 24px; + top: 8px; // equal to padding-top of parent + left: 0; + border-radius: 12px; + background-color: rgba(141, 151, 165, 0.1); + } + } + + &::before { + content: ''; + position: absolute; + height: 16px; + width: 16px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 16px; + background-color: $icon-button-color; + } + } + + .mx_RoomSummaryCard_app_pinToggle { + right: 24px; + + &::before { + mask-image: url('$(res)/img/element-icons/room/pin-upright.svg'); + } + } + + .mx_RoomSummaryCard_app_options { + right: 48px; + display: none; + + &::before { + mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); + } + } + + &.mx_RoomSummaryCard_Button_pinned { + &::after { + opacity: 0.2; + } + + .mx_RoomSummaryCard_app_pinToggle::before { + background-color: $accent-color; + } + } + + &:hover { + .mx_RoomSummaryCard_icon_app { + padding-right: 72px; + } + + .mx_RoomSummaryCard_app_options { + display: unset; + } + } + + &::before { + content: unset; + } + + &::after { + top: 8px; // re-align based on the height change + pointer-events: none; // pass through to the real button + } + } + } + + .mx_AccessibleButton_kind_link { + padding: 0; + margin-top: 12px; + margin-bottom: 12px; + font-size: $font-13px; + font-weight: $font-semi-bold; + } +} + +.mx_RoomSummaryCard_icon_people::before { + mask-image: url("$(res)/img/element-icons/room/members.svg"); +} + +.mx_RoomSummaryCard_icon_files::before { + mask-image: url('$(res)/img/element-icons/room/files.svg'); +} + +.mx_RoomSummaryCard_icon_share::before { + mask-image: url('$(res)/img/element-icons/room/share.svg'); +} + +.mx_RoomSummaryCard_icon_settings::before { + mask-image: url('$(res)/img/element-icons/settings.svg'); +} diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index a4d88f9882..6632ccddf9 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -15,7 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_UserInfo { +.mx_UserInfo.mx_BaseCard { + // UserInfo has a circular image at the top so it fits between the back & close buttons + padding-top: 0; display: flex; flex-direction: column; flex: 1; @@ -53,7 +55,7 @@ limitations under the License. } .mx_UserInfo_separator { - border-bottom: 1px solid lightgray; + border-bottom: 1px solid rgba($primary-fg-color, .1); } .mx_UserInfo_memberDetailsContainer { @@ -98,8 +100,8 @@ limitations under the License. position: absolute; top: 0; left: 0; - width: 100%; - height: 100%; + width: 100% !important; + height: 100% !important; } .mx_UserInfo_avatar .mx_BaseAvatar_initial { @@ -109,7 +111,7 @@ limitations under the License. justify-content: center; // override the calculated sizes so that the letter isn't HUGE - font-size: 56px !important; + font-size: 6rem !important; width: 100% !important; transition: font-size 0.5s; } @@ -121,7 +123,7 @@ limitations under the License. h3 { text-transform: uppercase; color: $notice-secondary-color; - font-weight: bold; + font-weight: 600; font-size: $font-12px; margin: 4px 0; } @@ -171,26 +173,12 @@ limitations under the License. margin: 6px 0; - .mx_IconButton, .mx_Spinner { - margin-left: 20px; - width: 16px; - height: 16px; - - &::before { - mask-size: 80%; - } - } - .mx_UserInfo_roleDescription { display: flex; justify-content: center; align-items: center; // try to make it the same height as the dropdown margin: 11px 0 12px 0; - - .mx_IconButton { - margin-left: 6px; - } } .mx_Field { @@ -217,9 +205,8 @@ limitations under the License. text-overflow: clip; } - .mx_UserInfo_scrollContainer { + .mx_AutoHideScrollbar { flex: 1 1 0; - padding-bottom: 16px; } .mx_UserInfo_container:not(.mx_UserInfo_separator) { @@ -272,16 +259,6 @@ limitations under the License. .mx_AccessibleButton.mx_AccessibleButton_hasKind { padding: 8px 18px; - - &.mx_AccessibleButton_kind_primary { - color: $accent-color; - background-color: $accent-bg-color; - } - - &.mx_AccessibleButton_kind_danger { - color: $notice-primary-color; - background-color: $notice-primary-bg-color; - } } .mx_VerificationShowSas .mx_AccessibleButton, diff --git a/res/css/views/right_panel/_VerificationPanel.scss b/res/css/views/right_panel/_VerificationPanel.scss index a8466a1626..12148b09de 100644 --- a/res/css/views/right_panel/_VerificationPanel.scss +++ b/res/css/views/right_panel/_VerificationPanel.scss @@ -58,7 +58,7 @@ limitations under the License. } .mx_VerificationPanel_reciprocate_section { - .mx_FormButton { + .mx_AccessibleButton { width: 100%; box-sizing: border-box; padding: 10px; diff --git a/res/css/views/right_panel/_WidgetCard.scss b/res/css/views/right_panel/_WidgetCard.scss new file mode 100644 index 0000000000..a90e744a5a --- /dev/null +++ b/res/css/views/right_panel/_WidgetCard.scss @@ -0,0 +1,63 @@ +/* +Copyright 2020 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_WidgetCard { + height: 100%; + display: contents; + + .mx_AppTileFullWidth { + max-width: unset; + height: 100%; + border: 0; + } + + .mx_BaseCard_header { + display: inline-flex; + + & > h2 { + margin-right: 0; + flex-grow: 1; + } + + .mx_WidgetCard_optionsButton { + position: relative; + margin-right: 44px; + height: 20px; + width: 20px; + min-width: 20px; // prevent crushing by the flexbox + padding: 0; + + &::before { + content: ""; + position: absolute; + width: 20px; + height: 20px; + top: 0; + left: 4px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); + background-color: $secondary-fg-color; + } + } + } +} + +.mx_WidgetCard_maxPinnedTooltip { + background-color: $notice-primary-color; + color: #ffffff; +} diff --git a/res/css/views/room_settings/_ColorSettings.scss b/res/css/views/room_settings/_ColorSettings.scss deleted file mode 100644 index fc6a4443ad..0000000000 --- a/res/css/views/room_settings/_ColorSettings.scss +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2019 New Vector Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_ColorSettings_roomColor { - display: inline-block; - position: relative; - width: 37px; - height: 37px; - border: 1px solid #979797; - margin-right: 13px; - cursor: pointer; -} - -.mx_ColorSettings_roomColor_selected { - position: absolute; - left: 10px; - top: 4px; - cursor: default !important; -} - -.mx_ColorSettings_roomColorPrimary { - height: 10px; - position: absolute; - bottom: 0px; - width: 100%; -} diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index 1b1bab67bc..fd80836237 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -15,89 +15,171 @@ See the License for the specific language governing permissions and limitations under the License. */ -/* -the tile title bar is 5 (top border) + 12 (title, buttons) + 5 (bottom padding) px = 22px -the body is assumed to be 300px (assumed by at least the sticker pickerm, perhaps elsewhere), -so the body height would be 300px - 22px (room for title bar) = 278px -BUT! the sticker picker also assumes it's a little less high than that because the iframe -for the sticker picker doesn't have any padding or margin on it's bottom. -so subtracking another 5px, which brings us at 273px. -*/ -$AppsDrawerBodyHeight: 273px; +$MiniAppTileHeight: 200px; .mx_AppsDrawer { - margin: 5px; + margin: 5px 5px 5px 18px; + position: relative; + display: flex; + flex-direction: column; + overflow: hidden; + + .mx_AppsContainer_resizerHandleContainer { + width: 100%; + height: 10px; + margin-top: -3px; // move it up so the interactions are slightly more comfortable + display: block; + position: relative; + } + + .mx_AppsContainer_resizerHandle { + cursor: ns-resize; + + // Override styles from library, making the whole area the target area + width: 100% !important; + height: 100% !important; + + // This is positioned directly below frame + position: absolute; + bottom: 0 !important; // override from library + + // We then render the pill handle in an ::after to keep it in the handle's + // area without being a massive line across the screen + &::after { + content: ''; + position: absolute; + border-radius: 3px; + + // The combination of these two should make the pill 4px high + top: 6px; + bottom: 0; + + // Together, these make the bar 64px wide + // These are also overridden from the library + left: calc(50% - 32px); + right: calc(50% - 32px); + } + } + + &:hover { + .mx_AppsContainer_resizerHandle::after { + opacity: 0.8; + background: $primary-fg-color; + } + + .mx_ResizeHandle_horizontal::before { + position: absolute; + left: 3px; + top: 50%; + transform: translate(0, -50%); + + height: 64px; // to match width of the ones on roomlist + width: 4px; + border-radius: 4px; + + content: ''; + + background-color: $primary-fg-color; + opacity: 0.8; + } + } } -.mx_AppsDrawer_hidden { - display: none; +.mx_AppsContainer_resizer { + margin-bottom: 8px; } .mx_AppsContainer { display: flex; flex-direction: row; - align-items: center; + align-items: stretch; justify-content: center; + height: 100%; + width: 100%; + flex: 1; + min-height: 0; + + .mx_AppTile:first-of-type { + border-left-width: 8px; + border-radius: 10px 0 0 10px; + } + .mx_AppTile:last-of-type { + border-right-width: 8px; + border-radius: 0 10px 10px 0; + } + + .mx_ResizeHandle_horizontal { + position: relative; + + > div { + width: 0; + } + } } -.mx_AddWidget_button { - order: 2; - cursor: pointer; - padding: 0; - margin: 5px auto 5px auto; - color: $accent-color; - font-size: $font-12px; -} +// TODO this should be 300px but that's too large +$MinWidth: 240px; -.mx_AddWidget_button_full_width { - max-width: 960px; -} +.mx_AppsDrawer_2apps .mx_AppTile { + width: 50%; -.mx_SetAppURLDialog_input { - border-radius: 3px; - border: 1px solid $input-border-color; - padding: 9px; - color: $primary-hairline-color; - background-color: $primary-bg-color; - font-size: $font-15px; + &:nth-child(3) { + flex-grow: 1; + width: 0 !important; + min-width: $MinWidth !important; + } +} +.mx_AppsDrawer_3apps .mx_AppTile { + width: 33%; + + &:nth-child(3) { + flex-grow: 1; + width: 0 !important; + min-width: $MinWidth !important; + } } .mx_AppTile { - max-width: 960px; width: 50%; - margin-right: 5px; - border: 5px solid $widget-menu-bar-bg-color; - border-radius: 4px; -} - -.mx_AppTile:last-child { - margin-right: 1px; + min-width: $MinWidth; + border: 8px solid $widget-menu-bar-bg-color; + border-left-width: 5px; + border-right-width: 5px; + display: flex; + flex-direction: column; + box-sizing: border-box; + background-color: $widget-menu-bar-bg-color; } .mx_AppTileFullWidth { - max-width: 960px; - width: 100%; - height: 100%; + width: 100% !important; // to override the inline style set by the resizer margin: 0; padding: 0; border: 5px solid $widget-menu-bar-bg-color; - border-radius: 4px; + border-radius: 8px; + display: flex; + flex-direction: column; + background-color: $widget-menu-bar-bg-color; } .mx_AppTile_mini { - max-width: 960px; width: 100%; - height: 100%; margin: 0; padding: 0; + display: flex; + flex-direction: column; + height: $MiniAppTileHeight; } -.mx_AppTile_persistedWrapper { - height: $AppsDrawerBodyHeight; -} - +.mx_AppTile .mx_AppTile_persistedWrapper, +.mx_AppTileFullWidth .mx_AppTile_persistedWrapper, .mx_AppTile_mini .mx_AppTile_persistedWrapper { - height: 114px; + flex: 1; +} + +.mx_AppTile_persistedWrapper div { + width: 100%; + height: 100%; } .mx_AppTileMenuBar { @@ -108,18 +190,20 @@ $AppsDrawerBodyHeight: 273px; flex-direction: row; align-items: center; justify-content: space-between; - cursor: pointer; -} - -.mx_AppTileMenuBar_expanded { - padding-bottom: 5px; + width: 100%; + padding-top: 2px; + padding-bottom: 8px; } .mx_AppTileMenuBarTitle { - display: flex; - flex-direction: row; - align-items: center; - pointer-events: none; + line-height: 20px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + .mx_WidgetAvatar { + margin-right: 12px; + } } .mx_AppTileMenuBarTitle > :last-child { @@ -143,43 +227,34 @@ $AppsDrawerBodyHeight: 273px; margin: 0 3px; } -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_minimise { - mask-image: url('$(res)/img/feather-customised/widget/minimise.svg'); - background-color: $accent-color; -} - -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_maximise { - mask-image: url('$(res)/img/feather-customised/widget/maximise.svg'); - background-color: $accent-color; -} - .mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_popout { mask-image: url('$(res)/img/feather-customised/widget/external-link.svg'); } .mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_menu { - mask-image: url('$(res)/img/icon_context.svg'); -} - -.mx_AppTileMenuBarWidgetDelete { - filter: none; -} - -.mx_AppTileMenuBarWidget:hover { - border: 1px solid $primary-fg-color; - border-radius: 2px; + mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); } .mx_AppTileBody { - height: $AppsDrawerBodyHeight; + height: 100%; width: 100%; overflow: hidden; + border-radius: 8px; + background-color: $widget-body-bg-color; } .mx_AppTileBody_mini { - height: 112px; + height: $MiniAppTileHeight; width: 100%; overflow: hidden; + border-radius: 8px; +} + +.mx_AppTile .mx_AppTileBody, +.mx_AppTileFullWidth .mx_AppTileBody, +.mx_AppTile_mini .mx_AppTileBody_mini { + height: inherit; + flex: 1; } .mx_AppTileBody_mini iframe { @@ -190,7 +265,7 @@ $AppsDrawerBodyHeight: 273px; .mx_AppTileBody iframe { width: 100%; - height: $AppsDrawerBodyHeight; + height: 100%; overflow: hidden; border: none; padding: 0; @@ -198,75 +273,8 @@ $AppsDrawerBodyHeight: 273px; display: block; } -.mx_AppTileMenuBarWidgetPadding { - margin-right: 5px; -} - -.mx_AppIconTile { - background-color: $lightbox-bg-color; - border: 1px solid rgba(0, 0, 0, 0); - width: 200px; - box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); - transition: 0.3s; - border-radius: 3px; - margin: 5px; - display: inline-block; -} - -.mx_AppIconTile.mx_AppIconTile_active { - color: $accent-color; - border-color: $accent-color; -} - -.mx_AppIconTile:hover { - border: 1px solid $accent-color; - box-shadow: 0 0 10px 5px rgba(200, 200, 200, 0.5); -} - -.mx_AppIconTile_content { - padding: 2px 16px; - height: 60px; - overflow: hidden; -} - -.mx_AppIconTile_content h4 { - margin-top: 5px; - margin-bottom: 2px; -} - -.mx_AppIconTile_content p { - margin-top: 0; - margin-bottom: 5px; - font-size: smaller; -} - -.mx_AppIconTile_image { - padding: 10px; - max-width: 100px; - max-height: 100px; - width: auto; - height: auto; -} - -.mx_AppIconTile_imageContainer { - text-align: center; - width: 100%; - background-color: white; - border-radius: 3px 3px 0 0; - height: 155px; - display: flex; - justify-content: center; - align-items: center; -} - -form.mx_Custom_Widget_Form div { - margin-top: 10px; - margin-bottom: 10px; -} - .mx_AppPermissionWarning { text-align: center; - background-color: $widget-menu-bar-bg-color; display: flex; height: 100%; flex-direction: column; @@ -330,7 +338,11 @@ form.mx_Custom_Widget_Form div { align-items: center; font-weight: bold; position: relative; - height: $AppsDrawerBodyHeight; + height: 100%; + + // match bg of border so that the cut corners have the right fill + background-color: $widget-body-bg-color !important; + border-radius: 8px; } .mx_AppLoading .mx_Spinner { @@ -357,3 +369,7 @@ form.mx_Custom_Widget_Form div { .mx_AppLoading iframe { display: none; } + +.mx_AppsDrawer_resizing .mx_AppTile_persistedWrapper { + z-index: 1; +} diff --git a/res/css/views/rooms/_Autocomplete.scss b/res/css/views/rooms/_Autocomplete.scss index e5316f5a46..f8e0a382b1 100644 --- a/res/css/views/rooms/_Autocomplete.scss +++ b/res/css/views/rooms/_Autocomplete.scss @@ -6,9 +6,10 @@ border: 1px solid $primary-hairline-color; background: $primary-bg-color; border-bottom: none; - border-radius: 4px 4px 0 0; + border-radius: 8px 8px 0 0; max-height: 50vh; overflow: auto; + box-shadow: 0px -16px 32px $composer-shadow-color; } .mx_Autocomplete_ProviderSection { @@ -31,9 +32,10 @@ } .mx_Autocomplete_Completion_pill { - border-radius: 17px; - height: 34px; - padding: 0px 5px; + box-sizing: border-box; + border-radius: 2rem; + height: $font-34px; + padding: 0.4rem; display: flex; user-select: none; cursor: pointer; @@ -42,7 +44,7 @@ } .mx_Autocomplete_Completion_pill > * { - margin: 0 3px; + margin-right: 0.3rem; } /* styling for common completion elements */ diff --git a/res/css/views/rooms/_AuxPanel.scss b/res/css/views/rooms/_AuxPanel.scss index 34ef5e01d4..17a6294bf0 100644 --- a/res/css/views/rooms/_AuxPanel.scss +++ b/res/css/views/rooms/_AuxPanel.scss @@ -17,7 +17,7 @@ limitations under the License. .m_RoomView_auxPanel_stateViews { padding: 5px; padding-left: 19px; - border-bottom: 1px solid #e5e5e5; + border-bottom: 1px solid $primary-hairline-color; } .m_RoomView_auxPanel_stateViews_span a { diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index e9013eb7b7..e1ba468204 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -46,22 +46,19 @@ limitations under the License. &.mx_BasicMessageComposer_input_shouldShowPillAvatar { span.mx_UserPill, span.mx_RoomPill { - padding-left: 21px; position: relative; // avatar psuedo element &::before { - position: absolute; - left: 2px; - top: 2px; content: var(--avatar-letter); - width: 16px; - height: 16px; + width: $font-16px; + height: $font-16px; + margin-right: 0.24rem; background: var(--avatar-background), $avatar-bg-color; color: $avatar-initial-color; background-repeat: no-repeat; - background-size: 16px; - border-radius: 8px; + background-size: $font-16px; + border-radius: $font-16px; text-align: center; font-weight: normal; line-height: $font-16px; @@ -69,6 +66,11 @@ limitations under the License. } } } + + &.mx_BasicMessageComposer_input_disabled { + // Ignore all user input to avoid accidentally triggering the composer + pointer-events: none; + } } .mx_BasicMessageComposer_AutoCompleteWrapper { diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index 584ea17433..68ad44cf6a 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -22,28 +22,58 @@ limitations under the License. display: block; } -.mx_E2EIcon_warning::after, -.mx_E2EIcon_normal::after, -.mx_E2EIcon_verified::after { - content: ""; - display: block; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - background-repeat: no-repeat; - background-size: contain; +.mx_E2EIcon_warning, +.mx_E2EIcon_normal, +.mx_E2EIcon_verified { + &::before, &::after { + content: ""; + display: block; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + } +} + +// white infill for the transparency +.mx_E2EIcon::before { + background-color: #ffffff; + mask-image: url('$(res)/img/e2e/normal.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: 80%; +} + +// transparent-looking border surrounding the shield for when overlain over avatars +.mx_E2EIcon_bordered { + mask-image: url('$(res)/img/e2e/normal.svg'); + background-color: $header-panel-bg-color; + + // shrink the actual badge + &::after { + mask-size: 75%; + } + // shrink the infill of the badge + &::before { + mask-size: 60%; + } } .mx_E2EIcon_warning::after { - background-image: url('$(res)/img/e2e/warning.svg'); + mask-image: url('$(res)/img/e2e/warning.svg'); + background-color: $notice-primary-color; } .mx_E2EIcon_normal::after { - background-image: url('$(res)/img/e2e/normal.svg'); + mask-image: url('$(res)/img/e2e/normal.svg'); + background-color: $composer-e2e-icon-color; } .mx_E2EIcon_verified::after { - background-image: url('$(res)/img/e2e/verified.svg'); + mask-image: url('$(res)/img/e2e/verified.svg'); + background-color: $accent-color; } diff --git a/res/css/views/rooms/_EntityTile.scss b/res/css/views/rooms/_EntityTile.scss index 966d2c4e70..27a4e67089 100644 --- a/res/css/views/rooms/_EntityTile.scss +++ b/res/css/views/rooms/_EntityTile.scss @@ -26,8 +26,6 @@ limitations under the License. position: absolute; bottom: 2px; right: 7px; - height: 15px; - width: 15px; } } @@ -69,8 +67,6 @@ limitations under the License. padding-right: 12px; padding-top: 4px; padding-bottom: 4px; - width: 36px; - height: 36px; position: relative; } diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index e015f30e48..55f73c0315 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -15,6 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ +$left-gutter: 64px; +$hover-select-border: 4px; + .mx_EventTile { max-width: 100%; clear: both; @@ -23,21 +26,11 @@ limitations under the License. position: relative; } -.mx_EventTile_bubble { - background-color: $dark-panel-bg-color; - padding: 10px; - border-radius: 5px; - margin: 10px auto; - max-width: 75%; - box-sizing: border-box; -} - .mx_EventTile.mx_EventTile_info { - padding-top: 0px; + padding-top: 1px; } .mx_EventTile_avatar { - position: absolute; top: 14px; left: 8px; cursor: pointer; @@ -45,8 +38,8 @@ limitations under the License. } .mx_EventTile.mx_EventTile_info .mx_EventTile_avatar { - top: 8px; - left: 65px; + top: $font-6px; + left: $left-gutter; } .mx_EventTile_continuation { @@ -68,15 +61,13 @@ limitations under the License. display: inline-block; /* anti-zalgo, with overflow hidden */ overflow: hidden; cursor: pointer; - padding-left: 65px; /* left gutter */ padding-bottom: 0px; padding-top: 0px; margin: 0px; - line-height: $font-17px; /* the next three lines, along with overflow hidden, truncate long display names */ white-space: nowrap; text-overflow: ellipsis; - max-width: calc(100% - 65px); + max-width: calc(100% - $left-gutter); } .mx_EventTile .mx_SenderProfile .mx_Flair { @@ -84,7 +75,6 @@ limitations under the License. margin-left: 5px; display: inline-block; vertical-align: top; - height: 16px; overflow: hidden; user-select: none; @@ -96,35 +86,31 @@ limitations under the License. } .mx_EventTile_isEditing .mx_MessageTimestamp { - visibility: hidden !important; + visibility: hidden; } .mx_EventTile .mx_MessageTimestamp { display: block; - visibility: hidden; white-space: nowrap; left: 0px; - width: 46px; /* 8 + 30 (avatar) + 8 */ text-align: center; - position: absolute; user-select: none; } -.mx_EventTile_line, .mx_EventTile_reply { +.mx_EventTile_continuation .mx_EventTile_line { clear: both; +} + +.mx_EventTile_line, .mx_EventTile_reply { position: relative; - padding-left: 65px; /* left gutter */ - padding-top: 4px; - padding-bottom: 2px; - border-radius: 4px; - min-height: 24px; - line-height: $font-22px; + padding-left: $left-gutter; + border-radius: 8px; } .mx_RoomView_timeline_rr_enabled, // on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter .mx_EventListSummary { - .mx_EventTile_line, .mx_EventTile_reply { + .mx_EventTile_line { /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ margin-right: 110px; } @@ -135,9 +121,10 @@ limitations under the License. grid-template-columns: 1fr 100px; .mx_EventTile_line { - margin-right: 0px; + margin-right: 0; grid-column: 1 / 3; - padding: 0; + // override default padding of mx_EventTile_line so that we can be centered + padding: 0 !important; } .mx_EventTile_msgOption { @@ -149,31 +136,14 @@ limitations under the License. margin-right: 10px; } -.mx_EventTile_info .mx_EventTile_line { - padding-left: 83px; -} - /* HACK to override line-height which is already marked important elsewhere */ .mx_EventTile_bigEmoji.mx_EventTile_bigEmoji { font-size: 48px !important; line-height: 57px !important; } -.mx_MessagePanel_alwaysShowTimestamps .mx_MessageTimestamp { - visibility: visible; -} - .mx_EventTile_selected > div > a > .mx_MessageTimestamp { - left: 3px; - width: auto; -} - -// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) -.mx_EventTile_last > div > a > .mx_MessageTimestamp, -.mx_EventTile:hover > div > a > .mx_MessageTimestamp, -.mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp, -.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp { - visibility: visible; + left: calc(-$hover-select-border); } .mx_EventTile:hover .mx_MessageActionBar, @@ -187,8 +157,8 @@ limitations under the License. * TODO: ultimately we probably want some transition on here. */ .mx_EventTile_selected > .mx_EventTile_line { - border-left: $accent-color 5px solid; - padding-left: 60px; + border-left: $accent-color 4px solid; + padding-left: calc($left-gutter - $hover-select-border); background-color: $event-selected-color; } @@ -201,8 +171,12 @@ limitations under the License. } } +.mx_EventTile_info .mx_EventTile_line { + padding-left: calc($left-gutter + 18px); +} + .mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { - padding-left: 78px; + padding-left: calc($left-gutter + 18px - $hover-select-border); } .mx_EventTile:hover .mx_EventTile_line, @@ -225,49 +199,30 @@ limitations under the License. color: $accent-fg-color; } -.mx_EventTile_encrypting { - color: $event-encrypting-color !important; -} +.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 -.mx_EventTile_sending { - color: $event-sending-color; + &::before { + background-color: $tertiary-fg-color; + 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_sending .mx_UserPill, -.mx_EventTile_sending .mx_RoomPill { - opacity: 0.5; +.mx_EventTile_receiptSent::before { + mask-image: url('$(res)/img/element-icons/circle-sent.svg'); } - -.mx_EventTile_notSent { - color: $event-notsent-color; -} - -.mx_EventTile_redacted .mx_EventTile_line .mx_UnknownBody, -.mx_EventTile_redacted .mx_EventTile_reply .mx_UnknownBody { - --lozenge-color: $event-redacted-fg-color; - --lozenge-border-color: $event-redacted-border-color; - display: block; - height: 22px; - width: 250px; - border-radius: 11px; - background: - repeating-linear-gradient( - -45deg, - var(--lozenge-color), - var(--lozenge-color) 3px, - transparent 3px, - transparent 6px - ); - box-shadow: 0px 0px 3px var(--lozenge-border-color) inset; -} - -.mx_EventTile_sending.mx_EventTile_redacted .mx_UnknownBody { - opacity: 0.4; -} - -div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { - --lozenge-color: $event-notsent-color; - --lozenge-border-color: $event-notsent-color; +.mx_EventTile_receiptSending::before { + mask-image: url('$(res)/img/element-icons/circle-sending.svg'); } .mx_EventTile_contextual { @@ -297,20 +252,23 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { display: inline-block; width: 14px; height: 14px; - top: 29px; + // This aligns the avatar with the last line of the + // message. We want to move it one line up - 2.2rem + top: -2.2rem; user-select: none; z-index: 1; } -.mx_EventTile_continuation .mx_EventTile_readAvatars, -.mx_EventTile_info .mx_EventTile_readAvatars, -.mx_EventTile_emote .mx_EventTile_readAvatars { - top: 7px; -} - .mx_EventTile_readAvatars .mx_BaseAvatar { position: absolute; display: inline-block; + height: $font-14px; + width: $font-14px; + + will-change: left, top; + transition: + left var(--transition-short) ease-out, + top var(--transition-standard) ease-out; } .mx_EventTile_readAvatarRemainder { @@ -359,40 +317,68 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { .mx_EventTile_e2eIcon { position: absolute; top: 6px; - left: 46px; - width: 15px; - height: 15px; + left: 44px; + width: 14px; + height: 14px; display: block; bottom: 0; right: 0; opacity: 0.2; background-repeat: no-repeat; background-size: contain; + + &::before, &::after { + content: ""; + display: block; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + } + + &::before { + background-color: #ffffff; + mask-image: url('$(res)/img/e2e/normal.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: 80%; + } } .mx_EventTile_e2eIcon_undecryptable, .mx_EventTile_e2eIcon_unverified { - background-image: url('$(res)/img/e2e/warning.svg'); + &::after { + mask-image: url('$(res)/img/e2e/warning.svg'); + background-color: $notice-primary-color; + } opacity: 1; } .mx_EventTile_e2eIcon_unknown { - background-image: url('$(res)/img/e2e/warning.svg'); + &::after { + mask-image: url('$(res)/img/e2e/warning.svg'); + background-color: $notice-primary-color; + } opacity: 1; } .mx_EventTile_e2eIcon_unencrypted { - background-image: url('$(res)/img/e2e/warning.svg'); + &::after { + mask-image: url('$(res)/img/e2e/warning.svg'); + background-color: $notice-primary-color; + } opacity: 1; } -.mx_EventTile_e2eIcon_hidden { - display: none; -} - -/* always override hidden attribute for blocked and warning */ -.mx_EventTile_e2eIcon_hidden[src*="img/e2e-blocked.svg"], -.mx_EventTile_e2eIcon_hidden[src*="img/e2e-warning.svg"] { - display: block; +.mx_EventTile_e2eIcon_unauthenticated { + &::after { + mask-image: url('$(res)/img/e2e/normal.svg'); + background-color: $composer-e2e-icon-color; + } + opacity: 1; } .mx_EventTile_keyRequestInfo { @@ -423,32 +409,28 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { margin-bottom: 0px; } -.mx_EventTile_12hr .mx_EventTile_e2eIcon { - padding-left: 5px; -} - .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { - padding-left: 60px; + padding-left: calc($left-gutter - $hover-select-border); } .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line { - border-left: $e2e-verified-color 5px solid; + border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid; } .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line { - border-left: $e2e-unverified-color 5px solid; + border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid; } .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { - border-left: $e2e-unknown-color 5px solid; + border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid; } .mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line { - padding-left: 78px; + padding-left: calc($left-gutter + 18px - $hover-select-border); } /* End to end encryption stuff */ @@ -460,8 +442,7 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { - left: 3px; - width: auto; + left: calc(-$hover-select-border); } // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) @@ -496,8 +477,7 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { pre, code { font-family: $monospace-font-family !important; - // deliberate constants as we're behind an invert filter - color: #333; + background-color: $header-panel-bg-color; } pre { @@ -506,13 +486,23 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { // https://github.com/vector-im/vector-web/issues/754 overflow-x: overlay; overflow-y: visible; - max-height: 30vh; } +} - code { - // deliberate constants as we're behind an invert filter - background-color: #f8f8f8; - } +.mx_EventTile_lineNumbers { + float: left; + margin: 0 0.5em 0 -1.5em; + color: gray; +} + +.mx_EventTile_lineNumber { + text-align: right; + display: block; + padding-left: 1em; +} + +.mx_EventTile_collapsedCodeBlock { + max-height: 30vh; } .mx_EventTile:hover .mx_EventTile_body pre, @@ -526,20 +516,42 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { } // Inserted adjacent to
 blocks, (See TextualBody)
-.mx_EventTile_copyButton {
+.mx_EventTile_button {
     position: absolute;
     display: inline-block;
     visibility: hidden;
     cursor: pointer;
-    top: 6px;
-    right: 6px;
+    top: 8px;
+    right: 8px;
     width: 19px;
     height: 19px;
-    background-image: url($copy-button-url);
+    background-color: $message-action-bar-fg-color;
+}
+.mx_EventTile_buttonBottom {
+    top: 33px;
+}
+.mx_EventTile_copyButton {
+    mask-image: url($copy-button-url);
+}
+.mx_EventTile_collapseButton {
+    mask-size: 75%;
+    mask-position: center;
+    mask-repeat: no-repeat;
+    mask-image: url($collapse-button-url);
+}
+.mx_EventTile_expandButton {
+    mask-size: 75%;
+    mask-position: center;
+    mask-repeat: no-repeat;
+    mask-image: url($expand-button-url);
 }
 
 .mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_copyButton,
-.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_copyButton {
+.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_copyButton,
+.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_collapseButton,
+.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_collapseButton,
+.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_expandButton,
+.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_expandButton {
     visibility: visible;
 }
 
@@ -563,7 +575,6 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody {
 
 .mx_EventTile_content .markdown-body a {
     color: $accent-color-alt;
-    text-decoration: underline;
 }
 
 .mx_EventTile_content .markdown-body .hljs {
@@ -584,80 +595,33 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody {
 
 /* end of overrides */
 
-.mx_MatrixChat_useCompactLayout {
-    .mx_EventTile {
-        padding-top: 4px;
+.mx_EventTile_tileError {
+    color: red;
+    text-align: center;
+
+    // Remove some of the default tile padding so that the error is centered
+    margin-right: 0;
+    .mx_EventTile_line {
+        padding-left: 0;
+        margin-right: 0;
     }
 
-    .mx_EventTile.mx_EventTile_info {
-        // same as the padding for non-compact .mx_EventTile.mx_EventTile_info
-        padding-top: 0px;
-        font-size: $font-13px;
-        .mx_EventTile_line, .mx_EventTile_reply {
-            line-height: $font-20px;
-        }
-        .mx_EventTile_avatar {
-            top: 4px;
-        }
+    .mx_EventTile_line span {
+        padding: 4px 8px;
     }
 
-    .mx_EventTile .mx_SenderProfile {
-        font-size: $font-13px;
-    }
-
-    .mx_EventTile.mx_EventTile_emote {
-        // add a bit more space for emotes so that avatars don't collide
-        padding-top: 8px;
-        .mx_EventTile_avatar {
-            top: 2px;
-        }
-        .mx_EventTile_line, .mx_EventTile_reply {
-            padding-top: 0px;
-            padding-bottom: 1px;
-        }
-    }
-
-    .mx_EventTile.mx_EventTile_emote.mx_EventTile_continuation {
-        padding-top: 0;
-        .mx_EventTile_line, .mx_EventTile_reply {
-            padding-top: 0px;
-            padding-bottom: 0px;
-        }
-    }
-
-    .mx_EventTile_line, .mx_EventTile_reply {
-        padding-top: 0px;
-        padding-bottom: 0px;
-    }
-
-    .mx_EventTile_avatar {
-        top: 2px;
-    }
-
-    .mx_EventTile_e2eIcon {
-        top: 3px;
-    }
-
-    .mx_EventTile_readAvatars {
-        top: 27px;
-    }
-
-    .mx_EventTile_continuation .mx_EventTile_readAvatars,
-    .mx_EventTile_emote .mx_EventTile_readAvatars {
-        top: 5px;
-    }
-
-    .mx_EventTile_info .mx_EventTile_readAvatars {
-        top: 4px;
-    }
-
-    .mx_RoomView_MessageList h2 {
-        margin-top: 6px;
-    }
-
-    .mx_EventTile_content .markdown-body {
-        p, ul, ol, dl, blockquote, pre, table {
-            margin-bottom: 4px; // 1/4 of the non-compact margin-bottom
-        }
+    a {
+        margin-left: 1em;
+    }
+}
+
+@media only screen and (max-width: 480px) {
+    .mx_EventTile_line, .mx_EventTile_reply {
+        padding-left: 0;
+        margin-right: 0;
+    }
+    .mx_EventTile_content {
+        margin-top: 10px;
+        margin-right: 0;
     }
 }
diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss
new file mode 100644
index 0000000000..ddee81a914
--- /dev/null
+++ b/res/css/views/rooms/_GroupLayout.scss
@@ -0,0 +1,115 @@
+/*
+Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2020 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.
+*/
+
+$left-gutter: 64px;
+
+.mx_GroupLayout {
+    .mx_EventTile {
+        > .mx_SenderProfile {
+            line-height: $font-20px;
+            margin-left: $left-gutter;
+        }
+
+        > .mx_EventTile_avatar {
+            position: absolute;
+        }
+
+        .mx_MessageTimestamp {
+            position: absolute;
+            width: $MessageTimestamp_width;
+        }
+
+        .mx_EventTile_line, .mx_EventTile_reply {
+            padding-top: 1px;
+            padding-bottom: 3px;
+            line-height: $font-22px;
+        }
+    }
+}
+
+/* Compact layout overrides */
+
+.mx_MatrixChat_useCompactLayout {
+    .mx_EventTile {
+        padding-top: 4px;
+
+        .mx_EventTile_line, .mx_EventTile_reply {
+            padding-top: 0;
+            padding-bottom: 0;
+        }
+
+        &.mx_EventTile_info {
+            // same as the padding for non-compact .mx_EventTile.mx_EventTile_info
+            padding-top: 0px;
+            font-size: $font-13px;
+            .mx_EventTile_line, .mx_EventTile_reply {
+                line-height: $font-20px;
+            }
+            .mx_EventTile_avatar {
+                top: 4px;
+            }
+        }
+
+        .mx_SenderProfile {
+            font-size: $font-13px;
+        }
+
+        &.mx_EventTile_emote {
+            // add a bit more space for emotes so that avatars don't collide
+            padding-top: 8px;
+            .mx_EventTile_avatar {
+                top: 2px;
+            }
+            .mx_EventTile_line, .mx_EventTile_reply {
+                padding-top: 0px;
+                padding-bottom: 1px;
+            }
+        }
+
+        &.mx_EventTile_emote.mx_EventTile_continuation {
+            padding-top: 0;
+            .mx_EventTile_line, .mx_EventTile_reply {
+                padding-top: 0px;
+                padding-bottom: 0px;
+            }
+        }
+
+        .mx_EventTile_avatar {
+            top: 2px;
+        }
+
+        .mx_EventTile_e2eIcon {
+            top: 3px;
+        }
+
+        .mx_EventTile_readAvatars {
+            // This aligns the avatar with the last line of the
+            // message. We want to move it one line up - 2rem
+            top: -2rem;
+        }
+
+        .mx_EventTile_content .markdown-body {
+            p, ul, ol, dl, blockquote, pre, table {
+                margin-bottom: 4px; // 1/4 of the non-compact margin-bottom
+            }
+        }
+    }
+
+    .mx_RoomView_MessageList h2 {
+        margin-top: 6px;
+    }
+}
diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss
new file mode 100644
index 0000000000..97190807ca
--- /dev/null
+++ b/res/css/views/rooms/_IRCLayout.scss
@@ -0,0 +1,241 @@
+/*
+Copyright 2020 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.
+*/
+
+$icon-width: 14px;
+$timestamp-width: 45px;
+$right-padding: 5px;
+$irc-line-height: $font-18px;
+
+.mx_IRCLayout {
+    --name-width: 70px;
+
+    line-height: $irc-line-height !important;
+
+    .mx_EventTile {
+
+        // timestamps are links which shouldn't be underlined
+        > a {
+            text-decoration: none;
+            min-width: 45px;
+        }
+
+        display: flex;
+        flex-direction: row;
+        align-items: flex-start;
+        padding-top: 0;
+
+        > * {
+            margin-right: $right-padding;
+        }
+
+        > .mx_EventTile_msgOption {
+            order: 5;
+            flex-shrink: 0;
+
+            .mx_EventTile_readAvatars {
+                top: 0.2rem; // ($irc-line-height - avatar height) / 2
+            }
+        }
+
+        .mx_EventTile_line, .mx_EventTile_reply {
+            padding: 0;
+            display: flex;
+            flex-direction: column;
+            order: 3;
+            flex-grow: 1;
+            flex-shrink: 1;
+            min-width: 0;
+        }
+
+        > .mx_EventTile_avatar {
+            order: 1;
+            position: relative;
+            top: 0;
+            left: 0;
+            flex-shrink: 0;
+            height: $irc-line-height;
+            display: flex;
+            align-items: center;
+
+            // Need to use important to override the js provided height and width values.
+            > .mx_BaseAvatar, > .mx_BaseAvatar > * {
+                height: $font-14px !important;
+                width: $font-14px !important;
+                font-size: $font-10px !important;
+                line-height: $font-15px !important;
+            }
+        }
+
+        .mx_MessageTimestamp {
+            font-size: $font-10px;
+            width: $timestamp-width;
+            text-align: right;
+        }
+
+        > .mx_EventTile_e2eIcon {
+            position: absolute;
+            right: unset;
+            left: unset;
+            top: 0;
+
+            padding: 0;
+
+            flex-shrink: 0;
+            flex-grow: 0;
+
+            height: $font-18px;
+
+            background-position: center;
+        }
+
+        .mx_EventTile_line {
+            .mx_EventTile_e2eIcon,
+            .mx_TextualEvent,
+            .mx_MTextBody {
+                display: inline-block;
+            }
+        }
+
+        .mx_EventTile_reply {
+            order: 4;
+        }
+
+        .mx_EditMessageComposer_buttons {
+            position: relative;
+        }
+    }
+
+    .mx_EventTile_emote {
+        > .mx_EventTile_avatar {
+            margin-left: calc(var(--name-width) + $icon-width + $right-padding);
+        }
+    }
+
+    blockquote {
+        margin: 0;
+    }
+
+    .mx_EventListSummary {
+        > .mx_EventTile_line {
+            padding-left: calc(var(--name-width) + $icon-width + $timestamp-width + 3 * $right-padding); // 15 px of padding
+        }
+
+        .mx_EventListSummary_avatars {
+            padding: 0;
+            margin: 0 9px 0 0;
+        }
+    }
+
+    .mx_EventTile.mx_EventTile_info {
+        .mx_EventTile_avatar {
+            left: calc(var(--name-width) + 10px + $icon-width);
+            top: 0;
+        }
+
+        .mx_EventTile_line {
+            left: calc(var(--name-width) + 10px + $icon-width);
+        }
+
+        .mx_TextualEvent {
+            line-height: $irc-line-height;
+        }
+    }
+
+    // Suppress highlight thing from the normal Layout.
+    .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line,
+    .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line,
+    .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
+        padding-left: 0;
+        border-left: 0;
+    }
+
+    .mx_SenderProfile {
+        width: var(--name-width);
+        display: flex;
+        order: 2;
+        flex-shrink: 0;
+        justify-content: flex-start;
+        align-items: center;
+
+        > .mx_SenderProfile_displayName {
+            width: 100%;
+            text-align: end;
+            overflow: hidden;
+            text-overflow: ellipsis;
+        }
+
+        > .mx_SenderProfile_mxid {
+            visibility: collapse;
+        }
+    }
+
+    .mx_SenderProfile:hover {
+        overflow: visible;
+        z-index: 10;
+
+        > .mx_SenderProfile_displayName {
+            overflow: visible;
+        }
+
+        > .mx_SenderProfile_mxid {
+            visibility: visible;
+        }
+    }
+
+    .mx_ReplyThread {
+        margin: 0;
+        .mx_SenderProfile {
+            order: unset;
+            max-width: unset;
+            width: unset;
+            background: transparent;
+        }
+
+        .mx_EventTile_emote {
+            > .mx_EventTile_avatar {
+                margin-left: initial;
+            }
+        }
+
+        .mx_MessageTimestamp {
+            width: initial;
+        }
+
+        /**
+         * adding the icon back in the document flow
+         * if it's not present, there's no unwanted wasted space
+         */
+        .mx_EventTile_e2eIcon {
+            position: relative;
+            order: -1;
+        }
+    }
+
+    .mx_ProfileResizer {
+        position: absolute;
+        height: 100%;
+        width: 15px;
+        left: calc(80px + var(--name-width));
+        cursor: col-resize;
+        z-index: 100;
+    }
+
+    // Need to use important to override the js provided height and width values.
+    .mx_Flair > img {
+        height: $font-14px !important;
+        width: $font-14px !important;
+    }
+}
diff --git a/res/css/views/rooms/_InviteOnlyIcon.scss b/res/css/views/rooms/_InviteOnlyIcon.scss
deleted file mode 100644
index b71fd6348d..0000000000
--- a/res/css/views/rooms/_InviteOnlyIcon.scss
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
-Copyright 2020 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.
-*/
-
-@define-mixin mx_InviteOnlyIcon {
-    width: 12px;
-    height: 12px;
-    position: relative;
-    display: block !important;
-}
-
-@define-mixin mx_InviteOnlyIcon_padlock {
-    background-color: $roomtile-name-color;
-    mask-image: url("$(res)/img/feather-customised/lock-solid.svg");
-    mask-position: center;
-    mask-repeat: no-repeat;
-    mask-size: contain;
-    content: "";
-    position: absolute;
-    top: 0;
-    bottom: 0;
-    left: 0;
-    right: 0;
-}
-
-.mx_InviteOnlyIcon_large {
-    @mixin mx_InviteOnlyIcon;
-    margin: 0 4px;
-
-    &::before {
-        @mixin mx_InviteOnlyIcon_padlock;
-        width: 12px;
-        height: 12px;
-    }
-}
-
-.mx_InviteOnlyIcon_small {
-    @mixin mx_InviteOnlyIcon;
-    left: -2px;
-
-    &::before {
-        @mixin mx_InviteOnlyIcon_padlock;
-        width: 10px;
-        height: 10px;
-    }
-}
diff --git a/res/css/views/rooms/_JumpToBottomButton.scss b/res/css/views/rooms/_JumpToBottomButton.scss
index 63cf574596..a8dc2ce11c 100644
--- a/res/css/views/rooms/_JumpToBottomButton.scss
+++ b/res/css/views/rooms/_JumpToBottomButton.scss
@@ -41,17 +41,23 @@ limitations under the License.
     // with text-align in parent
     display: inline-block;
     padding: 0 4px;
+    color: $accent-fg-color;
+    background-color: $muted-fg-color;
+}
+
+.mx_JumpToBottomButton_highlight .mx_JumpToBottomButton_badge {
     color: $secondary-accent-color;
     background-color: $warning-color;
 }
 
 .mx_JumpToBottomButton_scrollDown {
     position: relative;
+    display: block;
     height: 38px;
     border-radius: 19px;
     box-sizing: border-box;
     background: $primary-bg-color;
-    border: 1.3px solid $roomtile-name-color;
+    border: 1.3px solid $muted-fg-color;
     cursor: pointer;
 }
 
@@ -62,8 +68,8 @@ limitations under the License.
     bottom: 0;
     left: 0;
     right: 0;
-    mask: url('$(res)/img/icon-jump-to-bottom.svg');
+    mask-image: url('$(res)/img/feather-customised/chevron-down-thin.svg');
     mask-repeat: no-repeat;
-    mask-position: 9px 14px;
-    background: $roomtile-name-color;
+    mask-size: contain;
+    background: $muted-fg-color;
 }
diff --git a/res/css/views/rooms/_LinkPreviewGroup.scss b/res/css/views/rooms/_LinkPreviewGroup.scss
new file mode 100644
index 0000000000..ed341904fd
--- /dev/null
+++ b/res/css/views/rooms/_LinkPreviewGroup.scss
@@ -0,0 +1,38 @@
+/*
+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_LinkPreviewGroup {
+    .mx_LinkPreviewGroup_hide {
+        cursor: pointer;
+        width: 18px;
+        height: 18px;
+
+        img {
+            flex: 0 0 40px;
+            visibility: hidden;
+        }
+    }
+
+    &:hover .mx_LinkPreviewGroup_hide img,
+    .mx_LinkPreviewGroup_hide.focus-visible:focus img {
+        visibility: visible;
+    }
+
+    > .mx_AccessibleButton {
+        color: $accent-color;
+        text-align: center;
+    }
+}
diff --git a/res/css/views/rooms/_LinkPreviewWidget.scss b/res/css/views/rooms/_LinkPreviewWidget.scss
index 022cf3ed28..0832337ecd 100644
--- a/res/css/views/rooms/_LinkPreviewWidget.scss
+++ b/res/css/views/rooms/_LinkPreviewWidget.scss
@@ -33,38 +33,29 @@ limitations under the License.
 .mx_LinkPreviewWidget_caption {
     margin-left: 15px;
     flex: 1 1 auto;
+    overflow-x: hidden; // cause it to wrap rather than clip
 }
 
 .mx_LinkPreviewWidget_title {
-    display: inline;
     font-weight: bold;
     white-space: normal;
-}
+    display: -webkit-box;
+    -webkit-line-clamp: 2;
+    -webkit-box-orient: vertical;
+    overflow: hidden;
 
-.mx_LinkPreviewWidget_siteName {
-    display: inline;
+    .mx_LinkPreviewWidget_siteName {
+        font-weight: normal;
+    }
 }
 
 .mx_LinkPreviewWidget_description {
     margin-top: 8px;
     white-space: normal;
     word-wrap: break-word;
-}
-
-.mx_LinkPreviewWidget_cancel {
-    cursor: pointer;
-    width: 18px;
-    height: 18px;
-
-    img {
-        flex: 0 0 40px;
-        visibility: hidden;
-    }
-}
-
-.mx_LinkPreviewWidget:hover .mx_LinkPreviewWidget_cancel img,
-.mx_LinkPreviewWidget_cancel.focus-visible:focus img {
-    visibility: visible;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    -webkit-box-orient: vertical;
 }
 
 .mx_MatrixChat_useCompactLayout {
diff --git a/res/css/views/rooms/_MemberDeviceInfo.scss b/res/css/views/rooms/_MemberDeviceInfo.scss
deleted file mode 100644
index 71b05a93fc..0000000000
--- a/res/css/views/rooms/_MemberDeviceInfo.scss
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
-Copyright 2016 OpenMarket Ltd
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-.mx_MemberDeviceInfo {
-    display: flex;
-    padding-bottom: 10px;
-    align-items: flex-start;
-}
-
-.mx_MemberDeviceInfo_icon {
-    margin-top: 4px;
-    width: 12px;
-    height: 12px;
-    mask-repeat: no-repeat;
-    mask-size: 100%;
-}
-.mx_MemberDeviceInfo_icon_blacklisted {
-    mask-image: url('$(res)/img/e2e/blacklisted.svg');
-    background-color: $warning-color;
-}
-.mx_MemberDeviceInfo_icon_verified {
-    mask-image: url('$(res)/img/e2e/verified.svg');
-    background-color: $accent-color;
-}
-.mx_MemberDeviceInfo_icon_unverified {
-    mask-image: url('$(res)/img/e2e/warning.svg');
-    background-color: $warning-color;
-}
-
-.mx_MemberDeviceInfo > .mx_DeviceVerifyButtons {
-    display: flex;
-    flex-direction: column;
-    flex: 0 1 auto;
-    align-items: stretch;
-}
-
-.mx_MemberDeviceInfo_textButton {
-    @mixin mx_DialogButton_small;
-    margin: 2px;
-    flex: 1;
-}
-
-.mx_MemberDeviceInfo_textButton:hover {
-    @mixin mx_DialogButton_hover;
-}
-
-.mx_MemberDeviceInfo_deviceId {
-    word-break: break-word;
-    font-size: $font-13px;
-}
-
-.mx_MemberDeviceInfo_deviceInfo {
-    margin: 0 5px 5px 8px;
-    flex: 1;
-}
-
-/* "Unblacklist" is too long for a regular button: make it wider and
-   reduce the padding. */
-.mx_EncryptedEventDialog .mx_MemberDeviceInfo_blacklist,
-.mx_EncryptedEventDialog .mx_MemberDeviceInfo_unblacklist {
-    padding-left: 1em;
-    padding-right: 1em;
-}
-
-.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_verified,
-.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_unverified,
-.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_blacklisted {
-    float: right;
-    padding-left: 1em;
-}
-
-.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_verified {
-    color: $e2e-verified-color;
-}
-
-.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_unverified {
-    color: $e2e-unverified-color;
-}
-
-.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_blacklisted {
-    color: $e2e-warning-color;
-}
diff --git a/res/css/views/rooms/_MemberInfo.scss b/res/css/views/rooms/_MemberInfo.scss
index fb082843f1..3f7f83d334 100644
--- a/res/css/views/rooms/_MemberInfo.scss
+++ b/res/css/views/rooms/_MemberInfo.scss
@@ -19,6 +19,7 @@ limitations under the License.
     flex-direction: column;
     flex: 1;
     overflow-y: auto;
+    margin-top: 8px;
 }
 
 .mx_MemberInfo_name {
@@ -70,7 +71,7 @@ limitations under the License.
 }
 
 .mx_MemberInfo_avatar {
-    background: $tagpanel-bg-color;
+    background: $groupFilterPanel-bg-color;
     margin-bottom: 16px;
 }
 
diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss
index 99dc2338d4..075e9ff585 100644
--- a/res/css/views/rooms/_MemberList.scss
+++ b/res/css/views/rooms/_MemberList.scss
@@ -26,6 +26,10 @@ limitations under the License.
         flex: 1 0 auto;
     }
 
+    .mx_SearchBox {
+        margin-bottom: 5px;
+    }
+
     h2 {
         text-transform: uppercase;
         color: $h3-color;
@@ -40,6 +44,17 @@ limitations under the License.
     .mx_AutoHideScrollbar {
         flex: 1 1 0;
     }
+
+    .mx_RightPanel_scopeHeader {
+        // vertically align with position on other right panel cards
+        // to prevent it bouncing as user navigates right panel
+        margin-top: -8px;
+    }
+}
+
+.mx_GroupMemberList_query,
+.mx_GroupRoomList_query {
+    flex: 0 0 auto;
 }
 
 .mx_MemberList_chevron {
@@ -55,36 +70,29 @@ limitations under the License.
     flex: 1 1 0px;
 }
 
-.mx_MemberList_query,
-.mx_GroupMemberList_query,
-.mx_GroupRoomList_query {
-    flex: 1 1 0;
+.mx_MemberList_query {
+    height: 16px;
+
+    // stricter rule to override the one in _common.scss
+    &[type="text"] {
+        font-size: $font-12px;
+    }
 }
 
-
-
 .mx_MemberList_wrapper {
     padding: 10px;
 }
 
-
-.mx_MemberList_invite,
-.mx_RightPanel_invite {
+.mx_MemberList_invite {
     flex: 0 0 auto;
     position: relative;
     background-color: $button-bg-color;
     border-radius: 4px;
-    padding: 8px;
-    margin: 9px;
+    margin: 5px 9px 9px;
     display: flex;
     justify-content: center;
     color: $button-fg-color;
     font-weight: 600;
-
-    .mx_RightPanel_icon {
-        padding-right: 5px;
-        padding-top: 2px;
-    }
 }
 
 .mx_MemberList_invite.mx_AccessibleButton_disabled {
@@ -93,8 +101,27 @@ limitations under the License.
 }
 
 .mx_MemberList_invite span {
-    background-image: url('$(res)/img/feather-customised/user-add.svg');
-    background-repeat: no-repeat;
-    background-position: center left;
-    padding-left: 25px;
+    padding: 8px 0;
+    display: inline-flex;
+
+    &::before {
+        content: '';
+        display: inline-block;
+        background-color: $button-fg-color;
+        mask-image: url('$(res)/img/element-icons/room/invite.svg');
+        mask-position: center;
+        mask-repeat: no-repeat;
+        mask-size: 20px;
+        width: 20px;
+        height: 20px;
+        margin-right: 5px;
+    }
+}
+
+.mx_MemberList_inviteCommunity span::before {
+    mask-image: url('$(res)/img/icon-invite-people.svg');
+}
+
+.mx_MemberList_addRoomToCommunity span::before {
+    mask-image: url('$(res)/img/icons-room-add.svg');
 }
diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss
index 7b223be3a4..e6c0cc3f46 100644
--- a/res/css/views/rooms/_MessageComposer.scss
+++ b/res/css/views/rooms/_MessageComposer.scss
@@ -20,7 +20,8 @@ limitations under the License.
     margin: auto;
     border-top: 1px solid $primary-hairline-color;
     position: relative;
-    padding-left: 84px;
+    padding-left: 82px;
+    padding-right: 6px;
 }
 
 .mx_MessageComposer_replaced_wrapper {
@@ -60,7 +61,7 @@ limitations under the License.
 
 .mx_MessageComposer .mx_MessageComposer_avatar {
     position: absolute;
-    left: 27px;
+    left: 26px;
 }
 
 .mx_MessageComposer .mx_MessageComposer_avatar .mx_BaseAvatar {
@@ -76,8 +77,8 @@ limitations under the License.
     left: 60px;
     margin-right: 0; // Counteract the E2EIcon class
     margin-left: 3px; // Counteract the E2EIcon class
-    width: 15px;
-    height: 15px;
+    width: 12px;
+    height: 12px;
 }
 
 .mx_MessageComposer_noperm_error {
@@ -178,44 +179,90 @@ limitations under the License.
     color: $accent-color;
 }
 
+.mx_MessageComposer_button_highlight {
+    background: rgba($accent-color, 0.25);
+    // make the icon the accent color too
+    &::before {
+        background-color: $accent-color !important;
+    }
+}
+
 .mx_MessageComposer_button {
     position: relative;
-    margin-right: 12px;
+    margin-right: 6px;
     cursor: pointer;
-    height: 20px;
-    width: 20px;
+    height: 26px;
+    width: 26px;
+    border-radius: 100%;
 
     &::before {
         content: '';
         position: absolute;
 
+        top: 3px;
+        left: 3px;
         height: 20px;
         width: 20px;
-        background-color: $composer-button-color;
+        background-color: $icon-button-color;
         mask-repeat: no-repeat;
         mask-size: contain;
         mask-position: center;
     }
+
+    &:hover {
+        background: rgba($accent-color, 0.1);
+
+        &::before {
+            background-color: $accent-color;
+        }
+    }
+
+    &.mx_MessageComposer_hangup:not(.mx_AccessibleButton_disabled)::before {
+        background-color: $warning-color;
+    }
 }
 
+
 .mx_MessageComposer_upload::before {
-    mask-image: url('$(res)/img/feather-customised/paperclip.svg');
+    mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
 }
 
-.mx_MessageComposer_hangup::before {
-    mask-image: url('$(res)/img/hangup.svg');
+.mx_MessageComposer_voiceMessage::before {
+    mask-image: url('$(res)/img/voip/mic-on-mask.svg');
 }
 
-.mx_MessageComposer_voicecall::before {
-    mask-image: url('$(res)/img/feather-customised/phone.svg');
-}
-
-.mx_MessageComposer_videocall::before {
-    mask-image: url('$(res)/img/feather-customised/video.svg');
+.mx_MessageComposer_emoji::before {
+    mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
 }
 
 .mx_MessageComposer_stickers::before {
-    mask-image: url('$(res)/img/feather-customised/face.svg');
+    mask-image: url('$(res)/img/element-icons/room/composer/sticker.svg');
+}
+
+.mx_MessageComposer_sendMessage {
+    cursor: pointer;
+    position: relative;
+    margin-right: 6px;
+    width: 32px;
+    height: 32px;
+    border-radius: 100%;
+    background-color: $button-bg-color;
+
+    &::before {
+        position: absolute;
+        height: 16px;
+        width: 16px;
+        top: 8px;
+        left: 9px;
+
+        mask-image: url('$(res)/img/element-icons/send-message.svg');
+        mask-repeat: no-repeat;
+        mask-size: contain;
+        mask-position: center;
+
+        background-color: $button-fg-color;
+        content: '';
+    }
 }
 
 .mx_MessageComposer_formatting {
@@ -279,7 +326,7 @@ limitations under the License.
     mask-size: contain;
     mask-position: center;
     mask-repeat: no-repeat;
-    background-color: $composer-button-color;
+    background-color: $icon-button-color;
 
     &.mx_MessageComposer_markdownDisabled {
         opacity: 0.2;
diff --git a/res/css/views/rooms/_MessageComposerFormatBar.scss b/res/css/views/rooms/_MessageComposerFormatBar.scss
index 27ee7b9795..b305e91db0 100644
--- a/res/css/views/rooms/_MessageComposerFormatBar.scss
+++ b/res/css/views/rooms/_MessageComposerFormatBar.scss
@@ -60,6 +60,8 @@ limitations under the License.
         width: 27px;
         height: 24px;
         box-sizing: border-box;
+        background: none;
+        vertical-align: middle;
     }
 
     .mx_MessageComposerFormatBar_button::after {
@@ -75,23 +77,23 @@ limitations under the License.
     }
 
     .mx_MessageComposerFormatBar_buttonIconBold::after {
-        mask-image: url('$(res)/img/format/bold.svg');
+        mask-image: url('$(res)/img/element-icons/room/format-bar/bold.svg');
     }
 
     .mx_MessageComposerFormatBar_buttonIconItalic::after {
-        mask-image: url('$(res)/img/format/italics.svg');
+        mask-image: url('$(res)/img/element-icons/room/format-bar/italic.svg');
     }
 
     .mx_MessageComposerFormatBar_buttonIconStrikethrough::after {
-        mask-image: url('$(res)/img/format/strikethrough.svg');
+        mask-image: url('$(res)/img/element-icons/room/format-bar/strikethrough.svg');
     }
 
     .mx_MessageComposerFormatBar_buttonIconQuote::after {
-        mask-image: url('$(res)/img/format/quote.svg');
+        mask-image: url('$(res)/img/element-icons/room/format-bar/quote.svg');
     }
 
     .mx_MessageComposerFormatBar_buttonIconCode::after {
-        mask-image: url('$(res)/img/format/code.svg');
+        mask-image: url('$(res)/img/element-icons/room/format-bar/code.svg');
     }
 }
 
diff --git a/res/css/views/rooms/_NewRoomIntro.scss b/res/css/views/rooms/_NewRoomIntro.scss
new file mode 100644
index 0000000000..e0cccfa885
--- /dev/null
+++ b/res/css/views/rooms/_NewRoomIntro.scss
@@ -0,0 +1,72 @@
+/*
+Copyright 2020 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_NewRoomIntro {
+    margin: 40px 0 48px 64px;
+
+    .mx_MiniAvatarUploader_hasAvatar:not(.mx_MiniAvatarUploader_busy):not(:hover) {
+        .mx_MiniAvatarUploader_indicator {
+            display: none;
+        }
+    }
+
+    .mx_AccessibleButton_kind_link {
+        padding: 0;
+        font-size: inherit;
+    }
+
+    .mx_NewRoomIntro_buttons {
+        margin-top: 28px;
+
+        .mx_AccessibleButton {
+            line-height: $font-24px;
+            display: inline-block;
+
+            & + .mx_AccessibleButton {
+                margin-left: 12px;
+            }
+
+            &:not(.mx_AccessibleButton_kind_primary_outline)::before {
+                content: '';
+                display: inline-block;
+                background-color: $button-fg-color;
+                mask-position: center;
+                mask-repeat: no-repeat;
+                mask-size: 20px;
+                width: 20px;
+                height: 20px;
+                margin-right: 5px;
+                vertical-align: text-bottom;
+            }
+        }
+
+        .mx_NewRoomIntro_inviteButton::before {
+            mask-image: url('$(res)/img/element-icons/room/invite.svg');
+        }
+    }
+
+    > h2 {
+        margin-top: 24px;
+        font-size: $font-24px;
+        font-weight: 600;
+    }
+
+    > p {
+        margin: 0;
+        font-size: $font-15px;
+        color: $secondary-fg-color;
+    }
+}
diff --git a/res/css/views/rooms/_NotificationBadge.scss b/res/css/views/rooms/_NotificationBadge.scss
new file mode 100644
index 0000000000..64b2623238
--- /dev/null
+++ b/res/css/views/rooms/_NotificationBadge.scss
@@ -0,0 +1,72 @@
+/*
+Copyright 2020 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_NotificationBadge {
+    &:not(.mx_NotificationBadge_visible) {
+        display: none;
+    }
+
+    // Badges are structured a bit weirdly to work around issues with non-monospace
+    // font styles. The badge pill is actually a background div and the count floats
+    // within that. For example:
+    //
+    //  ( 99+ ) <-- Rounded pill is a _bg class.
+    //     ^- The count is an element floating within that.
+
+    &.mx_NotificationBadge_visible {
+        background-color: $roomtile-default-badge-bg-color;
+
+        // Create a flexbox to order the count a bit easier
+        display: flex;
+        align-items: center;
+        justify-content: center;
+
+        &.mx_NotificationBadge_highlighted {
+            // TODO: Use a more specific variable
+            background-color: $warning-color;
+        }
+
+        // These are the 3 background types
+
+        &.mx_NotificationBadge_dot {
+            background-color: $primary-fg-color; // increased visibility
+
+            width: 6px;
+            height: 6px;
+            border-radius: 6px;
+        }
+
+        &.mx_NotificationBadge_2char {
+            width: $font-16px;
+            height: $font-16px;
+            border-radius: $font-16px;
+        }
+
+        &.mx_NotificationBadge_3char {
+            width: $font-26px;
+            height: $font-16px;
+            border-radius: $font-16px;
+        }
+
+        // The following is the floating badge
+
+        .mx_NotificationBadge_count {
+            font-size: $font-10px;
+            line-height: $font-14px;
+            color: #fff; // TODO: Variable
+        }
+    }
+}
diff --git a/res/css/views/rooms/_PinnedEventTile.scss b/res/css/views/rooms/_PinnedEventTile.scss
index 030a76674a..15b3c16faa 100644
--- a/res/css/views/rooms/_PinnedEventTile.scss
+++ b/res/css/views/rooms/_PinnedEventTile.scss
@@ -16,62 +16,91 @@ limitations under the License.
 
 .mx_PinnedEventTile {
     min-height: 40px;
-    margin-bottom: 5px;
     width: 100%;
-    border-radius: 5px; // for the hover
-}
+    padding: 0 4px 12px;
 
-.mx_PinnedEventTile:hover {
-    background-color: $event-selected-color;
-}
+    display: grid;
+    grid-template-areas:
+        "avatar name remove"
+        "content content content"
+        "footer footer footer";
+    grid-template-rows: max-content auto max-content;
+    grid-template-columns: 24px auto 24px;
+    grid-row-gap: 12px;
+    grid-column-gap: 8px;
 
-.mx_PinnedEventTile .mx_PinnedEventTile_sender,
-.mx_PinnedEventTile .mx_PinnedEventTile_timestamp {
-    color: #868686;
-    font-size: 0.8em;
-    vertical-align: top;
-    display: inline-block;
-    padding-bottom: 3px;
-}
+    & + .mx_PinnedEventTile {
+        padding: 12px 4px;
+        border-top: 1px solid $menu-border-color;
+    }
 
-.mx_PinnedEventTile .mx_PinnedEventTile_timestamp {
-    padding-left: 15px;
-    display: none;
-}
+    .mx_PinnedEventTile_senderAvatar {
+        grid-area: avatar;
+    }
 
-.mx_PinnedEventTile .mx_PinnedEventTile_senderAvatar .mx_BaseAvatar {
-    float: left;
-    margin-right: 10px;
-}
+    .mx_PinnedEventTile_sender {
+        grid-area: name;
+        font-weight: $font-semi-bold;
+        font-size: $font-15px;
+        line-height: $font-24px;
+        text-overflow: ellipsis;
+        overflow: hidden;
+        white-space: nowrap;
+    }
 
-.mx_PinnedEventTile_actions {
-    float: right;
-    margin-right: 10px;
-    display: none;
-}
+    .mx_PinnedEventTile_unpinButton {
+        visibility: hidden;
+        grid-area: remove;
+        position: relative;
+        width: 24px;
+        height: 24px;
+        border-radius: 8px;
 
-.mx_PinnedEventTile:hover .mx_PinnedEventTile_timestamp {
-    display: inline-block;
-}
+        &:hover {
+            background-color: $roomheader-addroom-bg-color;
+        }
 
-.mx_PinnedEventTile:hover .mx_PinnedEventTile_actions {
-    display: block;
-}
+        &::before {
+            content: "";
+            position: absolute;
+            //top: 0;
+            //left: 0;
+            height: inherit;
+            width: inherit;
+            background: $secondary-fg-color;
+            mask-position: center;
+            mask-size: 8px;
+            mask-repeat: no-repeat;
+            mask-image: url('$(res)/img/image-view/close.svg');
+        }
+    }
 
-.mx_PinnedEventTile_unpinButton {
-    display: inline-block;
-    cursor: pointer;
-    margin-left: 10px;
-}
+    .mx_PinnedEventTile_message {
+        grid-area: content;
+    }
 
-.mx_PinnedEventTile_gotoButton {
-    display: inline-block;
-    font-size: 0.7em; // Smaller text to avoid conflicting with the layout
-}
+    .mx_PinnedEventTile_footer {
+        grid-area: footer;
+        font-size: 10px;
+        line-height: 12px;
 
-.mx_PinnedEventTile_message {
-    margin-left: 50px;
-    position: relative;
-    top: 0;
-    left: 0;
+        .mx_PinnedEventTile_timestamp {
+            font-size: inherit;
+            line-height: inherit;
+            color: $secondary-fg-color;
+        }
+
+        .mx_AccessibleButton_kind_link {
+            padding: 0;
+            margin-left: 12px;
+            font-size: inherit;
+            line-height: inherit;
+        }
+    }
+
+    &:hover {
+        .mx_PinnedEventTile_unpinButton {
+            visibility: visible;
+        }
+    }
 }
diff --git a/res/css/views/rooms/_PinnedEventsPanel.scss b/res/css/views/rooms/_PinnedEventsPanel.scss
deleted file mode 100644
index 663d5bdf6e..0000000000
--- a/res/css/views/rooms/_PinnedEventsPanel.scss
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
-Copyright 2017 Travis Ralston
-
-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_PinnedEventsPanel {
-    border-top: 1px solid $primary-hairline-color;
-}
-
-.mx_PinnedEventsPanel_body {
-    max-height: 300px;
-    overflow-y: auto;
-    padding-bottom: 15px;
-}
-
-.mx_PinnedEventsPanel_header {
-    margin: 0;
-    padding-top: 8px;
-    padding-bottom: 15px;
-}
-
-.mx_PinnedEventsPanel_cancel {
-    margin: 12px;
-    float: right;
-    display: inline-block;
-}
diff --git a/res/css/views/rooms/_ReplyPreview.scss b/res/css/views/rooms/_ReplyPreview.scss
index 4dc4cb2c40..c1fe1d9a8b 100644
--- a/res/css/views/rooms/_ReplyPreview.scss
+++ b/res/css/views/rooms/_ReplyPreview.scss
@@ -15,16 +15,13 @@ limitations under the License.
 */
 
 .mx_ReplyPreview {
-    position: absolute;
-    bottom: 0;
-    z-index: 1000;
-    width: 100%;
     border: 1px solid $primary-hairline-color;
     background: $primary-bg-color;
     border-bottom: none;
-    border-radius: 4px 4px 0 0;
+    border-radius: 8px 8px 0 0;
     max-height: 50vh;
     overflow: auto;
+    box-shadow: 0px -16px 32px $composer-shadow-color;
 }
 
 .mx_ReplyPreview_section {
@@ -32,12 +29,16 @@ limitations under the License.
 }
 
 .mx_ReplyPreview_header {
-    margin: 12px;
+    margin: 8px;
     color: $primary-fg-color;
     font-weight: 400;
     opacity: 0.4;
 }
 
+.mx_ReplyPreview_tile {
+    margin: 0 8px;
+}
+
 .mx_ReplyPreview_title {
     float: left;
 }
@@ -45,6 +46,7 @@ limitations under the License.
 .mx_ReplyPreview_cancel {
     float: right;
     cursor: pointer;
+    display: flex;
 }
 
 .mx_ReplyPreview_clear {
diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss
new file mode 100644
index 0000000000..c8f76ee995
--- /dev/null
+++ b/res/css/views/rooms/_ReplyTile.scss
@@ -0,0 +1,123 @@
+/*
+Copyright 2020 Tulir Asokan 
+
+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_ReplyTile {
+    padding-top: 2px;
+    padding-bottom: 2px;
+    font-size: $font-14px;
+    position: relative;
+    line-height: $font-16px;
+
+    &.mx_ReplyTile_audio .mx_MFileBody_info_icon::before {
+        mask-image: url("$(res)/img/element-icons/speaker.svg");
+    }
+
+    &.mx_ReplyTile_video .mx_MFileBody_info_icon::before {
+        mask-image: url("$(res)/img/element-icons/call/video-call.svg");
+    }
+
+    .mx_MFileBody {
+        .mx_MFileBody_info {
+            margin: 5px 0;
+        }
+
+        .mx_MFileBody_download {
+            display: none;
+        }
+    }
+}
+
+.mx_ReplyTile > a {
+    display: flex;
+    flex-direction: column;
+    text-decoration: none;
+    color: $primary-fg-color;
+}
+
+.mx_ReplyTile .mx_RedactedBody {
+    padding: 4px 0 2px 20px;
+
+    &::before {
+        height: 13px;
+        width: 13px;
+        top: 5px;
+    }
+}
+
+// We do reply size limiting with CSS to avoid duplicating the TextualBody component.
+.mx_ReplyTile .mx_EventTile_content {
+    $reply-lines: 2;
+    $line-height: $font-22px;
+
+    pointer-events: none;
+
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-box-orient: vertical;
+    -webkit-line-clamp: $reply-lines;
+    line-height: $line-height;
+
+    .mx_EventTile_body.mx_EventTile_bigEmoji {
+        line-height: $line-height !important;
+        // Override the big emoji override
+        font-size: $font-14px !important;
+    }
+
+    // Hide line numbers
+    .mx_EventTile_lineNumbers {
+        display: none;
+    }
+
+    // Hack to cut content in 
 tags too
+    .mx_EventTile_pre_container > pre {
+        overflow: hidden;
+        text-overflow: ellipsis;
+        display: -webkit-box;
+        -webkit-box-orient: vertical;
+        -webkit-line-clamp: $reply-lines;
+        padding: 4px;
+    }
+
+    .markdown-body blockquote,
+    .markdown-body dl,
+    .markdown-body ol,
+    .markdown-body p,
+    .markdown-body pre,
+    .markdown-body table,
+    .markdown-body ul {
+        margin-bottom: 4px;
+    }
+}
+
+.mx_ReplyTile.mx_ReplyTile_info {
+    padding-top: 0;
+}
+
+.mx_ReplyTile .mx_SenderProfile {
+    color: $primary-fg-color;
+    font-size: $font-14px;
+    display: inline-block; /* anti-zalgo, with overflow hidden */
+    overflow: hidden;
+    cursor: pointer;
+    padding-left: 0; /* left gutter */
+    padding-bottom: 0;
+    padding-top: 0;
+    margin: 0;
+    line-height: $font-17px;
+    /* the next three lines, along with overflow hidden, truncate long display names */
+    white-space: nowrap;
+    text-overflow: ellipsis;
+}
diff --git a/res/css/views/rooms/_RoomBreadcrumbs.scss b/res/css/views/rooms/_RoomBreadcrumbs.scss
index 3858d836e6..152b0a45cd 100644
--- a/res/css/views/rooms/_RoomBreadcrumbs.scss
+++ b/res/css/views/rooms/_RoomBreadcrumbs.scss
@@ -1,5 +1,5 @@
 /*
-Copyright 2019 New Vector Ltd
+Copyright 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,98 +15,42 @@ limitations under the License.
 */
 
 .mx_RoomBreadcrumbs {
-    position: relative;
-    height: 42px;
-    padding: 8px;
-    padding-bottom: 0;
+    width: 100%;
+
+    // Create a flexbox for the crumbs
     display: flex;
     flex-direction: row;
-
-    // repeating circles as empty placeholders
-    background:
-        radial-gradient(
-            circle at center,
-            $breadcrumb-placeholder-bg-color,
-            $breadcrumb-placeholder-bg-color 15px,
-            transparent 16px
-        );
-    background-size: 36px;
-    background-position: 6px -1px;
-    background-repeat: repeat-x;
-
-
-    // Autohide the scrollbar
-    overflow-x: hidden;
-    &:hover {
-        overflow-x: visible;
-    }
-
-    .mx_AutoHideScrollbar {
-        display: flex;
-        flex-direction: row;
-        height: 100%;
-    }
+    align-items: flex-start;
 
     .mx_RoomBreadcrumbs_crumb {
-        margin-left: 4px;
-        height: 32px;
-        display: inline-block;
-        transition: transform 0.3s, width 0.3s;
-        position: relative;
-
-        .mx_RoomTile_badge {
-            position: absolute;
-            top: -3px;
-            right: -4px;
-        }
-
-        .mx_RoomBreadcrumbs_dmIndicator {
-            position: absolute;
-            bottom: 0;
-            right: -4px;
-        }
-    }
-
-    .mx_RoomBreadcrumbs_animate {
-        margin-left: 0;
+        margin-right: 8px;
         width: 32px;
-        transform: scale(1);
     }
 
-    .mx_RoomBreadcrumbs_preAnimate {
-        width: 0;
-        transform: scale(0);
+    // These classes come from the CSSTransition component. There's many more classes we
+    // could care about, but this is all we worried about for now. The animation works by
+    // first triggering the enter state with the newest breadcrumb off screen (-40px) then
+    // sliding it into view.
+    &.mx_RoomBreadcrumbs-enter {
+        transform: translateX(-40px); // 32px for the avatar, 8px for the margin
+    }
+    &.mx_RoomBreadcrumbs-enter-active {
+        transform: translateX(0);
+
+        // Timing function is as-requested by design.
+        // NOTE: The transition time MUST match the value passed to CSSTransition!
+        transition: transform 640ms cubic-bezier(0.66, 0.02, 0.36, 1);
     }
 
-    .mx_RoomBreadcrumbs_left {
-        opacity: 0.5;
-    }
-
-    // Note: we have to manually control the gradient and stuff, but the IndicatorScrollbar
-    // will deal with left/right positioning for us. Normally we'd use position:sticky on
-    // a few key elements, however that doesn't work in horizontal scrolling scenarios.
-
-    .mx_IndicatorScrollbar_leftOverflowIndicator,
-    .mx_IndicatorScrollbar_rightOverflowIndicator {
-        display: none;
-    }
-
-    .mx_IndicatorScrollbar_leftOverflowIndicator {
-        background: linear-gradient(to left, $panel-gradient);
-    }
-
-    .mx_IndicatorScrollbar_rightOverflowIndicator {
-        background: linear-gradient(to right, $panel-gradient);
-    }
-
-    &.mx_IndicatorScrollbar_leftOverflow .mx_IndicatorScrollbar_leftOverflowIndicator,
-    &.mx_IndicatorScrollbar_rightOverflow .mx_IndicatorScrollbar_rightOverflowIndicator {
-        position: absolute;
-        top: 0;
-        bottom: 0;
-        width: 15px;
-        display: block;
-        pointer-events: none;
-        z-index: 100;
+    .mx_RoomBreadcrumbs_placeholder {
+        font-weight: 600;
+        font-size: $font-14px;
+        line-height: 32px; // specifically to match the height this is not scaled
+        height: 32px;
     }
 }
+
+.mx_RoomBreadcrumbs_Tooltip {
+    margin-left: -42px;
+    margin-top: -42px;
+}
diff --git a/res/css/views/rooms/_RoomDropTarget.scss b/res/css/views/rooms/_RoomDropTarget.scss
deleted file mode 100644
index 2e8145c2c9..0000000000
--- a/res/css/views/rooms/_RoomDropTarget.scss
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-.mx_RoomDropTarget_container {
-    background-color: $secondary-accent-color;
-    padding-left: 18px;
-    padding-right: 18px;
-    padding-top: 8px;
-    padding-bottom: 7px;
-}
-
-.collapsed .mx_RoomDropTarget_container {
-    padding-right: 10px;
-    padding-left: 10px;
-}
-
-.mx_RoomDropTarget {
-    font-size: $font-13px;
-    padding-top: 5px;
-    padding-bottom: 5px;
-    border: 1px dashed $accent-color;
-    color: $primary-fg-color;
-    background-color: $droptarget-bg-color;
-    border-radius: 4px;
-}
-
-
-.mx_RoomDropTarget_label {
-    position: relative;
-    margin-top: 3px;
-    line-height: $font-21px;
-    z-index: 1;
-    text-align: center;
-}
-
-.collapsed .mx_RoomDropTarget_avatar {
-    float: none;
-}
-
-.collapsed .mx_RoomDropTarget_label {
-    display: none;
-}
diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss
index 969106c9ea..4142b0a2ef 100644
--- a/res/css/views/rooms/_RoomHeader.scss
+++ b/res/css/views/rooms/_RoomHeader.scss
@@ -15,26 +15,34 @@ limitations under the License.
 */
 
 .mx_RoomHeader {
-    flex: 0 0 52px;
+    flex: 0 0 50px;
     border-bottom: 1px solid $primary-hairline-color;
+    background-color: $roomheader-bg-color;
 
-    .mx_E2EIcon {
-        margin: 0;
-        position: absolute;
-        bottom: -2px;
-        right: -6px;
-        height: 15px;
-        width: 15px;
+    .mx_RoomHeader_e2eIcon {
+        height: 12px;
+        width: 12px;
+
+        .mx_E2EIcon {
+            margin: 0;
+            position: absolute;
+            height: 12px;
+            width: 12px;
+        }
     }
 }
 
 .mx_RoomHeader_wrapper {
     margin: auto;
-    height: 52px;
+    height: 50px;
     display: flex;
     align-items: center;
     min-width: 0;
-    padding: 0 10px 0 19px;
+    padding: 0 10px 0 18px;
+
+    .mx_InviteOnlyIcon_large {
+        margin: 0;
+    }
 }
 
 .mx_RoomHeader_spinner {
@@ -67,7 +75,6 @@ limitations under the License.
 .mx_RoomHeader_buttons {
     display: flex;
     background-color: $primary-bg-color;
-    padding-right: 5px;
 }
 
 .mx_RoomHeader_info {
@@ -173,9 +180,7 @@ limitations under the License.
 
 .mx_RoomHeader_avatar {
     flex: 0;
-    width: 28px;
-    height: 28px;
-    margin: 0 7px;
+    margin: 0 6px 0 7px;
     position: relative;
 }
 
@@ -203,41 +208,61 @@ limitations under the License.
 
 .mx_RoomHeader_button {
     position: relative;
-    margin-left: 10px;
+    margin-left: 1px;
+    margin-right: 1px;
     cursor: pointer;
-    height: 20px;
-    width: 20px;
+    height: 32px;
+    width: 32px;
+    border-radius: 100%;
 
     &::before {
         content: '';
         position: absolute;
-        height: 20px;
-        width: 20px;
-        background-color: $roomheader-button-color;
+        top: 4px; // center with parent of 32px
+        left: 4px; // center with parent of 32px
+        height: 24px;
+        width: 24px;
+        background-color: $icon-button-color;
         mask-repeat: no-repeat;
         mask-size: contain;
     }
-}
 
-.mx_RoomHeader_settingsButton::before {
-    mask-image: url('$(res)/img/feather-customised/settings.svg');
+    &:hover {
+        background: rgba($accent-color, 0.1);
+
+        &::before {
+            background-color: $accent-color;
+        }
+    }
 }
 
 .mx_RoomHeader_forgetButton::before {
-    mask-image: url('$(res)/img/leave.svg');
+    mask-image: url('$(res)/img/element-icons/leave.svg');
     width: 26px;
 }
 
+.mx_RoomHeader_appsButton::before {
+    mask-image: url('$(res)/img/element-icons/room/apps.svg');
+}
+.mx_RoomHeader_appsButton_highlight::before {
+    background-color: $accent-color;
+}
+
 .mx_RoomHeader_searchButton::before {
-    mask-image: url('$(res)/img/feather-customised/search.svg');
+    mask-image: url('$(res)/img/element-icons/room/search-inset.svg');
 }
 
-.mx_RoomHeader_shareButton::before {
-    mask-image: url('$(res)/img/feather-customised/share.svg');
+.mx_RoomHeader_voiceCallButton::before {
+    mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
+
+    // The call button SVG is padded slightly differently, so match it up to the size
+    // of the other icons
+    mask-size: 20px;
+    mask-position: center;
 }
 
-.mx_RoomHeader_manageIntegsButton::before {
-    mask-image: url('$(res)/img/feather-customised/grid.svg');
+.mx_RoomHeader_videoCallButton::before {
+    mask-image: url('$(res)/img/element-icons/call/video-call.svg');
 }
 
 .mx_RoomHeader_showPanel {
@@ -252,20 +277,11 @@ limitations under the License.
     margin-top: 18px;
 }
 
-.mx_RoomHeader_pinnedButton::before {
-    mask-image: url('$(res)/img/icons-pin.svg');
-}
-
-.mx_RoomHeader_pinsIndicator {
-    position: absolute;
-    right: 0;
-    bottom: 4px;
-    width: 8px;
-    height: 8px;
-    border-radius: 8px;
-    background-color: $pinned-color;
-}
-
-.mx_RoomHeader_pinsIndicatorUnread {
-    background-color: $pinned-unread-color;
+@media only screen and (max-width: 480px) {
+    .mx_RoomHeader_wrapper {
+        padding: 0;
+    }
+    .mx_RoomHeader {
+        overflow: hidden;
+    }
 }
diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss
index 50a9e7ee1f..8eda25d0c9 100644
--- a/res/css/views/rooms/_RoomList.scss
+++ b/res/css/views/rooms/_RoomList.scss
@@ -1,6 +1,5 @@
 /*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2017 Vector Creations Ltd
+Copyright 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -16,51 +15,75 @@ limitations under the License.
 */
 
 .mx_RoomList {
-    /* take up remaining space below TopLeftMenu */
-    flex: 1;
-    min-height: 0;
-    overflow-y: hidden;
+    padding-right: 7px; // width of the scrollbar, to line things up
 }
 
-.mx_RoomList .mx_ResizeHandle {
-    // needed so the z-index takes effect
-    position: relative;
+.mx_RoomList_iconPlus::before {
+    mask-image: url('$(res)/img/element-icons/roomlist/plus-circle.svg');
+}
+.mx_RoomList_iconHash::before {
+    mask-image: url('$(res)/img/element-icons/roomlist/hash-circle.svg');
+}
+.mx_RoomList_iconExplore::before {
+    mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
+}
+.mx_RoomList_iconBrowse::before {
+    mask-image: url('$(res)/img/element-icons/roomlist/browse.svg');
+}
+.mx_RoomList_iconDialpad::before {
+    mask-image: url('$(res)/img/element-icons/roomlist/dialpad.svg');
 }
 
-/* hide resize handles next to collapsed / empty sublists */
-.mx_RoomList .mx_RoomSubList:not(.mx_RoomSubList_nonEmpty) + .mx_ResizeHandle {
-    display: none;
-}
+.mx_RoomList_explorePrompt {
+    margin: 4px 12px 4px;
+    padding-top: 12px;
+    border-top: 1px solid $input-border-color;
+    font-size: $font-14px;
 
-.mx_RoomList_expandButton {
-    margin-left: 8px;
-    cursor: pointer;
-    padding-left: 12px;
-    padding-right: 12px;
-}
+    div:first-child {
+        font-weight: $font-semi-bold;
+        line-height: $font-18px;
+        color: $primary-fg-color;
+    }
 
-.mx_RoomList_emptySubListTip_container {
-    padding-left: 18px;
-    padding-right: 18px;
-    padding-top: 8px;
-    padding-bottom: 7px;
-}
+    .mx_AccessibleButton {
+        color: $primary-fg-color;
+        position: relative;
+        padding: 8px 8px 8px 32px;
+        font-size: inherit;
+        margin-top: 12px;
+        display: block;
+        text-align: start;
+        background-color: $roomlist-button-bg-color;
+        border-radius: 4px;
 
-.mx_RoomList_emptySubListTip {
-    font-size: $font-13px;
-    padding: 5px;
-    border: 1px dashed $accent-color;
-    color: $primary-fg-color;
-    background-color: $droptarget-bg-color;
-    border-radius: 4px;
-    line-height: $font-16px;
-}
+        &::before {
+            content: '';
+            width: 16px;
+            height: 16px;
+            position: absolute;
+            top: 8px;
+            left: 8px;
+            background: $secondary-fg-color;
+            mask-position: center;
+            mask-size: contain;
+            mask-repeat: no-repeat;
+        }
 
-.mx_RoomList_emptySubListTip .mx_RoleButton {
-    vertical-align: -2px;
-}
+        &.mx_RoomList_explorePrompt_startChat::before {
+            mask-image: url('$(res)/img/element-icons/feedback.svg');
+        }
 
-.mx_RoomList_headerButtons {
-    position: absolute;
-    right: 60px;
+        &.mx_RoomList_explorePrompt_explore::before {
+            mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
+        }
+
+        &.mx_RoomList_explorePrompt_spaceInvite::before {
+            mask-image: url('$(res)/img/element-icons/room/invite.svg');
+        }
+
+        &.mx_RoomList_explorePrompt_spaceExplore::before {
+            mask-image: url('$(res)/img/element-icons/roomlist/browse.svg');
+        }
+    }
 }
diff --git a/res/css/views/rooms/_RoomPreviewBar.scss b/res/css/views/rooms/_RoomPreviewBar.scss
index 8708f13ada..0b1da7a41c 100644
--- a/res/css/views/rooms/_RoomPreviewBar.scss
+++ b/res/css/views/rooms/_RoomPreviewBar.scss
@@ -58,11 +58,6 @@ limitations under the License.
     }
 }
 
-.mx_RoomPreviewBar_dark {
-    background-color: $tagpanel-bg-color;
-    color: $accent-fg-color;
-}
-
 .mx_RoomPreviewBar_actions {
     display: flex;
 }
diff --git a/res/css/views/rooms/_RoomRecoveryReminder.scss b/res/css/views/rooms/_RoomRecoveryReminder.scss
deleted file mode 100644
index 85d42ca4b4..0000000000
--- a/res/css/views/rooms/_RoomRecoveryReminder.scss
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-.mx_RoomRecoveryReminder {
-    display: flex;
-    flex-direction: column;
-    text-align: center;
-    background-color: $room-warning-bg-color;
-    padding: 20px;
-    border: 1px solid $primary-hairline-color;
-    border-bottom: unset;
-}
-
-.mx_RoomRecoveryReminder_header {
-    font-weight: bold;
-    margin-bottom: 1em;
-}
-
-.mx_RoomRecoveryReminder_body {
-    margin-bottom: 1em;
-}
-
-.mx_RoomRecoveryReminder_button {
-    @mixin mx_DialogButton;
-    margin: 0 10px;
-}
-
-.mx_RoomRecoveryReminder_secondary {
-    font-size: 90%;
-    margin-top: 1em;
-}
diff --git a/res/css/views/rooms/_RoomSublist.scss b/res/css/views/rooms/_RoomSublist.scss
new file mode 100644
index 0000000000..146b3edf71
--- /dev/null
+++ b/res/css/views/rooms/_RoomSublist.scss
@@ -0,0 +1,414 @@
+/*
+Copyright 2020 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_RoomSublist {
+    margin-left: 8px;
+    margin-bottom: 4px;
+
+    &.mx_RoomSublist_hidden {
+        display: none;
+    }
+
+    .mx_RoomSublist_headerContainer {
+        // Create a flexbox to make alignment easy
+        display: flex;
+        align-items: center;
+
+        // ***************************
+        // Sticky Headers Start
+
+        // Ideally we'd be able to use `position: sticky; top: 0; bottom: 0;` on the
+        // headerContainer, however due to our layout concerns we actually have to
+        // calculate it manually so we can sticky things in the right places. We also
+        // target the headerText instead of the container to reduce jumps when scrolling,
+        // and to help hide the badges/other buttons that could appear on hover. This
+        // all works by ensuring the header text has a fixed height when sticky so the
+        // fixed height of the container can maintain the scroll position.
+
+        // The combined height must be set in the LeftPanel component for sticky headers
+        // to work correctly.
+        padding-bottom: 8px;
+        // Allow the container to collapse on itself if its children
+        // are not in the normal document flow
+        max-height: 24px;
+        color: $roomlist-header-color;
+
+        .mx_RoomSublist_stickable {
+            flex: 1;
+            max-width: 100%;
+
+            // Create a flexbox to make ordering easy
+            display: flex;
+            align-items: center;
+
+            // We use a generic sticky class for 2 reasons: to reduce style duplication and
+            // to identify when a header is sticky. If we didn't have a consistent sticky class,
+            // we'd have to do the "is sticky" checks again on click, as clicking the header
+            // when sticky scrolls instead of collapses the list.
+            &.mx_RoomSublist_headerContainer_sticky {
+                position: fixed;
+                height: 32px; // to match the header container
+                // width set by JS because of a compat issue between Firefox and Chrome
+                width: calc(100% - 15px);
+            }
+
+            // We don't have a top style because the top is dependent on the room list header's
+            // height, and is therefore calculated in JS.
+            // The class, mx_RoomSublist_headerContainer_stickyTop, is applied though.
+        }
+
+        // Sticky Headers End
+        // ***************************
+
+        .mx_RoomSublist_badgeContainer {
+            // Create another flexbox row because it's super easy to position the badge this way.
+            display: flex;
+            align-items: center;
+            justify-content: center;
+
+            // Apply the width and margin to the badge so the container doesn't occupy dead space
+            .mx_NotificationBadge {
+                // Do not set a width so the badges get properly sized
+                margin-left: 8px; // same as menu+aux buttons
+            }
+        }
+
+        &:not(.mx_RoomSublist_headerContainer_withAux) {
+            .mx_NotificationBadge {
+                margin-right: 4px; // just to push it over a bit, aligning it with the other elements
+            }
+        }
+
+        .mx_RoomSublist_auxButton,
+        .mx_RoomSublist_menuButton {
+            margin-left: 8px; // should be the same as the notification badge
+            position: relative;
+            width: 24px;
+            height: 24px;
+            border-radius: 8px;
+
+            &::before {
+                content: '';
+                width: 16px;
+                height: 16px;
+                position: absolute;
+                top: 4px;
+                left: 4px;
+                mask-position: center;
+                mask-size: contain;
+                mask-repeat: no-repeat;
+                background: $muted-fg-color;
+            }
+        }
+
+        .mx_RoomSublist_auxButton:hover,
+        .mx_RoomSublist_menuButton:hover {
+            background: $roomlist-button-bg-color;
+        }
+
+        // Hide the menu button by default
+        .mx_RoomSublist_menuButton {
+            visibility: hidden;
+            width: 0;
+            margin: 0;
+        }
+
+        .mx_RoomSublist_auxButton::before {
+            mask-image: url('$(res)/img/element-icons/roomlist/plus.svg');
+        }
+
+        .mx_RoomSublist_menuButton::before {
+            mask-image: url('$(res)/img/element-icons/context-menu.svg');
+        }
+
+        .mx_RoomSublist_headerText {
+            flex: 1;
+            max-width: calc(100% - 16px); // 16px is the badge width
+            line-height: $font-16px;
+            font-size: $font-13px;
+            font-weight: 600;
+
+            // Ellipsize any text overflow
+            text-overflow: ellipsis;
+            overflow: hidden;
+            white-space: nowrap;
+
+            .mx_RoomSublist_collapseBtn {
+                display: inline-block;
+                position: relative;
+                width: 14px;
+                height: 14px;
+                margin-right: 6px;
+
+                &::before {
+                    content: '';
+                    width: 18px;
+                    height: 18px;
+                    position: absolute;
+                    mask-position: center;
+                    mask-size: contain;
+                    mask-repeat: no-repeat;
+                    background-color: $roomlist-header-color;
+                    mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
+                }
+
+                &.mx_RoomSublist_collapseBtn_collapsed::before {
+                    transform: rotate(-90deg);
+                }
+            }
+        }
+    }
+
+    // In the general case, we leave height of headers alone even if sticky, so
+    // that the sublists below them do not jump. However, that leaves a gap
+    // when scrolled to the top above the first sublist (whose header can only
+    // ever stick to top), so we force height to 0 for only that first header.
+    // See also https://github.com/vector-im/element-web/issues/14429.
+    &:first-child .mx_RoomSublist_headerContainer {
+        height: 0;
+        padding-bottom: 4px;
+    }
+
+    .mx_RoomSublist_resizeBox {
+        position: relative;
+
+        // Create another flexbox column for the tiles
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+
+        .mx_RoomSublist_tiles {
+            flex: 1 0 0;
+            overflow: hidden;
+            // need this to be flex otherwise the overflow hidden from above
+            // sometimes vertically centers the clipped list ... no idea why it would do this
+            // as the box model should be top aligned. Happens in both FF and Chromium
+            display: flex;
+            flex-direction: column;
+            align-self: stretch;
+
+            mask-image: linear-gradient(0deg, transparent, black 4px);
+        }
+
+        .mx_RoomSublist_resizerHandles_showNButton {
+            flex: 0 0 32px;
+        }
+
+        .mx_RoomSublist_resizerHandles {
+            flex: 0 0 4px;
+            display: flex;
+            justify-content: center;
+            width: 100%;
+        }
+
+        // Class name comes from the ResizableBox component
+        // The hover state needs to use the whole sublist, not just the resizable box,
+        // so that selector is below and one level higher.
+        .mx_RoomSublist_resizerHandle {
+            cursor: ns-resize;
+            border-radius: 3px;
+
+            // Override styles from library
+            max-width: 64px;
+            height: 4px !important; // Update RESIZE_HANDLE_HEIGHT if this changes
+
+            // This is positioned directly below the 'show more' button.
+            position: relative !important;
+            bottom: 0 !important; // override from library
+        }
+
+        &:hover, &.mx_RoomSublist_hasMenuOpen {
+            .mx_RoomSublist_resizerHandle {
+                opacity: 0.8;
+                background-color: $primary-fg-color;
+            }
+        }
+    }
+
+    .mx_RoomSublist_showNButton {
+        cursor: pointer;
+        font-size: $font-13px;
+        line-height: $font-18px;
+        color: $roomtile-preview-color;
+
+        // Update the render() function for RoomSublist if these change
+        // Update the ListLayout class for minVisibleTiles if these change.
+        height: 24px;
+        padding-bottom: 4px;
+
+        // We create a flexbox to cheat at alignment
+        display: flex;
+        align-items: center;
+
+        .mx_RoomSublist_showNButtonChevron {
+            position: relative;
+            width: 18px;
+            height: 18px;
+            margin-left: 12px;
+            margin-right: 16px;
+            mask-position: center;
+            mask-size: contain;
+            mask-repeat: no-repeat;
+            background: $roomlist-header-color;
+            left: -1px; // adjust for image position
+        }
+
+        .mx_RoomSublist_showMoreButtonChevron,
+        .mx_RoomSublist_showLessButtonChevron {
+            mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
+        }
+
+        .mx_RoomSublist_showLessButtonChevron {
+            transform: rotate(180deg);
+        }
+    }
+
+    &.mx_RoomSublist_hasMenuOpen,
+    &:not(.mx_RoomSublist_minimized) > .mx_RoomSublist_headerContainer:focus-within,
+    &:not(.mx_RoomSublist_minimized) > .mx_RoomSublist_headerContainer:hover {
+        .mx_RoomSublist_menuButton {
+            visibility: visible;
+            width: 24px;
+            margin-left: 8px;
+        }
+    }
+
+    &.mx_RoomSublist_minimized {
+        .mx_RoomSublist_headerContainer {
+            height: auto;
+            flex-direction: column;
+            position: relative;
+
+            .mx_RoomSublist_badgeContainer {
+                order: 0;
+                align-self: flex-end;
+                margin-right: 0;
+            }
+
+            .mx_RoomSublist_stickable {
+                order: 1;
+                max-width: 100%;
+            }
+
+            .mx_RoomSublist_auxButton {
+                order: 2;
+                visibility: visible;
+                width: 32px !important; // !important to override hover styles
+                height: 32px !important; // !important to override hover styles
+                margin-left: 0 !important; // !important to override hover styles
+                background-color: $roomlist-button-bg-color;
+                margin-top: 8px;
+
+                &::before {
+                    top: 8px;
+                    left: 8px;
+                }
+            }
+        }
+
+        .mx_RoomSublist_resizeBox {
+            align-items: center;
+        }
+
+        .mx_RoomSublist_showNButton {
+            flex-direction: column;
+
+            .mx_RoomSublist_showNButtonChevron {
+                margin-right: 12px; // to center
+            }
+        }
+
+        .mx_RoomSublist_menuButton {
+            height: 16px;
+        }
+
+        &.mx_RoomSublist_hasMenuOpen,
+        & > .mx_RoomSublist_headerContainer:hover {
+            .mx_RoomSublist_menuButton {
+                visibility: visible;
+                position: absolute;
+                bottom: 48px; // align to middle of name, 40px for aux button (with padding) and 8px for alignment
+                right: 0;
+                width: 16px;
+                height: 16px;
+                border-radius: 0;
+                z-index: 1; // occlude the list name
+
+                // This is the same color as the left panel background because it needs
+                // to occlude the sublist title
+                background-color: $roomlist-bg-color;
+
+                &::before {
+                    top: 0;
+                    left: 0;
+                }
+            }
+
+            &.mx_RoomSublist_headerContainer:not(.mx_RoomSublist_headerContainer_withAux) {
+                .mx_RoomSublist_menuButton {
+                    bottom: 8px; // align to the middle of name, 40px less than the `bottom` above.
+                }
+            }
+        }
+    }
+}
+
+.mx_RoomSublist_contextMenu {
+    padding: 20px 16px;
+    width: 250px;
+
+    hr {
+        margin-top: 16px;
+        margin-bottom: 16px;
+        margin-right: 16px; // additional 16px
+        border: 1px solid $roomsublist-divider-color;
+        opacity: 0.1;
+    }
+
+    .mx_RoomSublist_contextMenu_title {
+        font-size: $font-15px;
+        line-height: $font-20px;
+        font-weight: 600;
+        margin-bottom: 4px;
+    }
+
+    .mx_RadioButton, .mx_Checkbox {
+        margin-top: 8px;
+    }
+}
+
+.mx_RoomSublist_addRoomTooltip {
+    margin-top: -3px;
+}
+
+.mx_RoomSublist_skeletonUI {
+    position: relative;
+    margin-left: 4px;
+    height: 288px;
+
+    &::before {
+        background: $roomsublist-skeleton-ui-bg;
+
+        width: 100%;
+        height: 100%;
+
+        content: '';
+        position: absolute;
+        mask-repeat: repeat-y;
+        mask-size: auto 48px;
+        mask-image: url('$(res)/img/element-icons/roomlist/skeleton-ui.svg');
+    }
+}
diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss
index 7be2a4e3d4..b8f4aeb6e7 100644
--- a/res/css/views/rooms/_RoomTile.scss
+++ b/res/css/views/rooms/_RoomTile.scss
@@ -1,6 +1,5 @@
 /*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,202 +14,194 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+// Note: the room tile expects to be in a flexbox column container
 .mx_RoomTile {
-    display: flex;
-    flex-direction: row;
-    align-items: center;
-    cursor: pointer;
-    height: 34px;
-    margin: 0;
-    padding: 0 8px 0 10px;
-    position: relative;
-}
-
-.mx_RoomTile:focus {
-    filter: none !important;
-    background-color: $roomtile-focused-bg-color;
-}
-
-.mx_RoomTile_menuButton {
-    display: none;
-    flex: 0 0 16px;
-    height: 16px;
-    background-image: url('$(res)/img/icon_context.svg');
-    background-repeat: no-repeat;
-    background-position: center;
-}
-
-.mx_RoomTile_tooltip {
-    display: inline-block;
-    position: relative;
-    top: -54px;
-    left: -12px;
-}
-
-.mx_RoomTile_nameContainer {
-    display: flex;
-    align-items: center;
-    flex: 1;
-    vertical-align: middle;
-    min-width: 0;
-}
-
-.mx_RoomTile_labelContainer {
-    display: flex;
-    flex-direction: column;
-    flex: 1;
-    min-width: 0;
-}
-
-.mx_RoomTile_subtext {
-    display: inline-block;
-    font-size: $font-11px;
-    padding: 0 0 0 7px;
-    margin: 0;
-    overflow: hidden;
-    white-space: nowrap;
-    text-overflow: clip;
-    position: relative;
-    bottom: 4px;
-}
-
-.mx_RoomTile_avatar_container {
-    position: relative;
-}
-
-.mx_RoomTile_avatar {
-    flex: 0;
+    margin-bottom: 4px;
     padding: 4px;
-    width: 24px;
-    vertical-align: middle;
-}
 
-.mx_RoomTile_hasSubtext .mx_RoomTile_avatar {
-    padding-top: 0;
-    vertical-align: super;
-}
+    contain: content; // Not strict as it will break when resizing a sublist vertically
+    height: 40px;
+    box-sizing: border-box;
 
-.mx_RoomTile_dm {
-    display: block;
-    position: absolute;
-    bottom: 0;
-    right: -5px;
-    z-index: 2;
-}
+    // The tile is also a flexbox row itself
+    display: flex;
 
-// Note we match .mx_E2EIcon to make sure this matches more tightly than just
-// .mx_E2EIcon on its own
-.mx_RoomTile_e2eIcon.mx_E2EIcon {
-    height: 14px;
-    width: 14px;
-    display: block;
-    position: absolute;
-    bottom: -2px;
-    right: -5px;
-    z-index: 1;
-    margin: 0;
-}
+    &.mx_RoomTile_selected,
+    &:hover,
+    &:focus-within,
+    &.mx_RoomTile_hasMenuOpen {
+        background-color: $roomtile-selected-bg-color;
+        border-radius: 8px;
+    }
 
-.mx_RoomTile_name {
-    font-size: $font-14px;
-    padding: 0 4px;
-    color: $roomtile-name-color;
-    white-space: nowrap;
-    overflow-x: hidden;
-    text-overflow: ellipsis;
-}
+    .mx_DecoratedRoomAvatar, .mx_RoomTile_avatarContainer {
+        margin-right: 8px;
+    }
 
-.mx_RoomTile_badge {
-    flex: 0 1 content;
-    border-radius: 0.8em;
-    padding: 0 0.4em;
-    color: $roomtile-badge-fg-color;
-    font-weight: 600;
-    font-size: $font-12px;
-}
+    .mx_RoomTile_nameContainer {
+        flex-grow: 1;
+        min-width: 0; // allow flex to shrink it
+        margin-right: 8px; // spacing to buttons/badges
 
-.collapsed {
-    .mx_RoomTile {
-        margin: 0 6px;
-        padding: 0 2px;
-        position: relative;
+        // Create a new column layout flexbox for the name parts
+        display: flex;
+        flex-direction: column;
         justify-content: center;
+
+        .mx_RoomTile_name,
+        .mx_RoomTile_messagePreview {
+            margin: 0 2px;
+            width: 100%;
+
+            // Ellipsize any text overflow
+            text-overflow: ellipsis;
+            overflow: hidden;
+            white-space: nowrap;
+        }
+
+        .mx_RoomTile_name {
+            font-size: $font-14px;
+            line-height: $font-18px;
+        }
+
+        .mx_RoomTile_name.mx_RoomTile_nameHasUnreadEvents {
+            font-weight: 600;
+        }
+
+        .mx_RoomTile_messagePreview {
+            font-size: $font-13px;
+            line-height: $font-18px;
+            color: $roomtile-preview-color;
+        }
+
+        .mx_RoomTile_nameWithPreview {
+            margin-top: -4px; // shift the name up a bit more
+        }
     }
 
-    .mx_RoomTile_name {
+    .mx_RoomTile_notificationsButton {
+        margin-left: 4px; // spacing between buttons
+    }
+
+    .mx_RoomTile_badgeContainer {
+        height: 16px;
+        // don't set width so that it takes no space when there is no badge to show
+        margin: auto 0; // vertically align
+
+        // Create a flexbox to make aligning dot badges easier
+        display: flex;
+        align-items: center;
+
+        .mx_NotificationBadge {
+            margin-right: 2px; // centering
+        }
+
+        .mx_NotificationBadge_dot {
+            // make the smaller dot occupy the same width for centering
+            margin-left: 5px;
+            margin-right: 7px;
+        }
+    }
+
+    // The context menu buttons are hidden by default
+    .mx_RoomTile_menuButton,
+    .mx_RoomTile_notificationsButton {
+        width: 20px;
+        min-width: 20px; // yay flex
+        height: 20px;
+        margin-top: auto;
+        margin-bottom: auto;
+        position: relative;
         display: none;
+
+        &::before {
+            top: 2px;
+            left: 2px;
+            content: '';
+            width: 16px;
+            height: 16px;
+            position: absolute;
+            mask-position: center;
+            mask-size: contain;
+            mask-repeat: no-repeat;
+            background: $primary-fg-color;
+        }
     }
 
-    .mx_RoomTile_badge {
-        position: absolute;
-        right: 6px;
-        top: 0px;
-        border-radius: 16px;
-        z-index: 3;
-        border: 0.18em solid $secondary-accent-color;
-    }
-
-    .mx_RoomTile_menuButton {
-        display: none;  //no design for this for now
-    }
-}
-
-// toggle menuButton and badge on menu displayed
-.mx_RoomTile_menuDisplayed,
-// or on keyboard focus of room tile
-.mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:focus-within,
-// or on pointer hover
-.mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:hover {
-    .mx_RoomTile_menuButton {
+    // If the room has an overriden notification setting then we always show the notifications menu button
+    .mx_RoomTile_notificationsButton.mx_RoomTile_notificationsButton_show {
         display: block;
     }
-}
 
-.mx_RoomTile_unreadNotify .mx_RoomTile_badge,
-.mx_RoomTile_badge.mx_RoomTile_badgeUnread {
-    background-color: $roomtile-name-color;
-}
+    .mx_RoomTile_menuButton::before {
+        mask-image: url('$(res)/img/element-icons/context-menu.svg');
+    }
 
-.mx_RoomTile_highlight .mx_RoomTile_badge,
-.mx_RoomTile_badge.mx_RoomTile_badgeRed {
-    color: $accent-fg-color;
-    background-color: $warning-color;
-}
+    &:not(.mx_RoomTile_minimized) {
+        &:hover,
+        &:focus-within,
+        &.mx_RoomTile_hasMenuOpen {
+            // Hide the badge container on hover because it'll be a menu button
+            .mx_RoomTile_badgeContainer {
+                width: 0;
+                height: 0;
+                display: none;
+            }
 
-.mx_RoomTile_unread, .mx_RoomTile_highlight {
-    .mx_RoomTile_name {
-        font-weight: 600;
-        color: $roomtile-selected-color;
+            .mx_RoomTile_notificationsButton,
+            .mx_RoomTile_menuButton {
+                display: block;
+            }
+        }
+    }
+
+    &.mx_RoomTile_minimized {
+        flex-direction: column;
+        align-items: center;
+        position: relative;
+
+        .mx_DecoratedRoomAvatar, .mx_RoomTile_avatarContainer {
+            margin-right: 0;
+        }
     }
 }
 
-.mx_RoomTile_selected {
-    border-radius: 4px;
-    background-color: $roomtile-selected-bg-color;
+// We use these both in context menus and the room tiles
+.mx_RoomTile_iconBell::before {
+    mask-image: url('$(res)/img/element-icons/notifications.svg');
+}
+.mx_RoomTile_iconBellDot::before {
+    mask-image: url('$(res)/img/element-icons/roomlist/notifications-default.svg');
+}
+.mx_RoomTile_iconBellCrossed::before {
+    mask-image: url('$(res)/img/element-icons/roomlist/notifications-off.svg');
+}
+.mx_RoomTile_iconBellMentions::before {
+    mask-image: url('$(res)/img/element-icons/roomlist/notifications-dm.svg');
 }
 
-.mx_DNDRoomTile {
-    transform: none;
-    transition: transform 0.2s;
-}
+.mx_RoomTile_contextMenu {
+    .mx_RoomTile_iconStar::before {
+        mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg');
+    }
 
-.mx_DNDRoomTile_dragging {
-    transform: scale(1.05, 1.05);
-}
+    .mx_RoomTile_iconArrowDown::before {
+        mask-image: url('$(res)/img/element-icons/roomlist/low-priority.svg');
+    }
 
-.mx_RoomTile_arrow {
-    position: absolute;
-    right: 0px;
-}
+    .mx_RoomTile_iconSettings::before {
+        mask-image: url('$(res)/img/element-icons/settings.svg');
+    }
 
-.mx_RoomTile.mx_RoomTile_transparent {
-    background-color: transparent;
-}
+    .mx_RoomTile_iconCopyLink::before {
+        mask-image: url('$(res)/img/element-icons/link.svg');
+    }
 
-.mx_RoomTile.mx_RoomTile_transparent:focus {
-    background-color: $roomtile-transparent-focused-color;
-}
+    .mx_RoomTile_iconInvite::before {
+        mask-image: url('$(res)/img/element-icons/room/invite.svg');
+    }
 
-.mx_GroupInviteTile .mx_RoomTile_name {
-    flex: 1;
+    .mx_RoomTile_iconSignOut::before {
+        mask-image: url('$(res)/img/element-icons/leave.svg');
+    }
 }
diff --git a/res/css/views/rooms/_SearchBar.scss b/res/css/views/rooms/_SearchBar.scss
index fecc8d78d8..d9f730a8b6 100644
--- a/res/css/views/rooms/_SearchBar.scss
+++ b/res/css/views/rooms/_SearchBar.scss
@@ -68,3 +68,4 @@ limitations under the License.
         cursor: pointer;
     }
 }
+
diff --git a/res/css/views/rooms/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss
index 0b646666e7..9f6a8d52ce 100644
--- a/res/css/views/rooms/_SendMessageComposer.scss
+++ b/res/css/views/rooms/_SendMessageComposer.scss
@@ -44,10 +44,5 @@ limitations under the License.
             overflow-y: auto;
         }
     }
-
-    .mx_SendMessageComposer_overlayWrapper {
-        position: relative;
-        height: 0;
-    }
 }
 
diff --git a/res/css/views/rooms/_Stickers.scss b/res/css/views/rooms/_Stickers.scss
index d33ecc0bb6..da86797f42 100644
--- a/res/css/views/rooms/_Stickers.scss
+++ b/res/css/views/rooms/_Stickers.scss
@@ -7,12 +7,23 @@
     height: 300px;
 }
 
-#mx_persistedElement_stickerPicker .mx_AppTileFullWidth {
-    height: unset;
-    box-sizing: border-box;
-    border-left: none;
-    border-right: none;
-    border-bottom: none;
+#mx_persistedElement_stickerPicker {
+    .mx_AppTileFullWidth {
+        height: unset;
+        box-sizing: border-box;
+        border-left: none;
+        border-right: none;
+        border-bottom: none;
+    }
+
+    .mx_AppTileMenuBar {
+        padding: 0;
+    }
+
+    iframe {
+        // Sticker picker depends on the fixed height previously used for all tiles
+        height: 283px; // height of the popout minus the AppTile menu bar
+    }
 }
 
 .mx_Stickers_contentPlaceholder {
@@ -31,7 +42,7 @@
 .mx_Stickers_addLink {
     display: inline;
     cursor: pointer;
-    text-decoration: underline;
+    color: $button-link-fg-color;
 }
 
 .mx_Stickers_hideStickers {
diff --git a/res/css/views/rooms/_TopUnreadMessagesBar.scss b/res/css/views/rooms/_TopUnreadMessagesBar.scss
index 28eddf1fa2..8841b042a0 100644
--- a/res/css/views/rooms/_TopUnreadMessagesBar.scss
+++ b/res/css/views/rooms/_TopUnreadMessagesBar.scss
@@ -28,7 +28,7 @@ limitations under the License.
     content: "";
     position: absolute;
     top: -8px;
-    left: 11px;
+    left: 10.5px;
     width: 4px;
     height: 4px;
     border-radius: 16px;
@@ -42,19 +42,20 @@ limitations under the License.
     border-radius: 19px;
     box-sizing: border-box;
     background: $primary-bg-color;
-    border: 1.3px solid $roomtile-name-color;
+    border: 1.3px solid $muted-fg-color;
     cursor: pointer;
 }
 
 .mx_TopUnreadMessagesBar_scrollUp::before {
     content: "";
     position: absolute;
-    width: 38px;
-    height: 38px;
-    mask-image: url('$(res)/img/icon-jump-to-first-unread.svg');
+    width: 36px;
+    height: 36px;
+    mask-image: url('$(res)/img/feather-customised/chevron-down-thin.svg');
     mask-repeat: no-repeat;
-    mask-position: 9px 13px;
-    background: $roomtile-name-color;
+    mask-size: contain;
+    background: $muted-fg-color;
+    transform: rotate(180deg);
 }
 
 .mx_TopUnreadMessagesBar_markAsRead {
@@ -62,7 +63,7 @@ limitations under the License.
     width: 18px;
     height: 18px;
     background: $primary-bg-color;
-    border: 1.3px solid $roomtile-name-color;
+    border: 1.3px solid $muted-fg-color;
     border-radius: 10px;
     margin: 5px auto;
 }
@@ -76,5 +77,5 @@ limitations under the License.
     mask-repeat: no-repeat;
     mask-size: 10px;
     mask-position: 4px 4px;
-    background: $roomtile-name-color;
+    background: $muted-fg-color;
 }
diff --git a/res/css/views/rooms/_UserOnlineDot.scss b/res/css/views/rooms/_UserOnlineDot.scss
deleted file mode 100644
index 339e5cc48a..0000000000
--- a/res/css/views/rooms/_UserOnlineDot.scss
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
-Copyright 2019 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_UserOnlineDot {
-    border-radius: 50%;
-    background-color: $accent-color;
-    height: 5px;
-    width: 5px;
-    display: inline-block;
-}
diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss
new file mode 100644
index 0000000000..5501ab343e
--- /dev/null
+++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss
@@ -0,0 +1,98 @@
+/*
+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_VoiceRecordComposerTile_stop {
+    // 28px plus a 2px border makes this a 32px square (as intended)
+    width: 28px;
+    height: 28px;
+    border: 2px solid $voice-record-stop-border-color;
+    border-radius: 32px;
+    margin-right: 16px; // between us and the send button
+    position: relative;
+
+    &::after {
+        content: '';
+        width: 14px;
+        height: 14px;
+        position: absolute;
+        top: 7px;
+        left: 7px;
+        border-radius: 2px;
+        background-color: $voice-record-stop-symbol-color;
+    }
+}
+
+.mx_VoiceRecordComposerTile_delete {
+    width: 24px;
+    height: 24px;
+    vertical-align: middle;
+    margin-right: 8px; // distance from left edge of waveform container (container has some margin too)
+    background-color: $voice-record-icon-color;
+    mask-repeat: no-repeat;
+    mask-size: contain;
+    mask-image: url('$(res)/img/element-icons/trashcan.svg');
+}
+
+.mx_MessageComposer_row .mx_VoiceMessagePrimaryContainer {
+    // Note: remaining class properties are in the PlayerContainer CSS.
+
+    margin: 6px; // force the composer area to put a gutter around us
+    margin-right: 12px; // isolate from stop/send button
+
+    position: relative; // important for the live circle
+
+    &.mx_VoiceRecordComposerTile_recording {
+        // We are putting the circle in this padding, so we need +10px from the regular
+        // padding on the left side.
+        padding-left: 22px;
+
+        &::before {
+            animation: recording-pulse 2s infinite;
+
+            content: '';
+            background-color: $voice-record-live-circle-color;
+            width: 10px;
+            height: 10px;
+            position: absolute;
+            left: 12px; // 12px from the left edge for container padding
+            top: 18px; // vertically center (middle align with clock)
+            border-radius: 10px;
+        }
+    }
+}
+
+// The keyframes are slightly weird here to help make a ramping/punch effect
+// for the recording dot. We start and end at 100% opacity to help make the
+// dot feel a bit like a real lamp that is blinking: the animation ends up
+// spending a lot of its time showing a steady state without a fade effect.
+// This lamp effect extends into why the 0% opacity keyframe is not in the
+// midpoint: lamps take longer to turn off than they do to turn on, and the
+// extra frames give it a bit of a realistic punch for when the animation is
+// ramping back up to 100% opacity.
+//
+// Target animation timings: steady for 1.5s, fade out for 0.3s, fade in for 0.2s
+// (intended to be used in a loop for 2s animation speed)
+@keyframes recording-pulse {
+    0% {
+        opacity: 1;
+    }
+    35% {
+        opacity: 0;
+    }
+    65% {
+        opacity: 1;
+    }
+}
diff --git a/res/css/views/rooms/_WhoIsTypingTile.scss b/res/css/views/rooms/_WhoIsTypingTile.scss
index 8b135152d6..1c0dabbeb5 100644
--- a/res/css/views/rooms/_WhoIsTypingTile.scss
+++ b/res/css/views/rooms/_WhoIsTypingTile.scss
@@ -59,7 +59,7 @@ limitations under the License.
     flex: 1;
     font-size: $font-14px;
     font-weight: 600;
-    color: $eventtile-meta-color;
+    color: $roomtopic-color;
 }
 
 .mx_WhoIsTypingTile_label > span {
diff --git a/res/css/views/settings/_AvatarSetting.scss b/res/css/views/settings/_AvatarSetting.scss
index 35dba90f85..a350605ab1 100644
--- a/res/css/views/settings/_AvatarSetting.scss
+++ b/res/css/views/settings/_AvatarSetting.scss
@@ -1,5 +1,5 @@
 /*
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,44 +15,65 @@ limitations under the License.
 */
 
 .mx_AvatarSetting_avatar {
-    width: 88px;
-    height: 88px;
-    margin-left: 13px;
+    width: 90px;
+    min-width: 90px; // so it doesn't get crushed by the flexbox in languages with longer words
+    height: 90px;
+    margin-top: 8px;
     position: relative;
 
+    .mx_AvatarSetting_hover {
+        transition: opacity $hover-transition;
+
+        // position to place the hover bg over the entire thing
+        position: absolute;
+        top: 0;
+        bottom: 0;
+        left: 0;
+        right: 0;
+
+        pointer-events: none; // let the pointer fall through the underlying thing
+
+        line-height: 90px;
+        text-align: center;
+
+        > span {
+            color: #fff; // hardcoded to contrast with background
+            position: relative; // tricks the layout engine into putting this on top of the bg
+            font-weight: 500;
+        }
+
+        .mx_AvatarSetting_hoverBg {
+            // absolute position to lazily fill the entire container
+            position: absolute;
+            top: 0;
+            bottom: 0;
+            left: 0;
+            right: 0;
+
+            opacity: 0.5;
+            background-color: $settings-profile-overlay-placeholder-fg-color;
+            border-radius: 90px;
+        }
+    }
+
+    &.mx_AvatarSetting_avatar_hovering .mx_AvatarSetting_hover {
+        opacity: 1;
+    }
+
+    &:not(.mx_AvatarSetting_avatar_hovering) .mx_AvatarSetting_hover {
+        opacity: 0;
+    }
+
     & > * {
-        width: 88px;
         box-sizing: border-box;
     }
 
     .mx_AccessibleButton.mx_AccessibleButton_kind_primary {
         margin-top: 8px;
-
-        div {
-            position: relative;
-            height: 12px;
-            width: 12px;
-            display: inline;
-            padding-right: 6px; // 0.5 * 12px
-            left: -6px; // 0.5 * 12px
-            top: 3px;
-        }
-
-        div::before {
-            content: '';
-            position: absolute;
-            height: 12px;
-            width: 12px;
-
-            background-color: $button-primary-fg-color;
-            mask-repeat: no-repeat;
-            mask-size: contain;
-            mask-image: url('$(res)/img/feather-customised/upload.svg');
-        }
     }
 
     .mx_AccessibleButton.mx_AccessibleButton_kind_link_sm {
-        color: $button-danger-bg-color;
+        width: 100%;
     }
 
     & > img {
@@ -63,8 +84,10 @@ limitations under the License.
     & > img,
     .mx_AvatarSetting_avatarPlaceholder {
         display: block;
-        height: 88px;
-        border-radius: 4px;
+        height: 90px;
+        width: inherit;
+        border-radius: 90px;
+        cursor: pointer;
     }
 
     .mx_AvatarSetting_avatarPlaceholder::before {
@@ -80,6 +103,29 @@ limitations under the License.
         left: 0;
         right: 0;
     }
+
+    .mx_AvatarSetting_uploadButton {
+        width: 32px;
+        height: 32px;
+        border-radius: 32px;
+        background-color: $settings-profile-button-bg-color;
+
+        position: absolute;
+        bottom: 0;
+        right: 0;
+    }
+
+    .mx_AvatarSetting_uploadButton::before {
+        content: "";
+        display: block;
+        width: 100%;
+        height: 100%;
+        mask-repeat: no-repeat;
+        mask-position: center;
+        mask-size: 55%;
+        background-color: $settings-profile-button-fg-color;
+        mask-image: url('$(res)/img/feather-customised/edit.svg');
+    }
 }
 
 .mx_AvatarSetting_avatar .mx_AvatarSetting_avatarPlaceholder {
diff --git a/res/css/views/settings/_CrossSigningPanel.scss b/res/css/views/settings/_CrossSigningPanel.scss
index fa9f76a963..12a0e36835 100644
--- a/res/css/views/settings/_CrossSigningPanel.scss
+++ b/res/css/views/settings/_CrossSigningPanel.scss
@@ -28,4 +28,8 @@ limitations under the License.
 
 .mx_CrossSigningPanel_buttonRow {
     margin: 1em 0;
+
+    :nth-child(n + 1) {
+        margin-inline-end: 10px;
+    }
 }
diff --git a/res/css/views/settings/_KeyBackupPanel.scss b/res/css/views/settings/_KeyBackupPanel.scss
deleted file mode 100644
index 872162caad..0000000000
--- a/res/css/views/settings/_KeyBackupPanel.scss
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 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_KeyBackupPanel_sigValid, .mx_KeyBackupPanel_sigInvalid,
-.mx_KeyBackupPanel_deviceVerified, .mx_KeyBackupPanel_deviceNotVerified {
-    font-weight: bold;
-}
-
-.mx_KeyBackupPanel_sigValid, .mx_KeyBackupPanel_deviceVerified {
-    color: $e2e-verified-color;
-}
-
-.mx_KeyBackupPanel_sigInvalid, .mx_KeyBackupPanel_deviceNotVerified {
-    color: $e2e-warning-color;
-}
-
-.mx_KeyBackupPanel_deviceName {
-    font-style: italic;
-}
-
-.mx_KeyBackupPanel_buttonRow {
-    margin: 1em 0;
-}
diff --git a/res/css/views/settings/_Notifications.scss b/res/css/views/settings/_Notifications.scss
index e6d09b9a2a..77a7bc5b68 100644
--- a/res/css/views/settings/_Notifications.scss
+++ b/res/css/views/settings/_Notifications.scss
@@ -64,6 +64,7 @@ limitations under the License.
 
 .mx_UserNotifSettings_notifTable {
     display: table;
+    position: relative;
 }
 
 .mx_UserNotifSettings_notifTable .mx_Spinner {
diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss
index 58624d1597..4cbcb8e708 100644
--- a/res/css/views/settings/_ProfileSettings.scss
+++ b/res/css/views/settings/_ProfileSettings.scss
@@ -1,5 +1,5 @@
 /*
-Copyright 2019 New Vector Ltd
+Copyright 2019, 2020 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,12 +14,25 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+.mx_ProfileSettings_controls_topic {
+    & > textarea {
+        resize: vertical;
+    }
+}
+
 .mx_ProfileSettings_profile {
     display: flex;
 }
 
 .mx_ProfileSettings_controls {
     flex-grow: 1;
+    margin-right: 54px;
+
+    // We put the header under the controls with some minor styling to cheat
+    // alignment of the field with the avatar
+    .mx_SettingsTab_subheading {
+        margin-top: 0;
+    }
 }
 
 .mx_ProfileSettings_controls .mx_Field #profileTopic {
@@ -41,3 +54,17 @@ limitations under the License.
 .mx_ProfileSettings_avatarUpload {
     display: none;
 }
+
+.mx_ProfileSettings_profileForm {
+    @mixin mx_Settings_fullWidthField;
+    border-bottom: 1px solid $menu-border-color;
+}
+
+.mx_ProfileSettings_buttons {
+    margin-top: 10px; // 18px is already accounted for by the 

above the buttons + margin-bottom: 28px; + + > .mx_AccessibleButton_kind_link { + padding-left: 0; // to align with left side + } +} diff --git a/res/css/views/settings/_SecureBackupPanel.scss b/res/css/views/settings/_SecureBackupPanel.scss new file mode 100644 index 0000000000..a9dab06b57 --- /dev/null +++ b/res/css/views/settings/_SecureBackupPanel.scss @@ -0,0 +1,53 @@ +/* +Copyright 2018 New Vector Ltd +Copyright 2019, 2020 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_SecureBackupPanel_sigValid, .mx_SecureBackupPanel_sigInvalid, +.mx_SecureBackupPanel_deviceVerified, .mx_SecureBackupPanel_deviceNotVerified { + font-weight: bold; +} + +.mx_SecureBackupPanel_sigValid, .mx_SecureBackupPanel_deviceVerified { + color: $e2e-verified-color; +} + +.mx_SecureBackupPanel_sigInvalid, .mx_SecureBackupPanel_deviceNotVerified { + color: $e2e-warning-color; +} + +.mx_SecureBackupPanel_deviceName { + font-style: italic; +} + +.mx_SecureBackupPanel_buttonRow { + margin: 1em 0; + + :nth-child(n + 1) { + margin-inline-end: 10px; + } +} + +.mx_SecureBackupPanel_statusList { + border-spacing: 0; + + td { + padding: 0; + + &:first-of-type { + padding-inline-end: 1em; + } + } +} diff --git a/res/css/views/settings/_SpellCheckLanguages.scss b/res/css/views/settings/_SpellCheckLanguages.scss new file mode 100644 index 0000000000..bb322c983f --- /dev/null +++ b/res/css/views/settings/_SpellCheckLanguages.scss @@ -0,0 +1,35 @@ +/* +Copyright 2021 Šimon Brandner + +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_ExistingSpellCheckLanguage { + display: flex; + align-items: center; + margin-bottom: 5px; +} + +.mx_ExistingSpellCheckLanguage_language { + flex: 1; + margin-right: 10px; +} + +.mx_GeneralUserSettingsTab_spellCheckLanguageInput { + margin-top: 1em; + margin-bottom: 1em; +} + +.mx_SpellCheckLanguages { + @mixin mx_Settings_fullWidthField; +} diff --git a/res/css/views/settings/_UpdateCheckButton.scss b/res/css/views/settings/_UpdateCheckButton.scss new file mode 100644 index 0000000000..f35a023ac1 --- /dev/null +++ b/res/css/views/settings/_UpdateCheckButton.scss @@ -0,0 +1,23 @@ +/* +Copyright 2020 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_UpdateCheckButton_summary { + margin-left: 16px; + + .mx_AccessibleButton_kind_link { + padding: 0; + } +} diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 1fbfb35927..892f5fe744 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019, 2020 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,6 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_SettingsTab { + color: $muted-fg-color; +} + .mx_SettingsTab_warningText { color: $warning-color; } @@ -22,6 +26,7 @@ limitations under the License. font-size: $font-20px; font-weight: 600; color: $primary-fg-color; + margin-bottom: 10px; } .mx_SettingsTab_heading:nth-child(n + 2) { @@ -63,7 +68,7 @@ limitations under the License. display: inline-block; font-size: $font-14px; color: $primary-fg-color; - max-width: calc(100% - 48px); // Force word wrap instead of colliding with the switch + max-width: calc(100% - $font-48px); // Force word wrap instead of colliding with the switch box-sizing: border-box; padding-right: 10px; } diff --git a/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss index b5a57dfefb..23dcc532b2 100644 --- a/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss +++ b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_SecurityRoomSettingsTab label { - display: block; -} - .mx_SecurityRoomSettingsTab_warning { display: block; diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss new file mode 100644 index 0000000000..94983a60bf --- /dev/null +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -0,0 +1,230 @@ +/* +Copyright 2020 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_AppearanceUserSettingsTab_fontSlider, +.mx_AppearanceUserSettingsTab_fontSlider_preview, +.mx_AppearanceUserSettingsTab_Layout { + @mixin mx_Settings_fullWidthField; +} + +.mx_AppearanceUserSettingsTab .mx_Field { + width: 256px; +} + +.mx_AppearanceUserSettingsTab_fontScaling { + color: $primary-fg-color; +} + +.mx_AppearanceUserSettingsTab_fontSlider { + display: flex; + flex-direction: row; + align-items: center; + padding: 15px; + background: rgba($appearance-tab-border-color, 0.2); + border-radius: 10px; + font-size: 10px; + margin-top: 24px; + margin-bottom: 24px; +} + +.mx_AppearanceUserSettingsTab_fontSlider_preview { + border: 1px solid $appearance-tab-border-color; + border-radius: 10px; + padding: 0 16px 9px 16px; + pointer-events: none; + + .mx_EventTile_msgOption { + display: none; + } + + &.mx_IRCLayout { + padding-top: 9px; + } +} + +.mx_AppearanceUserSettingsTab_fontSlider_smallText { + font-size: 15px; + padding-right: 20px; + padding-left: 5px; + font-weight: 500; +} + +.mx_AppearanceUserSettingsTab_fontSlider_largeText { + font-size: 18px; + padding-left: 20px; + padding-right: 5px; + font-weight: 500; +} + +.mx_AppearanceUserSettingsTab { + > .mx_SettingsTab_SubHeading { + margin-bottom: 32px; + } +} + +.mx_AppearanceUserSettingsTab_themeSection { + $radio-bg-color: $input-darker-bg-color; + color: $primary-fg-color; + + > .mx_ThemeSelectors { + display: flex; + flex-direction: row; + flex-wrap: wrap; + + margin-top: 4px; + margin-bottom: 30px; + + > .mx_RadioButton { + padding: $font-16px; + box-sizing: border-box; + border-radius: 10px; + width: 180px; + + background: $radio-bg-color; + opacity: 0.4; + + flex-shrink: 1; + flex-grow: 0; + + margin-right: 15px; + margin-top: 10px; + + font-weight: 600; + color: $muted-fg-color; + + > span { + justify-content: center; + } + } + + > .mx_RadioButton_enabled { + opacity: 1; + + // These colors need to be hardcoded because they don't change with the theme + &.mx_ThemeSelector_light { + background-color: #f3f8fd; + color: #2e2f32; + } + + &.mx_ThemeSelector_dark { + // 5% lightened version of 181b21 + background-color: #25282e; + color: #f3f8fd; + + > input > div { + border-color: $input-darker-bg-color; + > div { + border-color: $input-darker-bg-color; + } + } + } + + &.mx_ThemeSelector_black { + background-color: #000000; + color: #f3f8fd; + + > input > div { + border-color: $input-darker-bg-color; + > div { + border-color: $input-darker-bg-color; + } + } + } + } + } +} + +.mx_SettingsTab_customFontSizeField { + margin-left: calc($font-16px + 10px); +} + +.mx_AppearanceUserSettingsTab_Layout_RadioButtons { + display: flex; + flex-direction: row; + + color: $primary-fg-color; + + .mx_AppearanceUserSettingsTab_spacer { + width: 24px; + } + + > .mx_AppearanceUserSettingsTab_Layout_RadioButton { + flex-grow: 0; + flex-shrink: 1; + display: flex; + flex-direction: column; + + width: 300px; + + border: 1px solid $appearance-tab-border-color; + border-radius: 10px; + + .mx_EventTile_msgOption, + .mx_MessageActionBar { + display: none; + } + + .mx_AppearanceUserSettingsTab_Layout_RadioButton_preview { + flex-grow: 1; + display: flex; + align-items: center; + padding: 10px; + pointer-events: none; + } + + .mx_RadioButton { + flex-grow: 0; + padding: 10px; + } + + .mx_EventTile_content { + margin-right: 0; + } + + &.mx_AppearanceUserSettingsTab_Layout_RadioButton_selected { + border-color: $accent-color; + } + } + + .mx_RadioButton { + border-top: 1px solid $appearance-tab-border-color; + + > input + div { + border-color: rgba($muted-fg-color, 0.2); + } + } + + .mx_RadioButton_checked { + background-color: rgba($accent-color, 0.08); + } +} + +.mx_AppearanceUserSettingsTab_Advanced { + color: $primary-fg-color; + + > * { + margin-bottom: 16px; + } + + .mx_AppearanceUserSettingsTab_AdvancedToggle { + color: $accent-color; + cursor: pointer; + } + + .mx_AppearanceUserSettingsTab_systemFont { + margin-left: calc($font-16px + 10px); + } +} diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss index 62d230e752..8b73e69031 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_GeneralUserSettingsTab_changePassword .mx_Field, -.mx_GeneralUserSettingsTab_themeSection .mx_Field { +.mx_GeneralUserSettingsTab_changePassword .mx_Field { @mixin mx_Settings_fullWidthField; } @@ -23,6 +22,19 @@ limitations under the License. margin-top: 0; } +// TODO: Make this selector less painful +.mx_GeneralUserSettingsTab_accountSection .mx_SettingsTab_subheading:nth-child(n + 1), +.mx_GeneralUserSettingsTab_discovery .mx_SettingsTab_subheading:nth-child(n + 2), +.mx_SetIdServer .mx_SettingsTab_subheading { + margin-top: 24px; +} + +.mx_GeneralUserSettingsTab_accountSection .mx_Spinner, +.mx_GeneralUserSettingsTab_discovery .mx_Spinner { + // Move the spinner to the left side of the container (default center) + justify-content: left; +} + .mx_GeneralUserSettingsTab_accountSection .mx_EmailAddresses, .mx_GeneralUserSettingsTab_accountSection .mx_PhoneNumbers, .mx_GeneralUserSettingsTab_discovery .mx_ExistingEmailAddress, diff --git a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss index 109edfff81..0f879d209e 100644 --- a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss @@ -22,3 +22,34 @@ limitations under the License. .mx_HelpUserSettingsTab span.mx_AccessibleButton { word-break: break-word; } + +.mx_HelpUserSettingsTab code { + word-break: break-all; + user-select: all; +} + +.mx_HelpUserSettingsTab_accessToken { + display: flex; + justify-content: space-between; + border-radius: 5px; + border: solid 1px $light-fg-color; + margin-bottom: 10px; + margin-top: 10px; + padding: 10px; +} + +.mx_HelpUserSettingsTab_accessToken_copy { + flex-shrink: 0; + cursor: pointer; + margin-left: 20px; + display: inherit; +} + +.mx_HelpUserSettingsTab_accessToken_copy > div { + mask-image: url($copy-button-url); + background-color: $message-action-bar-fg-color; + margin-left: 5px; + width: 20px; + height: 20px; + background-repeat: no-repeat; +} diff --git a/res/css/views/settings/tabs/user/_LabsUserSettingsTab.scss b/res/css/views/settings/tabs/user/_LabsUserSettingsTab.scss new file mode 100644 index 0000000000..540db48d65 --- /dev/null +++ b/res/css/views/settings/tabs/user/_LabsUserSettingsTab.scss @@ -0,0 +1,25 @@ +/* +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_LabsUserSettingsTab { + .mx_SettingsTab_section { + margin-top: 32px; + + .mx_SettingsFlag { + margin-right: 0; // remove right margin to align with beta cards + } + } +} diff --git a/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss index b5a6693006..d6466a03f9 100644 --- a/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss @@ -55,3 +55,33 @@ limitations under the License. .mx_SecurityUserSettingsTab_ignoredUser .mx_AccessibleButton { margin-right: 10px; } + +.mx_SecurityUserSettingsTab { + .mx_SettingsTab_section { + .mx_AccessibleButton_kind_link { + padding: 0; + font-size: inherit; + } + } + + .mx_SecurityUserSettingsTab_warning { + color: $notice-primary-color; + position: relative; + padding-left: 40px; + margin-top: 30px; + + &::before { + mask-repeat: no-repeat; + mask-position: 0 center; + mask-size: $font-24px; + position: absolute; + width: $font-24px; + height: $font-24px; + content: ""; + top: 0; + left: 0; + background-color: $notice-primary-color; + mask-image: url('$(res)/img/feather-customised/alert-triangle.svg'); + } + } +} diff --git a/res/css/views/spaces/_SpaceBasicSettings.scss b/res/css/views/spaces/_SpaceBasicSettings.scss new file mode 100644 index 0000000000..c73e0715dd --- /dev/null +++ b/res/css/views/spaces/_SpaceBasicSettings.scss @@ -0,0 +1,86 @@ +/* +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_SpaceBasicSettings { + .mx_Field { + margin: 24px 0; + } + + .mx_SpaceBasicSettings_avatarContainer { + display: flex; + margin-top: 24px; + + .mx_SpaceBasicSettings_avatar { + position: relative; + height: 80px; + width: 80px; + background-color: $tertiary-fg-color; + border-radius: 16px; + } + + img.mx_SpaceBasicSettings_avatar { + width: 80px; + height: 80px; + object-fit: cover; + border-radius: 16px; + } + + // only show it when the button is a div and not an img (has avatar) + div.mx_SpaceBasicSettings_avatar { + cursor: pointer; + + &::before { + content: ""; + position: absolute; + height: 80px; + width: 80px; + top: 0; + left: 0; + background-color: #ffffff; // white icon fill + mask-repeat: no-repeat; + mask-position: center; + mask-size: 20px; + mask-image: url('$(res)/img/element-icons/camera.svg'); + } + } + + > input[type="file"] { + display: none; + } + + > .mx_AccessibleButton_kind_link { + display: inline-block; + padding: 0; + margin: auto 16px; + color: #368bd6; + } + + > .mx_SpaceBasicSettings_avatar_remove { + color: $notice-primary-color; + } + } + + .mx_AccessibleButton_hasKind { + padding: 8px 22px; + margin-left: auto; + display: block; + width: min-content; + } + + .mx_AccessibleButton_disabled { + cursor: not-allowed; + } +} diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss new file mode 100644 index 0000000000..88b9d8f693 --- /dev/null +++ b/res/css/views/spaces/_SpaceCreateMenu.scss @@ -0,0 +1,101 @@ +/* +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. +*/ + +$spacePanelWidth: 71px; + +.mx_SpaceCreateMenu_wrapper { + // background blur everything except SpacePanel + .mx_ContextualMenu_background { + background-color: $dialog-backdrop-color; + opacity: 0.6; + left: $spacePanelWidth; + } + + .mx_ContextualMenu { + padding: 24px; + width: 480px; + box-sizing: border-box; + background-color: $primary-bg-color; + position: relative; + + > div { + > h2 { + font-weight: $font-semi-bold; + font-size: $font-18px; + margin-top: 4px; + } + + > p { + font-size: $font-15px; + color: $secondary-fg-color; + margin: 0; + } + } + + // XXX remove this when spaces leaves Beta + .mx_BetaCard_betaPill { + position: absolute; + top: 24px; + right: 24px; + } + + .mx_SpaceCreateMenuType { + @mixin SpacePillButton; + } + + .mx_SpaceCreateMenuType_public::before { + mask-image: url('$(res)/img/globe.svg'); + } + .mx_SpaceCreateMenuType_private::before { + mask-image: url('$(res)/img/element-icons/lock.svg'); + } + + .mx_SpaceCreateMenu_back { + width: 28px; + height: 28px; + position: relative; + background-color: $roomlist-button-bg-color; + border-radius: 14px; + margin-bottom: 12px; + + &::before { + content: ""; + position: absolute; + height: 28px; + width: 28px; + top: 0; + left: 0; + background-color: $tertiary-fg-color; + transform: rotate(90deg); + mask-repeat: no-repeat; + mask-position: 2px 3px; + mask-size: 24px; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + } + } + + .mx_AccessibleButton_kind_primary { + padding: 8px 22px; + margin-left: auto; + display: block; + width: min-content; + } + + .mx_AccessibleButton_disabled { + cursor: not-allowed; + } + } +} diff --git a/res/css/views/spaces/_SpacePublicShare.scss b/res/css/views/spaces/_SpacePublicShare.scss new file mode 100644 index 0000000000..373fa94e00 --- /dev/null +++ b/res/css/views/spaces/_SpacePublicShare.scss @@ -0,0 +1,29 @@ +/* +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_SpacePublicShare { + .mx_AccessibleButton { + @mixin SpacePillButton; + + &.mx_SpacePublicShare_shareButton::before { + mask-image: url('$(res)/img/element-icons/link.svg'); + } + + &.mx_SpacePublicShare_inviteButton::before { + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } + } +} diff --git a/res/css/views/toasts/_AnalyticsToast.scss b/res/css/views/toasts/_AnalyticsToast.scss new file mode 100644 index 0000000000..fdbe7f1c76 --- /dev/null +++ b/res/css/views/toasts/_AnalyticsToast.scss @@ -0,0 +1,27 @@ +/* +Copyright 2020 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_AnalyticsToast { + .mx_AccessibleButton_kind_danger { + background: none; + color: $accent-color; + } + + .mx_AccessibleButton_kind_primary { + background: $accent-color; + color: #ffffff; + } +} diff --git a/res/css/views/toasts/_NonUrgentEchoFailureToast.scss b/res/css/views/toasts/_NonUrgentEchoFailureToast.scss new file mode 100644 index 0000000000..9a8229b38e --- /dev/null +++ b/res/css/views/toasts/_NonUrgentEchoFailureToast.scss @@ -0,0 +1,37 @@ +/* +Copyright 2020 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_NonUrgentEchoFailureToast { + .mx_NonUrgentEchoFailureToast_icon { + display: inline-block; + width: $font-18px; + height: $font-18px; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background-color: #fff; // we know that non-urgent toasts are always styled the same + mask-image: url('$(res)/img/element-icons/cloud-off.svg'); + margin-right: 8px; + } + + span { // includes the i18n block + vertical-align: middle; + } + + .mx_AccessibleButton { + padding: 0; + } +} diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss new file mode 100644 index 0000000000..0c09070334 --- /dev/null +++ b/res/css/views/voip/_CallContainer.scss @@ -0,0 +1,126 @@ +/* +Copyright 2020 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_CallContainer { + position: absolute; + right: 20px; + bottom: 72px; + z-index: 100; + + // Disable pointer events for Jitsi widgets to function. Direct + // calls have their own cursor and behaviour, but we need to make + // sure the cursor hits the iframe for Jitsi which will be at a + // different level. + pointer-events: none; + + .mx_CallPreview { + pointer-events: initial; // restore pointer events so the user can leave/interact + cursor: pointer; + + .mx_VideoFeed_remote.mx_VideoFeed_voice { + min-height: 150px; + } + + .mx_VideoFeed_local { + border-radius: 8px; + overflow: hidden; + } + } + + .mx_AppTile_persistedWrapper div { + min-width: 350px; + } + + .mx_IncomingCallBox { + min-width: 250px; + background-color: $voipcall-plinth-color; + padding: 8px; + box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); + border-radius: 8px; + + pointer-events: initial; // restore pointer events so the user can accept/decline + cursor: pointer; + + .mx_IncomingCallBox_CallerInfo { + display: flex; + direction: row; + + img, .mx_BaseAvatar_initial { + margin: 8px; + } + + > div { + display: flex; + flex-direction: column; + + justify-content: center; + } + + h1, p { + margin: 0px; + padding: 0px; + font-size: $font-14px; + line-height: $font-16px; + } + + h1 { + font-weight: bold; + } + } + + .mx_IncomingCallBox_buttons { + padding: 8px; + display: flex; + flex-direction: row; + + > .mx_IncomingCallBox_spacer { + width: 8px; + } + + > * { + flex-shrink: 0; + flex-grow: 1; + margin-right: 0; + font-size: $font-15px; + line-height: $font-24px; + } + } + + .mx_IncomingCallBox_iconButton { + position: absolute; + right: 8px; + + &::before { + content: ''; + + height: 20px; + width: 20px; + background-color: $icon-button-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } + } + + .mx_IncomingCallBox_silence::before { + mask-image: url('$(res)/img/voip/silence.svg'); + } + + .mx_IncomingCallBox_unSilence::before { + mask-image: url('$(res)/img/voip/un-silence.svg'); + } + } +} diff --git a/res/css/views/voip/_CallPreview.scss b/res/css/views/voip/_CallPreview.scss new file mode 100644 index 0000000000..92348fb465 --- /dev/null +++ b/res/css/views/voip/_CallPreview.scss @@ -0,0 +1,21 @@ +/* +Copyright 2021 Šimon Brandner + +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_CallPreview { + position: fixed; + left: 0; + top: 0; +} diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 4650f30c1d..205d431752 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 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,12 +15,363 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_CallView_voice { - background-color: $accent-color; - color: $accent-fg-color; - cursor: pointer; - text-align: center; - padding: 6px; - font-weight: bold; - font-size: $font-13px; +.mx_CallView { + border-radius: 8px; + background-color: $dark-panel-bg-color; + padding-left: 8px; + padding-right: 8px; + // XXX: CallContainer sets pointer-events: none - should probably be set back in a better place + pointer-events: initial; +} + +.mx_CallView_large { + padding-bottom: 10px; + margin: 5px 5px 5px 18px; + display: flex; + flex-direction: column; + flex: 1; + + .mx_CallView_voice { + flex: 1; + } +} + +.mx_CallView_pip { + width: 320px; + padding-bottom: 8px; + background-color: $voipcall-plinth-color; + box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20); + border-radius: 8px; + + .mx_CallView_voice { + height: 180px; + } + + .mx_CallView_callControls { + bottom: 0px; + } + + .mx_CallView_callControls_button { + &::before { + width: 36px; + height: 36px; + } + } + + .mx_CallView_holdTransferContent { + padding-top: 10px; + padding-bottom: 25px; + } +} + +.mx_CallView_content { + position: relative; + display: flex; + border-radius: 8px; +} + +.mx_CallView_voice { + align-items: center; + justify-content: center; + flex-direction: column; + background-color: $inverted-bg-color; +} + +.mx_CallView_voice_avatarsContainer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + div { + margin-left: 12px; + margin-right: 12px; + } +} + +.mx_CallView_voice .mx_CallView_holdTransferContent { + // This masks the avatar image so when it's blurred, the edge is still crisp + .mx_CallView_voice_avatarContainer { + border-radius: 2000px; + overflow: hidden; + position: relative; + } +} + +.mx_CallView_holdTransferContent { + height: 20px; + padding-top: 20px; + padding-bottom: 15px; + color: $accent-fg-color; + .mx_AccessibleButton_hasKind { + padding: 0px; + font-weight: bold; + } +} + +.mx_CallView_video { + width: 100%; + height: 100%; + z-index: 30; + overflow: hidden; +} + +.mx_CallView_video_hold { + overflow: hidden; + + // we keep these around in the DOM: it saved wiring them up again when the call + // is resumed and keeps the container the right size + .mx_VideoFeed { + visibility: hidden; + } +} + +.mx_CallView_video_holdBackground { + position: absolute; + width: 100%; + height: 100%; + left: 0; + right: 0; + background-repeat: no-repeat; + background-size: cover; + background-position: center; + filter: blur(20px); + &::after { + content: ''; + display: block; + position: absolute; + width: 100%; + height: 100%; + left: 0; + right: 0; + background-color: rgba(0, 0, 0, 0.6); + } +} + +.mx_CallView_video .mx_CallView_holdTransferContent { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-weight: bold; + color: $accent-fg-color; + text-align: center; + + &::before { + display: block; + margin-left: auto; + margin-right: auto; + content: ''; + width: 40px; + height: 40px; + background-image: url('$(res)/img/voip/paused.svg'); + background-position: center; + background-size: cover; + } + .mx_CallView_pip &::before { + width: 30px; + height: 30px; + } + .mx_AccessibleButton_hasKind { + padding: 0px; + } +} + +.mx_CallView_header { + height: 44px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: left; + flex-shrink: 0; +} + +.mx_CallView_header_callType { + font-size: 1.2rem; + font-weight: bold; + vertical-align: middle; +} + +.mx_CallView_header_secondaryCallInfo { + &::before { + content: '·'; + margin-left: 6px; + margin-right: 6px; + } +} + +.mx_CallView_header_controls { + margin-left: auto; +} + +.mx_CallView_header_button { + display: inline-block; + vertical-align: middle; + cursor: pointer; + + &::before { + content: ''; + display: inline-block; + height: 20px; + width: 20px; + vertical-align: middle; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } +} + +.mx_CallView_header_button_fullscreen { + &::before { + mask-image: url('$(res)/img/element-icons/call/fullscreen.svg'); + } +} + +.mx_CallView_header_button_expand { + &::before { + mask-image: url('$(res)/img/element-icons/call/expand.svg'); + } +} + +.mx_CallView_header_callInfo { + margin-left: 12px; + margin-right: 16px; +} + +.mx_CallView_header_roomName { + font-weight: bold; + font-size: 12px; + line-height: initial; + height: 15px; +} + +.mx_CallView_secondaryCall_roomName { + margin-left: 4px; +} + +.mx_CallView_header_callTypeSmall { + font-size: 12px; + color: $secondary-fg-color; + line-height: initial; + height: 15px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + max-width: 240px; +} + +.mx_CallView_header_phoneIcon { + display: inline-block; + margin-right: 6px; + height: 16px; + width: 16px; + vertical-align: middle; + + &::before { + content: ''; + display: inline-block; + vertical-align: top; + + height: 16px; + width: 16px; + background-color: $warning-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); + } +} + +.mx_CallView_callControls { + position: absolute; + display: flex; + justify-content: center; + bottom: 5px; + width: 100%; + opacity: 1; + transition: opacity 0.5s; +} + +.mx_CallView_callControls_hidden { + opacity: 0.001; // opacity 0 can cause a re-layout + pointer-events: none; +} + +.mx_CallView_callControls_button { + cursor: pointer; + margin-left: 8px; + margin-right: 8px; + + + &::before { + content: ''; + display: inline-block; + + height: 48px; + width: 48px; + + background-repeat: no-repeat; + background-size: contain; + background-position: center; + } +} + +.mx_CallView_callControls_dialpad { + margin-right: auto; + &::before { + background-image: url('$(res)/img/voip/dialpad.svg'); + } +} + +.mx_CallView_callControls_button_dialpad_hidden { + margin-right: auto; + cursor: initial; +} + +.mx_CallView_callControls_button_micOn { + &::before { + background-image: url('$(res)/img/voip/mic-on.svg'); + } +} + +.mx_CallView_callControls_button_micOff { + &::before { + background-image: url('$(res)/img/voip/mic-off.svg'); + } +} + +.mx_CallView_callControls_button_vidOn { + &::before { + background-image: url('$(res)/img/voip/vid-on.svg'); + } +} + +.mx_CallView_callControls_button_vidOff { + &::before { + background-image: url('$(res)/img/voip/vid-off.svg'); + } +} + +.mx_CallView_callControls_button_hangup { + &::before { + background-image: url('$(res)/img/voip/hangup.svg'); + } +} + +.mx_CallView_callControls_button_more { + margin-left: auto; + &::before { + background-image: url('$(res)/img/voip/more.svg'); + } +} + +.mx_CallView_callControls_button_more_hidden { + margin-left: auto; + cursor: initial; +} + +.mx_CallView_callControls_button_invisible { + visibility: hidden; + pointer-events: none; + position: absolute; } diff --git a/res/css/views/voip/_CallViewForRoom.scss b/res/css/views/voip/_CallViewForRoom.scss new file mode 100644 index 0000000000..769e00338e --- /dev/null +++ b/res/css/views/voip/_CallViewForRoom.scss @@ -0,0 +1,46 @@ +/* +Copyright 2021 Šimon Brandner + +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_CallViewForRoom { + overflow: hidden; + + .mx_CallViewForRoom_ResizeWrapper { + display: flex; + margin-bottom: 8px; + + &:hover .mx_CallViewForRoom_ResizeHandle { + // Need to use important to override element style attributes + // set by re-resizable + width: 100% !important; + + display: flex; + justify-content: center; + + &::after { + content: ''; + margin-top: 3px; + + border-radius: 4px; + + height: 4px; + width: 100%; + max-width: 64px; + + background-color: $primary-fg-color; + } + } + } +} diff --git a/res/css/views/voip/_DialPad.scss b/res/css/views/voip/_DialPad.scss new file mode 100644 index 0000000000..eefd2e9ba5 --- /dev/null +++ b/res/css/views/voip/_DialPad.scss @@ -0,0 +1,67 @@ +/* +Copyright 2020 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_DialPad { + display: grid; + row-gap: 16px; + column-gap: 0px; + margin-top: 24px; + margin-left: auto; + margin-right: auto; + + /* squeeze the dial pad buttons together horizontally */ + grid-template-columns: repeat(3, 1fr); +} + +.mx_DialPad_button { + display: flex; + flex-direction: column; + justify-content: center; + + width: 40px; + height: 40px; + background-color: $dialpad-button-bg-color; + border-radius: 40px; + font-size: 18px; + font-weight: 600; + text-align: center; + vertical-align: middle; + margin-left: auto; + margin-right: auto; +} + +.mx_DialPad_button .mx_DialPad_buttonSubText { + font-size: 8px; +} + +.mx_DialPad_dialButton { + /* Always show the dial button in the center grid column */ + grid-column: 2; + background-color: $accent-color; + + &::before { + content: ''; + display: inline-block; + height: 40px; + width: 40px; + vertical-align: middle; + mask-repeat: no-repeat; + mask-size: 20px; + mask-position: center; + background-color: #FFF; // on all themes + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); + } +} diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss new file mode 100644 index 0000000000..0019994e72 --- /dev/null +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -0,0 +1,79 @@ +/* +Copyright 2020 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_DialPadContextMenu_dialPad .mx_DialPad { + row-gap: 16px; + column-gap: 32px; +} + +.mx_DialPadContextMenuWrapper { + padding: 15px; +} + +.mx_DialPadContextMenu_header { + border: none; + margin-top: 32px; + margin-left: 20px; + margin-right: 20px; + + /* a separator between the input line and the dial buttons */ + border-bottom: 1px solid $quaternary-fg-color; + transition: border-bottom 0.25s; +} + +.mx_DialPadContextMenu_cancel { + float: right; + mask: url('$(res)/img/feather-customised/cancel.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: cover; + width: 14px; + height: 14px; + background-color: $dialog-close-fg-color; + cursor: pointer; +} + +.mx_DialPadContextMenu_header:focus-within { + border-bottom: 1px solid $accent-color; +} + +.mx_DialPadContextMenu_title { + color: $muted-fg-color; + font-size: 12px; + font-weight: 600; +} + +.mx_DialPadContextMenu_dialled { + height: 1.5em; + font-size: 18px; + font-weight: 600; + border: none; + margin: 0px; +} +.mx_DialPadContextMenu_dialled input { + font-size: 18px; + font-weight: 600; + overflow: hidden; + max-width: 185px; + text-align: left; + direction: rtl; + padding: 8px 0px; + background-color: rgb(0, 0, 0, 0); +} + +.mx_DialPadContextMenu_dialPad { + margin: 16px; +} diff --git a/res/css/views/voip/_DialPadModal.scss b/res/css/views/voip/_DialPadModal.scss new file mode 100644 index 0000000000..b8042f77ae --- /dev/null +++ b/res/css/views/voip/_DialPadModal.scss @@ -0,0 +1,80 @@ +/* +Copyright 2020 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_Dialog_dialPadWrapper .mx_Dialog { + padding: 0px; +} + +.mx_DialPadModal { + width: 292px; + height: 370px; + padding: 16px 0px 0px 0px; +} + +.mx_DialPadModal_header { + margin-top: 32px; + margin-left: 40px; + margin-right: 40px; + + /* a separator between the input line and the dial buttons */ + border-bottom: 1px solid $quaternary-fg-color; + transition: border-bottom 0.25s; +} + +.mx_DialPadModal_header:focus-within { + border-bottom: 1px solid $accent-color; +} + +.mx_DialPadModal_title { + color: $muted-fg-color; + font-size: 12px; + font-weight: 600; +} + +.mx_DialPadModal_cancel { + float: right; + mask: url('$(res)/img/feather-customised/cancel.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: cover; + width: 14px; + height: 14px; + background-color: $dialog-close-fg-color; + cursor: pointer; + margin-right: 16px; +} + +.mx_DialPadModal_field { + border: none; + margin: 0px; + height: 30px; +} + +.mx_DialPadModal_field .mx_Field_postfix { + /* Remove border separator between postfix and field content */ + border-left: none; +} + +.mx_DialPadModal_field input { + font-size: 18px; + font-weight: 600; +} + +.mx_DialPadModal_dialPad { + margin-left: 16px; + margin-right: 16px; + margin-top: 16px; +} diff --git a/res/css/views/voip/_IncomingCallbox.scss b/res/css/views/voip/_IncomingCallbox.scss deleted file mode 100644 index ed33de470d..0000000000 --- a/res/css/views/voip/_IncomingCallbox.scss +++ /dev/null @@ -1,69 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_IncomingCallBox { - text-align: center; - border: 1px solid #a4a4a4; - border-radius: 8px; - background-color: $primary-bg-color; - position: fixed; - z-index: 1000; - padding: 6px; - margin-top: -3px; - margin-left: -20px; - width: 200px; -} - -.mx_IncomingCallBox_chevron { - padding: 12px; - position: absolute; - left: -21px; - top: 0px; -} - -.mx_IncomingCallBox_title { - padding: 6px; - font-weight: bold; -} - -.mx_IncomingCallBox_buttons { - display: flex; -} - -.mx_IncomingCallBox_buttons_cell { - vertical-align: middle; - padding: 6px; - flex: 1; -} - -.mx_IncomingCallBox_buttons_decline, -.mx_IncomingCallBox_buttons_accept { - vertical-align: middle; - width: 80px; - height: 36px; - line-height: $font-36px; - border-radius: 36px; - color: $accent-fg-color; - margin: auto; -} - -.mx_IncomingCallBox_buttons_decline { - background-color: $voip-decline-color; -} - -.mx_IncomingCallBox_buttons_accept { - background-color: $voip-accept-color; -} diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss new file mode 100644 index 0000000000..4a3fbdf597 --- /dev/null +++ b/res/css/views/voip/_VideoFeed.scss @@ -0,0 +1,50 @@ +/* +Copyright 2015, 2016, 2020 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_VideoFeed_voice { + background-color: $inverted-bg-color; +} + + +.mx_VideoFeed_remote { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + + &.mx_VideoFeed_video { + background-color: #000; + } +} + +.mx_VideoFeed_local { + max-width: 25%; + max-height: 25%; + position: absolute; + right: 10px; + top: 10px; + z-index: 100; + border-radius: 4px; + + &.mx_VideoFeed_video { + background-color: transparent; + } +} + +.mx_VideoFeed_mirror { + transform: scale(-1, 1); +} diff --git a/res/css/views/voip/_VideoView.scss b/res/css/views/voip/_VideoView.scss deleted file mode 100644 index feb60f4763..0000000000 --- a/res/css/views/voip/_VideoView.scss +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_VideoView { - width: 100%; - position: relative; - z-index: 30; -} - -.mx_VideoView video { - width: 100%; -} - -.mx_VideoView_remoteVideoFeed { - width: 100%; - background-color: #000; - z-index: 50; -} - -.mx_VideoView_localVideoFeed { - width: 25%; - height: 25%; - position: absolute; - left: 10px; - bottom: 10px; - z-index: 100; -} - -.mx_VideoView_localVideoFeed video { - width: auto; - height: 100%; -} - -.mx_VideoView_localVideoFeed.mx_VideoView_localVideoFeed_flipped video { - transform: scale(-1, 1); -} diff --git a/res/fonts/Inter/Inter-Bold.woff b/res/fonts/Inter/Inter-Bold.woff new file mode 100644 index 0000000000..2ec7ac3d21 Binary files /dev/null and b/res/fonts/Inter/Inter-Bold.woff differ diff --git a/res/fonts/Inter/Inter-Bold.woff2 b/res/fonts/Inter/Inter-Bold.woff2 new file mode 100644 index 0000000000..6989c99229 Binary files /dev/null and b/res/fonts/Inter/Inter-Bold.woff2 differ diff --git a/res/fonts/Inter/Inter-BoldItalic.woff b/res/fonts/Inter/Inter-BoldItalic.woff new file mode 100644 index 0000000000..aa35b79745 Binary files /dev/null and b/res/fonts/Inter/Inter-BoldItalic.woff differ diff --git a/res/fonts/Inter/Inter-BoldItalic.woff2 b/res/fonts/Inter/Inter-BoldItalic.woff2 new file mode 100644 index 0000000000..18b4c1ce5e Binary files /dev/null and b/res/fonts/Inter/Inter-BoldItalic.woff2 differ diff --git a/res/fonts/Inter/Inter-Italic.woff b/res/fonts/Inter/Inter-Italic.woff new file mode 100644 index 0000000000..4b765bd592 Binary files /dev/null and b/res/fonts/Inter/Inter-Italic.woff differ diff --git a/res/fonts/Inter/Inter-Italic.woff2 b/res/fonts/Inter/Inter-Italic.woff2 new file mode 100644 index 0000000000..bd5f255a98 Binary files /dev/null and b/res/fonts/Inter/Inter-Italic.woff2 differ diff --git a/res/fonts/Inter/Inter-Medium.woff b/res/fonts/Inter/Inter-Medium.woff new file mode 100644 index 0000000000..7d55f34cca Binary files /dev/null and b/res/fonts/Inter/Inter-Medium.woff differ diff --git a/res/fonts/Inter/Inter-Medium.woff2 b/res/fonts/Inter/Inter-Medium.woff2 new file mode 100644 index 0000000000..a916b47fc8 Binary files /dev/null and b/res/fonts/Inter/Inter-Medium.woff2 differ diff --git a/res/fonts/Inter/Inter-MediumItalic.woff b/res/fonts/Inter/Inter-MediumItalic.woff new file mode 100644 index 0000000000..422ab0576a Binary files /dev/null and b/res/fonts/Inter/Inter-MediumItalic.woff differ diff --git a/res/fonts/Inter/Inter-MediumItalic.woff2 b/res/fonts/Inter/Inter-MediumItalic.woff2 new file mode 100644 index 0000000000..f623924aea Binary files /dev/null and b/res/fonts/Inter/Inter-MediumItalic.woff2 differ diff --git a/res/fonts/Inter/Inter-Regular.woff b/res/fonts/Inter/Inter-Regular.woff new file mode 100644 index 0000000000..7ff51b7d8f Binary files /dev/null and b/res/fonts/Inter/Inter-Regular.woff differ diff --git a/res/fonts/Inter/Inter-Regular.woff2 b/res/fonts/Inter/Inter-Regular.woff2 new file mode 100644 index 0000000000..554aed6612 Binary files /dev/null and b/res/fonts/Inter/Inter-Regular.woff2 differ diff --git a/res/fonts/Inter/Inter-SemiBold.woff b/res/fonts/Inter/Inter-SemiBold.woff new file mode 100644 index 0000000000..76e507a515 Binary files /dev/null and b/res/fonts/Inter/Inter-SemiBold.woff differ diff --git a/res/fonts/Inter/Inter-SemiBold.woff2 b/res/fonts/Inter/Inter-SemiBold.woff2 new file mode 100644 index 0000000000..9307998993 Binary files /dev/null and b/res/fonts/Inter/Inter-SemiBold.woff2 differ diff --git a/res/fonts/Inter/Inter-SemiBoldItalic.woff b/res/fonts/Inter/Inter-SemiBoldItalic.woff new file mode 100644 index 0000000000..382181212d Binary files /dev/null and b/res/fonts/Inter/Inter-SemiBoldItalic.woff differ diff --git a/res/fonts/Inter/Inter-SemiBoldItalic.woff2 b/res/fonts/Inter/Inter-SemiBoldItalic.woff2 new file mode 100644 index 0000000000..f19f5505ec Binary files /dev/null and b/res/fonts/Inter/Inter-SemiBoldItalic.woff2 differ diff --git a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 index 593d7c8f5c..a52e5a3800 100644 Binary files a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 and b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 differ diff --git a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 index 277324851f..660a93193d 100644 Binary files a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 and b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 differ diff --git a/res/img/03b381.png b/res/img/03b381.png deleted file mode 100644 index cf28fc7e59..0000000000 Binary files a/res/img/03b381.png and /dev/null differ diff --git a/res/img/368bd6.png b/res/img/368bd6.png deleted file mode 100644 index a2700bd0ae..0000000000 Binary files a/res/img/368bd6.png and /dev/null differ diff --git a/res/img/ac3ba8.png b/res/img/ac3ba8.png deleted file mode 100644 index 031471d85a..0000000000 Binary files a/res/img/ac3ba8.png and /dev/null differ diff --git a/res/img/attach.png b/res/img/attach.png deleted file mode 100644 index 1bcb70045d..0000000000 Binary files a/res/img/attach.png and /dev/null differ diff --git a/res/img/betas/spaces.png b/res/img/betas/spaces.png new file mode 100644 index 0000000000..f4cfa90b4e Binary files /dev/null and b/res/img/betas/spaces.png differ diff --git a/res/img/button-text-block-quote-on.svg b/res/img/button-text-block-quote-on.svg deleted file mode 100644 index f8a86125c9..0000000000 --- a/res/img/button-text-block-quote-on.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - 3B24B8C7-64BE-4B3E-A748-94DB72E1210F - Created with sketchtool. - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-block-quote.svg b/res/img/button-text-block-quote.svg deleted file mode 100644 index d70c261f5d..0000000000 --- a/res/img/button-text-block-quote.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - BFC0418B-9081-4789-A231-B75953157748 - Created with sketchtool. - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-bold-on.svg b/res/img/button-text-bold-on.svg deleted file mode 100644 index 161e740e90..0000000000 --- a/res/img/button-text-bold-on.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - 01F3F9B2-8F38-4BAF-A345-AECAC3D88E79 - Created with sketchtool. - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-bold.svg b/res/img/button-text-bold.svg deleted file mode 100644 index 0fd0baa07e..0000000000 --- a/res/img/button-text-bold.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - 9BC64A5B-F157-43FF-BCC4-02D30CDF520B - Created with sketchtool. - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-bulleted-list-on.svg b/res/img/button-text-bulleted-list-on.svg deleted file mode 100644 index d4a40e889c..0000000000 --- a/res/img/button-text-bulleted-list-on.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - 654917CF-20A4-49B6-B0A1-9875D7B733C8 - Created with sketchtool. - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-bulleted-list.svg b/res/img/button-text-bulleted-list.svg deleted file mode 100644 index ae3e640d8e..0000000000 --- a/res/img/button-text-bulleted-list.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - B7D94619-44BC-4184-A60A-DBC5BB54E5F9 - Created with sketchtool. - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-deleted-on.svg b/res/img/button-text-deleted-on.svg deleted file mode 100644 index 2914fcabe6..0000000000 --- a/res/img/button-text-deleted-on.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - 69B11088-0F3A-4E14-BD9F-4FEF4115E99B - Created with sketchtool. - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-deleted.svg b/res/img/button-text-deleted.svg deleted file mode 100644 index 5f262dc350..0000000000 --- a/res/img/button-text-deleted.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - A34F2223-34C6-46AE-AA47-38EC8984E9B3 - Created with sketchtool. - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-formatting.svg b/res/img/button-text-formatting.svg deleted file mode 100644 index d697010d40..0000000000 --- a/res/img/button-text-formatting.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - diff --git a/res/img/button-text-inline-code-on.svg b/res/img/button-text-inline-code-on.svg deleted file mode 100644 index 8d1439c97b..0000000000 --- a/res/img/button-text-inline-code-on.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - B76754AB-42E6-48D2-9443-80CBC0DE02ED - Created with sketchtool. - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-inline-code.svg b/res/img/button-text-inline-code.svg deleted file mode 100644 index 24026cb709..0000000000 --- a/res/img/button-text-inline-code.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - 4CAFF494-61AE-4916-AFE8-D1E62F7CF0DE - Created with sketchtool. - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-italic-on.svg b/res/img/button-text-italic-on.svg deleted file mode 100644 index 15fe588596..0000000000 --- a/res/img/button-text-italic-on.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - 116426C2-0B55-480E-92B3-57D4B3ABAB90 - Created with sketchtool. - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-italic.svg b/res/img/button-text-italic.svg deleted file mode 100644 index b5722e827b..0000000000 --- a/res/img/button-text-italic.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - 9FBC844D-96CF-4DCB-B545-FCD23727218B - Created with sketchtool. - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-numbered-list-on.svg b/res/img/button-text-numbered-list-on.svg deleted file mode 100644 index 869a2c2cc2..0000000000 --- a/res/img/button-text-numbered-list-on.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - 294F929B-31AA-4D0C-98B3-9CA96764060D - Created with sketchtool. - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-numbered-list.svg b/res/img/button-text-numbered-list.svg deleted file mode 100644 index 8e5b8b87b6..0000000000 --- a/res/img/button-text-numbered-list.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - F0F58459-A13A-48C5-9332-ABFB96726F05 - Created with sketchtool. - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-underlined-on.svg b/res/img/button-text-underlined-on.svg deleted file mode 100644 index 870be3ce6a..0000000000 --- a/res/img/button-text-underlined-on.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - FD84FF7C-43E4-4312-90AB-5A59AD018377 - Created with sketchtool. - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/button-text-underlined.svg b/res/img/button-text-underlined.svg deleted file mode 100644 index 26f448539c..0000000000 --- a/res/img/button-text-underlined.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - 13E7EE68-9B16-4A3D-8F9F-31E4BAB7E438 - Created with sketchtool. - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/call.png b/res/img/call.png deleted file mode 100644 index a7805e0596..0000000000 Binary files a/res/img/call.png and /dev/null differ diff --git a/res/img/cancel-black.png b/res/img/cancel-black.png deleted file mode 100644 index 87dcfd41a8..0000000000 Binary files a/res/img/cancel-black.png and /dev/null differ diff --git a/res/img/cancel-black2.png b/res/img/cancel-black2.png deleted file mode 100644 index a928c61b09..0000000000 Binary files a/res/img/cancel-black2.png and /dev/null differ diff --git a/res/img/cancel-white.svg b/res/img/cancel-white.svg deleted file mode 100644 index 65e14c2fbc..0000000000 --- a/res/img/cancel-white.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - Slice 1 - Created with Sketch. - - - - - \ No newline at end of file diff --git a/res/img/cancel.png b/res/img/cancel.png deleted file mode 100644 index 2bda8ff5bf..0000000000 Binary files a/res/img/cancel.png and /dev/null differ diff --git a/res/img/chevron-left.png b/res/img/chevron-left.png deleted file mode 100644 index efb0065de9..0000000000 Binary files a/res/img/chevron-left.png and /dev/null differ diff --git a/res/img/chevron-right.png b/res/img/chevron-right.png deleted file mode 100644 index 18a4684e47..0000000000 Binary files a/res/img/chevron-right.png and /dev/null differ diff --git a/res/img/chevron.png b/res/img/chevron.png deleted file mode 100644 index 81236f91bc..0000000000 Binary files a/res/img/chevron.png and /dev/null differ diff --git a/res/img/close-white.png b/res/img/close-white.png deleted file mode 100644 index d8752ed9fe..0000000000 Binary files a/res/img/close-white.png and /dev/null differ diff --git a/res/img/create-big.png b/res/img/create-big.png deleted file mode 100644 index b7307a11c7..0000000000 Binary files a/res/img/create-big.png and /dev/null differ diff --git a/res/img/create-big.svg b/res/img/create-big.svg deleted file mode 100644 index 2450542b63..0000000000 --- a/res/img/create-big.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - icons_create_room - Created with sketchtool. - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/create.png b/res/img/create.png deleted file mode 100644 index 2d6107ac50..0000000000 Binary files a/res/img/create.png and /dev/null differ diff --git a/res/img/delete.png b/res/img/delete.png deleted file mode 100644 index 8ff20a116d..0000000000 Binary files a/res/img/delete.png and /dev/null differ diff --git a/res/img/directory-big.png b/res/img/directory-big.png deleted file mode 100644 index 03cab69c4a..0000000000 Binary files a/res/img/directory-big.png and /dev/null differ diff --git a/res/img/download.png b/res/img/download.png deleted file mode 100644 index 1999ebf7ab..0000000000 Binary files a/res/img/download.png and /dev/null differ diff --git a/res/img/e2e/blacklisted.svg b/res/img/e2e/blacklisted.svg deleted file mode 100644 index ac99d23f05..0000000000 --- a/res/img/e2e/blacklisted.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/e2e/disabled.svg b/res/img/e2e/disabled.svg new file mode 100644 index 0000000000..2f6110a36a --- /dev/null +++ b/res/img/e2e/disabled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/e2e/lock-verified.svg b/res/img/e2e/lock-verified.svg deleted file mode 100644 index 819dfacc49..0000000000 --- a/res/img/e2e/lock-verified.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/e2e/lock-warning.svg b/res/img/e2e/lock-warning.svg deleted file mode 100644 index de2bded7f8..0000000000 --- a/res/img/e2e/lock-warning.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/res/img/e2e/normal.svg b/res/img/e2e/normal.svg index 5b848bc27f..83b544a326 100644 --- a/res/img/e2e/normal.svg +++ b/res/img/e2e/normal.svg @@ -1,3 +1,3 @@ - - + + diff --git a/res/img/e2e/verified.svg b/res/img/e2e/verified.svg index 464b443dcf..f90d9db554 100644 --- a/res/img/e2e/verified.svg +++ b/res/img/e2e/verified.svg @@ -1,4 +1,3 @@ - - - + + diff --git a/res/img/e2e/warning.svg b/res/img/e2e/warning.svg index 209ae0f71f..58f5c3b7d1 100644 --- a/res/img/e2e/warning.svg +++ b/res/img/e2e/warning.svg @@ -1,5 +1,3 @@ - - - - + + diff --git a/res/img/edit.png b/res/img/edit.png deleted file mode 100644 index 6f373d3f3d..0000000000 Binary files a/res/img/edit.png and /dev/null differ diff --git a/res/img/edit.svg b/res/img/edit.svg deleted file mode 100644 index 9674b31690..0000000000 --- a/res/img/edit.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/res/img/element-desktop-logo.svg b/res/img/element-desktop-logo.svg new file mode 100644 index 0000000000..2031733ce3 --- /dev/null +++ b/res/img/element-desktop-logo.svg @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/img/element-icons/add-photo.svg b/res/img/element-icons/add-photo.svg new file mode 100644 index 0000000000..bde5253bea --- /dev/null +++ b/res/img/element-icons/add-photo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/element-icons/brands/apple.svg b/res/img/element-icons/brands/apple.svg new file mode 100644 index 0000000000..308c3c5d5a --- /dev/null +++ b/res/img/element-icons/brands/apple.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/brands/element.svg b/res/img/element-icons/brands/element.svg new file mode 100644 index 0000000000..6861de0955 --- /dev/null +++ b/res/img/element-icons/brands/element.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/img/element-icons/brands/facebook.svg b/res/img/element-icons/brands/facebook.svg new file mode 100644 index 0000000000..2742785424 --- /dev/null +++ b/res/img/element-icons/brands/facebook.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/res/img/element-icons/brands/github.svg b/res/img/element-icons/brands/github.svg new file mode 100644 index 0000000000..503719520b --- /dev/null +++ b/res/img/element-icons/brands/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/brands/gitlab.svg b/res/img/element-icons/brands/gitlab.svg new file mode 100644 index 0000000000..df84c41e21 --- /dev/null +++ b/res/img/element-icons/brands/gitlab.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/res/img/element-icons/brands/google.svg b/res/img/element-icons/brands/google.svg new file mode 100644 index 0000000000..1b0b19ae5b --- /dev/null +++ b/res/img/element-icons/brands/google.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/img/element-icons/brands/twitter.svg b/res/img/element-icons/brands/twitter.svg new file mode 100644 index 0000000000..43eb825a59 --- /dev/null +++ b/res/img/element-icons/brands/twitter.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/call/delete.svg b/res/img/element-icons/call/delete.svg new file mode 100644 index 0000000000..133bdad4ca --- /dev/null +++ b/res/img/element-icons/call/delete.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/img/element-icons/call/dialpad.svg b/res/img/element-icons/call/dialpad.svg new file mode 100644 index 0000000000..a97e80aa0b --- /dev/null +++ b/res/img/element-icons/call/dialpad.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/call/expand.svg b/res/img/element-icons/call/expand.svg new file mode 100644 index 0000000000..91ef4d8a76 --- /dev/null +++ b/res/img/element-icons/call/expand.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/call/fullscreen.svg b/res/img/element-icons/call/fullscreen.svg new file mode 100644 index 0000000000..d2a4c2aa8c --- /dev/null +++ b/res/img/element-icons/call/fullscreen.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/img/element-icons/call/hangup.svg b/res/img/element-icons/call/hangup.svg new file mode 100644 index 0000000000..1a1b82a1d7 --- /dev/null +++ b/res/img/element-icons/call/hangup.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/call/video-call.svg b/res/img/element-icons/call/video-call.svg new file mode 100644 index 0000000000..0c1cd2d419 --- /dev/null +++ b/res/img/element-icons/call/video-call.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/call/voice-call.svg b/res/img/element-icons/call/voice-call.svg new file mode 100644 index 0000000000..d32b703523 --- /dev/null +++ b/res/img/element-icons/call/voice-call.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/camera.svg b/res/img/element-icons/camera.svg new file mode 100644 index 0000000000..92d1f91dec --- /dev/null +++ b/res/img/element-icons/camera.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/img/element-icons/chat-bubbles.svg b/res/img/element-icons/chat-bubbles.svg new file mode 100644 index 0000000000..ac9db61f29 --- /dev/null +++ b/res/img/element-icons/chat-bubbles.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/element-icons/circle-sending.svg b/res/img/element-icons/circle-sending.svg new file mode 100644 index 0000000000..2d15a0f716 --- /dev/null +++ b/res/img/element-icons/circle-sending.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/circle-sent.svg b/res/img/element-icons/circle-sent.svg new file mode 100644 index 0000000000..04a00ceff7 --- /dev/null +++ b/res/img/element-icons/circle-sent.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/cloud-off.svg b/res/img/element-icons/cloud-off.svg new file mode 100644 index 0000000000..7faea7d3b5 --- /dev/null +++ b/res/img/element-icons/cloud-off.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/community-members.svg b/res/img/element-icons/community-members.svg new file mode 100644 index 0000000000..553ba3b1af --- /dev/null +++ b/res/img/element-icons/community-members.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/res/img/element-icons/community-rooms.svg b/res/img/element-icons/community-rooms.svg new file mode 100644 index 0000000000..570b45a488 --- /dev/null +++ b/res/img/element-icons/community-rooms.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/context-menu.svg b/res/img/element-icons/context-menu.svg new file mode 100644 index 0000000000..76a28d50d0 --- /dev/null +++ b/res/img/element-icons/context-menu.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/element-icons/email-prompt.svg b/res/img/element-icons/email-prompt.svg new file mode 100644 index 0000000000..19b8f82449 --- /dev/null +++ b/res/img/element-icons/email-prompt.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/res/img/element-icons/expand-space-panel.svg b/res/img/element-icons/expand-space-panel.svg new file mode 100644 index 0000000000..11232acd58 --- /dev/null +++ b/res/img/element-icons/expand-space-panel.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/eye.svg b/res/img/element-icons/eye.svg new file mode 100644 index 0000000000..0460a6201d --- /dev/null +++ b/res/img/element-icons/eye.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/feedback.svg b/res/img/element-icons/feedback.svg new file mode 100644 index 0000000000..3ee20d18d9 --- /dev/null +++ b/res/img/element-icons/feedback.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/hide.svg b/res/img/element-icons/hide.svg new file mode 100644 index 0000000000..8ea50a028f --- /dev/null +++ b/res/img/element-icons/hide.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/element-icons/home.svg b/res/img/element-icons/home.svg new file mode 100644 index 0000000000..a6c15456ff --- /dev/null +++ b/res/img/element-icons/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/i.svg b/res/img/element-icons/i.svg new file mode 100644 index 0000000000..6674f1ed8d --- /dev/null +++ b/res/img/element-icons/i.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/info.svg b/res/img/element-icons/info.svg new file mode 100644 index 0000000000..b5769074ab --- /dev/null +++ b/res/img/element-icons/info.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/leave.svg b/res/img/element-icons/leave.svg new file mode 100644 index 0000000000..773e27d4ce --- /dev/null +++ b/res/img/element-icons/leave.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/img/element-icons/link.svg b/res/img/element-icons/link.svg new file mode 100644 index 0000000000..ab3d54b838 --- /dev/null +++ b/res/img/element-icons/link.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/lock.svg b/res/img/element-icons/lock.svg new file mode 100644 index 0000000000..06fe52a391 --- /dev/null +++ b/res/img/element-icons/lock.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/message/chevron-up.svg b/res/img/element-icons/message/chevron-up.svg new file mode 100644 index 0000000000..4eb5ecc33e --- /dev/null +++ b/res/img/element-icons/message/chevron-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/img/element-icons/message/corner-up-right.svg b/res/img/element-icons/message/corner-up-right.svg new file mode 100644 index 0000000000..0b8f961b7b --- /dev/null +++ b/res/img/element-icons/message/corner-up-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/img/element-icons/message/fwd.svg b/res/img/element-icons/message/fwd.svg new file mode 100644 index 0000000000..8bcc70d092 --- /dev/null +++ b/res/img/element-icons/message/fwd.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/message/link.svg b/res/img/element-icons/message/link.svg new file mode 100644 index 0000000000..c89dd41c23 --- /dev/null +++ b/res/img/element-icons/message/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/img/element-icons/message/repeat.svg b/res/img/element-icons/message/repeat.svg new file mode 100644 index 0000000000..c7657b08ed --- /dev/null +++ b/res/img/element-icons/message/repeat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/img/element-icons/message/share.svg b/res/img/element-icons/message/share.svg new file mode 100644 index 0000000000..df38c14d63 --- /dev/null +++ b/res/img/element-icons/message/share.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/img/element-icons/notifications.svg b/res/img/element-icons/notifications.svg new file mode 100644 index 0000000000..7002782129 --- /dev/null +++ b/res/img/element-icons/notifications.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/element-icons/pause.svg b/res/img/element-icons/pause.svg new file mode 100644 index 0000000000..293c0a10d8 --- /dev/null +++ b/res/img/element-icons/pause.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/play.svg b/res/img/element-icons/play.svg new file mode 100644 index 0000000000..339e20b729 --- /dev/null +++ b/res/img/element-icons/play.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/plus.svg b/res/img/element-icons/plus.svg new file mode 100644 index 0000000000..ea1972237d --- /dev/null +++ b/res/img/element-icons/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/retry.svg b/res/img/element-icons/retry.svg new file mode 100644 index 0000000000..09448d6458 --- /dev/null +++ b/res/img/element-icons/retry.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/room/apps.svg b/res/img/element-icons/room/apps.svg new file mode 100644 index 0000000000..c90704752c --- /dev/null +++ b/res/img/element-icons/room/apps.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/img/element-icons/room/composer/attach.svg b/res/img/element-icons/room/composer/attach.svg new file mode 100644 index 0000000000..0cac44d29f --- /dev/null +++ b/res/img/element-icons/room/composer/attach.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/room/composer/emoji.svg b/res/img/element-icons/room/composer/emoji.svg new file mode 100644 index 0000000000..b02cb69364 --- /dev/null +++ b/res/img/element-icons/room/composer/emoji.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/img/element-icons/room/composer/sticker.svg b/res/img/element-icons/room/composer/sticker.svg new file mode 100644 index 0000000000..3d8f445926 --- /dev/null +++ b/res/img/element-icons/room/composer/sticker.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/img/element-icons/room/default_app.svg b/res/img/element-icons/room/default_app.svg new file mode 100644 index 0000000000..baf9bc37fa --- /dev/null +++ b/res/img/element-icons/room/default_app.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/res/img/element-icons/room/default_cal.svg b/res/img/element-icons/room/default_cal.svg new file mode 100644 index 0000000000..fc440b4553 --- /dev/null +++ b/res/img/element-icons/room/default_cal.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/img/element-icons/room/default_clock.svg b/res/img/element-icons/room/default_clock.svg new file mode 100644 index 0000000000..c7f453aadd --- /dev/null +++ b/res/img/element-icons/room/default_clock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/element-icons/room/default_doc.svg b/res/img/element-icons/room/default_doc.svg new file mode 100644 index 0000000000..aff393ffd5 --- /dev/null +++ b/res/img/element-icons/room/default_doc.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/room/default_video.svg b/res/img/element-icons/room/default_video.svg new file mode 100644 index 0000000000..022f1f43b1 --- /dev/null +++ b/res/img/element-icons/room/default_video.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/element-icons/room/ellipsis.svg b/res/img/element-icons/room/ellipsis.svg new file mode 100644 index 0000000000..db1db6ec8b --- /dev/null +++ b/res/img/element-icons/room/ellipsis.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/element-icons/room/files.svg b/res/img/element-icons/room/files.svg new file mode 100644 index 0000000000..6648ab00a5 --- /dev/null +++ b/res/img/element-icons/room/files.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/room/format-bar/bold.svg b/res/img/element-icons/room/format-bar/bold.svg new file mode 100644 index 0000000000..e21210c525 --- /dev/null +++ b/res/img/element-icons/room/format-bar/bold.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/element-icons/room/format-bar/code.svg b/res/img/element-icons/room/format-bar/code.svg new file mode 100644 index 0000000000..38f94457e8 --- /dev/null +++ b/res/img/element-icons/room/format-bar/code.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/element-icons/room/format-bar/italic.svg b/res/img/element-icons/room/format-bar/italic.svg new file mode 100644 index 0000000000..270c4f5f15 --- /dev/null +++ b/res/img/element-icons/room/format-bar/italic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/element-icons/room/format-bar/quote.svg b/res/img/element-icons/room/format-bar/quote.svg new file mode 100644 index 0000000000..3d3d444487 --- /dev/null +++ b/res/img/element-icons/room/format-bar/quote.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/img/element-icons/room/format-bar/strikethrough.svg b/res/img/element-icons/room/format-bar/strikethrough.svg new file mode 100644 index 0000000000..775e0cf8ec --- /dev/null +++ b/res/img/element-icons/room/format-bar/strikethrough.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/room/invite.svg b/res/img/element-icons/room/invite.svg new file mode 100644 index 0000000000..d2ecb837b2 --- /dev/null +++ b/res/img/element-icons/room/invite.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/room/members.svg b/res/img/element-icons/room/members.svg new file mode 100644 index 0000000000..03aba81ad4 --- /dev/null +++ b/res/img/element-icons/room/members.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/img/element-icons/room/message-bar/edit.svg b/res/img/element-icons/room/message-bar/edit.svg new file mode 100644 index 0000000000..d4a7e8eaaf --- /dev/null +++ b/res/img/element-icons/room/message-bar/edit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/room/message-bar/emoji.svg b/res/img/element-icons/room/message-bar/emoji.svg new file mode 100644 index 0000000000..07fee5b834 --- /dev/null +++ b/res/img/element-icons/room/message-bar/emoji.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/room/message-bar/reply.svg b/res/img/element-icons/room/message-bar/reply.svg new file mode 100644 index 0000000000..9900d4d19d --- /dev/null +++ b/res/img/element-icons/room/message-bar/reply.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/room/pin-upright.svg b/res/img/element-icons/room/pin-upright.svg new file mode 100644 index 0000000000..9297f62a02 --- /dev/null +++ b/res/img/element-icons/room/pin-upright.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/img/element-icons/room/pin.svg b/res/img/element-icons/room/pin.svg new file mode 100644 index 0000000000..2448fc61c5 --- /dev/null +++ b/res/img/element-icons/room/pin.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/img/element-icons/room/room-summary.svg b/res/img/element-icons/room/room-summary.svg new file mode 100644 index 0000000000..b6ac258b18 --- /dev/null +++ b/res/img/element-icons/room/room-summary.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/room/search-inset.svg b/res/img/element-icons/room/search-inset.svg new file mode 100644 index 0000000000..699cdd1d00 --- /dev/null +++ b/res/img/element-icons/room/search-inset.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/room/settings/advanced.svg b/res/img/element-icons/room/settings/advanced.svg new file mode 100644 index 0000000000..734ae543ea --- /dev/null +++ b/res/img/element-icons/room/settings/advanced.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/room/settings/roles.svg b/res/img/element-icons/room/settings/roles.svg new file mode 100644 index 0000000000..24bccf78f4 --- /dev/null +++ b/res/img/element-icons/room/settings/roles.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/room/share.svg b/res/img/element-icons/room/share.svg new file mode 100644 index 0000000000..dac35ae5a7 --- /dev/null +++ b/res/img/element-icons/room/share.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/archived.svg b/res/img/element-icons/roomlist/archived.svg new file mode 100644 index 0000000000..4d30195082 --- /dev/null +++ b/res/img/element-icons/roomlist/archived.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/browse.svg b/res/img/element-icons/roomlist/browse.svg new file mode 100644 index 0000000000..04714e2881 --- /dev/null +++ b/res/img/element-icons/roomlist/browse.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/roomlist/checkmark.svg b/res/img/element-icons/roomlist/checkmark.svg new file mode 100644 index 0000000000..3be39fc9b2 --- /dev/null +++ b/res/img/element-icons/roomlist/checkmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/dark-light-mode.svg b/res/img/element-icons/roomlist/dark-light-mode.svg new file mode 100644 index 0000000000..a6a6464b5c --- /dev/null +++ b/res/img/element-icons/roomlist/dark-light-mode.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/decorated-avatar-mask.svg b/res/img/element-icons/roomlist/decorated-avatar-mask.svg new file mode 100644 index 0000000000..fb09c16bba --- /dev/null +++ b/res/img/element-icons/roomlist/decorated-avatar-mask.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/dialpad.svg b/res/img/element-icons/roomlist/dialpad.svg new file mode 100644 index 0000000000..b51d4a4dc9 --- /dev/null +++ b/res/img/element-icons/roomlist/dialpad.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/explore.svg b/res/img/element-icons/roomlist/explore.svg new file mode 100644 index 0000000000..3786ce1153 --- /dev/null +++ b/res/img/element-icons/roomlist/explore.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/roomlist/favorite.svg b/res/img/element-icons/roomlist/favorite.svg new file mode 100644 index 0000000000..0c33999ea3 --- /dev/null +++ b/res/img/element-icons/roomlist/favorite.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/feedback.svg b/res/img/element-icons/roomlist/feedback.svg new file mode 100644 index 0000000000..c15edd709a --- /dev/null +++ b/res/img/element-icons/roomlist/feedback.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/img/element-icons/roomlist/hash-circle.svg b/res/img/element-icons/roomlist/hash-circle.svg new file mode 100644 index 0000000000..924b22cf32 --- /dev/null +++ b/res/img/element-icons/roomlist/hash-circle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/img/element-icons/roomlist/home.svg b/res/img/element-icons/roomlist/home.svg new file mode 100644 index 0000000000..9598ccf184 --- /dev/null +++ b/res/img/element-icons/roomlist/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/low-priority.svg b/res/img/element-icons/roomlist/low-priority.svg new file mode 100644 index 0000000000..832501527b --- /dev/null +++ b/res/img/element-icons/roomlist/low-priority.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/notifications-default.svg b/res/img/element-icons/roomlist/notifications-default.svg new file mode 100644 index 0000000000..59743f5d67 --- /dev/null +++ b/res/img/element-icons/roomlist/notifications-default.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/element-icons/roomlist/notifications-dm.svg b/res/img/element-icons/roomlist/notifications-dm.svg new file mode 100644 index 0000000000..e0bd435240 --- /dev/null +++ b/res/img/element-icons/roomlist/notifications-dm.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/notifications-off.svg b/res/img/element-icons/roomlist/notifications-off.svg new file mode 100644 index 0000000000..c848471f63 --- /dev/null +++ b/res/img/element-icons/roomlist/notifications-off.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/img/element-icons/roomlist/plus-circle.svg b/res/img/element-icons/roomlist/plus-circle.svg new file mode 100644 index 0000000000..251ded225c --- /dev/null +++ b/res/img/element-icons/roomlist/plus-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/plus.svg b/res/img/element-icons/roomlist/plus.svg new file mode 100644 index 0000000000..f6d80ac7ef --- /dev/null +++ b/res/img/element-icons/roomlist/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/search-clear.svg b/res/img/element-icons/roomlist/search-clear.svg new file mode 100644 index 0000000000..29fc097600 --- /dev/null +++ b/res/img/element-icons/roomlist/search-clear.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/search.svg b/res/img/element-icons/roomlist/search.svg new file mode 100644 index 0000000000..b706092a5c --- /dev/null +++ b/res/img/element-icons/roomlist/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/roomlist/skeleton-ui.svg b/res/img/element-icons/roomlist/skeleton-ui.svg new file mode 100644 index 0000000000..e95692536c --- /dev/null +++ b/res/img/element-icons/roomlist/skeleton-ui.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/element-icons/security.svg b/res/img/element-icons/security.svg new file mode 100644 index 0000000000..3fe62b7af9 --- /dev/null +++ b/res/img/element-icons/security.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/send-message.svg b/res/img/element-icons/send-message.svg new file mode 100644 index 0000000000..ce35bf8bc8 --- /dev/null +++ b/res/img/element-icons/send-message.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/settings.svg b/res/img/element-icons/settings.svg new file mode 100644 index 0000000000..05d640df27 --- /dev/null +++ b/res/img/element-icons/settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/settings/appearance.svg b/res/img/element-icons/settings/appearance.svg new file mode 100644 index 0000000000..6f91759354 --- /dev/null +++ b/res/img/element-icons/settings/appearance.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/settings/flair.svg b/res/img/element-icons/settings/flair.svg new file mode 100644 index 0000000000..e1ae44f386 --- /dev/null +++ b/res/img/element-icons/settings/flair.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/settings/help.svg b/res/img/element-icons/settings/help.svg new file mode 100644 index 0000000000..2ac4f675ec --- /dev/null +++ b/res/img/element-icons/settings/help.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/settings/lab-flags.svg b/res/img/element-icons/settings/lab-flags.svg new file mode 100644 index 0000000000..b96aa17d26 --- /dev/null +++ b/res/img/element-icons/settings/lab-flags.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/settings/preference.svg b/res/img/element-icons/settings/preference.svg new file mode 100644 index 0000000000..d466662117 --- /dev/null +++ b/res/img/element-icons/settings/preference.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/speaker.svg b/res/img/element-icons/speaker.svg new file mode 100644 index 0000000000..fd811d2cda --- /dev/null +++ b/res/img/element-icons/speaker.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/element-icons/trashcan.svg b/res/img/element-icons/trashcan.svg new file mode 100644 index 0000000000..4106f0bd60 --- /dev/null +++ b/res/img/element-icons/trashcan.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/upload.svg b/res/img/element-icons/upload.svg new file mode 100644 index 0000000000..71ad7ba1cf --- /dev/null +++ b/res/img/element-icons/upload.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/view-community.svg b/res/img/element-icons/view-community.svg new file mode 100644 index 0000000000..ee33aa525e --- /dev/null +++ b/res/img/element-icons/view-community.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/res/img/element-icons/warning-badge.svg b/res/img/element-icons/warning-badge.svg new file mode 100644 index 0000000000..1c8da9aa8e --- /dev/null +++ b/res/img/element-icons/warning-badge.svg @@ -0,0 +1,32 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/res/img/ems-logo.svg b/res/img/ems-logo.svg new file mode 100644 index 0000000000..5ad29173cb --- /dev/null +++ b/res/img/ems-logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/res/img/explore.svg b/res/img/explore.svg deleted file mode 100644 index 3956e912ac..0000000000 --- a/res/img/explore.svg +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - diff --git a/res/img/feather-customised/alert-triangle.svg b/res/img/feather-customised/alert-triangle.svg new file mode 100644 index 0000000000..ceb664790f --- /dev/null +++ b/res/img/feather-customised/alert-triangle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/feather-customised/bug.svg b/res/img/feather-customised/bug.svg new file mode 100644 index 0000000000..babc4fed0e --- /dev/null +++ b/res/img/feather-customised/bug.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/feather-customised/chevron-down-thin.svg b/res/img/feather-customised/chevron-down-thin.svg new file mode 100644 index 0000000000..109c83def6 --- /dev/null +++ b/res/img/feather-customised/chevron-down-thin.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/feather-customised/chevron-down.svg b/res/img/feather-customised/chevron-down.svg index bcb185ede7..a091913b42 100644 --- a/res/img/feather-customised/chevron-down.svg +++ b/res/img/feather-customised/chevron-down.svg @@ -1,3 +1,3 @@ - - + + diff --git a/res/img/feather-customised/clipboard.svg b/res/img/feather-customised/clipboard.svg new file mode 100644 index 0000000000..b25b97176c --- /dev/null +++ b/res/img/feather-customised/clipboard.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/feather-customised/face.svg b/res/img/feather-customised/face.svg deleted file mode 100644 index a8ca856b67..0000000000 --- a/res/img/feather-customised/face.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/res/img/feather-customised/flag.svg b/res/img/feather-customised/flag.svg deleted file mode 100644 index 983c02762b..0000000000 --- a/res/img/feather-customised/flag.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/feather-customised/flair.svg b/res/img/feather-customised/flair.svg deleted file mode 100644 index ce3a5ed6ad..0000000000 --- a/res/img/feather-customised/flair.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/feather-customised/grid.svg b/res/img/feather-customised/grid.svg deleted file mode 100644 index 4f7ab30d97..0000000000 --- a/res/img/feather-customised/grid.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/res/img/feather-customised/lock-solid.svg b/res/img/feather-customised/lock-solid.svg deleted file mode 100644 index 9eb8b6a4c5..0000000000 --- a/res/img/feather-customised/lock-solid.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/img/feather-customised/lock.svg b/res/img/feather-customised/lock.svg deleted file mode 100644 index 1330903b30..0000000000 --- a/res/img/feather-customised/lock.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/feather-customised/widget/maximise.svg b/res/img/feather-customised/maximise.svg similarity index 100% rename from res/img/feather-customised/widget/maximise.svg rename to res/img/feather-customised/maximise.svg diff --git a/res/img/feather-customised/widget/minimise.svg b/res/img/feather-customised/minimise.svg similarity index 100% rename from res/img/feather-customised/widget/minimise.svg rename to res/img/feather-customised/minimise.svg diff --git a/res/img/feather-customised/notifications.svg b/res/img/feather-customised/notifications.svg deleted file mode 100644 index a590031ac3..0000000000 --- a/res/img/feather-customised/notifications.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/res/img/feather-customised/paperclip.svg b/res/img/feather-customised/paperclip.svg deleted file mode 100644 index 74a90e0fa3..0000000000 --- a/res/img/feather-customised/paperclip.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/res/img/feather-customised/phone.svg b/res/img/feather-customised/phone.svg deleted file mode 100644 index 85661c5320..0000000000 --- a/res/img/feather-customised/phone.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/res/img/feather-customised/search.svg b/res/img/feather-customised/search.svg deleted file mode 100644 index 9ce0724ea7..0000000000 --- a/res/img/feather-customised/search.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/res/img/feather-customised/secure-backup.svg b/res/img/feather-customised/secure-backup.svg new file mode 100644 index 0000000000..c06f93c1fe --- /dev/null +++ b/res/img/feather-customised/secure-backup.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/feather-customised/secure-phrase.svg b/res/img/feather-customised/secure-phrase.svg new file mode 100644 index 0000000000..eb13d3f048 --- /dev/null +++ b/res/img/feather-customised/secure-phrase.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/feather-customised/share.svg b/res/img/feather-customised/share.svg deleted file mode 100644 index 7098af58aa..0000000000 --- a/res/img/feather-customised/share.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/res/img/feather-customised/sliders.svg b/res/img/feather-customised/sliders.svg deleted file mode 100644 index 5b5ec8656c..0000000000 --- a/res/img/feather-customised/sliders.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/feather-customised/trash.custom.svg b/res/img/feather-customised/trash.custom.svg new file mode 100644 index 0000000000..dc1985ddb2 --- /dev/null +++ b/res/img/feather-customised/trash.custom.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/img/feather-customised/upload.svg b/res/img/feather-customised/upload.svg deleted file mode 100644 index 30c89d3819..0000000000 --- a/res/img/feather-customised/upload.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/feather-customised/user-add.svg b/res/img/feather-customised/user-add.svg deleted file mode 100644 index 6b5210c1d6..0000000000 --- a/res/img/feather-customised/user-add.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/res/img/feather-customised/users-sm.svg b/res/img/feather-customised/users-sm.svg deleted file mode 100644 index 6098be38c3..0000000000 --- a/res/img/feather-customised/users-sm.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/res/img/feather-customised/users.svg b/res/img/feather-customised/users.svg deleted file mode 100644 index b90aafdd4a..0000000000 --- a/res/img/feather-customised/users.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/res/img/feather-customised/video.svg b/res/img/feather-customised/video.svg deleted file mode 100644 index da77b6c57a..0000000000 --- a/res/img/feather-customised/video.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/res/img/file.png b/res/img/file.png deleted file mode 100644 index 5904ea8284..0000000000 Binary files a/res/img/file.png and /dev/null differ diff --git a/res/img/filegrid.png b/res/img/filegrid.png deleted file mode 100644 index c2c2799f37..0000000000 Binary files a/res/img/filegrid.png and /dev/null differ diff --git a/res/img/filelist.png b/res/img/filelist.png deleted file mode 100644 index 3cf6cb494e..0000000000 Binary files a/res/img/filelist.png and /dev/null differ diff --git a/res/img/files.png b/res/img/files.png deleted file mode 100644 index 83932267f8..0000000000 Binary files a/res/img/files.png and /dev/null differ diff --git a/res/img/files.svg b/res/img/files.svg deleted file mode 100644 index 20aba851ea..0000000000 --- a/res/img/files.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - icons_browse_files - Created with bin/sketchtool. - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/flags/AD.png b/res/img/flags/AD.png deleted file mode 100644 index d5d59645fe..0000000000 Binary files a/res/img/flags/AD.png and /dev/null differ diff --git a/res/img/flags/AE.png b/res/img/flags/AE.png deleted file mode 100644 index 05c7418aa4..0000000000 Binary files a/res/img/flags/AE.png and /dev/null differ diff --git a/res/img/flags/AF.png b/res/img/flags/AF.png deleted file mode 100644 index bc7cef0916..0000000000 Binary files a/res/img/flags/AF.png and /dev/null differ diff --git a/res/img/flags/AG.png b/res/img/flags/AG.png deleted file mode 100644 index d48facad47..0000000000 Binary files a/res/img/flags/AG.png and /dev/null differ diff --git a/res/img/flags/AI.png b/res/img/flags/AI.png deleted file mode 100644 index 8fd27cd39e..0000000000 Binary files a/res/img/flags/AI.png and /dev/null differ diff --git a/res/img/flags/AL.png b/res/img/flags/AL.png deleted file mode 100644 index 883835ffb3..0000000000 Binary files a/res/img/flags/AL.png and /dev/null differ diff --git a/res/img/flags/AM.png b/res/img/flags/AM.png deleted file mode 100644 index b1bb36b987..0000000000 Binary files a/res/img/flags/AM.png and /dev/null differ diff --git a/res/img/flags/AO.png b/res/img/flags/AO.png deleted file mode 100644 index ae68b12c44..0000000000 Binary files a/res/img/flags/AO.png and /dev/null differ diff --git a/res/img/flags/AQ.png b/res/img/flags/AQ.png deleted file mode 100644 index 146e9c0a04..0000000000 Binary files a/res/img/flags/AQ.png and /dev/null differ diff --git a/res/img/flags/AR.png b/res/img/flags/AR.png deleted file mode 100644 index 8142adfc83..0000000000 Binary files a/res/img/flags/AR.png and /dev/null differ diff --git a/res/img/flags/AS.png b/res/img/flags/AS.png deleted file mode 100644 index cc5bf30daf..0000000000 Binary files a/res/img/flags/AS.png and /dev/null differ diff --git a/res/img/flags/AT.png b/res/img/flags/AT.png deleted file mode 100644 index e32414bd6a..0000000000 Binary files a/res/img/flags/AT.png and /dev/null differ diff --git a/res/img/flags/AU.png b/res/img/flags/AU.png deleted file mode 100644 index 8d1e143791..0000000000 Binary files a/res/img/flags/AU.png and /dev/null differ diff --git a/res/img/flags/AW.png b/res/img/flags/AW.png deleted file mode 100644 index 6ec178847e..0000000000 Binary files a/res/img/flags/AW.png and /dev/null differ diff --git a/res/img/flags/AX.png b/res/img/flags/AX.png deleted file mode 100644 index ba269c0453..0000000000 Binary files a/res/img/flags/AX.png and /dev/null differ diff --git a/res/img/flags/AZ.png b/res/img/flags/AZ.png deleted file mode 100644 index 2bf3c746e7..0000000000 Binary files a/res/img/flags/AZ.png and /dev/null differ diff --git a/res/img/flags/BA.png b/res/img/flags/BA.png deleted file mode 100644 index 3e3ec3fc76..0000000000 Binary files a/res/img/flags/BA.png and /dev/null differ diff --git a/res/img/flags/BB.png b/res/img/flags/BB.png deleted file mode 100644 index 694050ca46..0000000000 Binary files a/res/img/flags/BB.png and /dev/null differ diff --git a/res/img/flags/BD.png b/res/img/flags/BD.png deleted file mode 100644 index 6de2cde85b..0000000000 Binary files a/res/img/flags/BD.png and /dev/null differ diff --git a/res/img/flags/BE.png b/res/img/flags/BE.png deleted file mode 100644 index 742ba9231f..0000000000 Binary files a/res/img/flags/BE.png and /dev/null differ diff --git a/res/img/flags/BF.png b/res/img/flags/BF.png deleted file mode 100644 index 17f9f67d26..0000000000 Binary files a/res/img/flags/BF.png and /dev/null differ diff --git a/res/img/flags/BG.png b/res/img/flags/BG.png deleted file mode 100644 index b01d3ff57b..0000000000 Binary files a/res/img/flags/BG.png and /dev/null differ diff --git a/res/img/flags/BH.png b/res/img/flags/BH.png deleted file mode 100644 index d0f82e8285..0000000000 Binary files a/res/img/flags/BH.png and /dev/null differ diff --git a/res/img/flags/BI.png b/res/img/flags/BI.png deleted file mode 100644 index 21865ac720..0000000000 Binary files a/res/img/flags/BI.png and /dev/null differ diff --git a/res/img/flags/BJ.png b/res/img/flags/BJ.png deleted file mode 100644 index a7c6091434..0000000000 Binary files a/res/img/flags/BJ.png and /dev/null differ diff --git a/res/img/flags/BL.png b/res/img/flags/BL.png deleted file mode 100644 index 6d50a0f544..0000000000 Binary files a/res/img/flags/BL.png and /dev/null differ diff --git a/res/img/flags/BM.png b/res/img/flags/BM.png deleted file mode 100644 index 310a25ea23..0000000000 Binary files a/res/img/flags/BM.png and /dev/null differ diff --git a/res/img/flags/BN.png b/res/img/flags/BN.png deleted file mode 100644 index bc4da8d9a6..0000000000 Binary files a/res/img/flags/BN.png and /dev/null differ diff --git a/res/img/flags/BO.png b/res/img/flags/BO.png deleted file mode 100644 index 144b8d32db..0000000000 Binary files a/res/img/flags/BO.png and /dev/null differ diff --git a/res/img/flags/BQ.png b/res/img/flags/BQ.png deleted file mode 100644 index 0897943760..0000000000 Binary files a/res/img/flags/BQ.png and /dev/null differ diff --git a/res/img/flags/BR.png b/res/img/flags/BR.png deleted file mode 100644 index 0278492592..0000000000 Binary files a/res/img/flags/BR.png and /dev/null differ diff --git a/res/img/flags/BS.png b/res/img/flags/BS.png deleted file mode 100644 index 2b05a8fc7c..0000000000 Binary files a/res/img/flags/BS.png and /dev/null differ diff --git a/res/img/flags/BT.png b/res/img/flags/BT.png deleted file mode 100644 index 1f031df071..0000000000 Binary files a/res/img/flags/BT.png and /dev/null differ diff --git a/res/img/flags/BV.png b/res/img/flags/BV.png deleted file mode 100644 index aafb0f1776..0000000000 Binary files a/res/img/flags/BV.png and /dev/null differ diff --git a/res/img/flags/BW.png b/res/img/flags/BW.png deleted file mode 100644 index 3084016718..0000000000 Binary files a/res/img/flags/BW.png and /dev/null differ diff --git a/res/img/flags/BY.png b/res/img/flags/BY.png deleted file mode 100644 index ce9de9c9c7..0000000000 Binary files a/res/img/flags/BY.png and /dev/null differ diff --git a/res/img/flags/BZ.png b/res/img/flags/BZ.png deleted file mode 100644 index 33620c3f31..0000000000 Binary files a/res/img/flags/BZ.png and /dev/null differ diff --git a/res/img/flags/CA.png b/res/img/flags/CA.png deleted file mode 100644 index 4bbf8b1169..0000000000 Binary files a/res/img/flags/CA.png and /dev/null differ diff --git a/res/img/flags/CC.png b/res/img/flags/CC.png deleted file mode 100644 index fd40fc8a78..0000000000 Binary files a/res/img/flags/CC.png and /dev/null differ diff --git a/res/img/flags/CD.png b/res/img/flags/CD.png deleted file mode 100644 index 230aacd454..0000000000 Binary files a/res/img/flags/CD.png and /dev/null differ diff --git a/res/img/flags/CF.png b/res/img/flags/CF.png deleted file mode 100644 index c58ed4f7b2..0000000000 Binary files a/res/img/flags/CF.png and /dev/null differ diff --git a/res/img/flags/CG.png b/res/img/flags/CG.png deleted file mode 100644 index 6c2441e3e0..0000000000 Binary files a/res/img/flags/CG.png and /dev/null differ diff --git a/res/img/flags/CH.png b/res/img/flags/CH.png deleted file mode 100644 index 9fd87167df..0000000000 Binary files a/res/img/flags/CH.png and /dev/null differ diff --git a/res/img/flags/CI.png b/res/img/flags/CI.png deleted file mode 100644 index 9741b9b11f..0000000000 Binary files a/res/img/flags/CI.png and /dev/null differ diff --git a/res/img/flags/CK.png b/res/img/flags/CK.png deleted file mode 100644 index 6cca35967c..0000000000 Binary files a/res/img/flags/CK.png and /dev/null differ diff --git a/res/img/flags/CL.png b/res/img/flags/CL.png deleted file mode 100644 index 13b993d15d..0000000000 Binary files a/res/img/flags/CL.png and /dev/null differ diff --git a/res/img/flags/CM.png b/res/img/flags/CM.png deleted file mode 100644 index bca5730fb5..0000000000 Binary files a/res/img/flags/CM.png and /dev/null differ diff --git a/res/img/flags/CN.png b/res/img/flags/CN.png deleted file mode 100644 index e086855c73..0000000000 Binary files a/res/img/flags/CN.png and /dev/null differ diff --git a/res/img/flags/CO.png b/res/img/flags/CO.png deleted file mode 100644 index 65c0aba447..0000000000 Binary files a/res/img/flags/CO.png and /dev/null differ diff --git a/res/img/flags/CR.png b/res/img/flags/CR.png deleted file mode 100644 index b351c67a53..0000000000 Binary files a/res/img/flags/CR.png and /dev/null differ diff --git a/res/img/flags/CU.png b/res/img/flags/CU.png deleted file mode 100644 index e7a25c60b3..0000000000 Binary files a/res/img/flags/CU.png and /dev/null differ diff --git a/res/img/flags/CV.png b/res/img/flags/CV.png deleted file mode 100644 index f249bbaa46..0000000000 Binary files a/res/img/flags/CV.png and /dev/null differ diff --git a/res/img/flags/CW.png b/res/img/flags/CW.png deleted file mode 100644 index e02cacd3dd..0000000000 Binary files a/res/img/flags/CW.png and /dev/null differ diff --git a/res/img/flags/CX.png b/res/img/flags/CX.png deleted file mode 100644 index 3ea21422f0..0000000000 Binary files a/res/img/flags/CX.png and /dev/null differ diff --git a/res/img/flags/CY.png b/res/img/flags/CY.png deleted file mode 100644 index 3182f48bd2..0000000000 Binary files a/res/img/flags/CY.png and /dev/null differ diff --git a/res/img/flags/CZ.png b/res/img/flags/CZ.png deleted file mode 100644 index 5462334638..0000000000 Binary files a/res/img/flags/CZ.png and /dev/null differ diff --git a/res/img/flags/DE.png b/res/img/flags/DE.png deleted file mode 100644 index 93e269166b..0000000000 Binary files a/res/img/flags/DE.png and /dev/null differ diff --git a/res/img/flags/DJ.png b/res/img/flags/DJ.png deleted file mode 100644 index 243bb7390d..0000000000 Binary files a/res/img/flags/DJ.png and /dev/null differ diff --git a/res/img/flags/DK.png b/res/img/flags/DK.png deleted file mode 100644 index fc74cc396c..0000000000 Binary files a/res/img/flags/DK.png and /dev/null differ diff --git a/res/img/flags/DM.png b/res/img/flags/DM.png deleted file mode 100644 index c3a0e9d102..0000000000 Binary files a/res/img/flags/DM.png and /dev/null differ diff --git a/res/img/flags/DO.png b/res/img/flags/DO.png deleted file mode 100644 index 5c4a004fef..0000000000 Binary files a/res/img/flags/DO.png and /dev/null differ diff --git a/res/img/flags/DZ.png b/res/img/flags/DZ.png deleted file mode 100644 index 1589d0cc40..0000000000 Binary files a/res/img/flags/DZ.png and /dev/null differ diff --git a/res/img/flags/EC.png b/res/img/flags/EC.png deleted file mode 100644 index 4c53dead1c..0000000000 Binary files a/res/img/flags/EC.png and /dev/null differ diff --git a/res/img/flags/EE.png b/res/img/flags/EE.png deleted file mode 100644 index 3668de7919..0000000000 Binary files a/res/img/flags/EE.png and /dev/null differ diff --git a/res/img/flags/EG.png b/res/img/flags/EG.png deleted file mode 100644 index 66ec709df7..0000000000 Binary files a/res/img/flags/EG.png and /dev/null differ diff --git a/res/img/flags/EH.png b/res/img/flags/EH.png deleted file mode 100644 index 148be93c08..0000000000 Binary files a/res/img/flags/EH.png and /dev/null differ diff --git a/res/img/flags/ER.png b/res/img/flags/ER.png deleted file mode 100644 index 7cb8441514..0000000000 Binary files a/res/img/flags/ER.png and /dev/null differ diff --git a/res/img/flags/ES.png b/res/img/flags/ES.png deleted file mode 100644 index aae73b6fcb..0000000000 Binary files a/res/img/flags/ES.png and /dev/null differ diff --git a/res/img/flags/ET.png b/res/img/flags/ET.png deleted file mode 100644 index 7b420f02f4..0000000000 Binary files a/res/img/flags/ET.png and /dev/null differ diff --git a/res/img/flags/FI.png b/res/img/flags/FI.png deleted file mode 100644 index 42f64bf360..0000000000 Binary files a/res/img/flags/FI.png and /dev/null differ diff --git a/res/img/flags/FJ.png b/res/img/flags/FJ.png deleted file mode 100644 index cecc683c9c..0000000000 Binary files a/res/img/flags/FJ.png and /dev/null differ diff --git a/res/img/flags/FK.png b/res/img/flags/FK.png deleted file mode 100644 index 6074fea09c..0000000000 Binary files a/res/img/flags/FK.png and /dev/null differ diff --git a/res/img/flags/FM.png b/res/img/flags/FM.png deleted file mode 100644 index 45fdb66426..0000000000 Binary files a/res/img/flags/FM.png and /dev/null differ diff --git a/res/img/flags/FO.png b/res/img/flags/FO.png deleted file mode 100644 index d8fd75c638..0000000000 Binary files a/res/img/flags/FO.png and /dev/null differ diff --git a/res/img/flags/FR.png b/res/img/flags/FR.png deleted file mode 100644 index 6d50a0f544..0000000000 Binary files a/res/img/flags/FR.png and /dev/null differ diff --git a/res/img/flags/GA.png b/res/img/flags/GA.png deleted file mode 100644 index 3808a61f1d..0000000000 Binary files a/res/img/flags/GA.png and /dev/null differ diff --git a/res/img/flags/GB.png b/res/img/flags/GB.png deleted file mode 100644 index 589be70063..0000000000 Binary files a/res/img/flags/GB.png and /dev/null differ diff --git a/res/img/flags/GD.png b/res/img/flags/GD.png deleted file mode 100644 index babe1e4cc6..0000000000 Binary files a/res/img/flags/GD.png and /dev/null differ diff --git a/res/img/flags/GE.png b/res/img/flags/GE.png deleted file mode 100644 index d34cddeca9..0000000000 Binary files a/res/img/flags/GE.png and /dev/null differ diff --git a/res/img/flags/GF.png b/res/img/flags/GF.png deleted file mode 100644 index 98828a5906..0000000000 Binary files a/res/img/flags/GF.png and /dev/null differ diff --git a/res/img/flags/GG.png b/res/img/flags/GG.png deleted file mode 100644 index aec8969b28..0000000000 Binary files a/res/img/flags/GG.png and /dev/null differ diff --git a/res/img/flags/GH.png b/res/img/flags/GH.png deleted file mode 100644 index 70b1a623de..0000000000 Binary files a/res/img/flags/GH.png and /dev/null differ diff --git a/res/img/flags/GI.png b/res/img/flags/GI.png deleted file mode 100644 index 9aa58327e3..0000000000 Binary files a/res/img/flags/GI.png and /dev/null differ diff --git a/res/img/flags/GL.png b/res/img/flags/GL.png deleted file mode 100644 index cf1645c2b5..0000000000 Binary files a/res/img/flags/GL.png and /dev/null differ diff --git a/res/img/flags/GM.png b/res/img/flags/GM.png deleted file mode 100644 index ec374fb3c3..0000000000 Binary files a/res/img/flags/GM.png and /dev/null differ diff --git a/res/img/flags/GN.png b/res/img/flags/GN.png deleted file mode 100644 index 46874b4d98..0000000000 Binary files a/res/img/flags/GN.png and /dev/null differ diff --git a/res/img/flags/GP.png b/res/img/flags/GP.png deleted file mode 100644 index 81b7abdf0e..0000000000 Binary files a/res/img/flags/GP.png and /dev/null differ diff --git a/res/img/flags/GQ.png b/res/img/flags/GQ.png deleted file mode 100644 index 7fd1015e8b..0000000000 Binary files a/res/img/flags/GQ.png and /dev/null differ diff --git a/res/img/flags/GR.png b/res/img/flags/GR.png deleted file mode 100644 index 101de51eab..0000000000 Binary files a/res/img/flags/GR.png and /dev/null differ diff --git a/res/img/flags/GS.png b/res/img/flags/GS.png deleted file mode 100644 index 772c2cbe6d..0000000000 Binary files a/res/img/flags/GS.png and /dev/null differ diff --git a/res/img/flags/GT.png b/res/img/flags/GT.png deleted file mode 100644 index d5bd8c1e46..0000000000 Binary files a/res/img/flags/GT.png and /dev/null differ diff --git a/res/img/flags/GU.png b/res/img/flags/GU.png deleted file mode 100644 index 8923085d5a..0000000000 Binary files a/res/img/flags/GU.png and /dev/null differ diff --git a/res/img/flags/GW.png b/res/img/flags/GW.png deleted file mode 100644 index 20c268ce06..0000000000 Binary files a/res/img/flags/GW.png and /dev/null differ diff --git a/res/img/flags/GY.png b/res/img/flags/GY.png deleted file mode 100644 index 86f56635ef..0000000000 Binary files a/res/img/flags/GY.png and /dev/null differ diff --git a/res/img/flags/HK.png b/res/img/flags/HK.png deleted file mode 100644 index 907dc59624..0000000000 Binary files a/res/img/flags/HK.png and /dev/null differ diff --git a/res/img/flags/HM.png b/res/img/flags/HM.png deleted file mode 100644 index 8d1e143791..0000000000 Binary files a/res/img/flags/HM.png and /dev/null differ diff --git a/res/img/flags/HN.png b/res/img/flags/HN.png deleted file mode 100644 index 4cf8c3112c..0000000000 Binary files a/res/img/flags/HN.png and /dev/null differ diff --git a/res/img/flags/HR.png b/res/img/flags/HR.png deleted file mode 100644 index 413ceb1586..0000000000 Binary files a/res/img/flags/HR.png and /dev/null differ diff --git a/res/img/flags/HT.png b/res/img/flags/HT.png deleted file mode 100644 index 097abeb434..0000000000 Binary files a/res/img/flags/HT.png and /dev/null differ diff --git a/res/img/flags/HU.png b/res/img/flags/HU.png deleted file mode 100644 index 23499bf63c..0000000000 Binary files a/res/img/flags/HU.png and /dev/null differ diff --git a/res/img/flags/ID.png b/res/img/flags/ID.png deleted file mode 100644 index 80200657c6..0000000000 Binary files a/res/img/flags/ID.png and /dev/null differ diff --git a/res/img/flags/IE.png b/res/img/flags/IE.png deleted file mode 100644 index 63f2220118..0000000000 Binary files a/res/img/flags/IE.png and /dev/null differ diff --git a/res/img/flags/IL.png b/res/img/flags/IL.png deleted file mode 100644 index 0268826321..0000000000 Binary files a/res/img/flags/IL.png and /dev/null differ diff --git a/res/img/flags/IM.png b/res/img/flags/IM.png deleted file mode 100644 index c777acc490..0000000000 Binary files a/res/img/flags/IM.png and /dev/null differ diff --git a/res/img/flags/IN.png b/res/img/flags/IN.png deleted file mode 100644 index 85fa9bfe72..0000000000 Binary files a/res/img/flags/IN.png and /dev/null differ diff --git a/res/img/flags/IO.png b/res/img/flags/IO.png deleted file mode 100644 index 1675d8e7db..0000000000 Binary files a/res/img/flags/IO.png and /dev/null differ diff --git a/res/img/flags/IQ.png b/res/img/flags/IQ.png deleted file mode 100644 index f2c21f7260..0000000000 Binary files a/res/img/flags/IQ.png and /dev/null differ diff --git a/res/img/flags/IR.png b/res/img/flags/IR.png deleted file mode 100644 index 0b8e67506c..0000000000 Binary files a/res/img/flags/IR.png and /dev/null differ diff --git a/res/img/flags/IS.png b/res/img/flags/IS.png deleted file mode 100644 index 5ee3e63c5c..0000000000 Binary files a/res/img/flags/IS.png and /dev/null differ diff --git a/res/img/flags/IT.png b/res/img/flags/IT.png deleted file mode 100644 index 53b967be99..0000000000 Binary files a/res/img/flags/IT.png and /dev/null differ diff --git a/res/img/flags/JE.png b/res/img/flags/JE.png deleted file mode 100644 index a1437aba78..0000000000 Binary files a/res/img/flags/JE.png and /dev/null differ diff --git a/res/img/flags/JM.png b/res/img/flags/JM.png deleted file mode 100644 index 0d462fa3ae..0000000000 Binary files a/res/img/flags/JM.png and /dev/null differ diff --git a/res/img/flags/JO.png b/res/img/flags/JO.png deleted file mode 100644 index 8934db7eca..0000000000 Binary files a/res/img/flags/JO.png and /dev/null differ diff --git a/res/img/flags/JP.png b/res/img/flags/JP.png deleted file mode 100644 index 6f92d52365..0000000000 Binary files a/res/img/flags/JP.png and /dev/null differ diff --git a/res/img/flags/KE.png b/res/img/flags/KE.png deleted file mode 100644 index 866b3f15dc..0000000000 Binary files a/res/img/flags/KE.png and /dev/null differ diff --git a/res/img/flags/KG.png b/res/img/flags/KG.png deleted file mode 100644 index 56b433c756..0000000000 Binary files a/res/img/flags/KG.png and /dev/null differ diff --git a/res/img/flags/KH.png b/res/img/flags/KH.png deleted file mode 100644 index e1ddd5f84c..0000000000 Binary files a/res/img/flags/KH.png and /dev/null differ diff --git a/res/img/flags/KI.png b/res/img/flags/KI.png deleted file mode 100644 index 8b7c54bc0f..0000000000 Binary files a/res/img/flags/KI.png and /dev/null differ diff --git a/res/img/flags/KM.png b/res/img/flags/KM.png deleted file mode 100644 index 227a3b3396..0000000000 Binary files a/res/img/flags/KM.png and /dev/null differ diff --git a/res/img/flags/KN.png b/res/img/flags/KN.png deleted file mode 100644 index bc6189bed1..0000000000 Binary files a/res/img/flags/KN.png and /dev/null differ diff --git a/res/img/flags/KP.png b/res/img/flags/KP.png deleted file mode 100644 index c92248b910..0000000000 Binary files a/res/img/flags/KP.png and /dev/null differ diff --git a/res/img/flags/KR.png b/res/img/flags/KR.png deleted file mode 100644 index ab1cb94943..0000000000 Binary files a/res/img/flags/KR.png and /dev/null differ diff --git a/res/img/flags/KW.png b/res/img/flags/KW.png deleted file mode 100644 index 0b41c7a532..0000000000 Binary files a/res/img/flags/KW.png and /dev/null differ diff --git a/res/img/flags/KY.png b/res/img/flags/KY.png deleted file mode 100644 index 7af5290d31..0000000000 Binary files a/res/img/flags/KY.png and /dev/null differ diff --git a/res/img/flags/KZ.png b/res/img/flags/KZ.png deleted file mode 100644 index e10a1255a0..0000000000 Binary files a/res/img/flags/KZ.png and /dev/null differ diff --git a/res/img/flags/LA.png b/res/img/flags/LA.png deleted file mode 100644 index 6ad67d4255..0000000000 Binary files a/res/img/flags/LA.png and /dev/null differ diff --git a/res/img/flags/LB.png b/res/img/flags/LB.png deleted file mode 100644 index 865df57a42..0000000000 Binary files a/res/img/flags/LB.png and /dev/null differ diff --git a/res/img/flags/LC.png b/res/img/flags/LC.png deleted file mode 100644 index e83a2d08bc..0000000000 Binary files a/res/img/flags/LC.png and /dev/null differ diff --git a/res/img/flags/LI.png b/res/img/flags/LI.png deleted file mode 100644 index 57034d367c..0000000000 Binary files a/res/img/flags/LI.png and /dev/null differ diff --git a/res/img/flags/LK.png b/res/img/flags/LK.png deleted file mode 100644 index 6e7ad58254..0000000000 Binary files a/res/img/flags/LK.png and /dev/null differ diff --git a/res/img/flags/LR.png b/res/img/flags/LR.png deleted file mode 100644 index 46c3b84a92..0000000000 Binary files a/res/img/flags/LR.png and /dev/null differ diff --git a/res/img/flags/LS.png b/res/img/flags/LS.png deleted file mode 100644 index 79b505d490..0000000000 Binary files a/res/img/flags/LS.png and /dev/null differ diff --git a/res/img/flags/LT.png b/res/img/flags/LT.png deleted file mode 100644 index 7740cdc0a0..0000000000 Binary files a/res/img/flags/LT.png and /dev/null differ diff --git a/res/img/flags/LU.png b/res/img/flags/LU.png deleted file mode 100644 index 8f383e674e..0000000000 Binary files a/res/img/flags/LU.png and /dev/null differ diff --git a/res/img/flags/LV.png b/res/img/flags/LV.png deleted file mode 100644 index a0f36d89c4..0000000000 Binary files a/res/img/flags/LV.png and /dev/null differ diff --git a/res/img/flags/LY.png b/res/img/flags/LY.png deleted file mode 100644 index 2884c4c0a9..0000000000 Binary files a/res/img/flags/LY.png and /dev/null differ diff --git a/res/img/flags/MA.png b/res/img/flags/MA.png deleted file mode 100644 index 1f76cfc9bd..0000000000 Binary files a/res/img/flags/MA.png and /dev/null differ diff --git a/res/img/flags/MC.png b/res/img/flags/MC.png deleted file mode 100644 index 06fc2ad166..0000000000 Binary files a/res/img/flags/MC.png and /dev/null differ diff --git a/res/img/flags/MD.png b/res/img/flags/MD.png deleted file mode 100644 index 8e54c2b815..0000000000 Binary files a/res/img/flags/MD.png and /dev/null differ diff --git a/res/img/flags/ME.png b/res/img/flags/ME.png deleted file mode 100644 index 97424d4ec2..0000000000 Binary files a/res/img/flags/ME.png and /dev/null differ diff --git a/res/img/flags/MF.png b/res/img/flags/MF.png deleted file mode 100644 index 6d50a0f544..0000000000 Binary files a/res/img/flags/MF.png and /dev/null differ diff --git a/res/img/flags/MG.png b/res/img/flags/MG.png deleted file mode 100644 index 28bfccc9e8..0000000000 Binary files a/res/img/flags/MG.png and /dev/null differ diff --git a/res/img/flags/MH.png b/res/img/flags/MH.png deleted file mode 100644 index e482a65924..0000000000 Binary files a/res/img/flags/MH.png and /dev/null differ diff --git a/res/img/flags/MK.png b/res/img/flags/MK.png deleted file mode 100644 index 84e2e65e76..0000000000 Binary files a/res/img/flags/MK.png and /dev/null differ diff --git a/res/img/flags/ML.png b/res/img/flags/ML.png deleted file mode 100644 index 38fec34796..0000000000 Binary files a/res/img/flags/ML.png and /dev/null differ diff --git a/res/img/flags/MM.png b/res/img/flags/MM.png deleted file mode 100644 index 70a03c6b14..0000000000 Binary files a/res/img/flags/MM.png and /dev/null differ diff --git a/res/img/flags/MN.png b/res/img/flags/MN.png deleted file mode 100644 index 1e1bbe6089..0000000000 Binary files a/res/img/flags/MN.png and /dev/null differ diff --git a/res/img/flags/MO.png b/res/img/flags/MO.png deleted file mode 100644 index 3833d683e7..0000000000 Binary files a/res/img/flags/MO.png and /dev/null differ diff --git a/res/img/flags/MP.png b/res/img/flags/MP.png deleted file mode 100644 index 63119096b0..0000000000 Binary files a/res/img/flags/MP.png and /dev/null differ diff --git a/res/img/flags/MQ.png b/res/img/flags/MQ.png deleted file mode 100644 index 9cab441aec..0000000000 Binary files a/res/img/flags/MQ.png and /dev/null differ diff --git a/res/img/flags/MR.png b/res/img/flags/MR.png deleted file mode 100644 index c144de17f7..0000000000 Binary files a/res/img/flags/MR.png and /dev/null differ diff --git a/res/img/flags/MS.png b/res/img/flags/MS.png deleted file mode 100644 index 1221707042..0000000000 Binary files a/res/img/flags/MS.png and /dev/null differ diff --git a/res/img/flags/MT.png b/res/img/flags/MT.png deleted file mode 100644 index 7963aa618a..0000000000 Binary files a/res/img/flags/MT.png and /dev/null differ diff --git a/res/img/flags/MU.png b/res/img/flags/MU.png deleted file mode 100644 index d5d4d4008d..0000000000 Binary files a/res/img/flags/MU.png and /dev/null differ diff --git a/res/img/flags/MV.png b/res/img/flags/MV.png deleted file mode 100644 index 0f2ecb4389..0000000000 Binary files a/res/img/flags/MV.png and /dev/null differ diff --git a/res/img/flags/MW.png b/res/img/flags/MW.png deleted file mode 100644 index d0a5d24f55..0000000000 Binary files a/res/img/flags/MW.png and /dev/null differ diff --git a/res/img/flags/MX.png b/res/img/flags/MX.png deleted file mode 100644 index 096cb1111f..0000000000 Binary files a/res/img/flags/MX.png and /dev/null differ diff --git a/res/img/flags/MY.png b/res/img/flags/MY.png deleted file mode 100644 index 17f18ac519..0000000000 Binary files a/res/img/flags/MY.png and /dev/null differ diff --git a/res/img/flags/MZ.png b/res/img/flags/MZ.png deleted file mode 100644 index 66be6563c6..0000000000 Binary files a/res/img/flags/MZ.png and /dev/null differ diff --git a/res/img/flags/NA.png b/res/img/flags/NA.png deleted file mode 100644 index 7ecfd317c7..0000000000 Binary files a/res/img/flags/NA.png and /dev/null differ diff --git a/res/img/flags/NC.png b/res/img/flags/NC.png deleted file mode 100644 index 11126ade77..0000000000 Binary files a/res/img/flags/NC.png and /dev/null differ diff --git a/res/img/flags/NE.png b/res/img/flags/NE.png deleted file mode 100644 index d584fa8429..0000000000 Binary files a/res/img/flags/NE.png and /dev/null differ diff --git a/res/img/flags/NF.png b/res/img/flags/NF.png deleted file mode 100644 index c054042591..0000000000 Binary files a/res/img/flags/NF.png and /dev/null differ diff --git a/res/img/flags/NG.png b/res/img/flags/NG.png deleted file mode 100644 index 73aee15b3f..0000000000 Binary files a/res/img/flags/NG.png and /dev/null differ diff --git a/res/img/flags/NI.png b/res/img/flags/NI.png deleted file mode 100644 index fd044933e4..0000000000 Binary files a/res/img/flags/NI.png and /dev/null differ diff --git a/res/img/flags/NL.png b/res/img/flags/NL.png deleted file mode 100644 index 0897943760..0000000000 Binary files a/res/img/flags/NL.png and /dev/null differ diff --git a/res/img/flags/NO.png b/res/img/flags/NO.png deleted file mode 100644 index aafb0f1776..0000000000 Binary files a/res/img/flags/NO.png and /dev/null differ diff --git a/res/img/flags/NP.png b/res/img/flags/NP.png deleted file mode 100644 index 744458e17e..0000000000 Binary files a/res/img/flags/NP.png and /dev/null differ diff --git a/res/img/flags/NR.png b/res/img/flags/NR.png deleted file mode 100644 index 58c2afb228..0000000000 Binary files a/res/img/flags/NR.png and /dev/null differ diff --git a/res/img/flags/NU.png b/res/img/flags/NU.png deleted file mode 100644 index 007c99eca5..0000000000 Binary files a/res/img/flags/NU.png and /dev/null differ diff --git a/res/img/flags/NZ.png b/res/img/flags/NZ.png deleted file mode 100644 index 839368dd7b..0000000000 Binary files a/res/img/flags/NZ.png and /dev/null differ diff --git a/res/img/flags/OM.png b/res/img/flags/OM.png deleted file mode 100644 index 63a893367f..0000000000 Binary files a/res/img/flags/OM.png and /dev/null differ diff --git a/res/img/flags/PA.png b/res/img/flags/PA.png deleted file mode 100644 index 3515d95d37..0000000000 Binary files a/res/img/flags/PA.png and /dev/null differ diff --git a/res/img/flags/PE.png b/res/img/flags/PE.png deleted file mode 100644 index 58f70b8d18..0000000000 Binary files a/res/img/flags/PE.png and /dev/null differ diff --git a/res/img/flags/PF.png b/res/img/flags/PF.png deleted file mode 100644 index 2f33f2574f..0000000000 Binary files a/res/img/flags/PF.png and /dev/null differ diff --git a/res/img/flags/PG.png b/res/img/flags/PG.png deleted file mode 100644 index c796f587c6..0000000000 Binary files a/res/img/flags/PG.png and /dev/null differ diff --git a/res/img/flags/PH.png b/res/img/flags/PH.png deleted file mode 100644 index 0d98de0386..0000000000 Binary files a/res/img/flags/PH.png and /dev/null differ diff --git a/res/img/flags/PK.png b/res/img/flags/PK.png deleted file mode 100644 index 87f4e2f492..0000000000 Binary files a/res/img/flags/PK.png and /dev/null differ diff --git a/res/img/flags/PL.png b/res/img/flags/PL.png deleted file mode 100644 index 273869dfc6..0000000000 Binary files a/res/img/flags/PL.png and /dev/null differ diff --git a/res/img/flags/PM.png b/res/img/flags/PM.png deleted file mode 100644 index b74c396d92..0000000000 Binary files a/res/img/flags/PM.png and /dev/null differ diff --git a/res/img/flags/PN.png b/res/img/flags/PN.png deleted file mode 100644 index e34c62d598..0000000000 Binary files a/res/img/flags/PN.png and /dev/null differ diff --git a/res/img/flags/PR.png b/res/img/flags/PR.png deleted file mode 100644 index 8efdb91252..0000000000 Binary files a/res/img/flags/PR.png and /dev/null differ diff --git a/res/img/flags/PS.png b/res/img/flags/PS.png deleted file mode 100644 index 7a0cceec00..0000000000 Binary files a/res/img/flags/PS.png and /dev/null differ diff --git a/res/img/flags/PT.png b/res/img/flags/PT.png deleted file mode 100644 index 49e290827c..0000000000 Binary files a/res/img/flags/PT.png and /dev/null differ diff --git a/res/img/flags/PW.png b/res/img/flags/PW.png deleted file mode 100644 index 6cb2e1e70d..0000000000 Binary files a/res/img/flags/PW.png and /dev/null differ diff --git a/res/img/flags/PY.png b/res/img/flags/PY.png deleted file mode 100644 index a61c42c423..0000000000 Binary files a/res/img/flags/PY.png and /dev/null differ diff --git a/res/img/flags/QA.png b/res/img/flags/QA.png deleted file mode 100644 index bb091cc88c..0000000000 Binary files a/res/img/flags/QA.png and /dev/null differ diff --git a/res/img/flags/RE.png b/res/img/flags/RE.png deleted file mode 100644 index 6d50a0f544..0000000000 Binary files a/res/img/flags/RE.png and /dev/null differ diff --git a/res/img/flags/RO.png b/res/img/flags/RO.png deleted file mode 100644 index 4495d29eb0..0000000000 Binary files a/res/img/flags/RO.png and /dev/null differ diff --git a/res/img/flags/RS.png b/res/img/flags/RS.png deleted file mode 100644 index ebb0f28a7b..0000000000 Binary files a/res/img/flags/RS.png and /dev/null differ diff --git a/res/img/flags/RU.png b/res/img/flags/RU.png deleted file mode 100644 index 64532ffa58..0000000000 Binary files a/res/img/flags/RU.png and /dev/null differ diff --git a/res/img/flags/RW.png b/res/img/flags/RW.png deleted file mode 100644 index 64b3cfff04..0000000000 Binary files a/res/img/flags/RW.png and /dev/null differ diff --git a/res/img/flags/SA.png b/res/img/flags/SA.png deleted file mode 100644 index 250de6f6f5..0000000000 Binary files a/res/img/flags/SA.png and /dev/null differ diff --git a/res/img/flags/SB.png b/res/img/flags/SB.png deleted file mode 100644 index 5833c130eb..0000000000 Binary files a/res/img/flags/SB.png and /dev/null differ diff --git a/res/img/flags/SC.png b/res/img/flags/SC.png deleted file mode 100644 index ce5248f434..0000000000 Binary files a/res/img/flags/SC.png and /dev/null differ diff --git a/res/img/flags/SD.png b/res/img/flags/SD.png deleted file mode 100644 index d8711a83d6..0000000000 Binary files a/res/img/flags/SD.png and /dev/null differ diff --git a/res/img/flags/SE.png b/res/img/flags/SE.png deleted file mode 100644 index 81880931f3..0000000000 Binary files a/res/img/flags/SE.png and /dev/null differ diff --git a/res/img/flags/SG.png b/res/img/flags/SG.png deleted file mode 100644 index 6f00e57923..0000000000 Binary files a/res/img/flags/SG.png and /dev/null differ diff --git a/res/img/flags/SH.png b/res/img/flags/SH.png deleted file mode 100644 index 055dde68bc..0000000000 Binary files a/res/img/flags/SH.png and /dev/null differ diff --git a/res/img/flags/SI.png b/res/img/flags/SI.png deleted file mode 100644 index 9635983406..0000000000 Binary files a/res/img/flags/SI.png and /dev/null differ diff --git a/res/img/flags/SJ.png b/res/img/flags/SJ.png deleted file mode 100644 index aafb0f1776..0000000000 Binary files a/res/img/flags/SJ.png and /dev/null differ diff --git a/res/img/flags/SK.png b/res/img/flags/SK.png deleted file mode 100644 index 84c7021f0a..0000000000 Binary files a/res/img/flags/SK.png and /dev/null differ diff --git a/res/img/flags/SL.png b/res/img/flags/SL.png deleted file mode 100644 index c5ed199141..0000000000 Binary files a/res/img/flags/SL.png and /dev/null differ diff --git a/res/img/flags/SM.png b/res/img/flags/SM.png deleted file mode 100644 index 1af1ca284f..0000000000 Binary files a/res/img/flags/SM.png and /dev/null differ diff --git a/res/img/flags/SN.png b/res/img/flags/SN.png deleted file mode 100644 index d0b1843561..0000000000 Binary files a/res/img/flags/SN.png and /dev/null differ diff --git a/res/img/flags/SO.png b/res/img/flags/SO.png deleted file mode 100644 index 64e2970b9d..0000000000 Binary files a/res/img/flags/SO.png and /dev/null differ diff --git a/res/img/flags/SR.png b/res/img/flags/SR.png deleted file mode 100644 index b072dda835..0000000000 Binary files a/res/img/flags/SR.png and /dev/null differ diff --git a/res/img/flags/SS.png b/res/img/flags/SS.png deleted file mode 100644 index 83933d4521..0000000000 Binary files a/res/img/flags/SS.png and /dev/null differ diff --git a/res/img/flags/ST.png b/res/img/flags/ST.png deleted file mode 100644 index c102721a86..0000000000 Binary files a/res/img/flags/ST.png and /dev/null differ diff --git a/res/img/flags/SV.png b/res/img/flags/SV.png deleted file mode 100644 index 80de92e556..0000000000 Binary files a/res/img/flags/SV.png and /dev/null differ diff --git a/res/img/flags/SX.png b/res/img/flags/SX.png deleted file mode 100644 index dd52215c5d..0000000000 Binary files a/res/img/flags/SX.png and /dev/null differ diff --git a/res/img/flags/SY.png b/res/img/flags/SY.png deleted file mode 100644 index 78f45b7c0b..0000000000 Binary files a/res/img/flags/SY.png and /dev/null differ diff --git a/res/img/flags/SZ.png b/res/img/flags/SZ.png deleted file mode 100644 index 2182f4ff93..0000000000 Binary files a/res/img/flags/SZ.png and /dev/null differ diff --git a/res/img/flags/TC.png b/res/img/flags/TC.png deleted file mode 100644 index 3e3e19d4b3..0000000000 Binary files a/res/img/flags/TC.png and /dev/null differ diff --git a/res/img/flags/TD.png b/res/img/flags/TD.png deleted file mode 100644 index 753bec22b0..0000000000 Binary files a/res/img/flags/TD.png and /dev/null differ diff --git a/res/img/flags/TF.png b/res/img/flags/TF.png deleted file mode 100644 index 6d50a0f544..0000000000 Binary files a/res/img/flags/TF.png and /dev/null differ diff --git a/res/img/flags/TG.png b/res/img/flags/TG.png deleted file mode 100644 index 8501ada655..0000000000 Binary files a/res/img/flags/TG.png and /dev/null differ diff --git a/res/img/flags/TH.png b/res/img/flags/TH.png deleted file mode 100644 index 0c884c329e..0000000000 Binary files a/res/img/flags/TH.png and /dev/null differ diff --git a/res/img/flags/TJ.png b/res/img/flags/TJ.png deleted file mode 100644 index 3c9026fa0f..0000000000 Binary files a/res/img/flags/TJ.png and /dev/null differ diff --git a/res/img/flags/TK.png b/res/img/flags/TK.png deleted file mode 100644 index fd605749ea..0000000000 Binary files a/res/img/flags/TK.png and /dev/null differ diff --git a/res/img/flags/TL.png b/res/img/flags/TL.png deleted file mode 100644 index b4c834b1d6..0000000000 Binary files a/res/img/flags/TL.png and /dev/null differ diff --git a/res/img/flags/TM.png b/res/img/flags/TM.png deleted file mode 100644 index d18cb939a9..0000000000 Binary files a/res/img/flags/TM.png and /dev/null differ diff --git a/res/img/flags/TN.png b/res/img/flags/TN.png deleted file mode 100644 index 21c4b98be7..0000000000 Binary files a/res/img/flags/TN.png and /dev/null differ diff --git a/res/img/flags/TO.png b/res/img/flags/TO.png deleted file mode 100644 index c828206e35..0000000000 Binary files a/res/img/flags/TO.png and /dev/null differ diff --git a/res/img/flags/TR.png b/res/img/flags/TR.png deleted file mode 100644 index f2a5bd22c8..0000000000 Binary files a/res/img/flags/TR.png and /dev/null differ diff --git a/res/img/flags/TT.png b/res/img/flags/TT.png deleted file mode 100644 index 66d698334b..0000000000 Binary files a/res/img/flags/TT.png and /dev/null differ diff --git a/res/img/flags/TV.png b/res/img/flags/TV.png deleted file mode 100644 index 7a127f51ae..0000000000 Binary files a/res/img/flags/TV.png and /dev/null differ diff --git a/res/img/flags/TW.png b/res/img/flags/TW.png deleted file mode 100644 index 2353ba1b0a..0000000000 Binary files a/res/img/flags/TW.png and /dev/null differ diff --git a/res/img/flags/TZ.png b/res/img/flags/TZ.png deleted file mode 100644 index 7949f65d8a..0000000000 Binary files a/res/img/flags/TZ.png and /dev/null differ diff --git a/res/img/flags/UA.png b/res/img/flags/UA.png deleted file mode 100644 index 687e305294..0000000000 Binary files a/res/img/flags/UA.png and /dev/null differ diff --git a/res/img/flags/UG.png b/res/img/flags/UG.png deleted file mode 100644 index 0a21ad15c3..0000000000 Binary files a/res/img/flags/UG.png and /dev/null differ diff --git a/res/img/flags/US.png b/res/img/flags/US.png deleted file mode 100644 index c3a245b767..0000000000 Binary files a/res/img/flags/US.png and /dev/null differ diff --git a/res/img/flags/UY.png b/res/img/flags/UY.png deleted file mode 100644 index 21a347c6fc..0000000000 Binary files a/res/img/flags/UY.png and /dev/null differ diff --git a/res/img/flags/UZ.png b/res/img/flags/UZ.png deleted file mode 100644 index 643b6ae0cf..0000000000 Binary files a/res/img/flags/UZ.png and /dev/null differ diff --git a/res/img/flags/VA.png b/res/img/flags/VA.png deleted file mode 100644 index 63a13c0e81..0000000000 Binary files a/res/img/flags/VA.png and /dev/null differ diff --git a/res/img/flags/VC.png b/res/img/flags/VC.png deleted file mode 100644 index da991a9344..0000000000 Binary files a/res/img/flags/VC.png and /dev/null differ diff --git a/res/img/flags/VE.png b/res/img/flags/VE.png deleted file mode 100644 index e75e17c9f0..0000000000 Binary files a/res/img/flags/VE.png and /dev/null differ diff --git a/res/img/flags/VG.png b/res/img/flags/VG.png deleted file mode 100644 index 46f93cad1e..0000000000 Binary files a/res/img/flags/VG.png and /dev/null differ diff --git a/res/img/flags/VI.png b/res/img/flags/VI.png deleted file mode 100644 index 8c849a733e..0000000000 Binary files a/res/img/flags/VI.png and /dev/null differ diff --git a/res/img/flags/VN.png b/res/img/flags/VN.png deleted file mode 100644 index 6ea2122f9d..0000000000 Binary files a/res/img/flags/VN.png and /dev/null differ diff --git a/res/img/flags/VU.png b/res/img/flags/VU.png deleted file mode 100644 index bad3ba4d46..0000000000 Binary files a/res/img/flags/VU.png and /dev/null differ diff --git a/res/img/flags/WF.png b/res/img/flags/WF.png deleted file mode 100644 index d94359dcc4..0000000000 Binary files a/res/img/flags/WF.png and /dev/null differ diff --git a/res/img/flags/WS.png b/res/img/flags/WS.png deleted file mode 100644 index f8b80e5ba9..0000000000 Binary files a/res/img/flags/WS.png and /dev/null differ diff --git a/res/img/flags/YE.png b/res/img/flags/YE.png deleted file mode 100644 index 8b9bbd8942..0000000000 Binary files a/res/img/flags/YE.png and /dev/null differ diff --git a/res/img/flags/YT.png b/res/img/flags/YT.png deleted file mode 100644 index 328879361e..0000000000 Binary files a/res/img/flags/YT.png and /dev/null differ diff --git a/res/img/flags/ZA.png b/res/img/flags/ZA.png deleted file mode 100644 index 7f0a52d3b2..0000000000 Binary files a/res/img/flags/ZA.png and /dev/null differ diff --git a/res/img/flags/ZM.png b/res/img/flags/ZM.png deleted file mode 100644 index 87adc3afaa..0000000000 Binary files a/res/img/flags/ZM.png and /dev/null differ diff --git a/res/img/flags/ZW.png b/res/img/flags/ZW.png deleted file mode 100644 index 742c9f7e71..0000000000 Binary files a/res/img/flags/ZW.png and /dev/null differ diff --git a/res/img/fullscreen.svg b/res/img/fullscreen.svg deleted file mode 100644 index e333abb6fb..0000000000 --- a/res/img/fullscreen.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - Zoom - Created with Sketch. - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/globe.svg b/res/img/globe.svg new file mode 100644 index 0000000000..635fa91cce --- /dev/null +++ b/res/img/globe.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/hangup.svg b/res/img/hangup.svg deleted file mode 100644 index be038d2b30..0000000000 --- a/res/img/hangup.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - Fill 72 + Path 98 - Created with Sketch. - - - - - - - - - - \ No newline at end of file diff --git a/res/img/hide.png b/res/img/hide.png deleted file mode 100644 index c5aaf0dd0d..0000000000 Binary files a/res/img/hide.png and /dev/null differ diff --git a/res/img/icon-jump-to-bottom.svg b/res/img/icon-jump-to-bottom.svg deleted file mode 100644 index c4210b4ebe..0000000000 --- a/res/img/icon-jump-to-bottom.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - diff --git a/res/img/icon-jump-to-first-unread.svg b/res/img/icon-jump-to-first-unread.svg deleted file mode 100644 index 652ccec20d..0000000000 --- a/res/img/icon-jump-to-first-unread.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/res/img/icon-text-cancel.svg b/res/img/icon-text-cancel.svg deleted file mode 100644 index ce28d128aa..0000000000 --- a/res/img/icon-text-cancel.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - 28D80248-63BA-4A5F-9216-4CFE72784BAC - Created with sketchtool. - - - - - - - - - - \ No newline at end of file diff --git a/res/img/icon_context.svg b/res/img/icon_context.svg deleted file mode 100644 index 600c5bbd1d..0000000000 --- a/res/img/icon_context.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/img/icon_copy_message.svg b/res/img/icon_copy_message.svg deleted file mode 100644 index 8d8887bb22..0000000000 --- a/res/img/icon_copy_message.svg +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - image/svg+xml - - ED5D3E59-2561-4AC1-9B43-82FBC51767FC - - - - - - ED5D3E59-2561-4AC1-9B43-82FBC51767FC - Created with sketchtool. - - - - - - - - - diff --git a/res/img/icon_person.svg b/res/img/icon_person.svg deleted file mode 100644 index 4be70df0db..0000000000 --- a/res/img/icon_person.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - 815EF7DE-169A-4322-AE2A-B65CBE91DCED - Created with sketchtool. - - - - - - - - - - - - - - - - - - diff --git a/res/img/icons-pin.svg b/res/img/icons-pin.svg deleted file mode 100644 index a6fbf13baa..0000000000 --- a/res/img/icons-pin.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/res/img/icons-room-nobg.svg b/res/img/icons-room-nobg.svg deleted file mode 100644 index 8ca7ab272b..0000000000 --- a/res/img/icons-room-nobg.svg +++ /dev/null @@ -1,28 +0,0 @@ - -image/svg+xml - - - - - - - \ No newline at end of file diff --git a/res/img/icons-share.svg b/res/img/icons-share.svg deleted file mode 100644 index aac19080f4..0000000000 --- a/res/img/icons-share.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/image-view/close.svg b/res/img/image-view/close.svg new file mode 100644 index 0000000000..d603b7f5cc --- /dev/null +++ b/res/img/image-view/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/image-view/download.svg b/res/img/image-view/download.svg new file mode 100644 index 0000000000..c51deed876 --- /dev/null +++ b/res/img/image-view/download.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/image-view/more.svg b/res/img/image-view/more.svg new file mode 100644 index 0000000000..4f5fa6f9b9 --- /dev/null +++ b/res/img/image-view/more.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/image-view/rotate-ccw.svg b/res/img/image-view/rotate-ccw.svg new file mode 100644 index 0000000000..85ea3198de --- /dev/null +++ b/res/img/image-view/rotate-ccw.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/image-view/rotate-cw.svg b/res/img/image-view/rotate-cw.svg new file mode 100644 index 0000000000..e337f3420e --- /dev/null +++ b/res/img/image-view/rotate-cw.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/image-view/zoom-in.svg b/res/img/image-view/zoom-in.svg new file mode 100644 index 0000000000..c0816d489e --- /dev/null +++ b/res/img/image-view/zoom-in.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/image-view/zoom-out.svg b/res/img/image-view/zoom-out.svg new file mode 100644 index 0000000000..0539e8c81a --- /dev/null +++ b/res/img/image-view/zoom-out.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/info.png b/res/img/info.png deleted file mode 100644 index 699fd64e01..0000000000 Binary files a/res/img/info.png and /dev/null differ diff --git a/res/img/leave.svg b/res/img/leave.svg deleted file mode 100644 index 1acbe59313..0000000000 --- a/res/img/leave.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/res/img/list-close.png b/res/img/list-close.png deleted file mode 100644 index 82b322f9d4..0000000000 Binary files a/res/img/list-close.png and /dev/null differ diff --git a/res/img/list-open.png b/res/img/list-open.png deleted file mode 100644 index f8c8063197..0000000000 Binary files a/res/img/list-open.png and /dev/null differ diff --git a/res/img/matrix-m.svg b/res/img/matrix-m.svg deleted file mode 100644 index ccb1df0fc5..0000000000 --- a/res/img/matrix-m.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - diff --git a/res/img/menu.png b/res/img/menu.png deleted file mode 100755 index b45f88950f..0000000000 Binary files a/res/img/menu.png and /dev/null differ diff --git a/res/img/modular-bw-logo.svg b/res/img/modular-bw-logo.svg deleted file mode 100644 index 924a587805..0000000000 --- a/res/img/modular-bw-logo.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/res/img/network-matrix.svg b/res/img/network-matrix.svg deleted file mode 100644 index bb8278ae39..0000000000 --- a/res/img/network-matrix.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - diff --git a/res/img/newmessages.png b/res/img/newmessages.png deleted file mode 100644 index a22156ab21..0000000000 Binary files a/res/img/newmessages.png and /dev/null differ diff --git a/res/img/placeholder.png b/res/img/placeholder.png deleted file mode 100644 index 7da32f259c..0000000000 Binary files a/res/img/placeholder.png and /dev/null differ diff --git a/res/img/react.svg b/res/img/react.svg deleted file mode 100644 index dd23c41c2c..0000000000 --- a/res/img/react.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/res/img/reply.svg b/res/img/reply.svg deleted file mode 100644 index 540e228883..0000000000 --- a/res/img/reply.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/res/img/room-continuation.svg b/res/img/room-continuation.svg deleted file mode 100644 index dc7e15462a..0000000000 --- a/res/img/room-continuation.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/rotate-ccw.svg b/res/img/rotate-ccw.svg deleted file mode 100644 index 3924eca040..0000000000 --- a/res/img/rotate-ccw.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/res/img/rotate-cw.svg b/res/img/rotate-cw.svg deleted file mode 100644 index 91021c96d8..0000000000 --- a/res/img/rotate-cw.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/res/img/search.png b/res/img/search.png deleted file mode 100644 index 2f98d29048..0000000000 Binary files a/res/img/search.png and /dev/null differ diff --git a/res/img/selected.png b/res/img/selected.png deleted file mode 100644 index 8931cba75f..0000000000 Binary files a/res/img/selected.png and /dev/null differ diff --git a/res/img/settings-big.png b/res/img/settings-big.png deleted file mode 100644 index cb2e0a62d0..0000000000 Binary files a/res/img/settings-big.png and /dev/null differ diff --git a/res/img/settings.png b/res/img/settings.png deleted file mode 100644 index 264b3c9bc3..0000000000 Binary files a/res/img/settings.png and /dev/null differ diff --git a/res/img/sound-indicator.svg b/res/img/sound-indicator.svg deleted file mode 100644 index 9b8de53d81..0000000000 --- a/res/img/sound-indicator.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - sound_indicator - Created with Sketch. - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/spinner.gif b/res/img/spinner.gif deleted file mode 100644 index ab4871214b..0000000000 Binary files a/res/img/spinner.gif and /dev/null differ diff --git a/res/img/spinner.svg b/res/img/spinner.svg new file mode 100644 index 0000000000..c3680f19d2 --- /dev/null +++ b/res/img/spinner.svg @@ -0,0 +1,96 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/res/img/trans.png b/res/img/trans.png deleted file mode 100644 index 8ba2310a06..0000000000 Binary files a/res/img/trans.png and /dev/null differ diff --git a/res/img/typing.png b/res/img/typing.png deleted file mode 100644 index 066a0ce8fd..0000000000 Binary files a/res/img/typing.png and /dev/null differ diff --git a/res/img/upload-big.png b/res/img/upload-big.png deleted file mode 100644 index c11c0c452d..0000000000 Binary files a/res/img/upload-big.png and /dev/null differ diff --git a/res/img/upload-big.svg b/res/img/upload-big.svg index 6099c2e976..9a6a265fdb 100644 --- a/res/img/upload-big.svg +++ b/res/img/upload-big.svg @@ -1,19 +1,3 @@ - - - - icons_upload_drop - Created with bin/sketchtool. - - - - - - - - - - - - - + + diff --git a/res/img/upload.png b/res/img/upload.png deleted file mode 100644 index 7457bcd0f1..0000000000 Binary files a/res/img/upload.png and /dev/null differ diff --git a/res/img/video-mute.svg b/res/img/video-mute.svg deleted file mode 100644 index 6de60ba39b..0000000000 --- a/res/img/video-mute.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - icons_video copy - Created with Sketch. - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/video-unmute.svg b/res/img/video-unmute.svg deleted file mode 100644 index a6c6c3b681..0000000000 --- a/res/img/video-unmute.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - icons_video copy - Created with Sketch. - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/img/video.png b/res/img/video.png deleted file mode 100644 index 2a788f6fa4..0000000000 Binary files a/res/img/video.png and /dev/null differ diff --git a/res/img/voice-mute.svg b/res/img/voice-mute.svg deleted file mode 100644 index 336641078e..0000000000 --- a/res/img/voice-mute.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - Audio - Created with Sketch. - - - - - - - - - \ No newline at end of file diff --git a/res/img/voice-unmute.svg b/res/img/voice-unmute.svg deleted file mode 100644 index 0d7e6f429f..0000000000 --- a/res/img/voice-unmute.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - Audio - Created with Sketch. - - - - - - - - - - \ No newline at end of file diff --git a/res/img/voice.png b/res/img/voice.png deleted file mode 100644 index 5ba765b0f4..0000000000 Binary files a/res/img/voice.png and /dev/null differ diff --git a/res/img/voip-chevron.svg b/res/img/voip-chevron.svg deleted file mode 100644 index 5f7cbe7153..0000000000 --- a/res/img/voip-chevron.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - Triangle 1 - Created with Sketch. - - - - - - - \ No newline at end of file diff --git a/res/img/voip-mute.png b/res/img/voip-mute.png deleted file mode 100644 index a16d1001e5..0000000000 Binary files a/res/img/voip-mute.png and /dev/null differ diff --git a/res/img/voip.png b/res/img/voip.png deleted file mode 100644 index e8f05bcc37..0000000000 Binary files a/res/img/voip.png and /dev/null differ diff --git a/res/img/voip/dialpad.svg b/res/img/voip/dialpad.svg new file mode 100644 index 0000000000..79c9ba1612 --- /dev/null +++ b/res/img/voip/dialpad.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/hangup.svg b/res/img/voip/hangup.svg new file mode 100644 index 0000000000..dfb20bd519 --- /dev/null +++ b/res/img/voip/hangup.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/mic-off.svg b/res/img/voip/mic-off.svg new file mode 100644 index 0000000000..6409f1fd07 --- /dev/null +++ b/res/img/voip/mic-off.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/mic-on-mask.svg b/res/img/voip/mic-on-mask.svg new file mode 100644 index 0000000000..418316b164 --- /dev/null +++ b/res/img/voip/mic-on-mask.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/voip/mic-on.svg b/res/img/voip/mic-on.svg new file mode 100644 index 0000000000..3493b3c581 --- /dev/null +++ b/res/img/voip/mic-on.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/more.svg b/res/img/voip/more.svg new file mode 100644 index 0000000000..7990f6bcff --- /dev/null +++ b/res/img/voip/more.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/paused.svg b/res/img/voip/paused.svg new file mode 100644 index 0000000000..a967bf8ddf --- /dev/null +++ b/res/img/voip/paused.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/voip/silence.svg b/res/img/voip/silence.svg new file mode 100644 index 0000000000..332932dfff --- /dev/null +++ b/res/img/voip/silence.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/voip/tab-dialpad.svg b/res/img/voip/tab-dialpad.svg new file mode 100644 index 0000000000..b7add0addb --- /dev/null +++ b/res/img/voip/tab-dialpad.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/voip/tab-userdirectory.svg b/res/img/voip/tab-userdirectory.svg new file mode 100644 index 0000000000..792ded7be4 --- /dev/null +++ b/res/img/voip/tab-userdirectory.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/img/voip/un-silence.svg b/res/img/voip/un-silence.svg new file mode 100644 index 0000000000..c00b366f84 --- /dev/null +++ b/res/img/voip/un-silence.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/voip/vid-off.svg b/res/img/voip/vid-off.svg new file mode 100644 index 0000000000..199d97ab97 --- /dev/null +++ b/res/img/voip/vid-off.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/vid-on.svg b/res/img/voip/vid-on.svg new file mode 100644 index 0000000000..d8146d01d3 --- /dev/null +++ b/res/img/voip/vid-on.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/img/warning.png b/res/img/warning.png deleted file mode 100644 index c5553530a8..0000000000 Binary files a/res/img/warning.png and /dev/null differ diff --git a/res/img/warning2.png b/res/img/warning2.png deleted file mode 100644 index db0fd4a897..0000000000 Binary files a/res/img/warning2.png and /dev/null differ diff --git a/res/img/zoom.png b/res/img/zoom.png deleted file mode 100644 index f05ea959b4..0000000000 Binary files a/res/img/zoom.png and /dev/null differ diff --git a/res/themes/dark-custom/css/dark-custom.scss b/res/themes/dark-custom/css/dark-custom.scss index 03ceef45c6..a5fed6a320 100644 --- a/res/themes/dark-custom/css/dark-custom.scss +++ b/res/themes/dark-custom/css/dark-custom.scss @@ -1,7 +1,7 @@ @import "../../../../res/css/_font-sizes.scss"; -@import "../../light/css/_paths.scss"; -@import "../../light/css/_fonts.scss"; -@import "../../light/css/_light.scss"; -@import "../../dark/css/_dark.scss"; +@import "../../legacy-light/css/_paths.scss"; +@import "../../legacy-light/css/_fonts.scss"; +@import "../../legacy-light/css/_legacy-light.scss"; +@import "../../legacy-dark/css/_legacy-dark.scss"; @import "../../light-custom/css/_custom.scss"; @import "../../../../res/css/_components.scss"; diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 5d6ba033c8..74b33fbd02 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -1,14 +1,15 @@ // unified palette // try to use these colors when possible -$bg-color: #181b21; -$base-color: #15171b; -$base-text-color: #edf3ff; -$header-panel-bg-color: #22262e; +$bg-color: #15191E; +$base-color: $bg-color; +$base-text-color: #ffffff; +$header-panel-bg-color: #20252B; $header-panel-border-color: #000000; -$header-panel-text-primary-color: #a1b2d1; +$header-panel-text-primary-color: #B9BEC6; $header-panel-text-secondary-color: #c8c8cd; -$text-primary-color: #edf3ff; -$text-secondary-color: #a1b2d1; +$text-primary-color: #ffffff; +$text-secondary-color: #B9BEC6; +$quaternary-fg-color: #6F7882; $search-bg-color: #181b21; $search-placeholder-color: #61708b; $room-highlight-color: #343a46; @@ -18,6 +19,10 @@ $primary-fg-color: $text-primary-color; $primary-bg-color: $bg-color; $muted-fg-color: $header-panel-text-primary-color; +// additional text colors +$secondary-fg-color: #A9B2BC; +$tertiary-fg-color: #8E99A4; + // used for dialog box text $light-fg-color: $header-panel-text-secondary-color; @@ -35,19 +40,20 @@ $info-plinth-fg-color: #888; $preview-bar-bg-color: $header-panel-bg-color; -$tagpanel-bg-color: $base-color; +$groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82); +$inverted-bg-color: $base-color; // used by AddressSelector $selected-color: $room-highlight-color; // selected for hoverover & selected event tiles -$event-selected-color: $header-panel-bg-color; +$event-selected-color: #21262c; // used for the hairline dividers in RoomView -$primary-hairline-color: $header-panel-border-color; +$primary-hairline-color: transparent; // used for the border of input text fields -$input-border-color: #e7e7e7; +$input-border-color: rgba(231, 231, 231, 0.2); $input-darker-bg-color: $search-bg-color; $input-darker-fg-color: $search-placeholder-color; $input-lighter-bg-color: #f2f5f8; @@ -58,6 +64,8 @@ $input-invalid-border-color: $warning-color; $field-focused-label-bg-color: $bg-color; +$resend-button-divider-color: #b9bec64a; // muted-text with a 4A opacity. + // scrollbars $scrollbar-thumb-color: rgba(255, 255, 255, 0.2); $scrollbar-track-color: transparent; @@ -80,43 +88,59 @@ $dialog-close-fg-color: #9fa9ba; $dialog-background-bg-color: $header-panel-bg-color; $lightbox-background-bg-color: #000; +$lightbox-background-bg-opacity: 0.85; $settings-grey-fg-color: #a2a2a2; -$settings-profile-placeholder-bg-color: #e7e7e7; -$settings-profile-overlay-bg-color: #000; -$settings-profile-overlay-placeholder-bg-color: transparent; -$settings-profile-overlay-fg-color: #fff; +$settings-profile-placeholder-bg-color: #21262c; $settings-profile-overlay-placeholder-fg-color: #454545; +$settings-profile-button-bg-color: #e7e7e7; +$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color; $settings-subsection-fg-color: $text-secondary-color; $topleftmenu-color: $text-primary-color; $roomheader-color: $text-primary-color; -$roomheader-addroom-bg-color: #3c4556; // $search-placeholder-color at 0.5 opacity +$roomheader-bg-color: $bg-color; +$roomheader-addroom-bg-color: rgba(92, 100, 112, 0.3); $roomheader-addroom-fg-color: $text-primary-color; -$tagpanel-button-color: $header-panel-text-primary-color; -$roomheader-button-color: $header-panel-text-primary-color; +$groupFilterPanel-button-color: $header-panel-text-primary-color; $groupheader-button-color: $header-panel-text-primary-color; $rightpanel-button-color: $header-panel-text-primary-color; -$composer-button-color: $header-panel-text-primary-color; +$icon-button-color: #8E99A4; $roomtopic-color: $text-secondary-color; $eventtile-meta-color: $roomtopic-color; $header-divider-color: $header-panel-text-primary-color; +$composer-e2e-icon-color: $header-panel-text-primary-color; -$roomtile-name-color: $header-panel-text-primary-color; -$roomtile-selected-color: $text-primary-color; -$roomtile-notified-color: $text-primary-color; -$roomtile-selected-bg-color: $room-highlight-color; -$roomtile-focused-bg-color: $room-highlight-color; +// this probably shouldn't have it's own colour +$voipcall-plinth-color: #394049; -$roomtile-transparent-focused-color: rgba(0, 0, 0, 0.1); +// ******************** -$panel-divider-color: $header-panel-border-color; +$theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #394049; + +$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons +$roomlist-filter-active-bg-color: $bg-color; +$roomlist-bg-color: rgba(33, 38, 44, 0.90); +$roomlist-header-color: $tertiary-fg-color; +$roomsublist-divider-color: $primary-fg-color; +$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%); + +$groupFilterPanel-divider-color: $roomlist-header-color; + +$roomtile-preview-color: $secondary-fg-color; +$roomtile-default-badge-bg-color: #61708b; +$roomtile-selected-bg-color: rgba(141, 151, 165, 0.2); + +// ******************** + +$notice-secondary-color: $roomlist-header-color; + +$panel-divider-color: transparent; $widget-menu-bar-bg-color: $header-panel-bg-color; - -// event tile lifecycle -$event-sending-color: $text-secondary-color; +$widget-body-bg-color: rgba(141, 151, 165, 0.2); // event redaction $event-redacted-fg-color: #606060; @@ -147,6 +171,12 @@ $button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color $button-link-fg-color: $accent-color; $button-link-bg-color: transparent; +// Toggle switch +$togglesw-off-color: $room-highlight-color; + +$progressbar-fg-color: $accent-color; +$progressbar-bg-color: #21262c; + $visual-bell-bg-color: #800; $room-warning-bg-color: $header-panel-bg-color; @@ -167,7 +197,7 @@ $reaction-row-button-selected-border-color: $accent-color; $kbd-border-color: #000000; -$tooltip-timeline-bg-color: $tagpanel-bg-color; +$tooltip-timeline-bg-color: $groupFilterPanel-bg-color; $tooltip-timeline-fg-color: #ffffff; $interactive-tooltip-bg-color: $base-color; @@ -177,13 +207,33 @@ $breadcrumb-placeholder-bg-color: #272c35; $user-tile-hover-bg-color: $header-panel-bg-color; +$message-body-panel-fg-color: $secondary-fg-color; +$message-body-panel-bg-color: #394049; // "Dark Tile" +$message-body-panel-icon-fg-color: #21262C; // "Separator" +$message-body-panel-icon-bg-color: $tertiary-fg-color; + +$voice-record-stop-border-color: $quaternary-fg-color; +$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; +$voice-record-icon-color: $quaternary-fg-color; +$voice-playback-button-bg-color: $message-body-panel-icon-bg-color; +$voice-playback-button-fg-color: $message-body-panel-icon-fg-color; + +// Appearance tab colors +$appearance-tab-border-color: $room-highlight-color; + +// blur amounts for left left panel (only for element theme, used in _mods.scss) +$roomlist-background-blur-amount: 60px; +$groupFilterPanel-background-blur-amount: 30px; + +$composer-shadow-color: rgba(0, 0, 0, 0.28); + // ***** Mixins! ***** @define-mixin mx_DialogButton { /* align images in buttons (eg spinners) */ vertical-align: middle; border: 0px; - border-radius: 4px; + border-radius: 8px; font-family: $font-family; font-size: $font-14px; color: $button-fg-color; @@ -204,7 +254,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color; @define-mixin mx_DialogButton_secondary { // flip colours for the secondary ones font-weight: 600; - border: 1px solid $accent-color ! important; + border: 1px solid $accent-color !important; color: $accent-color; background-color: $button-secondary-bg-color; } @@ -222,18 +272,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color; } // markdown overrides: -.mx_EventTile_content .markdown-body pre:hover { - border-color: #808080 !important; // inverted due to rules below -} .mx_EventTile_content .markdown-body { - pre, code { - filter: invert(1); - } - - pre code { - filter: none; - } - table { tr { background-color: #000000; @@ -245,12 +284,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color; } } -// diff highlight colors -// intentionally swapped to avoid inversion -.hljs-addition { - background: #fdd; -} - -.hljs-deletion { - background: #dfd; +// highlight.js overrides +.hljs-tag { + color: inherit; // Without this they'd be weirdly blue which doesn't match the theme } diff --git a/res/themes/dark/css/dark.scss b/res/themes/dark/css/dark.scss index d81db4595f..600cfd528a 100644 --- a/res/themes/dark/css/dark.scss +++ b/res/themes/dark/css/dark.scss @@ -2,5 +2,11 @@ @import "../../light/css/_paths.scss"; @import "../../light/css/_fonts.scss"; @import "../../light/css/_light.scss"; +// important this goes before _mods, +// as $groupFilterPanel-background-blur-amount and +// $roomlist-background-blur-amount +// are overridden in _dark.scss @import "_dark.scss"; +@import "../../light/css/_mods.scss"; @import "../../../../res/css/_components.scss"; +@import url("highlight.js/styles/atom-one-dark.css"); diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss new file mode 100644 index 0000000000..555ef4f66c --- /dev/null +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -0,0 +1,285 @@ +// unified palette +// try to use these colors when possible +$bg-color: #181b21; +$base-color: #15171b; +$base-text-color: #edf3ff; +$header-panel-bg-color: #22262e; +$header-panel-border-color: #000000; +$header-panel-text-primary-color: #a1b2d1; +$header-panel-text-secondary-color: #c8c8cd; +$text-primary-color: #edf3ff; +$text-secondary-color: #a1b2d1; +$search-bg-color: #181b21; +$search-placeholder-color: #61708b; +$room-highlight-color: #343a46; + +// typical text (dark-on-white in light skin) +$primary-fg-color: $text-primary-color; +$secondary-fg-color: $primary-fg-color; +$tertiary-fg-color: $primary-fg-color; +$primary-bg-color: $bg-color; +$muted-fg-color: $header-panel-text-primary-color; + +// Legacy theme backports +$quaternary-fg-color: #6F7882; + +// used for dialog box text +$light-fg-color: $header-panel-text-secondary-color; + +// used for focusing form controls +$focus-bg-color: $room-highlight-color; + +$mention-user-pill-bg-color: $warning-color; +$other-user-pill-bg-color: $room-highlight-color; +$rte-room-pill-color: $room-highlight-color; +$rte-group-pill-color: $room-highlight-color; + +// informational plinth +$info-plinth-bg-color: $header-panel-bg-color; +$info-plinth-fg-color: #888; + +$preview-bar-bg-color: $header-panel-bg-color; + +$groupFilterPanel-bg-color: $base-color; +$inverted-bg-color: $groupFilterPanel-bg-color; + +// used by AddressSelector +$selected-color: $room-highlight-color; + +// selected for hoverover & selected event tiles +$event-selected-color: $header-panel-bg-color; + +// used for the hairline dividers in RoomView +$primary-hairline-color: $header-panel-border-color; + +// used for the border of input text fields +$input-border-color: #e7e7e7; +$input-darker-bg-color: $search-bg-color; +$input-darker-fg-color: $search-placeholder-color; +$input-lighter-bg-color: #f2f5f8; +$input-lighter-fg-color: $input-darker-fg-color; +$input-focused-border-color: #238cf5; +$input-valid-border-color: $accent-color; +$input-invalid-border-color: $warning-color; + +$field-focused-label-bg-color: $bg-color; + +$resend-button-divider-color: $muted-fg-color; + +// scrollbars +$scrollbar-thumb-color: rgba(255, 255, 255, 0.2); +$scrollbar-track-color: transparent; + +// context menus +$menu-border-color: $header-panel-border-color; +$menu-bg-color: $header-panel-bg-color; +$menu-box-shadow-color: $bg-color; +$menu-selected-color: $room-highlight-color; + +$avatar-initial-color: #ffffff; +$avatar-bg-color: $bg-color; + +$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: #9fa9ba; + +$dialog-background-bg-color: $header-panel-bg-color; +$lightbox-background-bg-color: #000; +$lightbox-background-bg-opacity: 0.85; + +$settings-grey-fg-color: #a2a2a2; +$settings-profile-placeholder-bg-color: #e7e7e7; +$settings-profile-overlay-placeholder-fg-color: #454545; +$settings-profile-button-bg-color: #e7e7e7; +$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color; +$settings-subsection-fg-color: $text-secondary-color; + +$topleftmenu-color: $text-primary-color; +$roomheader-color: $text-primary-color; +$roomheader-addroom-bg-color: #3c4556; // $search-placeholder-color at 0.5 opacity +$roomheader-addroom-fg-color: $text-primary-color; +$groupFilterPanel-button-color: $header-panel-text-primary-color; +$groupheader-button-color: $header-panel-text-primary-color; +$rightpanel-button-color: $header-panel-text-primary-color; +$icon-button-color: $header-panel-text-primary-color; +$roomtopic-color: $text-secondary-color; +$eventtile-meta-color: $roomtopic-color; + +$header-divider-color: $header-panel-text-primary-color; +$composer-e2e-icon-color: $header-panel-text-primary-color; + +// this probably shouldn't have it's own colour +$voipcall-plinth-color: #394049; + +// ******************** + +$theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #6F7882; + + +$roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons +$roomlist-filter-active-bg-color: $roomlist-button-bg-color; +$roomlist-bg-color: $header-panel-bg-color; + +$roomsublist-divider-color: $primary-fg-color; +$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%); + +$groupFilterPanel-divider-color: $roomlist-header-color; + +$roomtile-preview-color: #9e9e9e; +$roomtile-default-badge-bg-color: #61708b; +$roomtile-selected-bg-color: #1A1D23; + +// ******************** + +$panel-divider-color: $header-panel-border-color; + +$widget-menu-bar-bg-color: $header-panel-bg-color; +$widget-body-bg-color: #1A1D23; + +// event redaction +$event-redacted-fg-color: #606060; +$event-redacted-border-color: #000000; + +$event-highlight-fg-color: $warning-color; +$event-highlight-bg-color: #25271F; + +// event timestamp +$event-timestamp-color: $text-secondary-color; + +// Tabbed views +$tab-label-fg-color: $text-primary-color; +$tab-label-active-fg-color: $text-primary-color; +$tab-label-bg-color: transparent; +$tab-label-active-bg-color: $accent-color; +$tab-label-icon-bg-color: $text-primary-color; +$tab-label-active-icon-bg-color: $text-primary-color; + +// Buttons +$button-primary-fg-color: #ffffff; +$button-primary-bg-color: $accent-color; +$button-secondary-bg-color: transparent; +$button-danger-fg-color: #ffffff; +$button-danger-bg-color: $notice-primary-color; +$button-danger-disabled-fg-color: #ffffff; +$button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color +$button-link-fg-color: $accent-color; +$button-link-bg-color: transparent; + +// Toggle switch +$togglesw-off-color: $room-highlight-color; + +$progressbar-fg-color: $accent-color; +$progressbar-bg-color: #21262c; + +$visual-bell-bg-color: #800; + +$room-warning-bg-color: $header-panel-bg-color; + +$dark-panel-bg-color: $header-panel-bg-color; +$panel-gradient: rgba(34, 38, 46, 0), rgba(34, 38, 46, 1); + +$message-action-bar-bg-color: $header-panel-bg-color; +$message-action-bar-fg-color: $header-panel-text-primary-color; +$message-action-bar-border-color: #616b7f; +$message-action-bar-hover-border-color: $header-panel-text-primary-color; + +$reaction-row-button-bg-color: $header-panel-bg-color; +$reaction-row-button-border-color: #616b7f; +$reaction-row-button-hover-border-color: $header-panel-text-primary-color; +$reaction-row-button-selected-bg-color: #1f6954; +$reaction-row-button-selected-border-color: $accent-color; + +$kbd-border-color: #000000; + +$tooltip-timeline-bg-color: $groupFilterPanel-bg-color; +$tooltip-timeline-fg-color: #ffffff; + +$interactive-tooltip-bg-color: $base-color; +$interactive-tooltip-fg-color: #ffffff; + +$breadcrumb-placeholder-bg-color: #272c35; + +$user-tile-hover-bg-color: $header-panel-bg-color; + +$message-body-panel-fg-color: $secondary-fg-color; +$message-body-panel-bg-color: #394049; +$message-body-panel-icon-fg-color: $primary-bg-color; +$message-body-panel-icon-bg-color: $secondary-fg-color; + +// See non-legacy dark for variable information +$voice-record-stop-border-color: #6F7882; +$voice-record-waveform-incomplete-fg-color: #6F7882; +$voice-record-icon-color: #6F7882; +$voice-playback-button-bg-color: $tertiary-fg-color; +$voice-playback-button-fg-color: #21262C; + +// Appearance tab colors +$appearance-tab-border-color: $room-highlight-color; + +$composer-shadow-color: tranparent; + +// ***** Mixins! ***** + +@define-mixin mx_DialogButton { + /* align images in buttons (eg spinners) */ + vertical-align: middle; + border: 0px; + border-radius: 8px; + font-family: $font-family; + font-size: $font-14px; + color: $button-fg-color; + background-color: $button-bg-color; + width: auto; + padding: 7px; + padding-left: 1.5em; + padding-right: 1.5em; + cursor: pointer; + display: inline-block; + outline: none; +} + +@define-mixin mx_DialogButton_danger { + background-color: $accent-color; +} + +@define-mixin mx_DialogButton_secondary { + // flip colours for the secondary ones + font-weight: 600; + border: 1px solid $accent-color !important; + color: $accent-color; + background-color: $button-secondary-bg-color; +} + +@define-mixin mx_Dialog_link { + color: $accent-color; + text-decoration: none; +} + +// Nasty hacks to apply a filter to arbitrary monochrome artwork to make it +// better match the theme. Typically applied to dark grey 'off' buttons or +// light grey 'on' buttons. +.mx_filterFlipColor { + filter: invert(1); +} + +// markdown overrides: +.mx_EventTile_content .markdown-body { + table { + tr { + background-color: #000000; + } + + tr:nth-child(2n) { + background-color: #080808; + } + } +} + +// highlight.js overrides: +.hljs-tag { + color: inherit; // Without this they'd be weirdly blue which doesn't match the theme +} diff --git a/res/themes/legacy-dark/css/legacy-dark.scss b/res/themes/legacy-dark/css/legacy-dark.scss new file mode 100644 index 0000000000..840794f7c0 --- /dev/null +++ b/res/themes/legacy-dark/css/legacy-dark.scss @@ -0,0 +1,7 @@ +@import "../../../../res/css/_font-sizes.scss"; +@import "../../legacy-light/css/_paths.scss"; +@import "../../legacy-light/css/_fonts.scss"; +@import "../../legacy-light/css/_legacy-light.scss"; +@import "_legacy-dark.scss"; +@import "../../../../res/css/_components.scss"; +@import url("highlight.js/styles/atom-one-dark.css"); diff --git a/res/themes/legacy-light/css/_fonts.scss b/res/themes/legacy-light/css/_fonts.scss new file mode 100644 index 0000000000..1bc9b5a4a3 --- /dev/null +++ b/res/themes/legacy-light/css/_fonts.scss @@ -0,0 +1,84 @@ +/* + * Nunito. + * Includes extended Latin and Vietnamese character sets + * Current URLs are taken from + * https://github.com/alexeiva/NunitoFont/releases/tag/v3.500 + * ...in order to include cyrillic. + * + * Previously, they were + * https://fonts.googleapis.com/css?family=Nunito:400,400i,600,600i,700,700i&subset=latin-ext,vietnamese + * + * We explicitly do not include Nunito's italic variants, as they are not italic enough + * and it's better to rely on the browser's built-in obliquing behaviour. + */ + +/* the 'src' links are relative to the bundle.css, which is in a subdirectory. + */ +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 400; + src: url('$(res)/fonts/Nunito/Nunito-Regular.ttf') format('truetype'); +} +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 600; + src: url('$(res)/fonts/Nunito/Nunito-SemiBold.ttf') format('truetype'); +} +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 700; + src: url('$(res)/fonts/Nunito/Nunito-Bold.ttf') format('truetype'); +} + +/* latin-ext */ +@font-face { + font-family: 'Inconsolata'; + font-style: normal; + font-weight: 400; + src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2') format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Inconsolata'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* latin-ext */ +@font-face { + font-family: 'Inconsolata'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2') format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Inconsolata'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* a COLR/CPAL version of Twemoji used for consistent cross-browser emoji + * taken from https://github.com/mozilla/twemoji-colr + * using the fix from https://github.com/mozilla/twemoji-colr/issues/50 to + * work on macOS + */ +/* +// except we now load it dynamically via FontManager to handle browsers +// which can't render COLR/CPAL still +@font-face { + font-family: "Twemoji Mozilla"; + src: url('$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla.woff2') format('woff2'); +} +*/ \ No newline at end of file diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss new file mode 100644 index 0000000000..c7debcdabe --- /dev/null +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -0,0 +1,403 @@ +// XXX: check this? +/* Nunito lacks combining diacritics, so these will fall through + to the next font. Helevetica's diacritics however do not combine + nicely (on OSX, at least) and result in a huge horizontal mess. + Arial empirically gets it right, hence prioritising Arial here. */ +/* We fall through to Twemoji for emoji rather than falling through + to native Emoji fonts (if any) to ensure cross-browser consistency */ +/* Noto Color Emoji contains digits, in fixed-width, therefore causing + digits in flowed text to stand out. + TODO: Consider putting all emoji fonts to the end rather than the front. */ +$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji'; + +$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Courier, monospace, 'Noto Color Emoji'; + +// unified palette +// try to use these colors when possible +$accent-color: #03b381; +$accent-bg-color: rgba(3, 179, 129, 0.16); +$notice-primary-color: #ff4b55; +$notice-primary-bg-color: rgba(255, 75, 85, 0.16); +$notice-secondary-color: #61708b; +$header-panel-bg-color: #f3f8fd; + +// typical text (dark-on-white in light skin) +$primary-fg-color: #2e2f32; +$secondary-fg-color: $primary-fg-color; +$tertiary-fg-color: $primary-fg-color; +$primary-bg-color: #ffffff; +$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text + +// Legacy theme backports +$quaternary-fg-color: #C1C6CD; + +// used for dialog box text +$light-fg-color: #747474; + +// used for focusing form controls +$focus-bg-color: #dddddd; + +// button UI (white-on-green in light skin) +$accent-fg-color: #ffffff; +$accent-color-50pct: rgba(3, 179, 129, 0.5); //#03b381 in rgb +$accent-color-darker: #92caad; +$accent-color-alt: #238cf5; + +$selection-fg-color: $primary-bg-color; + +$focus-brightness: 105%; + +// warning colours +$warning-color: $notice-primary-color; // red +$orange-warning-color: #ff8d13; // used for true warnings +// background colour for warnings +$warning-bg-color: #df2a8b; +$info-bg-color: #2a9edf; +$mention-user-pill-bg-color: $warning-color; +$other-user-pill-bg-color: rgba(0, 0, 0, 0.1); + +// pinned events indicator +$pinned-unread-color: $notice-primary-color; +$pinned-color: $notice-secondary-color; + +// informational plinth +$info-plinth-bg-color: #f7f7f7; +$info-plinth-fg-color: #888; + +$preview-bar-bg-color: #f7f7f7; + +// left-panel style muted accent color +$secondary-accent-color: #f2f5f8; +$tertiary-accent-color: #d3efe1; + +$groupFilterPanel-bg-color: #27303a; +$inverted-bg-color: $groupFilterPanel-bg-color; + +// used by RoomDirectory permissions +$plinth-bg-color: $secondary-accent-color; + +// used by RoomDropTarget +$droptarget-bg-color: rgba(255, 255, 255, 0.5); + +// used by AddressSelector +$selected-color: $secondary-accent-color; + +// selected for hoverover & selected event tiles +$event-selected-color: $header-panel-bg-color; + +// used for the hairline dividers in RoomView +$primary-hairline-color: #e5e5e5; + +// used for the border of input text fields +$input-border-color: #e7e7e7; +$input-darker-bg-color: #e3e8f0; +$input-darker-fg-color: #9fa9ba; +$input-lighter-bg-color: #f2f5f8; +$input-lighter-fg-color: $input-darker-fg-color; +$input-focused-border-color: #238cf5; +$input-valid-border-color: $accent-color; +$input-invalid-border-color: $warning-color; + +$field-focused-label-bg-color: #ffffff; + +$resend-button-divider-color: $input-darker-bg-color; + +$button-bg-color: $accent-color; +$button-fg-color: white; + +// apart from login forms, which have stronger border +$strong-input-border-color: #c7c7c7; + +// used for UserSettings EditableText +$input-underline-color: rgba(151, 151, 151, 0.5); +$input-fg-color: rgba(74, 74, 74, 0.9); +// scrollbars +$scrollbar-thumb-color: rgba(0, 0, 0, 0.2); +$scrollbar-track-color: transparent; +// context menus +$menu-border-color: #e7e7e7; +$menu-bg-color: #fff; +$menu-box-shadow-color: rgba(118, 131, 156, 0.6); +$menu-selected-color: #f5f8fa; + +$avatar-initial-color: #ffffff; +$avatar-bg-color: #ffffff; + +$h3-color: #3d3b39; + +$dialog-title-fg-color: #45474a; +$dialog-backdrop-color: rgba(46, 48, 51, 0.38); +$dialog-shadow-color: rgba(0, 0, 0, 0.48); +$dialog-close-fg-color: #c1c1c1; + +$dialog-background-bg-color: #e9e9e9; +$lightbox-background-bg-color: #000; +$lightbox-background-bg-opacity: 0.95; + +$imagebody-giflabel: rgba(0, 0, 0, 0.7); +$imagebody-giflabel-border: rgba(0, 0, 0, 0.2); +$imagebody-giflabel-color: rgba(255, 255, 255, 1); + +$greyed-fg-color: #888; + +$neutral-badge-color: #dbdbdb; + +$preview-widget-bar-color: #ddd; +$preview-widget-fg-color: $greyed-fg-color; + +$blockquote-bar-color: #ddd; +$blockquote-fg-color: #777; + +$settings-grey-fg-color: #a2a2a2; +$settings-profile-placeholder-bg-color: #e7e7e7; +$settings-profile-overlay-placeholder-fg-color: #2e2f32; +$settings-profile-button-bg-color: #e7e7e7; +$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color; +$settings-subsection-fg-color: #61708b; + +$voip-decline-color: #f48080; +$voip-accept-color: #80f480; + +$rte-bg-color: #e9e9e9; +$rte-code-bg-color: rgba(0, 0, 0, 0.04); +$rte-room-pill-color: #aaa; +$rte-group-pill-color: #aaa; + +$topleftmenu-color: #212121; +$roomheader-color: #45474a; +$roomheader-bg-color: $primary-bg-color; +$roomheader-addroom-bg-color: #91a1c0; +$roomheader-addroom-fg-color: $accent-fg-color; +$groupFilterPanel-button-color: #91a1c0; +$groupheader-button-color: #91a1c0; +$rightpanel-button-color: #91a1c0; +$icon-button-color: #91a1c0; +$roomtopic-color: #9e9e9e; +$eventtile-meta-color: $roomtopic-color; + +$composer-e2e-icon-color: #91a1c0; +$header-divider-color: #91a1c0; + +// this probably shouldn't have it's own colour +$voipcall-plinth-color: #F4F6FA; + +// ******************** + +$theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #e3e8f0; + + +$roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons +$roomlist-filter-active-bg-color: $roomlist-button-bg-color; +$roomlist-bg-color: $header-panel-bg-color; +$roomlist-header-color: $primary-fg-color; +$roomsublist-divider-color: $primary-fg-color; +$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%); + +$groupFilterPanel-divider-color: $roomlist-header-color; + +$roomtile-preview-color: #9e9e9e; +$roomtile-default-badge-bg-color: #61708b; +$roomtile-selected-bg-color: #fff; + +$presence-online: $accent-color; +$presence-away: #d9b072; +$presence-offline: #e3e8f0; + +// ******************** + +$username-variant1-color: #368bd6; +$username-variant2-color: #ac3ba8; +$username-variant3-color: #03b381; +$username-variant4-color: #e64f7a; +$username-variant5-color: #ff812d; +$username-variant6-color: #2dc2c5; +$username-variant7-color: #5c56f5; +$username-variant8-color: #74d12c; + +$panel-divider-color: #dee1f3; + +// ******************** + +$widget-menu-bar-bg-color: $secondary-accent-color; +$widget-body-bg-color: #fff; + +// ******************** + +// both $event-highlight-bg-color and $room-warning-bg-color share this value, +// so to not make their order dependent on who depends on who, have a shared value +// defined before both +$yellow-background: #fff8e3; + +// event tile lifecycle +$event-notsent-color: #f44; + +$event-highlight-fg-color: $warning-color; +$event-highlight-bg-color: $yellow-background; + +// event redaction +$event-redacted-fg-color: #e2e2e2; +$event-redacted-border-color: #cccccc; + +// event timestamp +$event-timestamp-color: #acacac; + +$copy-button-url: "$(res)/img/feather-customised/clipboard.svg"; +$collapse-button-url: "$(res)/img/feather-customised/minimise.svg"; +$expand-button-url: "$(res)/img/feather-customised/maximise.svg"; + +// e2e +$e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color +$e2e-unknown-color: #e8bf37; +$e2e-unverified-color: #e8bf37; +$e2e-warning-color: #ba6363; + +/*** ImageView ***/ +$lightbox-bg-color: #454545; +$lightbox-fg-color: #ffffff; +$lightbox-border-color: #ffffff; + +// Tabbed views +$tab-label-fg-color: #45474a; +$tab-label-active-fg-color: #ffffff; +$tab-label-bg-color: transparent; +$tab-label-active-bg-color: $accent-color; +$tab-label-icon-bg-color: #454545; +$tab-label-active-icon-bg-color: $tab-label-active-fg-color; + +// Buttons +$button-primary-fg-color: #ffffff; +$button-primary-bg-color: $accent-color; +$button-secondary-bg-color: $accent-fg-color; +$button-danger-fg-color: #ffffff; +$button-danger-bg-color: $notice-primary-color; +$button-danger-disabled-fg-color: #ffffff; +$button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color +$button-link-fg-color: $accent-color; +$button-link-bg-color: transparent; + +$visual-bell-bg-color: #faa; + +// Toggle switch +$togglesw-off-color: #c1c9d6; +$togglesw-on-color: $accent-color; +$togglesw-ball-color: #fff; + +// Slider +$slider-selection-color: $accent-color; +$slider-background-color: #c1c9d6; + +$progressbar-fg-color: $accent-color; +$progressbar-bg-color: rgba(141, 151, 165, 0.2); + +$room-warning-bg-color: $yellow-background; + +$memberstatus-placeholder-color: $muted-fg-color; + +$authpage-bg-color: #2e3649; +$authpage-modal-bg-color: rgba(255, 255, 255, 0.59); +$authpage-body-bg-color: #ffffff; +$authpage-focus-bg-color: #dddddd; +$authpage-lang-color: #4e5054; +$authpage-primary-color: #232f32; +$authpage-secondary-color: #61708b; + +$dark-panel-bg-color: $secondary-accent-color; +$panel-gradient: rgba(242, 245, 248, 0), rgba(242, 245, 248, 1); + +$message-action-bar-bg-color: $primary-bg-color; +$message-action-bar-fg-color: $primary-fg-color; +$message-action-bar-border-color: #e9edf1; +$message-action-bar-hover-border-color: $focus-bg-color; + +$reaction-row-button-bg-color: $header-panel-bg-color; +$reaction-row-button-border-color: #e9edf1; +$reaction-row-button-hover-border-color: $focus-bg-color; +$reaction-row-button-selected-bg-color: #e9fff9; +$reaction-row-button-selected-border-color: $accent-color; + +$kbd-border-color: $reaction-row-button-border-color; + +$tooltip-timeline-bg-color: $groupFilterPanel-bg-color; +$tooltip-timeline-fg-color: #ffffff; + +$interactive-tooltip-bg-color: #27303a; +$interactive-tooltip-fg-color: #ffffff; + +$breadcrumb-placeholder-bg-color: #e8eef5; + +$user-tile-hover-bg-color: $header-panel-bg-color; + +$message-body-panel-fg-color: $secondary-fg-color; +$message-body-panel-bg-color: #E3E8F0; +$message-body-panel-icon-fg-color: $secondary-fg-color; +$message-body-panel-icon-bg-color: $primary-bg-color; + +// See non-legacy _light for variable information +$voice-record-stop-symbol-color: #ff4b55; +$voice-record-live-circle-color: #ff4b55; +$voice-record-stop-border-color: #E3E8F0; +$voice-record-waveform-incomplete-fg-color: #C1C6CD; +$voice-record-icon-color: $tertiary-fg-color; +$voice-playback-button-bg-color: $message-body-panel-icon-bg-color; +$voice-playback-button-fg-color: $message-body-panel-icon-fg-color; + +// FontSlider colors +$appearance-tab-border-color: $input-darker-bg-color; + +$composer-shadow-color: tranparent; + +// ***** Mixins! ***** + +@define-mixin mx_DialogButton { + /* align images in buttons (eg spinners) */ + vertical-align: middle; + border: 0px; + border-radius: 8px; + font-family: $font-family; + font-size: $font-14px; + color: $button-fg-color; + background-color: $button-bg-color; + width: auto; + padding: 7px; + padding-left: 1.5em; + padding-right: 1.5em; + cursor: pointer; + display: inline-block; + outline: none; +} + +@define-mixin mx_DialogButton_hover { +} + +@define-mixin mx_DialogButton_danger { + background-color: $accent-color; +} + +@define-mixin mx_DialogButton_small { + @mixin mx_DialogButton; + font-size: $font-15px; + padding: 0px 1.5em 0px 1.5em; +} + +@define-mixin mx_DialogButton_secondary { + // flip colours for the secondary ones + font-weight: 600; + border: 1px solid $accent-color ! important; + color: $accent-color; + background-color: $button-secondary-bg-color; +} + +@define-mixin mx_Dialog_link { + color: $accent-color; + text-decoration: none; +} + +// diff highlight colors +.hljs-addition { + background: #dfd; +} + +.hljs-deletion { + background: #fdd; +} diff --git a/res/themes/legacy-light/css/_paths.scss b/res/themes/legacy-light/css/_paths.scss new file mode 100644 index 0000000000..3944076004 --- /dev/null +++ b/res/themes/legacy-light/css/_paths.scss @@ -0,0 +1,3 @@ +// Path from root SCSS file (such as `light.scss`) to `res` dir in the source tree +// This value is overridden by external themes in `element-web`. +$res: ../../..; diff --git a/res/themes/legacy-light/css/legacy-light.scss b/res/themes/legacy-light/css/legacy-light.scss new file mode 100644 index 0000000000..347d240fc6 --- /dev/null +++ b/res/themes/legacy-light/css/legacy-light.scss @@ -0,0 +1,6 @@ +@import "../../../../res/css/_font-sizes.scss"; +@import "_paths.scss"; +@import "_fonts.scss"; +@import "_legacy-light.scss"; +@import "../../../../res/css/_components.scss"; +@import url("highlight.js/styles/atom-one-light.css"); diff --git a/res/themes/light-custom/css/_custom.scss b/res/themes/light-custom/css/_custom.scss index e4a08277f9..1b9254d100 100644 --- a/res/themes/light-custom/css/_custom.scss +++ b/res/themes/light-custom/css/_custom.scss @@ -14,15 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ +$font-family: var(--font-family, $font-family); +$monospace-font-family: var(--font-family-monospace, $monospace-font-family); // // --accent-color $accent-color: var(--accent-color); +$accent-bg-color: var(--accent-color-15pct); $button-bg-color: var(--accent-color); $button-link-fg-color: var(--accent-color); $button-primary-bg-color: var(--accent-color); $input-valid-border-color: var(--accent-color); $reaction-row-button-selected-border-color: var(--accent-color); -$roomsublist-chevron-color: var(--accent-color); $tab-label-active-bg-color: var(--accent-color); $togglesw-on-color: var(--accent-color); $username-variant3-color: var(--accent-color); @@ -37,22 +39,22 @@ $menu-bg-color: var(--timeline-background-color); $avatar-bg-color: var(--timeline-background-color); $message-action-bar-bg-color: var(--timeline-background-color); $primary-bg-color: var(--timeline-background-color); -$roomtile-focused-bg-color: var(--timeline-background-color); $togglesw-ball-color: var(--timeline-background-color); $droptarget-bg-color: var(--timeline-background-color-50pct); //still needs alpha at .5 $authpage-modal-bg-color: var(--timeline-background-color-50pct); //still needs alpha at .59 +$roomheader-bg-color: var(--timeline-background-color); // // --roomlist-highlights-color $roomtile-selected-bg-color: var(--roomlist-highlights-color); // // --sidebar-color $interactive-tooltip-bg-color: var(--sidebar-color); -$tagpanel-bg-color: var(--sidebar-color); +$groupFilterPanel-bg-color: var(--sidebar-color); $tooltip-timeline-bg-color: var(--sidebar-color); $dialog-backdrop-color: var(--sidebar-color-50pct); +$roomlist-button-bg-color: var(--sidebar-color-15pct); // // --roomlist-background-color -$event-selected-color: var(--roomlist-background-color); $header-panel-bg-color: var(--roomlist-background-color); $reaction-row-button-bg-color: var(--roomlist-background-color); $panel-gradient: var(--roomlist-background-color-0pct), var(--roomlist-background-color); @@ -60,11 +62,10 @@ $panel-gradient: var(--roomlist-background-color-0pct), var(--roomlist-backgroun $dark-panel-bg-color: var(--roomlist-background-color); $input-lighter-bg-color: var(--roomlist-background-color); $plinth-bg-color: var(--roomlist-background-color); -$roomsublist-background: var(--roomlist-background-color); $secondary-accent-color: var(--roomlist-background-color); $selected-color: var(--roomlist-background-color); $widget-menu-bar-bg-color: var(--roomlist-background-color); -$roomtile-badge-fg-color: var(--roomlist-background-color); +$roomlist-bg-color: var(--roomlist-background-color); // // --timeline-text-color $message-action-bar-fg-color: var(--timeline-text-color); @@ -81,19 +82,17 @@ $tab-label-fg-color: var(--timeline-text-color); // was #4e5054 $authpage-lang-color: var(--timeline-text-color); $roomheader-color: var(--timeline-text-color); -// -// --roomlist-text-color -$roomtile-notified-color: var(--roomlist-text-color); -$roomtile-selected-color: var(--roomlist-text-color); -// // --roomlist-text-secondary-color -$roomsublist-label-fg-color: var(--roomlist-text-secondary-color); -$roomtile-name-color: var(--roomlist-text-secondary-color); +$roomtile-preview-color: var(--roomlist-text-secondary-color); +$roomlist-header-color: var(--roomlist-text-secondary-color); +$roomtile-default-badge-bg-color: var(--roomlist-text-secondary-color); + // // --roomlist-separator-color $input-darker-bg-color: var(--roomlist-separator-color); $panel-divider-color: var(--roomlist-separator-color);// originally #dee1f3, but close enough $primary-hairline-color: var(--roomlist-separator-color);// originally #e5e5e5, but close enough +$roomsublist-divider-color: var(--roomlist-separator-color); // // --timeline-text-secondary-color $authpage-secondary-color: var(--timeline-text-secondary-color); @@ -124,3 +123,20 @@ $notice-primary-color: var(--warning-color); $pinned-unread-color: var(--warning-color); $warning-color: var(--warning-color); $button-danger-disabled-bg-color: var(--warning-color-50pct); // still needs alpha at 0.5 +// +// --username colors (which use a 0-based index) +$username-variant1-color: var(--username-colors_0, $username-variant1-color); +$username-variant2-color: var(--username-colors_1, $username-variant2-color); +$username-variant3-color: var(--username-colors_2, $username-variant3-color); +$username-variant4-color: var(--username-colors_3, $username-variant4-color); +$username-variant5-color: var(--username-colors_4, $username-variant5-color); +$username-variant6-color: var(--username-colors_5, $username-variant6-color); +$username-variant7-color: var(--username-colors_6, $username-variant7-color); +$username-variant8-color: var(--username-colors_7, $username-variant8-color); +// +// --timeline-highlights-color +$event-selected-color: var(--timeline-highlights-color); +$event-highlight-bg-color: var(--timeline-highlights-color); +// +// redirect some variables away from their hardcoded values in the light theme +$settings-grey-fg-color: $primary-fg-color; diff --git a/res/themes/light-custom/css/light-custom.scss b/res/themes/light-custom/css/light-custom.scss index 4f80647eba..6e9d0ff736 100644 --- a/res/themes/light-custom/css/light-custom.scss +++ b/res/themes/light-custom/css/light-custom.scss @@ -1,6 +1,6 @@ @import "../../../../res/css/_font-sizes.scss"; -@import "../../light/css/_paths.scss"; -@import "../../light/css/_fonts.scss"; -@import "../../light/css/_light.scss"; +@import "../../legacy-light/css/_paths.scss"; +@import "../../legacy-light/css/_fonts.scss"; +@import "../../legacy-light/css/_legacy-light.scss"; @import "_custom.scss"; @import "../../../../res/css/_components.scss"; diff --git a/res/themes/light/css/_fonts.scss b/res/themes/light/css/_fonts.scss index 1bc9b5a4a3..68d9496276 100644 --- a/res/themes/light/css/_fonts.scss +++ b/res/themes/light/css/_fonts.scss @@ -1,36 +1,88 @@ -/* - * Nunito. - * Includes extended Latin and Vietnamese character sets - * Current URLs are taken from - * https://github.com/alexeiva/NunitoFont/releases/tag/v3.500 - * ...in order to include cyrillic. - * - * Previously, they were - * https://fonts.googleapis.com/css?family=Nunito:400,400i,600,600i,700,700i&subset=latin-ext,vietnamese - * - * We explicitly do not include Nunito's italic variants, as they are not italic enough - * and it's better to rely on the browser's built-in obliquing behaviour. - */ - /* the 'src' links are relative to the bundle.css, which is in a subdirectory. */ + +/* Inter unexpectedly contains various codepoints which collide with emoji, even + when variation-16 is applied to request the emoji variant. From eyeballing + the emoji picker, these are: 20e3, 23cf, 24c2, 25a0-25c1, 2665, 2764, 2b06, 2b1c. + Therefore we define a unicode-range to load which excludes the glyphs + (to avoid having to maintain a fork of Inter). */ + +$inter-unicode-range: U+0000-20e2,U+20e4-23ce,U+23d0-24c1,U+24c3-259f,U+25c2-2664,U+2666-2763,U+2765-2b05,U+2b07-2b1b,U+2b1d-10FFFF; + @font-face { - font-family: 'Nunito'; - font-style: normal; - font-weight: 400; - src: url('$(res)/fonts/Nunito/Nunito-Regular.ttf') format('truetype'); + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + unicode-range: $inter-unicode-range; + src: url("$(res)/fonts/Inter/Inter-Regular.woff2?v=3.18") format("woff2"), + url("$(res)/fonts/Inter/Inter-Regular.woff?v=3.18") format("woff"); } @font-face { - font-family: 'Nunito'; - font-style: normal; - font-weight: 600; - src: url('$(res)/fonts/Nunito/Nunito-SemiBold.ttf') format('truetype'); + font-family: 'Inter'; + font-style: italic; + font-weight: 400; + font-display: swap; + unicode-range: $inter-unicode-range; + src: url("$(res)/fonts/Inter/Inter-Italic.woff2?v=3.18") format("woff2"), + url("$(res)/fonts/Inter/Inter-Italic.woff?v=3.18") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + unicode-range: $inter-unicode-range; + src: url("$(res)/fonts/Inter/Inter-Medium.woff2?v=3.18") format("woff2"), + url("$(res)/fonts/Inter/Inter-Medium.woff?v=3.18") format("woff"); } @font-face { - font-family: 'Nunito'; - font-style: normal; - font-weight: 700; - src: url('$(res)/fonts/Nunito/Nunito-Bold.ttf') format('truetype'); + font-family: 'Inter'; + font-style: italic; + font-weight: 500; + font-display: swap; + unicode-range: $inter-unicode-range; + src: url("$(res)/fonts/Inter/Inter-MediumItalic.woff2?v=3.18") format("woff2"), + url("$(res)/fonts/Inter/Inter-MediumItalic.woff?v=3.18") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + font-display: swap; + unicode-range: $inter-unicode-range; + src: url("$(res)/fonts/Inter/Inter-SemiBold.woff2?v=3.18") format("woff2"), + url("$(res)/fonts/Inter/Inter-SemiBold.woff?v=3.18") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 600; + font-display: swap; + unicode-range: $inter-unicode-range; + src: url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff2?v=3.18") format("woff2"), + url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff?v=3.18") format("woff"); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + font-display: swap; + unicode-range: $inter-unicode-range; + src: url("$(res)/fonts/Inter/Inter-Bold.woff2?v=3.18") format("woff2"), + url("$(res)/fonts/Inter/Inter-Bold.woff?v=3.18") format("woff"); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 700; + font-display: swap; + unicode-range: $inter-unicode-range; + src: url("$(res)/fonts/Inter/Inter-BoldItalic.woff2?v=3.18") format("woff2"), + url("$(res)/fonts/Inter/Inter-BoldItalic.woff?v=3.18") format("woff"); } /* latin-ext */ @@ -68,17 +120,3 @@ src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } - -/* a COLR/CPAL version of Twemoji used for consistent cross-browser emoji - * taken from https://github.com/mozilla/twemoji-colr - * using the fix from https://github.com/mozilla/twemoji-colr/issues/50 to - * work on macOS - */ -/* -// except we now load it dynamically via FontManager to handle browsers -// which can't render COLR/CPAL still -@font-face { - font-family: "Twemoji Mozilla"; - src: url('$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla.woff2') format('woff2'); -} -*/ \ No newline at end of file diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index f5f3013354..7e958c2af6 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -8,21 +8,23 @@ /* Noto Color Emoji contains digits, in fixed-width, therefore causing digits in flowed text to stand out. TODO: Consider putting all emoji fonts to the end rather than the front. */ -$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji'; +$font-family: Inter, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji'; $monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Courier, monospace, 'Noto Color Emoji'; // unified palette // try to use these colors when possible -$accent-color: #03b381; +$accent-color: #0DBD8B; $accent-bg-color: rgba(3, 179, 129, 0.16); $notice-primary-color: #ff4b55; $notice-primary-bg-color: rgba(255, 75, 85, 0.16); -$notice-secondary-color: #61708b; +$primary-fg-color: #2e2f32; +$secondary-fg-color: #737D8C; +$tertiary-fg-color: #8D99A5; +$quaternary-fg-color: #C1C6CD; $header-panel-bg-color: #f3f8fd; // typical text (dark-on-white in light skin) -$primary-fg-color: #2e2f32; $primary-bg-color: #ffffff; $muted-fg-color: #61708b; // Commonly used in headings and relevant alt text @@ -34,7 +36,7 @@ $focus-bg-color: #dddddd; // button UI (white-on-green in light skin) $accent-fg-color: #ffffff; -$accent-color-50pct: rgba(3, 179, 129, 0.5); //#03b381 in rgb +$accent-color-50pct: rgba($accent-color, 0.5); $accent-color-darker: #92caad; $accent-color-alt: #238CF5; @@ -51,10 +53,6 @@ $info-bg-color: #2A9EDF; $mention-user-pill-bg-color: $warning-color; $other-user-pill-bg-color: rgba(0, 0, 0, 0.1); -// pinned events indicator -$pinned-unread-color: $notice-primary-color; -$pinned-color: $notice-secondary-color; - // informational plinth $info-plinth-bg-color: #f7f7f7; $info-plinth-fg-color: #888; @@ -65,22 +63,19 @@ $preview-bar-bg-color: #f7f7f7; $secondary-accent-color: #f2f5f8; $tertiary-accent-color: #d3efe1; -$tagpanel-bg-color: #27303a; +$groupFilterPanel-bg-color: rgba(232, 232, 232, 0.77); // used by RoomDirectory permissions $plinth-bg-color: $secondary-accent-color; -// used by RoomDropTarget -$droptarget-bg-color: rgba(255,255,255,0.5); - // used by AddressSelector $selected-color: $secondary-accent-color; // selected for hoverover & selected event tiles -$event-selected-color: $header-panel-bg-color; +$event-selected-color: #f6f7f8; // used for the hairline dividers in RoomView -$primary-hairline-color: #e5e5e5; +$primary-hairline-color: transparent; // used for the border of input text fields $input-border-color: #e7e7e7; @@ -97,6 +92,8 @@ $field-focused-label-bg-color: #ffffff; $button-bg-color: $accent-color; $button-fg-color: white; +$resend-button-divider-color: $input-darker-bg-color; + // apart from login forms, which have stronger border $strong-input-border-color: #c7c7c7; @@ -124,6 +121,7 @@ $dialog-close-fg-color: #c1c1c1; $dialog-background-bg-color: #e9e9e9; $lightbox-background-bg-color: #000; +$lightbox-background-bg-opacity: 0.95; $imagebody-giflabel: rgba(0, 0, 0, 0.7); $imagebody-giflabel-border: rgba(0, 0, 0, 0.2); @@ -140,11 +138,10 @@ $blockquote-bar-color: #ddd; $blockquote-fg-color: #777; $settings-grey-fg-color: #a2a2a2; -$settings-profile-placeholder-bg-color: #e7e7e7; -$settings-profile-overlay-bg-color: #000; -$settings-profile-overlay-placeholder-bg-color: transparent; -$settings-profile-overlay-fg-color: #fff; +$settings-profile-placeholder-bg-color: #f4f6fa; $settings-profile-overlay-placeholder-fg-color: #2e2f32; +$settings-profile-button-bg-color: #e7e7e7; +$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color; $settings-subsection-fg-color: #61708b; $voip-decline-color: #f48080; @@ -157,49 +154,68 @@ $rte-group-pill-color: #aaa; $topleftmenu-color: #212121; $roomheader-color: #45474a; -$roomheader-addroom-bg-color: #91A1C0; -$roomheader-addroom-fg-color: $accent-fg-color; -$tagpanel-button-color: #91A1C0; -$roomheader-button-color: #91A1C0; +$roomheader-bg-color: $primary-bg-color; +$roomheader-addroom-bg-color: rgba(92, 100, 112, 0.2); +$roomheader-addroom-fg-color: #5c6470; +$groupFilterPanel-button-color: #91A1C0; $groupheader-button-color: #91A1C0; $rightpanel-button-color: #91A1C0; -$composer-button-color: #91A1C0; +$icon-button-color: #C1C6CD; $roomtopic-color: #9e9e9e; $eventtile-meta-color: $roomtopic-color; -$composer-e2e-icon-color: #c9ced6; +$composer-e2e-icon-color: #91A1C0; $header-divider-color: #91A1C0; +// this probably shouldn't have it's own colour +$voipcall-plinth-color: #F4F6FA; + // ******************** -$roomtile-name-color: #61708b; -$roomtile-badge-fg-color: $accent-fg-color; -$roomtile-selected-color: #212121; -$roomtile-notified-color: #212121; -$roomtile-selected-bg-color: #fff; -$roomtile-focused-bg-color: #fff; +$theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #e3e8f0; + + +$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons +$roomlist-filter-active-bg-color: #ffffff; +$roomlist-bg-color: rgba(245, 245, 245, 0.90); +$roomlist-header-color: $tertiary-fg-color; +$roomsublist-divider-color: $primary-fg-color; +$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%); + +$groupFilterPanel-divider-color: $roomlist-header-color; + +$roomtile-preview-color: $secondary-fg-color; +$roomtile-default-badge-bg-color: #61708b; +$roomtile-selected-bg-color: #FFF; + +$presence-online: $accent-color; +$presence-away: #d9b072; +$presence-offline: #E3E8F0; + +// ******************** $username-variant1-color: #368bd6; $username-variant2-color: #ac3ba8; -$username-variant3-color: #03b381; +$username-variant3-color: #0DBD8B; $username-variant4-color: #e64f7a; $username-variant5-color: #ff812d; $username-variant6-color: #2dc2c5; $username-variant7-color: #5c56f5; $username-variant8-color: #74d12c; -$roomtile-transparent-focused-color: rgba(0, 0, 0, 0.1); +$notice-secondary-color: $roomlist-header-color; -$roomsublist-background: $secondary-accent-color; -$roomsublist-label-fg-color: $roomtile-name-color; -$roomsublist-label-bg-color: $tertiary-accent-color; -$roomsublist-chevron-color: $accent-color; +$panel-divider-color: transparent; -$panel-divider-color: #dee1f3; +// pinned events indicator +$pinned-unread-color: $notice-primary-color; +$pinned-color: $notice-secondary-color; // ******************** $widget-menu-bar-bg-color: $secondary-accent-color; +$widget-body-bg-color: #FFF; // ******************** @@ -209,8 +225,6 @@ $widget-menu-bar-bg-color: $secondary-accent-color; $yellow-background: #fff8e3; // event tile lifecycle -$event-encrypting-color: #abddbc; -$event-sending-color: #ddd; $event-notsent-color: #f44; $event-highlight-fg-color: $warning-color; @@ -223,7 +237,9 @@ $event-redacted-border-color: #cccccc; // event timestamp $event-timestamp-color: #acacac; -$copy-button-url: "$(res)/img/icon_copy_message.svg"; +$copy-button-url: "$(res)/img/feather-customised/clipboard.svg"; +$collapse-button-url: "$(res)/img/feather-customised/minimise.svg"; +$expand-button-url: "$(res)/img/feather-customised/maximise.svg"; // e2e $e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color @@ -262,14 +278,19 @@ $togglesw-off-color: #c1c9d6; $togglesw-on-color: $accent-color; $togglesw-ball-color: #fff; -$progressbar-color: #000; +// Slider +$slider-selection-color: $accent-color; +$slider-background-color: #c1c9d6; + +$progressbar-fg-color: $accent-color; +$progressbar-bg-color: rgba(141, 151, 165, 0.2); $room-warning-bg-color: $yellow-background; -$memberstatus-placeholder-color: $roomtile-name-color; +$memberstatus-placeholder-color: $muted-fg-color; $authpage-bg-color: #2e3649; -$authpage-modal-bg-color: rgba(255, 255, 255, 0.59); +$authpage-modal-bg-color: rgba(245, 245, 245, 0.90); $authpage-body-bg-color: #ffffff; $authpage-focus-bg-color: #dddddd; $authpage-lang-color: #4e5054; @@ -292,7 +313,8 @@ $reaction-row-button-selected-border-color: $accent-color; $kbd-border-color: $reaction-row-button-border-color; -$tooltip-timeline-bg-color: $tagpanel-bg-color; +$inverted-bg-color: #27303a; +$tooltip-timeline-bg-color: $inverted-bg-color; $tooltip-timeline-fg-color: #ffffff; $interactive-tooltip-bg-color: #27303a; @@ -302,13 +324,38 @@ $breadcrumb-placeholder-bg-color: #e8eef5; $user-tile-hover-bg-color: $header-panel-bg-color; +$message-body-panel-fg-color: $secondary-fg-color; +$message-body-panel-bg-color: #E3E8F0; // "Separator" +$message-body-panel-icon-fg-color: $secondary-fg-color; +$message-body-panel-icon-bg-color: $primary-bg-color; + +// These two don't change between themes. They are the $warning-color, but we don't +// want custom themes to affect them by accident. +$voice-record-stop-symbol-color: #ff4b55; +$voice-record-live-circle-color: #ff4b55; + +$voice-record-stop-border-color: #E3E8F0; // "Separator" +$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; +$voice-record-icon-color: $tertiary-fg-color; +$voice-playback-button-bg-color: $message-body-panel-icon-bg-color; +$voice-playback-button-fg-color: $message-body-panel-icon-fg-color; + +// FontSlider colors +$appearance-tab-border-color: $input-darker-bg-color; + +// blur amounts for left left panel (only for element theme, used in _mods.scss) +$roomlist-background-blur-amount: 40px; +$groupFilterPanel-background-blur-amount: 20px; + +$composer-shadow-color: rgba(0, 0, 0, 0.04); + // ***** Mixins! ***** @define-mixin mx_DialogButton { /* align images in buttons (eg spinners) */ vertical-align: middle; border: 0px; - border-radius: 4px; + border-radius: 8px; font-family: $font-family; font-size: $font-14px; color: $button-fg-color; diff --git a/res/themes/light/css/_mods.scss b/res/themes/light/css/_mods.scss new file mode 100644 index 0000000000..fbca58dfb1 --- /dev/null +++ b/res/themes/light/css/_mods.scss @@ -0,0 +1,36 @@ +// sidebar blurred avatar background +// +// if backdrop-filter is supported, +// set the user avatar (if any) as a background so +// it can be blurred by the tag panel and room list + +@supports (backdrop-filter: none) { + .mx_LeftPanel { + background-image: var(--avatar-url, unset); + background-repeat: no-repeat; + background-size: cover; + background-position: left top; + } + + .mx_GroupFilterPanel { + backdrop-filter: blur($groupFilterPanel-background-blur-amount); + } + + .mx_SpacePanel { + backdrop-filter: blur($groupFilterPanel-background-blur-amount); + } + + .mx_LeftPanel .mx_LeftPanel_roomListContainer { + backdrop-filter: blur($roomlist-background-blur-amount); + } +} + +.mx_RoomSublist_showNButton { + background-color: transparent !important; +} + +a:hover, +a:link, +a:visited { + text-decoration: none; +} diff --git a/res/themes/light/css/_paths.scss b/res/themes/light/css/_paths.scss index 0744347826..3944076004 100644 --- a/res/themes/light/css/_paths.scss +++ b/res/themes/light/css/_paths.scss @@ -1,3 +1,3 @@ // Path from root SCSS file (such as `light.scss`) to `res` dir in the source tree -// This value is overridden by external themes in `riot-web`. +// This value is overridden by external themes in `element-web`. $res: ../../..; diff --git a/res/themes/light/css/light.scss b/res/themes/light/css/light.scss index 4f48557648..4e912bc756 100644 --- a/res/themes/light/css/light.scss +++ b/res/themes/light/css/light.scss @@ -2,4 +2,6 @@ @import "_paths.scss"; @import "_fonts.scss"; @import "_light.scss"; +@import "_mods.scss"; @import "../../../../res/css/_components.scss"; +@import url("highlight.js/styles/atom-one-light.css"); diff --git a/scripts/ci/Dockerfile b/scripts/ci/Dockerfile index c153d11cc7..6d33987d8c 100644 --- a/scripts/ci/Dockerfile +++ b/scripts/ci/Dockerfile @@ -1,9 +1,8 @@ # Update on docker hub with the following commands in the directory of this file: -# docker build -t matrixdotorg/riotweb-ci-e2etests-env:latest . -# docker log -# docker push matrixdotorg/riotweb-ci-e2etests-env:latest -FROM node:10 +# docker build -t vectorim/element-web-ci-e2etests-env:latest . +# docker push vectorim/element-web-ci-e2etests-env:latest +FROM node:14-buster RUN apt-get update -RUN apt-get -y install build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime +RUN apt-get -y install jq build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime # dependencies for chrome (installed by puppeteer) -RUN apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget +RUN apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm-dev libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget diff --git a/scripts/ci/app-tests.sh b/scripts/ci/app-tests.sh new file mode 100755 index 0000000000..97e54dce66 --- /dev/null +++ b/scripts/ci/app-tests.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# +# script which is run by the CI build (after `yarn test`). +# +# clones element-web develop and runs the tests against our version of react-sdk. + +set -ev + +scripts/ci/layered.sh +cd element-web +yarn build:genfiles # so the tests can run. Faster version of `build` +yarn test diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh deleted file mode 100755 index 2f907dffa2..0000000000 --- a/scripts/ci/end-to-end-tests.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -# -# script which is run by the CI build (after `yarn test`). -# -# clones riot-web develop and runs the tests against our version of react-sdk. - -set -ev - -handle_error() { - EXIT_CODE=$? - exit $EXIT_CODE -} - -trap 'handle_error' ERR - - -echo "--- Building Riot" -scripts/ci/layered-riot-web.sh -cd ../riot-web -riot_web_dir=`pwd` -CI_PACKAGE=true yarn build -cd ../matrix-react-sdk -# run end to end tests -pushd test/end-to-end-tests -ln -s $riot_web_dir riot/riot-web -# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh -# CHROME_PATH=$(which google-chrome-stable) ./run.sh -echo "--- Install synapse & other dependencies" -./install.sh -# install static webserver to server symlinked local copy of riot -./riot/install-webserver.sh -rm -r logs || true -mkdir logs -echo "+++ Running end-to-end tests" -TESTS_STARTED=1 -./run.sh --no-sandbox --log-directory logs/ -popd diff --git a/scripts/ci/install-deps.sh b/scripts/ci/install-deps.sh index 14b5fc5393..fcbf6b1198 100755 --- a/scripts/ci/install-deps.sh +++ b/scripts/ci/install-deps.sh @@ -6,9 +6,8 @@ scripts/fetchdep.sh matrix-org matrix-js-sdk pushd matrix-js-sdk yarn link -yarn install $@ -yarn build +yarn install --pure-lockfile $@ popd yarn link matrix-js-sdk -yarn install $@ +yarn install --pure-lockfile $@ diff --git a/scripts/ci/layered-riot-web.sh b/scripts/ci/layered-riot-web.sh deleted file mode 100755 index f58794b451..0000000000 --- a/scripts/ci/layered-riot-web.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -# Creates an environment similar to one that riot-web would expect for -# development. This means going one directory up (and assuming we're in -# a directory like /workdir/matrix-react-sdk) and putting riot-web and -# the js-sdk there. - -cd ../ # Assume we're at something like /workdir/matrix-react-sdk - -# Set up the js-sdk first -matrix-react-sdk/scripts/fetchdep.sh matrix-org matrix-js-sdk -pushd matrix-js-sdk -yarn link -yarn install -popd - -# Now set up the react-sdk -pushd matrix-react-sdk -yarn link matrix-js-sdk -yarn link -yarn install -popd - -# Finally, set up riot-web -matrix-react-sdk/scripts/fetchdep.sh vector-im riot-web -pushd riot-web -yarn link matrix-js-sdk -yarn link matrix-react-sdk -yarn install -yarn build:res -popd diff --git a/scripts/ci/layered.sh b/scripts/ci/layered.sh new file mode 100755 index 0000000000..2e163456fe --- /dev/null +++ b/scripts/ci/layered.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Creates a layered environment with the full repo for the app and SDKs cloned +# and linked. + +# Note that this style is different from the recommended developer setup: this +# file nests js-sdk and element-web inside react-sdk, while the local +# development setup places them all at the same level. We are nesting them here +# because some CI systems do not allow moving to a directory above the checkout +# for the primary repo (react-sdk in this case). + +# Set up the js-sdk first +scripts/fetchdep.sh matrix-org matrix-js-sdk +pushd matrix-js-sdk +yarn link +yarn install --pure-lockfile +popd + +# Now set up the react-sdk +yarn link matrix-js-sdk +yarn link +yarn install --pure-lockfile +yarn reskindex + +# Finally, set up element-web +scripts/fetchdep.sh vector-im element-web +pushd element-web +yarn link matrix-js-sdk +yarn link matrix-react-sdk +yarn install --pure-lockfile +yarn build:res +popd diff --git a/scripts/ci/prepare-end-to-end-tests.sh b/scripts/ci/prepare-end-to-end-tests.sh new file mode 100755 index 0000000000..147e1f6445 --- /dev/null +++ b/scripts/ci/prepare-end-to-end-tests.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -ev + +handle_error() { + EXIT_CODE=$? + exit $EXIT_CODE +} + +trap 'handle_error' ERR + +echo "--- Building Element" +scripts/ci/layered.sh +cd element-web +element_web_dir=`pwd` +CI_PACKAGE=true yarn build +cd .. +# prepare end to end tests +pushd test/end-to-end-tests +ln -s $element_web_dir element/element-web +# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh +# CHROME_PATH=$(which google-chrome-stable) ./run.sh +echo "--- Install synapse & other dependencies" +./install.sh +# install static webserver to server symlinked local copy of element +./element/install-webserver.sh +popd diff --git a/scripts/ci/riot-unit-tests.sh b/scripts/ci/riot-unit-tests.sh deleted file mode 100755 index 337c0fe6c3..0000000000 --- a/scripts/ci/riot-unit-tests.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# -# script which is run by the CI build (after `yarn test`). -# -# clones riot-web develop and runs the tests against our version of react-sdk. - -set -ev - -scripts/ci/layered-riot-web.sh -cd ../riot-web -yarn build:genfiles # so the tests can run. Faster version of `build` -yarn test diff --git a/scripts/ci/run-end-to-end-tests.sh b/scripts/ci/run-end-to-end-tests.sh new file mode 100755 index 0000000000..3c99391fc7 --- /dev/null +++ b/scripts/ci/run-end-to-end-tests.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -ev + +handle_error() { + EXIT_CODE=$? + exit $EXIT_CODE +} + +trap 'handle_error' ERR + +# run end to end tests +pushd test/end-to-end-tests +rm -r logs || true +mkdir logs +echo "--- Running end-to-end tests" +TESTS_STARTED=1 +./run.sh --no-sandbox --log-directory logs/ +popd diff --git a/scripts/compare-file.js b/scripts/compare-file.js deleted file mode 100644 index f53275ebfa..0000000000 --- a/scripts/compare-file.js +++ /dev/null @@ -1,10 +0,0 @@ -const fs = require("fs"); - -if (process.argv.length < 4) throw new Error("Missing source and target file arguments"); - -const sourceFile = fs.readFileSync(process.argv[2], 'utf8'); -const targetFile = fs.readFileSync(process.argv[3], 'utf8'); - -if (sourceFile !== targetFile) { - throw new Error("Files do not match"); -} diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 0142305797..0990af70ce 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -22,19 +22,52 @@ clone() { } # Try the PR author's branch in case it exists on the deps as well. -# If BUILDKITE_BRANCH is set, it will contain either: -# * "branch" when the author's branch and target branch are in the same repo -# * "author:branch" when the author's branch is in their fork -# We can split on `:` into an array to check. -BUILDKITE_BRANCH_ARRAY=(${BUILDKITE_BRANCH//:/ }) -if [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "1" ]]; then - clone $deforg $defrepo $BUILDKITE_BRANCH -elif [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "2" ]]; then - clone ${BUILDKITE_BRANCH_ARRAY[0]} $defrepo ${BUILDKITE_BRANCH_ARRAY[1]} +# First we check if GITHUB_HEAD_REF is defined, +# Then we check if BUILDKITE_BRANCH is defined, +# if they aren't we can assume this is a Netlify build +if [ -n "$GITHUB_HEAD_REF" ]; then + head=$GITHUB_HEAD_REF +elif [ -n "$BUILDKITE_BRANCH" ]; then + head=$BUILDKITE_BRANCH +else + # Netlify doesn't give us info about the fork so we have to get it from GitHub API + apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/" + apiEndpoint+=$REVIEW_ID + head=$(curl $apiEndpoint | jq -r '.head.label') fi + +# If head is set, it will contain on Buildkite either: +# * "branch" when the author's branch and target branch are in the same repo +# * "fork:branch" when the author's branch is in their fork or if this is a Netlify build +# We can split on `:` into an array to check. +# For GitHub Actions we need to inspect GITHUB_REPOSITORY and GITHUB_ACTOR +# to determine whether the branch is from a fork or not +BRANCH_ARRAY=(${head//:/ }) +if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then + + if [ -n "$GITHUB_HEAD_REF" ]; then + if [[ "$GITHUB_REPOSITORY" == "$deforg"* ]]; then + clone $deforg $defrepo $GITHUB_HEAD_REF + else + REPO_ARRAY=(${GITHUB_REPOSITORY//\// }) + clone $REPO_ARRAY[0] $defrepo $GITHUB_HEAD_REF + fi + else + clone $deforg $defrepo $BUILDKITE_BRANCH + fi + +elif [[ "${#BRANCH_ARRAY[@]}" == "2" ]]; then + clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]} +fi + # Try the target branch of the push or PR. -clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH -# Try the current branch from Jenkins. -clone $deforg $defrepo `"echo $GIT_BRANCH" | sed -e 's/^origin\///'` +if [ -n $GITHUB_BASE_REF ]; then + clone $deforg $defrepo $GITHUB_BASE_REF +elif [ -n $BUILDKITE_PULL_REQUEST_BASE_BRANCH ]; then + clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH +fi + +# Try HEAD which is the branch name in Netlify (not BRANCH which is pull/xxxx/head for PR builds) +clone $deforg $defrepo $HEAD # Use the default branch as the last resort. clone $deforg $defrepo $defbranch diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js deleted file mode 100755 index a1823cdf50..0000000000 --- a/scripts/gen-i18n.js +++ /dev/null @@ -1,281 +0,0 @@ -#!/usr/bin/env node - -/* -Copyright 2017 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/** - * Regenerates the translations en_EN file by walking the source tree and - * parsing each file with flow-parser. Emits a JSON file with the - * translatable strings mapped to themselves in the order they appeared - * in the files and grouped by the file they appeared in. - * - * Usage: node scripts/gen-i18n.js - */ -const fs = require('fs'); -const path = require('path'); - -const walk = require('walk'); - -const flowParser = require('flow-parser'); -const estreeWalker = require('estree-walker'); - -const TRANSLATIONS_FUNCS = ['_t', '_td']; - -const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json'; -const OUTPUT_FILE = 'src/i18n/strings/en_EN.json'; - -// NB. The sync version of walk is broken for single files so we walk -// all of res rather than just res/home.html. -// https://git.daplie.com/Daplie/node-walk/merge_requests/1 fixes it, -// or if we get bored waiting for it to be merged, we could switch -// to a project that's actively maintained. -const SEARCH_PATHS = ['src', 'res']; - -const FLOW_PARSER_OPTS = { - esproposal_class_instance_fields: true, - esproposal_class_static_fields: true, - esproposal_decorators: true, - esproposal_export_star_as: true, - types: true, -}; - -function getObjectValue(obj, key) { - for (const prop of obj.properties) { - if (prop.key.type == 'Identifier' && prop.key.name == key) { - return prop.value; - } - } - return null; -} - -function getTKey(arg) { - if (arg.type == 'Literal') { - return arg.value; - } else if (arg.type == 'BinaryExpression' && arg.operator == '+') { - return getTKey(arg.left) + getTKey(arg.right); - } else if (arg.type == 'TemplateLiteral') { - return arg.quasis.map((q) => { - return q.value.raw; - }).join(''); - } - return null; -} - -function getFormatStrings(str) { - // Match anything that starts with % - // We could make a regex that matched the full placeholder, but this - // would just not match invalid placeholders and so wouldn't help us - // detect the invalid ones. - // Also note that for simplicity, this just matches a % character and then - // anything up to the next % character (or a single %, or end of string). - const formatStringRe = /%([^%]+|%|$)/g; - const formatStrings = new Set(); - - let match; - while ( (match = formatStringRe.exec(str)) !== null ) { - const placeholder = match[1]; // Minus the leading '%' - if (placeholder === '%') continue; // Literal % is %% - - const placeholderMatch = placeholder.match(/^\((.*?)\)(.)/); - if (placeholderMatch === null) { - throw new Error("Invalid format specifier: '"+match[0]+"'"); - } - if (placeholderMatch.length < 3) { - throw new Error("Malformed format specifier"); - } - const placeholderName = placeholderMatch[1]; - const placeholderFormat = placeholderMatch[2]; - - if (placeholderFormat !== 's') { - throw new Error(`'${placeholderFormat}' used as format character: you probably meant 's'`); - } - - formatStrings.add(placeholderName); - } - - return formatStrings; -} - -function getTranslationsJs(file) { - const tree = flowParser.parse(fs.readFileSync(file, { encoding: 'utf8' }), FLOW_PARSER_OPTS); - - const trs = new Set(); - - estreeWalker.walk(tree, { - enter: function(node, parent) { - if ( - node.type == 'CallExpression' && - TRANSLATIONS_FUNCS.includes(node.callee.name) - ) { - const tKey = getTKey(node.arguments[0]); - // This happens whenever we call _t with non-literals (ie. whenever we've - // had to use a _td to compensate) so is expected. - if (tKey === null) return; - - // check the format string against the args - // We only check _t: _td has no args - if (node.callee.name === '_t') { - try { - const placeholders = getFormatStrings(tKey); - for (const placeholder of placeholders) { - if (node.arguments.length < 2) { - throw new Error(`Placeholder found ('${placeholder}') but no substitutions given`); - } - const value = getObjectValue(node.arguments[1], placeholder); - if (value === null) { - throw new Error(`No value found for placeholder '${placeholder}'`); - } - } - - // Validate tag replacements - if (node.arguments.length > 2) { - const tagMap = node.arguments[2]; - for (const prop of tagMap.properties || []) { - if (prop.key.type === 'Literal') { - const tag = prop.key.value; - // RegExp same as in src/languageHandler.js - const regexp = new RegExp(`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`); - if (!tKey.match(regexp)) { - throw new Error(`No match for ${regexp} in ${tKey}`); - } - } - } - } - - } catch (e) { - console.log(); - console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`); - console.error(e); - process.exit(1); - } - } - - let isPlural = false; - if (node.arguments.length > 1 && node.arguments[1].type == 'ObjectExpression') { - const countVal = getObjectValue(node.arguments[1], 'count'); - if (countVal) { - isPlural = true; - } - } - - if (isPlural) { - trs.add(tKey + "|other"); - const plurals = enPlurals[tKey]; - if (plurals) { - for (const pluralType of Object.keys(plurals)) { - trs.add(tKey + "|" + pluralType); - } - } - } else { - trs.add(tKey); - } - } - } - }); - - return trs; -} - -function getTranslationsOther(file) { - const contents = fs.readFileSync(file, { encoding: 'utf8' }); - - const trs = new Set(); - - // Taken from riot-web src/components/structures/HomePage.js - const translationsRegex = /_t\(['"]([\s\S]*?)['"]\)/mg; - let matches; - while (matches = translationsRegex.exec(contents)) { - trs.add(matches[1]); - } - return trs; -} - -// gather en_EN plural strings from the input translations file: -// the en_EN strings are all in the source with the exception of -// pluralised strings, which we need to pull in from elsewhere. -const inputTranslationsRaw = JSON.parse(fs.readFileSync(INPUT_TRANSLATIONS_FILE, { encoding: 'utf8' })); -const enPlurals = {}; - -for (const key of Object.keys(inputTranslationsRaw)) { - const parts = key.split("|"); - if (parts.length > 1) { - const plurals = enPlurals[parts[0]] || {}; - plurals[parts[1]] = inputTranslationsRaw[key]; - enPlurals[parts[0]] = plurals; - } -} - -const translatables = new Set(); - -const walkOpts = { - listeners: { - names: function(root, nodeNamesArray) { - // Sort the names case insensitively and alphabetically to - // maintain some sense of order between the different strings. - nodeNamesArray.sort((a, b) => { - a = a.toLowerCase(); - b = b.toLowerCase(); - if (a > b) return 1; - if (a < b) return -1; - return 0; - }); - }, - file: function(root, fileStats, next) { - const fullPath = path.join(root, fileStats.name); - - let trs; - if (fileStats.name.endsWith('.js') || fileStats.name.endsWith('.ts') || fileStats.name.endsWith('.tsx')) { - trs = getTranslationsJs(fullPath); - } else if (fileStats.name.endsWith('.html')) { - trs = getTranslationsOther(fullPath); - } else { - return; - } - console.log(`${fullPath} (${trs.size} strings)`); - for (const tr of trs.values()) { - // Convert DOS line endings to unix - translatables.add(tr.replace(/\r\n/g, "\n")); - } - }, - } -}; - -for (const path of SEARCH_PATHS) { - if (fs.existsSync(path)) { - walk.walkSync(path, walkOpts); - } -} - -const trObj = {}; -for (const tr of translatables) { - if (tr.includes("|")) { - if (inputTranslationsRaw[tr]) { - trObj[tr] = inputTranslationsRaw[tr]; - } else { - trObj[tr] = tr.split("|")[0]; - } - } else { - trObj[tr] = tr; - } -} - -fs.writeFileSync( - OUTPUT_FILE, - JSON.stringify(trObj, translatables.values(), 4) + "\n" -); - -console.log(); -console.log(`Wrote ${translatables.size} strings to ${OUTPUT_FILE}`); diff --git a/scripts/generate-eslint-error-ignore-file b/scripts/generate-eslint-error-ignore-file deleted file mode 100755 index 54aacfc9fa..0000000000 --- a/scripts/generate-eslint-error-ignore-file +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -# -# generates .eslintignore.errorfiles to list the files which have errors in, -# so that they can be ignored in future automated linting. - -out=.eslintignore.errorfiles - -cd `dirname $0`/.. - -echo "generating $out" - -{ - cat < 0) | .filePath' | - sed -e 's/.*matrix-react-sdk\///'; -} > "$out" -# also append rules from eslintignore file -cat .eslintignore >> $out diff --git a/scripts/prune-i18n.js b/scripts/prune-i18n.js deleted file mode 100755 index b4fe8d69f5..0000000000 --- a/scripts/prune-i18n.js +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env node - -/* -Copyright 2017 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/* - * Looks through all the translation files and removes any strings - * which don't appear in en_EN.json. - * Use this if you remove a translation, but merge any outstanding changes - * from weblate first or you'll need to resolve the conflict in weblate. - */ - -const fs = require('fs'); -const path = require('path'); - -const I18NDIR = 'src/i18n/strings'; - -const enStringsRaw = JSON.parse(fs.readFileSync(path.join(I18NDIR, 'en_EN.json'))); - -const enStrings = new Set(); -for (const str of Object.keys(enStringsRaw)) { - const parts = str.split('|'); - if (parts.length > 1) { - enStrings.add(parts[0]); - } else { - enStrings.add(str); - } -} - -for (const filename of fs.readdirSync(I18NDIR)) { - if (filename === 'en_EN.json') continue; - if (filename === 'basefile.json') continue; - if (!filename.endsWith('.json')) continue; - - const trs = JSON.parse(fs.readFileSync(path.join(I18NDIR, filename))); - const oldLen = Object.keys(trs).length; - for (const tr of Object.keys(trs)) { - const parts = tr.split('|'); - const trKey = parts.length > 1 ? parts[0] : tr; - if (!enStrings.has(trKey)) { - delete trs[tr]; - } - } - - const removed = oldLen - Object.keys(trs).length; - if (removed > 0) { - console.log(`${filename}: removed ${removed} translations`); - // XXX: This is totally relying on the impl serialising the JSON object in the - // same order as they were parsed from the file. JSON.stringify() has a specific argument - // that can be used to control the order, but JSON.parse() lacks any kind of equivalent. - // Empirically this does maintain the order on my system, so I'm going to leave it like - // this for now. - fs.writeFileSync(path.join(I18NDIR, filename), JSON.stringify(trs, undefined, 4) + "\n"); - } -} diff --git a/scripts/reskindex.js b/scripts/reskindex.js index 9fb0e1a7c0..5eaec4d1d5 100755 --- a/scripts/reskindex.js +++ b/scripts/reskindex.js @@ -1,29 +1,33 @@ #!/usr/bin/env node -var fs = require('fs'); -var path = require('path'); -var glob = require('glob'); -var args = require('minimist')(process.argv); -var chokidar = require('chokidar'); +const fs = require('fs'); +const { promises: fsp } = fs; +const path = require('path'); +const glob = require('glob'); +const util = require('util'); +const args = require('minimist')(process.argv); +const chokidar = require('chokidar'); -var componentIndex = path.join('src', 'component-index.js'); -var componentIndexTmp = componentIndex+".tmp"; -var componentsDir = path.join('src', 'components'); -var componentJsGlob = '**/*.js'; -var componentTsGlob = '**/*.tsx'; -var prevFiles = []; +const componentIndex = path.join('src', 'component-index.js'); +const componentIndexTmp = componentIndex+".tmp"; +const componentsDir = path.join('src', 'components'); +const componentJsGlob = '**/*.js'; +const componentTsGlob = '**/*.tsx'; +let prevFiles = []; -function reskindex() { - var jsFiles = glob.sync(componentJsGlob, {cwd: componentsDir}).sort(); - var tsFiles = glob.sync(componentTsGlob, {cwd: componentsDir}).sort(); - var files = [...tsFiles, ...jsFiles]; +async function reskindex() { + const jsFiles = glob.sync(componentJsGlob, {cwd: componentsDir}).sort(); + const tsFiles = glob.sync(componentTsGlob, {cwd: componentsDir}).sort(); + const files = [...tsFiles, ...jsFiles]; if (!filesHaveChanged(files, prevFiles)) { return; } prevFiles = files; - var header = args.h || args.header; + const header = args.h || args.header; - var strm = fs.createWriteStream(componentIndexTmp); + const strm = fs.createWriteStream(componentIndexTmp); + // Wait for the open event to ensure the file descriptor is set + await new Promise(resolve => strm.once("open", resolve)); if (header) { strm.write(fs.readFileSync(header)); @@ -38,11 +42,11 @@ function reskindex() { strm.write(" */\n\n"); strm.write("let components = {};\n"); - for (var i = 0; i < files.length; ++i) { - var file = files[i].replace('.js', '').replace('.tsx', ''); + for (let i = 0; i < files.length; ++i) { + const file = files[i].replace('.js', '').replace('.tsx', ''); - var moduleName = (file.replace(/\//g, '.')); - var importName = moduleName.replace(/\./g, "$"); + const moduleName = (file.replace(/\//g, '.')); + const importName = moduleName.replace(/\./g, "$"); strm.write("import " + importName + " from './components/" + file + "';\n"); strm.write(importName + " && (components['"+moduleName+"'] = " + importName + ");"); @@ -51,14 +55,10 @@ function reskindex() { } strm.write("export {components};\n"); - strm.end(); - fs.rename(componentIndexTmp, componentIndex, function(err) { - if(err) { - console.error("Error moving new index into place: " + err); - } else { - console.log('Reskindex: completed'); - } - }); + // Ensure the file has been fully written to disk before proceeding + await util.promisify(fs.fsync)(strm.fd); + await util.promisify(strm.end); + await fsp.rename(componentIndexTmp, componentIndex); } // Expects both arrays of file names to be sorted @@ -67,7 +67,7 @@ function filesHaveChanged(files, prevFiles) { return true; } // Check for name changes - for (var i = 0; i < files.length; i++) { + for (let i = 0; i < files.length; i++) { if (prevFiles[i] !== files[i]) { return true; } @@ -75,15 +75,23 @@ function filesHaveChanged(files, prevFiles) { return false; } +// Wrapper since await at the top level is not well supported yet +function run() { + (async function() { + await reskindex(); + console.log("Reskindex completed"); + })(); +} + // -w indicates watch mode where any FS events will trigger reskindex if (!args.w) { - reskindex(); + run(); return; } -var watchDebouncer = null; +let watchDebouncer = null; chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => { if (path === componentIndex) return; if (watchDebouncer) clearTimeout(watchDebouncer); - watchDebouncer = setTimeout(reskindex, 1000); + watchDebouncer = setTimeout(run, 1000); }); diff --git a/src/@types/common.ts b/src/@types/common.ts new file mode 100644 index 0000000000..1fb9ba4303 --- /dev/null +++ b/src/@types/common.ts @@ -0,0 +1,24 @@ +/* +Copyright 2020 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 { JSXElementConstructor } from "react"; + +// Based on https://stackoverflow.com/a/53229857/3532235 +export type Without = {[P in Exclude]?: never}; +export type XOR = (T | U) extends object ? (Without & U) | (Without & T) : T | U; +export type Writeable = { -readonly [P in keyof T]: T[P] }; + +export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor; diff --git a/src/@types/diff-dom.ts b/src/@types/diff-dom.ts new file mode 100644 index 0000000000..38ff6432cf --- /dev/null +++ b/src/@types/diff-dom.ts @@ -0,0 +1,38 @@ +/* +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. +*/ + +declare module "diff-dom" { + export interface IDiff { + action: string; + name: string; + text?: string; + route: number[]; + value: string; + element: unknown; + oldValue: string; + newValue: string; + } + + interface IOpts { + } + + export class DiffDOM { + public constructor(opts?: IOpts); + public apply(tree: unknown, diffs: IDiff[]): unknown; + public undo(tree: unknown, diffs: IDiff[]): unknown; + public diff(a: HTMLElement | string, b: HTMLElement | string): IDiff[]; + } +} diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 963ba9d702..7192eb81cc 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -1,5 +1,5 @@ /* -Copyright 2020 New Vector Ltd +Copyright 2020-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. @@ -14,18 +14,175 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as ModernizrStatic from "modernizr"; +import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first +// Load types for the WG CSS Font Loading APIs https://github.com/Microsoft/TypeScript/issues/13569 +import "@types/css-font-loading-module"; +import "@types/modernizr"; + +import ContentMessages from "../ContentMessages"; +import { IMatrixClientPeg } from "../MatrixClientPeg"; +import ToastStore from "../stores/ToastStore"; +import DeviceListener from "../DeviceListener"; +import { RoomListStoreClass } from "../stores/room-list/RoomListStore"; +import { PlatformPeg } from "../PlatformPeg"; +import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore"; +import { IntegrationManagers } from "../integrations/IntegrationManagers"; +import { ModalManager } from "../Modal"; +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 WidgetStore from "../stores/WidgetStore"; +import CallHandler from "../CallHandler"; +import { Analytics } from "../Analytics"; +import CountlyAnalytics from "../CountlyAnalytics"; +import UserActivity from "../UserActivity"; +import { ModalWidgetStore } from "../stores/ModalWidgetStore"; +import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; +import VoipUserMapper from "../VoipUserMapper"; +import { SpaceStoreClass } from "../stores/SpaceStore"; +import TypingStore from "../stores/TypingStore"; +import { EventIndexPeg } from "../indexing/EventIndexPeg"; +import { VoiceRecordingStore } from "../stores/VoiceRecordingStore"; +import PerformanceMonitor from "../performance"; +import UIStore from "../stores/UIStore"; +import { SetupEncryptionStore } from "../stores/SetupEncryptionStore"; +import { RoomScrollStateStore } from "../stores/RoomScrollStateStore"; declare global { interface Window { - Modernizr: ModernizrStatic; + matrixChat: ReturnType; + mxMatrixClientPeg: IMatrixClientPeg; Olm: { init: () => Promise; }; + + // Needed for Safari, unknown to TypeScript + webkitAudioContext: typeof AudioContext; + + mxContentMessages: ContentMessages; + mxToastStore: ToastStore; + mxDeviceListener: DeviceListener; + mxRoomListStore: RoomListStoreClass; + mxRoomListLayoutStore: RoomListLayoutStore; + mxActiveRoomObserver: ActiveRoomObserver; + mxPlatformPeg: PlatformPeg; + mxIntegrationManagers: typeof IntegrationManagers; + singletonModalManager: ModalManager; + mxSettingsStore: SettingsStore; + mxNotifier: typeof Notifier; + mxRightPanelStore: RightPanelStore; + mxWidgetStore: WidgetStore; + mxWidgetLayoutStore: WidgetLayoutStore; + mxCallHandler: CallHandler; + mxAnalytics: Analytics; + mxCountlyAnalytics: typeof CountlyAnalytics; + mxUserActivity: UserActivity; + mxModalWidgetStore: ModalWidgetStore; + mxVoipUserMapper: VoipUserMapper; + mxSpaceStore: SpaceStoreClass; + mxVoiceRecordingStore: VoiceRecordingStore; + mxTypingStore: TypingStore; + mxEventIndexPeg: EventIndexPeg; + mxPerformanceMonitor: PerformanceMonitor; + mxPerformanceEntryNames: any; + mxUIStore: UIStore; + mxSetupEncryptionStore?: SetupEncryptionStore; + mxRoomScrollStateStore?: RoomScrollStateStore; } - // workaround for https://github.com/microsoft/TypeScript/issues/30933 - interface ObjectConstructor { - fromEntries?(xs: [string|number|symbol, any][]): object + interface Document { + // https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess + hasStorageAccess?: () => Promise; + // https://developer.mozilla.org/en-US/docs/Web/API/Document/requestStorageAccess + requestStorageAccess?: () => Promise; + + // Safari & IE11 only have this prefixed: we used prefixed versions + // previously so let's continue to support them for now + webkitExitFullscreen(): Promise; + msExitFullscreen(): Promise; + readonly webkitFullscreenElement: Element | null; + readonly msFullscreenElement: Element | null; } + + interface Navigator { + userLanguage?: string; + // https://github.com/Microsoft/TypeScript/issues/19473 + // https://developer.mozilla.org/en-US/docs/Web/API/MediaSession + mediaSession: any; + } + + interface StorageEstimate { + usageDetails?: {[key: string]: number}; + } + + interface HTMLAudioElement { + type?: string; + // sinkId & setSinkId are experimental and typescript doesn't know about them + sinkId: string; + setSinkId(outputId: string); + } + + interface HTMLVideoElement { + type?: string; + // sinkId & setSinkId are experimental and typescript doesn't know about them + sinkId: string; + setSinkId(outputId: string); + } + + // Add Chrome-specific `instant` ScrollBehaviour + type _ScrollBehavior = ScrollBehavior | "instant"; + + interface _ScrollOptions { + behavior?: _ScrollBehavior; + } + + interface _ScrollIntoViewOptions extends _ScrollOptions { + block?: ScrollLogicalPosition; + inline?: ScrollLogicalPosition; + } + + interface Element { + // Safari & IE11 only have this prefixed: we used prefixed versions + // previously so let's continue to support them for now + webkitRequestFullScreen(options?: FullscreenOptions): Promise; + msRequestFullscreen(options?: FullscreenOptions): Promise; + scrollIntoView(arg?: boolean | _ScrollIntoViewOptions): void; + } + + interface Error { + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/fileName + fileName?: string; + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/lineNumber + lineNumber?: number; + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/columnNumber + columnNumber?: number; + } + + // https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278 + interface AudioWorkletProcessor { + readonly port: MessagePort; + process( + inputs: Float32Array[][], + outputs: Float32Array[][], + parameters: Record + ): boolean; + } + + // https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278 + const AudioWorkletProcessor: { + prototype: AudioWorkletProcessor; + new (options?: AudioWorkletNodeOptions): AudioWorkletProcessor; + }; + + // https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278 + function registerProcessor( + name: string, + processorCtor: (new ( + options?: AudioWorkletNodeOptions + ) => AudioWorkletProcessor) & { + parameterDescriptors?: AudioParamDescriptor[]; + } + ); } diff --git a/src/@types/polyfill.ts b/src/@types/polyfill.ts new file mode 100644 index 0000000000..3ce05d9c2f --- /dev/null +++ b/src/@types/polyfill.ts @@ -0,0 +1,38 @@ +/* +Copyright 2020 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. +*/ + +// This is intended to fix re-resizer because of its unguarded `instanceof TouchEvent` checks. +export function polyfillTouchEvent() { + // Firefox doesn't have touch events without touch devices being present, so create a fake + // one we can rely on lying about. + if (!window.TouchEvent) { + // We have no intention of actually using this, so just lie. + window.TouchEvent = class TouchEvent extends UIEvent { + public get altKey(): boolean { return false; } + public get changedTouches(): any { return []; } + public get ctrlKey(): boolean { return false; } + public get metaKey(): boolean { return false; } + public get shiftKey(): boolean { return false; } + public get targetTouches(): any { return []; } + public get touches(): any { return []; } + public get rotation(): number { return 0.0; } + public get scale(): number { return 0.0; } + constructor(eventType: string, params?: any) { + super(eventType, params); + } + }; + } +} diff --git a/src/@types/sanitize-html.ts b/src/@types/sanitize-html.ts new file mode 100644 index 0000000000..4cada29845 --- /dev/null +++ b/src/@types/sanitize-html.ts @@ -0,0 +1,23 @@ +/* +Copyright 2020 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 sanitizeHtml from 'sanitize-html'; + +export interface IExtendedSanitizeOptions extends sanitizeHtml.IOptions { + // This option only exists in 2.x RCs so far, so not yet present in the + // separate type definition module. + nestingLimit?: number; +} diff --git a/src/@types/worker-loader.d.ts b/src/@types/worker-loader.d.ts new file mode 100644 index 0000000000..a8f5d8e9a4 --- /dev/null +++ b/src/@types/worker-loader.d.ts @@ -0,0 +1,23 @@ +/* +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. +*/ + +declare module "*.worker.ts" { + class WebpackWorker extends Worker { + constructor(); + } + + export default WebpackWorker; +} diff --git a/src/ActiveRoomObserver.js b/src/ActiveRoomObserver.js deleted file mode 100644 index d6fbb460b5..0000000000 --- a/src/ActiveRoomObserver.js +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright 2017 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import RoomViewStore from './stores/RoomViewStore'; - -/** - * Consumes changes from the RoomViewStore and notifies specific things - * about when the active room changes. Unlike listening for RoomViewStore - * changes, you can subscribe to only changes relevant to a particular - * room. - * - * TODO: If we introduce an observer for something else, factor out - * the adding / removing of listeners & emitting into a common class. - */ -class ActiveRoomObserver { - constructor() { - this._listeners = {}; - - this._activeRoomId = RoomViewStore.getRoomId(); - // TODO: We could self-destruct when the last listener goes away, or at least - // stop listening. - this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate.bind(this)); - } - - addListener(roomId, listener) { - if (!this._listeners[roomId]) this._listeners[roomId] = []; - this._listeners[roomId].push(listener); - } - - removeListener(roomId, listener) { - if (this._listeners[roomId]) { - const i = this._listeners[roomId].indexOf(listener); - if (i > -1) { - this._listeners[roomId].splice(i, 1); - } - } else { - console.warn("Unregistering unrecognised listener (roomId=" + roomId + ")"); - } - } - - _emit(roomId) { - if (!this._listeners[roomId]) return; - - for (const l of this._listeners[roomId]) { - l.call(); - } - } - - _onRoomViewStoreUpdate() { - // emit for the old room ID - if (this._activeRoomId) this._emit(this._activeRoomId); - - // update our cache - this._activeRoomId = RoomViewStore.getRoomId(); - - // and emit for the new one - if (this._activeRoomId) this._emit(this._activeRoomId); - } -} - -if (global.mx_ActiveRoomObserver === undefined) { - global.mx_ActiveRoomObserver = new ActiveRoomObserver(); -} -export default global.mx_ActiveRoomObserver; diff --git a/src/ActiveRoomObserver.ts b/src/ActiveRoomObserver.ts new file mode 100644 index 0000000000..1126dc9496 --- /dev/null +++ b/src/ActiveRoomObserver.ts @@ -0,0 +1,83 @@ +/* +Copyright 2017 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import RoomViewStore from './stores/RoomViewStore'; + +type Listener = (isActive: boolean) => void; + +/** + * Consumes changes from the RoomViewStore and notifies specific things + * about when the active room changes. Unlike listening for RoomViewStore + * changes, you can subscribe to only changes relevant to a particular + * room. + * + * TODO: If we introduce an observer for something else, factor out + * the adding / removing of listeners & emitting into a common class. + */ +export class ActiveRoomObserver { + private listeners: {[key: string]: Listener[]} = {}; + private _activeRoomId = RoomViewStore.getRoomId(); + private readonly roomStoreToken: string; + + constructor() { + // TODO: We could self-destruct when the last listener goes away, or at least stop listening. + this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); + } + + public get activeRoomId(): string { + return this._activeRoomId; + } + + public addListener(roomId, listener) { + if (!this.listeners[roomId]) this.listeners[roomId] = []; + this.listeners[roomId].push(listener); + } + + public removeListener(roomId, listener) { + if (this.listeners[roomId]) { + const i = this.listeners[roomId].indexOf(listener); + if (i > -1) { + this.listeners[roomId].splice(i, 1); + } + } else { + console.warn("Unregistering unrecognised listener (roomId=" + roomId + ")"); + } + } + + private emit(roomId, isActive: boolean) { + if (!this.listeners[roomId]) return; + + for (const l of this.listeners[roomId]) { + l.call(null, isActive); + } + } + + private onRoomViewStoreUpdate = () => { + // emit for the old room ID + if (this._activeRoomId) this.emit(this._activeRoomId, false); + + // update our cache + this._activeRoomId = RoomViewStore.getRoomId(); + + // and emit for the new one + if (this._activeRoomId) this.emit(this._activeRoomId, true); + }; +} + +if (window.mxActiveRoomObserver === undefined) { + window.mxActiveRoomObserver = new ActiveRoomObserver(); +} +export default window.mxActiveRoomObserver; diff --git a/src/AddThreepid.js b/src/AddThreepid.js index f06f7c187d..ab291128a7 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -16,12 +16,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClientPeg} from './MatrixClientPeg'; +import { MatrixClientPeg } from './MatrixClientPeg'; import * as sdk from './index'; import Modal from './Modal'; import { _t } from './languageHandler'; import IdentityAuthClient from './IdentityAuthClient'; -import {SSOAuthEntry} from "./components/views/auth/InteractiveAuthEntryComponents"; +import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryComponents"; function getIdServerDomain() { return MatrixClientPeg.get().idBaseUrl.split("://")[1]; @@ -189,7 +189,6 @@ export default class AddThreepid { // pop up an interactive auth dialog const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); - const dialogAesthetics = { [SSOAuthEntry.PHASE_PREAUTH]: { title: _t("Use Single Sign On to continue"), @@ -249,7 +248,7 @@ export default class AddThreepid { /** * Takes a phone number verification code as entered by the user and validates - * it with the ID server, then if successful, adds the phone number. + * it with the identity server, then if successful, adds the phone number. * @param {string} msisdnToken phone number verification code as entered by the user * @return {Promise} Resolves if the phone number was added. Rejects with an object * with a "message" property which contains a human-readable message detailing why diff --git a/src/Analytics.js b/src/Analytics.js deleted file mode 100644 index e55612c4f1..0000000000 --- a/src/Analytics.js +++ /dev/null @@ -1,386 +0,0 @@ -/* -Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2020 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 from 'react'; - -import { getCurrentLanguage, _t, _td } from './languageHandler'; -import PlatformPeg from './PlatformPeg'; -import SdkConfig from './SdkConfig'; -import Modal from './Modal'; -import * as sdk from './index'; - -const hashRegex = /#\/(groups?|room|user|settings|register|login|forgot_password|home|directory)/; -const hashVarRegex = /#\/(group|room|user)\/.*$/; - -// Remove all but the first item in the hash path. Redact unexpected hashes. -function getRedactedHash(hash) { - // Don't leak URLs we aren't expecting - they could contain tokens/PII - const match = hashRegex.exec(hash); - if (!match) { - console.warn(`Unexpected hash location "${hash}"`); - return '#/'; - } - - if (hashVarRegex.test(hash)) { - return hash.replace(hashVarRegex, "#/$1/"); - } - - return hash.replace(hashRegex, "#/$1"); -} - -// Return the current origin, path and hash separated with a `/`. This does -// not include query parameters. -function getRedactedUrl() { - const { origin, hash } = window.location; - let { pathname } = window.location; - - // Redact paths which could contain unexpected PII - if (origin.startsWith('file://')) { - pathname = "//"; - } - - return origin + pathname + getRedactedHash(hash); -} - -const customVariables = { - // The Matomo installation at https://matomo.riot.im is currently configured - // with a limit of 10 custom variables. - 'App Platform': { - id: 1, - expl: _td('The platform you\'re on'), - example: 'Electron Platform', - }, - 'App Version': { - id: 2, - expl: _td('The version of Riot'), - example: '15.0.0', - }, - 'User Type': { - id: 3, - expl: _td('Whether or not you\'re logged in (we don\'t record your username)'), - example: 'Logged In', - }, - 'Chosen Language': { - id: 4, - expl: _td('Your language of choice'), - example: 'en', - }, - 'Instance': { - id: 5, - expl: _td('Which officially provided instance you are using, if any'), - example: 'app', - }, - 'RTE: Uses Richtext Mode': { - id: 6, - expl: _td('Whether or not you\'re using the Richtext mode of the Rich Text Editor'), - example: 'off', - }, - 'Homeserver URL': { - id: 7, - expl: _td('Your homeserver\'s URL'), - example: 'https://matrix.org', - }, - 'Touch Input': { - id: 8, - expl: _td("Whether you're using Riot on a device where touch is the primary input mechanism"), - example: 'false', - }, - 'Breadcrumbs': { - id: 9, - expl: _td("Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)"), - example: 'disabled', - }, - 'Installed PWA': { - id: 10, - expl: _td("Whether you're using Riot as an installed Progressive Web App"), - example: 'false', - }, -}; - -function whitelistRedact(whitelist, str) { - if (whitelist.includes(str)) return str; - return ''; -} - -const UID_KEY = "mx_Riot_Analytics_uid"; -const CREATION_TS_KEY = "mx_Riot_Analytics_cts"; -const VISIT_COUNT_KEY = "mx_Riot_Analytics_vc"; -const LAST_VISIT_TS_KEY = "mx_Riot_Analytics_lvts"; - -function getUid() { - try { - let data = localStorage && localStorage.getItem(UID_KEY); - if (!data && localStorage) { - localStorage.setItem(UID_KEY, data = [...Array(16)].map(() => Math.random().toString(16)[2]).join('')); - } - return data; - } catch (e) { - console.error("Analytics error: ", e); - return ""; - } -} - -const HEARTBEAT_INTERVAL = 30 * 1000; // seconds - -class Analytics { - constructor() { - this.baseUrl = null; - this.siteId = null; - this.visitVariables = {}; - - this.firstPage = true; - this._heartbeatIntervalID = null; - - this.creationTs = localStorage && localStorage.getItem(CREATION_TS_KEY); - if (!this.creationTs && localStorage) { - localStorage.setItem(CREATION_TS_KEY, this.creationTs = new Date().getTime()); - } - - this.lastVisitTs = localStorage && localStorage.getItem(LAST_VISIT_TS_KEY); - this.visitCount = localStorage && localStorage.getItem(VISIT_COUNT_KEY) || 0; - if (localStorage) { - localStorage.setItem(VISIT_COUNT_KEY, parseInt(this.visitCount, 10) + 1); - } - } - - get disabled() { - return !this.baseUrl; - } - - /** - * Enable Analytics if initialized but disabled - * otherwise try and initalize, no-op if piwik config missing - */ - async enable() { - if (!this.disabled) return; - - const config = SdkConfig.get(); - if (!config || !config.piwik || !config.piwik.url || !config.piwik.siteId) return; - - this.baseUrl = new URL("piwik.php", config.piwik.url); - // set constants - this.baseUrl.searchParams.set("rec", 1); // rec is required for tracking - this.baseUrl.searchParams.set("idsite", config.piwik.siteId); // rec is required for tracking - this.baseUrl.searchParams.set("apiv", 1); // API version to use - this.baseUrl.searchParams.set("send_image", 0); // we want a 204, not a tiny GIF - // set user parameters - this.baseUrl.searchParams.set("_id", getUid()); // uuid - this.baseUrl.searchParams.set("_idts", this.creationTs); // first ts - this.baseUrl.searchParams.set("_idvc", parseInt(this.visitCount, 10)+ 1); // visit count - if (this.lastVisitTs) { - this.baseUrl.searchParams.set("_viewts", this.lastVisitTs); // last visit ts - } - - const platform = PlatformPeg.get(); - this._setVisitVariable('App Platform', platform.getHumanReadableName()); - try { - this._setVisitVariable('App Version', await platform.getAppVersion()); - } catch (e) { - this._setVisitVariable('App Version', 'unknown'); - } - - this._setVisitVariable('Chosen Language', getCurrentLanguage()); - - if (window.location.hostname === 'riot.im') { - this._setVisitVariable('Instance', window.location.pathname); - } - - let installedPWA = "unknown"; - try { - // Known to work at least for desktop Chrome - installedPWA = window.matchMedia('(display-mode: standalone)').matches; - } catch (e) { } - this._setVisitVariable('Installed PWA', installedPWA); - - let touchInput = "unknown"; - try { - // MDN claims broad support across browsers - touchInput = window.matchMedia('(pointer: coarse)').matches; - } catch (e) { } - this._setVisitVariable('Touch Input', touchInput); - - // start heartbeat - this._heartbeatIntervalID = window.setInterval(this.ping.bind(this), HEARTBEAT_INTERVAL); - } - - /** - * Disable Analytics, stop the heartbeat and clear identifiers from localStorage - */ - disable() { - if (this.disabled) return; - this.trackEvent('Analytics', 'opt-out'); - window.clearInterval(this._heartbeatIntervalID); - this.baseUrl = null; - this.visitVariables = {}; - localStorage.removeItem(UID_KEY); - localStorage.removeItem(CREATION_TS_KEY); - localStorage.removeItem(VISIT_COUNT_KEY); - localStorage.removeItem(LAST_VISIT_TS_KEY); - } - - async _track(data) { - if (this.disabled) return; - - const now = new Date(); - const params = { - ...data, - url: getRedactedUrl(), - - _cvar: JSON.stringify(this.visitVariables), // user custom vars - res: `${window.screen.width}x${window.screen.height}`, // resolution as WWWWxHHHH - rand: String(Math.random()).slice(2, 8), // random nonce to cache-bust - h: now.getHours(), - m: now.getMinutes(), - s: now.getSeconds(), - }; - - const url = new URL(this.baseUrl); - for (const key in params) { - url.searchParams.set(key, params[key]); - } - - try { - await window.fetch(url, { - method: "GET", - mode: "no-cors", - cache: "no-cache", - redirect: "follow", - }); - } catch (e) { - console.error("Analytics error: ", e); - } - } - - ping() { - this._track({ - ping: 1, - }); - localStorage.setItem(LAST_VISIT_TS_KEY, new Date().getTime()); // update last visit ts - } - - trackPageChange(generationTimeMs) { - if (this.disabled) return; - if (this.firstPage) { - // De-duplicate first page - // router seems to hit the fn twice - this.firstPage = false; - return; - } - - if (typeof generationTimeMs !== 'number') { - console.warn('Analytics.trackPageChange: expected generationTimeMs to be a number'); - // But continue anyway because we still want to track the change - } - - this._track({ - gt_ms: generationTimeMs, - }); - } - - trackEvent(category, action, name, value) { - if (this.disabled) return; - this._track({ - e_c: category, - e_a: action, - e_n: name, - e_v: value, - }); - } - - _setVisitVariable(key, value) { - if (this.disabled) return; - this.visitVariables[customVariables[key].id] = [key, value]; - } - - setLoggedIn(isGuest, homeserverUrl, identityServerUrl) { - if (this.disabled) return; - - const config = SdkConfig.get(); - if (!config.piwik) return; - - const whitelistedHSUrls = config.piwik.whitelistedHSUrls || []; - - this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In'); - this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl)); - } - - setBreadcrumbs(state) { - if (this.disabled) return; - this._setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled'); - } - - showDetailsModal = () => { - let rows = []; - if (!this.disabled) { - rows = Object.values(this.visitVariables); - } else { - rows = Object.keys(customVariables).map( - (k) => [ - k, - _t('e.g. %(exampleValue)s', { exampleValue: customVariables[k].example }), - ], - ); - } - - const resolution = `${window.screen.width}x${window.screen.height}`; - const otherVariables = [ - { - expl: _td('Every page you use in the app'), - value: _t( - 'e.g. ', - {}, - { - CurrentPageURL: getRedactedUrl(), - }, - ), - }, - { expl: _td('Your user agent'), value: navigator.userAgent }, - { expl: _td('Your device resolution'), value: resolution }, - ]; - - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); - Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, { - title: _t('Analytics'), - description:

-
- { _t('The information being sent to us to help make Riot better includes:') } -
- - { rows.map((row) => - - { row[1] !== undefined && } - ) } - { otherVariables.map((item, index) => - - - - , - ) } -
{ _t(customVariables[row[0]].expl) }{ row[1] }
{ _t(item.expl) }{ item.value }
-
- { _t('Where this page includes identifiable information, such as a room, ' - + 'user or group ID, that data is removed before being sent to the server.') } -
-
, - }); - }; -} - -if (!global.mxAnalytics) { - global.mxAnalytics = new Analytics(); -} -export default global.mxAnalytics; diff --git a/src/Analytics.tsx b/src/Analytics.tsx new file mode 100644 index 0000000000..ce8287de56 --- /dev/null +++ b/src/Analytics.tsx @@ -0,0 +1,430 @@ +/* +Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2020 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 from 'react'; + +import { getCurrentLanguage, _t, _td, IVariables } from './languageHandler'; +import PlatformPeg from './PlatformPeg'; +import SdkConfig from './SdkConfig'; +import Modal from './Modal'; +import * as sdk from './index'; + +const hashRegex = /#\/(groups?|room|user|settings|register|login|forgot_password|home|directory)/; +const hashVarRegex = /#\/(group|room|user)\/.*$/; + +// Remove all but the first item in the hash path. Redact unexpected hashes. +function getRedactedHash(hash: string): string { + // Don't leak URLs we aren't expecting - they could contain tokens/PII + const match = hashRegex.exec(hash); + if (!match) { + console.warn(`Unexpected hash location "${hash}"`); + return '#/'; + } + + if (hashVarRegex.test(hash)) { + return hash.replace(hashVarRegex, "#/$1/"); + } + + return hash.replace(hashRegex, "#/$1"); +} + +// Return the current origin, path and hash separated with a `/`. This does +// not include query parameters. +function getRedactedUrl(): string { + const { origin, hash } = window.location; + let { pathname } = window.location; + + // Redact paths which could contain unexpected PII + if (origin.startsWith('file://')) { + pathname = "//"; + } + + return origin + pathname + getRedactedHash(hash); +} + +interface IData { + /* eslint-disable camelcase */ + gt_ms?: string; + e_c?: string; + e_a?: string; + e_n?: string; + e_v?: string; + ping?: string; + /* eslint-enable camelcase */ +} + +interface IVariable { + id: number; + expl: string; // explanation + example: string; // example value + getTextVariables?(): IVariables; // object to pass as 2nd argument to `_t` +} + +const customVariables: Record = { + // The Matomo installation at https://matomo.riot.im is currently configured + // with a limit of 10 custom variables. + 'App Platform': { + id: 1, + expl: _td('The platform you\'re on'), + example: 'Electron Platform', + }, + 'App Version': { + id: 2, + expl: _td('The version of %(brand)s'), + getTextVariables: () => ({ + brand: SdkConfig.get().brand, + }), + example: '15.0.0', + }, + 'User Type': { + id: 3, + expl: _td('Whether or not you\'re logged in (we don\'t record your username)'), + example: 'Logged In', + }, + 'Chosen Language': { + id: 4, + expl: _td('Your language of choice'), + example: 'en', + }, + 'Instance': { + id: 5, + expl: _td('Which officially provided instance you are using, if any'), + example: 'app', + }, + 'RTE: Uses Richtext Mode': { + id: 6, + expl: _td('Whether or not you\'re using the Richtext mode of the Rich Text Editor'), + example: 'off', + }, + 'Homeserver URL': { + id: 7, + expl: _td('Your homeserver\'s URL'), + example: 'https://matrix.org', + }, + 'Touch Input': { + id: 8, + expl: _td("Whether you're using %(brand)s on a device where touch is the primary input mechanism"), + getTextVariables: () => ({ + brand: SdkConfig.get().brand, + }), + example: 'false', + }, + 'Breadcrumbs': { + id: 9, + expl: _td("Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)"), + example: 'disabled', + }, + 'Installed PWA': { + id: 10, + expl: _td("Whether you're using %(brand)s as an installed Progressive Web App"), + getTextVariables: () => ({ + brand: SdkConfig.get().brand, + }), + example: 'false', + }, +}; + +function whitelistRedact(whitelist: string[], str: string): string { + if (whitelist.includes(str)) return str; + return ''; +} + +const UID_KEY = "mx_Riot_Analytics_uid"; +const CREATION_TS_KEY = "mx_Riot_Analytics_cts"; +const VISIT_COUNT_KEY = "mx_Riot_Analytics_vc"; +const LAST_VISIT_TS_KEY = "mx_Riot_Analytics_lvts"; + +function getUid(): string { + try { + let data = localStorage && localStorage.getItem(UID_KEY); + if (!data && localStorage) { + localStorage.setItem(UID_KEY, data = [...Array(16)].map(() => Math.random().toString(16)[2]).join('')); + } + return data; + } catch (e) { + console.error("Analytics error: ", e); + return ""; + } +} + +const HEARTBEAT_INTERVAL = 30 * 1000; // seconds + +export class Analytics { + private baseUrl: URL = null; + private siteId: string = null; + private visitVariables: Record = {}; // {[id: number]: [name: string, value: string]} + private firstPage = true; + private heartbeatIntervalID: number = null; + + private readonly creationTs: string; + private readonly lastVisitTs: string; + private readonly visitCount: string; + + constructor() { + this.creationTs = localStorage && localStorage.getItem(CREATION_TS_KEY); + if (!this.creationTs && localStorage) { + localStorage.setItem(CREATION_TS_KEY, this.creationTs = String(new Date().getTime())); + } + + this.lastVisitTs = localStorage && localStorage.getItem(LAST_VISIT_TS_KEY); + this.visitCount = localStorage && localStorage.getItem(VISIT_COUNT_KEY) || "0"; + this.visitCount = String(parseInt(this.visitCount, 10) + 1); // increment + if (localStorage) { + localStorage.setItem(VISIT_COUNT_KEY, this.visitCount); + } + } + + public get disabled() { + return !this.baseUrl; + } + + public canEnable() { + const config = SdkConfig.get(); + return navigator.doNotTrack !== "1" && config && config.piwik && config.piwik.url && config.piwik.siteId; + } + + /** + * Enable Analytics if initialized but disabled + * otherwise try and initalize, no-op if piwik config missing + */ + public async enable() { + if (!this.disabled) return; + if (!this.canEnable()) return; + const config = SdkConfig.get(); + + this.baseUrl = new URL("piwik.php", config.piwik.url); + // set constants + this.baseUrl.searchParams.set("rec", "1"); // rec is required for tracking + this.baseUrl.searchParams.set("idsite", config.piwik.siteId); // rec is required for tracking + this.baseUrl.searchParams.set("apiv", "1"); // API version to use + this.baseUrl.searchParams.set("send_image", "0"); // we want a 204, not a tiny GIF + // set user parameters + this.baseUrl.searchParams.set("_id", getUid()); // uuid + this.baseUrl.searchParams.set("_idts", this.creationTs); // first ts + this.baseUrl.searchParams.set("_idvc", this.visitCount); // visit count + if (this.lastVisitTs) { + this.baseUrl.searchParams.set("_viewts", this.lastVisitTs); // last visit ts + } + + const platform = PlatformPeg.get(); + this.setVisitVariable('App Platform', platform.getHumanReadableName()); + try { + this.setVisitVariable('App Version', await platform.getAppVersion()); + } catch (e) { + this.setVisitVariable('App Version', 'unknown'); + } + + this.setVisitVariable('Chosen Language', getCurrentLanguage()); + + const hostname = window.location.hostname; + if (hostname === 'riot.im') { + this.setVisitVariable('Instance', window.location.pathname); + } else if (hostname.endsWith('.element.io')) { + this.setVisitVariable('Instance', hostname.replace('.element.io', '')); + } + + let installedPWA = "unknown"; + try { + // Known to work at least for desktop Chrome + installedPWA = String(window.matchMedia('(display-mode: standalone)').matches); + } catch (e) { } + this.setVisitVariable('Installed PWA', installedPWA); + + let touchInput = "unknown"; + try { + // MDN claims broad support across browsers + touchInput = String(window.matchMedia('(pointer: coarse)').matches); + } catch (e) { } + this.setVisitVariable('Touch Input', touchInput); + + // start heartbeat + this.heartbeatIntervalID = window.setInterval(this.ping.bind(this), HEARTBEAT_INTERVAL); + } + + /** + * Disable Analytics, stop the heartbeat and clear identifiers from localStorage + */ + public disable() { + if (this.disabled) return; + this.trackEvent('Analytics', 'opt-out'); + window.clearInterval(this.heartbeatIntervalID); + this.baseUrl = null; + this.visitVariables = {}; + localStorage.removeItem(UID_KEY); + localStorage.removeItem(CREATION_TS_KEY); + localStorage.removeItem(VISIT_COUNT_KEY); + localStorage.removeItem(LAST_VISIT_TS_KEY); + } + + private async _track(data: IData) { + if (this.disabled) return; + + const now = new Date(); + const params = { + ...data, + url: getRedactedUrl(), + + _cvar: JSON.stringify(this.visitVariables), // user custom vars + res: `${window.screen.width}x${window.screen.height}`, // resolution as WWWWxHHHH + rand: String(Math.random()).slice(2, 8), // random nonce to cache-bust + h: now.getHours(), + m: now.getMinutes(), + s: now.getSeconds(), + }; + + const url = new URL(this.baseUrl.toString()); // copy + for (const key in params) { + url.searchParams.set(key, params[key]); + } + + try { + await window.fetch(url.toString(), { + method: "GET", + mode: "no-cors", + cache: "no-cache", + redirect: "follow", + }); + } catch (e) { + console.error("Analytics error: ", e); + } + } + + public ping() { + this._track({ + ping: "1", + }); + localStorage.setItem(LAST_VISIT_TS_KEY, String(new Date().getTime())); // update last visit ts + } + + public trackPageChange(generationTimeMs?: number) { + if (this.disabled) return; + if (this.firstPage) { + // De-duplicate first page + // router seems to hit the fn twice + this.firstPage = false; + return; + } + + if (typeof generationTimeMs !== 'number') { + console.warn('Analytics.trackPageChange: expected generationTimeMs to be a number'); + // But continue anyway because we still want to track the change + } + + this._track({ + gt_ms: String(generationTimeMs), + }); + } + + public trackEvent(category: string, action: string, name?: string, value?: string) { + if (this.disabled) return; + this._track({ + e_c: category, + e_a: action, + e_n: name, + e_v: value, + }); + } + + private setVisitVariable(key: keyof typeof customVariables, value: string) { + if (this.disabled) return; + this.visitVariables[customVariables[key].id] = [key, value]; + } + + public setLoggedIn(isGuest: boolean, homeserverUrl: string) { + if (this.disabled) return; + + const config = SdkConfig.get(); + if (!config.piwik) return; + + const whitelistedHSUrls = config.piwik.whitelistedHSUrls || []; + + this.setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In'); + this.setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl)); + } + + public setBreadcrumbs(state: boolean) { + if (this.disabled) return; + this.setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled'); + } + + public showDetailsModal = () => { + let rows = []; + if (!this.disabled) { + rows = Object.values(this.visitVariables); + } else { + rows = Object.keys(customVariables).map( + (k) => [ + k, + _t('e.g. %(exampleValue)s', { exampleValue: customVariables[k].example }), + ], + ); + } + + const resolution = `${window.screen.width}x${window.screen.height}`; + const otherVariables = [ + { + expl: _td('Every page you use in the app'), + value: _t( + 'e.g. ', + {}, + { + CurrentPageURL: getRedactedUrl, + }, + ), + }, + { expl: _td('Your user agent'), value: navigator.userAgent }, + { expl: _td('Your device resolution'), value: resolution }, + ]; + + // FIXME: Using an import will result in test failures + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, { + title: _t('Analytics'), + description:
+
{_t('The information being sent to us to help make %(brand)s better includes:', { + brand: SdkConfig.get().brand, + })}
+ + { rows.map((row) => + + { row[1] !== undefined && } + ) } + { otherVariables.map((item, index) => + + + + , + ) } +
{_t( + customVariables[row[0]].expl, + customVariables[row[0]].getTextVariables ? + customVariables[row[0]].getTextVariables() : + null, + )}{ row[1] }
{ _t(item.expl) }{ item.value }
+
+ { _t('Where this page includes identifiable information, such as a room, ' + + 'user or group ID, that data is removed before being sent to the server.') } +
+
, + }); + }; +} + +if (!window.mxAnalytics) { + window.mxAnalytics = new Analytics(); +} +export default window.mxAnalytics; diff --git a/src/AsyncWrapper.js b/src/AsyncWrapper.js deleted file mode 100644 index 05054cf63a..0000000000 --- a/src/AsyncWrapper.js +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2020 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 createReactClass from 'create-react-class'; -import * as sdk from './index'; -import PropTypes from 'prop-types'; -import { _t } from './languageHandler'; - -/** - * Wrap an asynchronous loader function with a react component which shows a - * spinner until the real component loads. - */ -export default createReactClass({ - propTypes: { - /** A promise which resolves with the real component - */ - prom: PropTypes.object.isRequired, - }, - - getInitialState: function() { - return { - component: null, - error: null, - }; - }, - - componentDidMount: function() { - this._unmounted = false; - // XXX: temporary logging to try to diagnose - // https://github.com/vector-im/riot-web/issues/3148 - console.log('Starting load of AsyncWrapper for modal'); - this.props.prom.then((result) => { - if (this._unmounted) { - return; - } - // Take the 'default' member if it's there, then we support - // passing in just an import()ed module, since ES6 async import - // always returns a module *namespace*. - const component = result.default ? result.default : result; - this.setState({component}); - }).catch((e) => { - console.warn('AsyncWrapper promise failed', e); - this.setState({error: e}); - }); - }, - - componentWillUnmount: function() { - this._unmounted = true; - }, - - _onWrapperCancelClick: function() { - this.props.onFinished(false); - }, - - render: function() { - if (this.state.component) { - const Component = this.state.component; - return ; - } else if (this.state.error) { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return - {_t("Unable to load! Check your network connectivity and try again.")} - - ; - } else { - // show a spinner until the component is loaded. - const Spinner = sdk.getComponent("elements.Spinner"); - return ; - } - }, -}); - diff --git a/src/AsyncWrapper.tsx b/src/AsyncWrapper.tsx new file mode 100644 index 0000000000..ef8924add8 --- /dev/null +++ b/src/AsyncWrapper.tsx @@ -0,0 +1,97 @@ +/* +Copyright 2015-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, { ComponentType } from "react"; + +import * as sdk from './index'; +import { _t } from './languageHandler'; +import { IDialogProps } from "./components/views/dialogs/IDialogProps"; + +type AsyncImport = { default: T }; + +interface IProps extends IDialogProps { + // A promise which resolves with the real component + prom: Promise>; +} + +interface IState { + component?: ComponentType; + error?: Error; +} + +/** + * Wrap an asynchronous loader function with a react component which shows a + * spinner until the real component loads. + */ +export default class AsyncWrapper extends React.Component { + private unmounted = false; + + public state = { + component: null, + error: null, + }; + + componentDidMount() { + // XXX: temporary logging to try to diagnose + // https://github.com/vector-im/element-web/issues/3148 + console.log('Starting load of AsyncWrapper for modal'); + this.props.prom.then((result) => { + if (this.unmounted) return; + + // Take the 'default' member if it's there, then we support + // passing in just an import()ed module, since ES6 async import + // always returns a module *namespace*. + const component = (result as AsyncImport).default + ? (result as AsyncImport).default + : result as ComponentType; + this.setState({ component }); + }).catch((e) => { + console.warn('AsyncWrapper promise failed', e); + this.setState({ error: e }); + }); + } + + componentWillUnmount() { + this.unmounted = true; + } + + private onWrapperCancelClick = () => { + this.props.onFinished(false); + }; + + render() { + if (this.state.component) { + const Component = this.state.component; + return ; + } else if (this.state.error) { + // FIXME: Using an import will result in test failures + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return + { _t("Unable to load! Check your network connectivity and try again.") } + + ; + } else { + // show a spinner until the component is loaded. + const Spinner = sdk.getComponent("elements.Spinner"); + return ; + } + } +} + diff --git a/src/Avatar.js b/src/Avatar.js deleted file mode 100644 index 217b196348..0000000000 --- a/src/Avatar.js +++ /dev/null @@ -1,137 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -'use strict'; -import {MatrixClientPeg} from './MatrixClientPeg'; -import DMRoomMap from './utils/DMRoomMap'; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; - -export function avatarUrlForMember(member, width, height, resizeMethod) { - let url; - if (member && member.getAvatarUrl) { - url = member.getAvatarUrl( - MatrixClientPeg.get().getHomeserverUrl(), - Math.floor(width * window.devicePixelRatio), - Math.floor(height * window.devicePixelRatio), - resizeMethod, - false, - false, - ); - } - if (!url) { - // member can be null here currently since on invites, the JS SDK - // does not have enough info to build a RoomMember object for - // the inviter. - url = defaultAvatarUrlForString(member ? member.userId : ''); - } - return url; -} - -export function avatarUrlForUser(user, width, height, resizeMethod) { - const url = getHttpUriForMxc( - MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl, - Math.floor(width * window.devicePixelRatio), - Math.floor(height * window.devicePixelRatio), - resizeMethod, - ); - if (!url || url.length === 0) { - return null; - } - return url; -} - -export function defaultAvatarUrlForString(s) { - const images = ['03b381', '368bd6', 'ac3ba8']; - let total = 0; - for (let i = 0; i < s.length; ++i) { - total += s.charCodeAt(i); - } - return require('../res/img/' + images[total % images.length] + '.png'); -} - -/** - * returns the first (non-sigil) character of 'name', - * converted to uppercase - * @param {string} name - * @return {string} the first letter - */ -export function getInitialLetter(name) { - if (!name) { - // XXX: We should find out what causes the name to sometimes be falsy. - console.trace("`name` argument to `getInitialLetter` not supplied"); - return undefined; - } - if (name.length < 1) { - return undefined; - } - - let idx = 0; - const initial = name[0]; - if ((initial === '@' || initial === '#' || initial === '+') && name[1]) { - idx++; - } - - // string.codePointAt(0) would do this, but that isn't supported by - // some browsers (notably PhantomJS). - let chars = 1; - const first = name.charCodeAt(idx); - - // check if it’s the start of a surrogate pair - if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) { - const second = name.charCodeAt(idx+1); - if (second >= 0xDC00 && second <= 0xDFFF) { - chars++; - } - } - - const firstChar = name.substring(idx, idx+chars); - return firstChar.toUpperCase(); -} - -export function avatarUrlForRoom(room, width, height, resizeMethod) { - if (!room) return null; // null-guard - - const explicitRoomAvatar = room.getAvatarUrl( - MatrixClientPeg.get().getHomeserverUrl(), - width, - height, - resizeMethod, - false, - ); - if (explicitRoomAvatar) { - return explicitRoomAvatar; - } - - let otherMember = null; - const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId); - if (otherUserId) { - otherMember = room.getMember(otherUserId); - } else { - // if the room is not marked as a 1:1, but only has max 2 members - // then still try to show any avatar (pref. other member) - otherMember = room.getAvatarFallbackMember(); - } - if (otherMember) { - return otherMember.getAvatarUrl( - MatrixClientPeg.get().getHomeserverUrl(), - width, - height, - resizeMethod, - false, - ); - } - return null; -} diff --git a/src/Avatar.ts b/src/Avatar.ts new file mode 100644 index 0000000000..198d4162a0 --- /dev/null +++ b/src/Avatar.ts @@ -0,0 +1,171 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { User } from "matrix-js-sdk/src/models/user"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { ResizeMethod } from "matrix-js-sdk/src/@types/partials"; + +import DMRoomMap from './utils/DMRoomMap'; +import { mediaFromMxc } from "./customisations/Media"; +import SpaceStore from "./stores/SpaceStore"; + +// Not to be used for BaseAvatar urls as that has similar default avatar fallback already +export function avatarUrlForMember( + member: RoomMember, + width: number, + height: number, + resizeMethod: ResizeMethod, +): string { + let url: string; + if (member?.getMxcAvatarUrl()) { + url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); + } + if (!url) { + // member can be null here currently since on invites, the JS SDK + // does not have enough info to build a RoomMember object for + // the inviter. + url = defaultAvatarUrlForString(member ? member.userId : ''); + } + return url; +} + +export function avatarUrlForUser( + user: Pick, + width: number, + height: number, + resizeMethod?: ResizeMethod, +): string | null { + if (!user.avatarUrl) return null; + return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(width, height, resizeMethod); +} + +function isValidHexColor(color: string): boolean { + return typeof color === "string" && + (color.length === 7 || color.length === 9) && + color.charAt(0) === "#" && + !color.substr(1).split("").some(c => isNaN(parseInt(c, 16))); +} + +function urlForColor(color: string): string { + const size = 40; + const canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext("2d"); + // bail out when using jsdom in unit tests + if (!ctx) { + return ""; + } + ctx.fillStyle = color; + ctx.fillRect(0, 0, size, size); + return canvas.toDataURL(); +} + +// XXX: Ideally we'd clear this cache when the theme changes +// but since this function is at global scope, it's a bit +// hard to install a listener here, even if there were a clear event to listen to +const colorToDataURLCache = new Map(); + +export function defaultAvatarUrlForString(s: string): string { + if (!s) return ""; // XXX: should never happen but empirically does by evidence of a rageshake + const defaultColors = ['#0DBD8B', '#368bd6', '#ac3ba8']; + let total = 0; + for (let i = 0; i < s.length; ++i) { + total += s.charCodeAt(i); + } + const colorIndex = total % defaultColors.length; + // overwritten color value in custom themes + const cssVariable = `--avatar-background-colors_${colorIndex}`; + const cssValue = document.body.style.getPropertyValue(cssVariable); + const color = cssValue || defaultColors[colorIndex]; + let dataUrl = colorToDataURLCache.get(color); + if (!dataUrl) { + // validate color as this can come from account_data + // with custom theming + if (isValidHexColor(color)) { + dataUrl = urlForColor(color); + colorToDataURLCache.set(color, dataUrl); + } else { + dataUrl = ""; + } + } + return dataUrl; +} + +/** + * returns the first (non-sigil) character of 'name', + * converted to uppercase + * @param {string} name + * @return {string} the first letter + */ +export function getInitialLetter(name: string): string { + if (!name) { + // XXX: We should find out what causes the name to sometimes be falsy. + console.trace("`name` argument to `getInitialLetter` not supplied"); + return undefined; + } + if (name.length < 1) { + return undefined; + } + + let idx = 0; + const initial = name[0]; + if ((initial === '@' || initial === '#' || initial === '+') && name[1]) { + idx++; + } + + // string.codePointAt(0) would do this, but that isn't supported by + // some browsers (notably PhantomJS). + let chars = 1; + const first = name.charCodeAt(idx); + + // check if it’s the start of a surrogate pair + if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) { + const second = name.charCodeAt(idx+1); + if (second >= 0xDC00 && second <= 0xDFFF) { + chars++; + } + } + + const firstChar = name.substring(idx, idx+chars); + return firstChar.toUpperCase(); +} + +export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) { + if (!room) return null; // null-guard + + if (room.getMxcAvatarUrl()) { + return mediaFromMxc(room.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); + } + + // space rooms cannot be DMs so skip the rest + if (SpaceStore.spacesEnabled && room.isSpaceRoom()) return null; + + let otherMember = null; + const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId); + if (otherUserId) { + otherMember = room.getMember(otherUserId); + } else { + // if the room is not marked as a 1:1, but only has max 2 members + // then still try to show any avatar (pref. other member) + otherMember = room.getAvatarFallbackMember(); + } + if (otherMember?.getMxcAvatarUrl()) { + return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); + } + return null; +} diff --git a/src/BasePlatform.js b/src/BasePlatform.js deleted file mode 100644 index 5d809eb28f..0000000000 --- a/src/BasePlatform.js +++ /dev/null @@ -1,191 +0,0 @@ -// @flow - -/* -Copyright 2016 Aviral Dasgupta -Copyright 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd -Copyright 2020 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 {MatrixClient} from "matrix-js-sdk"; -import dis from './dispatcher'; -import BaseEventIndexManager from './indexing/BaseEventIndexManager'; - -/** - * Base class for classes that provide platform-specific functionality - * eg. Setting an application badge or displaying notifications - * - * Instances of this class are provided by the application. - */ -export default class BasePlatform { - constructor() { - this.notificationCount = 0; - this.errorDidOccur = false; - - dis.register(this._onAction.bind(this)); - } - - _onAction(payload: Object) { - switch (payload.action) { - case 'on_client_not_viable': - case 'on_logged_out': - this.setNotificationCount(0); - break; - } - } - - // Used primarily for Analytics - getHumanReadableName(): string { - return 'Base Platform'; - } - - setNotificationCount(count: number) { - this.notificationCount = count; - } - - setErrorStatus(errorDidOccur: boolean) { - this.errorDidOccur = errorDidOccur; - } - - /** - * Returns true if the platform supports displaying - * notifications, otherwise false. - * @returns {boolean} whether the platform supports displaying notifications - */ - supportsNotifications(): boolean { - return false; - } - - /** - * Returns true if the application currently has permission - * to display notifications. Otherwise false. - * @returns {boolean} whether the application has permission to display notifications - */ - maySendNotifications(): boolean { - return false; - } - - /** - * Requests permission to send notifications. Returns - * a promise that is resolved when the user has responded - * to the request. The promise has a single string argument - * that is 'granted' if the user allowed the request or - * 'denied' otherwise. - */ - requestNotificationPermission(): Promise { - } - - displayNotification(title: string, msg: string, avatarUrl: string, room: Object) { - } - - loudNotification(ev: Event, room: Object) { - } - - /** - * Returns a promise that resolves to a string representing - * the current version of the application. - */ - getAppVersion(): Promise { - throw new Error("getAppVersion not implemented!"); - } - - /* - * If it's not expected that capturing the screen will work - * with getUserMedia, return a string explaining why not. - * Otherwise, return null. - */ - screenCaptureErrorString(): string { - return "Not implemented"; - } - - /** - * Restarts the application, without neccessarily reloading - * any application code - */ - reload() { - throw new Error("reload not implemented!"); - } - - supportsAutoLaunch(): boolean { - return false; - } - - // XXX: Surely this should be a setting like any other? - async getAutoLaunchEnabled(): boolean { - return false; - } - - async setAutoLaunchEnabled(enabled: boolean): void { - throw new Error("Unimplemented"); - } - - supportsAutoHideMenuBar(): boolean { - return false; - } - - async getAutoHideMenuBarEnabled(): boolean { - return false; - } - - async setAutoHideMenuBarEnabled(enabled: boolean): void { - throw new Error("Unimplemented"); - } - - supportsMinimizeToTray(): boolean { - return false; - } - - async getMinimizeToTrayEnabled(): boolean { - return false; - } - - async setMinimizeToTrayEnabled(enabled: boolean): void { - throw new Error("Unimplemented"); - } - - /** - * Get our platform specific EventIndexManager. - * - * @return {BaseEventIndexManager} The EventIndex manager for our platform, - * can be null if the platform doesn't support event indexing. - */ - getEventIndexingManager(): BaseEventIndexManager | null { - return null; - } - - setLanguage(preferredLangs: string[]) {} - - getSSOCallbackUrl(hsUrl: string, isUrl: string): URL { - const url = new URL(window.location.href); - // XXX: at this point, the fragment will always be #/login, which is no - // use to anyone. Ideally, we would get the intended fragment from - // MatrixChat.screenAfterLogin so that you could follow #/room links etc - // through an SSO login. - url.hash = ""; - url.searchParams.set("homeserver", hsUrl); - url.searchParams.set("identityServer", isUrl); - return url; - } - - /** - * Begin Single Sign On flows. - * @param {MatrixClient} mxClient the matrix client using which we should start the flow - * @param {"sso"|"cas"} loginType the type of SSO it is, CAS/SSO. - */ - startSingleSignOn(mxClient: MatrixClient, loginType: "sso"|"cas") { - const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl()); - window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO - } -} diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts new file mode 100644 index 0000000000..5b4b15cc67 --- /dev/null +++ b/src/BasePlatform.ts @@ -0,0 +1,399 @@ +/* +Copyright 2016 Aviral Dasgupta +Copyright 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd +Copyright 2020 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 { MatrixClient } from "matrix-js-sdk/src/client"; +import { encodeUnpaddedBase64 } from "matrix-js-sdk/src/crypto/olmlib"; +import dis from './dispatcher/dispatcher'; +import BaseEventIndexManager from './indexing/BaseEventIndexManager'; +import { ActionPayload } from "./dispatcher/payloads"; +import { CheckUpdatesPayload } from "./dispatcher/payloads/CheckUpdatesPayload"; +import { Action } from "./dispatcher/actions"; +import { hideToast as hideUpdateToast } from "./toasts/UpdateToast"; +import { MatrixClientPeg } from "./MatrixClientPeg"; +import { idbLoad, idbSave, idbDelete } from "./utils/StorageManager"; + +export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url"; +export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url"; +export const SSO_IDP_ID_KEY = "mx_sso_idp_id"; + +export enum UpdateCheckStatus { + Checking = "CHECKING", + Error = "ERROR", + NotAvailable = "NOTAVAILABLE", + Downloading = "DOWNLOADING", + Ready = "READY", +} + +const UPDATE_DEFER_KEY = "mx_defer_update"; + +/** + * Base class for classes that provide platform-specific functionality + * eg. Setting an application badge or displaying notifications + * + * Instances of this class are provided by the application. + */ +export default abstract class BasePlatform { + protected notificationCount = 0; + protected errorDidOccur = false; + + constructor() { + dis.register(this.onAction); + this.startUpdateCheck = this.startUpdateCheck.bind(this); + } + + abstract getConfig(): Promise<{}>; + + abstract getDefaultDeviceDisplayName(): string; + + protected onAction = (payload: ActionPayload) => { + switch (payload.action) { + case 'on_client_not_viable': + case 'on_logged_out': + this.setNotificationCount(0); + break; + } + }; + + // Used primarily for Analytics + abstract getHumanReadableName(): string; + + setNotificationCount(count: number) { + this.notificationCount = count; + } + + setErrorStatus(errorDidOccur: boolean) { + this.errorDidOccur = errorDidOccur; + } + + /** + * Whether we can call checkForUpdate on this platform build + */ + async canSelfUpdate(): Promise { + return false; + } + + startUpdateCheck() { + hideUpdateToast(); + localStorage.removeItem(UPDATE_DEFER_KEY); + dis.dispatch({ + action: Action.CheckUpdates, + status: UpdateCheckStatus.Checking, + }); + } + + /** + * Update the currently running app to the latest available version + * and replace this instance of the app with the new version. + */ + installUpdate() { + } + + /** + * Check if the version update has been deferred and that deferment is still in effect + * @param newVersion the version string to check + */ + protected shouldShowUpdate(newVersion: string): boolean { + // If the user registered on this client in the last 24 hours then do not show them the update toast + if (MatrixClientPeg.userRegisteredWithinLastHours(24)) return false; + + try { + const [version, deferUntil] = JSON.parse(localStorage.getItem(UPDATE_DEFER_KEY)); + return newVersion !== version || Date.now() > deferUntil; + } catch (e) { + return true; + } + } + + /** + * Ignore the pending update and don't prompt about this version + * until the next morning (8am). + */ + deferUpdate(newVersion: string) { + const date = new Date(Date.now() + 24 * 60 * 60 * 1000); + date.setHours(8, 0, 0, 0); // set to next 8am + localStorage.setItem(UPDATE_DEFER_KEY, JSON.stringify([newVersion, date.getTime()])); + hideUpdateToast(); + } + + /** + * Return true if platform supports multi-language + * spell-checking, otherwise false. + */ + supportsMultiLanguageSpellCheck(): boolean { + return false; + } + + /** + * Returns true if the platform supports displaying + * notifications, otherwise false. + * @returns {boolean} whether the platform supports displaying notifications + */ + supportsNotifications(): boolean { + return false; + } + + /** + * Returns true if the application currently has permission + * to display notifications. Otherwise false. + * @returns {boolean} whether the application has permission to display notifications + */ + maySendNotifications(): boolean { + return false; + } + + /** + * Requests permission to send notifications. Returns + * a promise that is resolved when the user has responded + * to the request. The promise has a single string argument + * that is 'granted' if the user allowed the request or + * 'denied' otherwise. + */ + abstract requestNotificationPermission(): Promise; + + abstract displayNotification(title: string, msg: string, avatarUrl: string, room: Object); + + loudNotification(ev: Event, room: Object) { + } + + clearNotification(notif: Notification) { + // Some browsers don't support this, e.g Safari on iOS + // https://developer.mozilla.org/en-US/docs/Web/API/Notification/close + if (notif.close) { + notif.close(); + } + } + + /** + * Returns a promise that resolves to a string representing the current version of the application. + */ + abstract getAppVersion(): Promise; + + /* + * If it's not expected that capturing the screen will work + * with getUserMedia, return a string explaining why not. + * Otherwise, return null. + */ + screenCaptureErrorString(): string { + return "Not implemented"; + } + + /** + * Restarts the application, without neccessarily reloading + * any application code + */ + abstract reload(); + + supportsAutoLaunch(): boolean { + return false; + } + + // XXX: Surely this should be a setting like any other? + async getAutoLaunchEnabled(): Promise { + return false; + } + + async setAutoLaunchEnabled(enabled: boolean): Promise { + throw new Error("Unimplemented"); + } + + supportsWarnBeforeExit(): boolean { + return false; + } + + async shouldWarnBeforeExit(): Promise { + return false; + } + + async setWarnBeforeExit(enabled: boolean): Promise { + throw new Error("Unimplemented"); + } + + supportsAutoHideMenuBar(): boolean { + return false; + } + + async getAutoHideMenuBarEnabled(): Promise { + return false; + } + + async setAutoHideMenuBarEnabled(enabled: boolean): Promise { + throw new Error("Unimplemented"); + } + + supportsMinimizeToTray(): boolean { + return false; + } + + async getMinimizeToTrayEnabled(): Promise { + return false; + } + + async setMinimizeToTrayEnabled(enabled: boolean): Promise { + throw new Error("Unimplemented"); + } + + /** + * Get our platform specific EventIndexManager. + * + * @return {BaseEventIndexManager} The EventIndex manager for our platform, + * can be null if the platform doesn't support event indexing. + */ + getEventIndexingManager(): BaseEventIndexManager | null { + return null; + } + + async setLanguage(preferredLangs: string[]) {} + + setSpellCheckLanguages(preferredLangs: string[]) {} + + getSpellCheckLanguages(): Promise | null { + return null; + } + + getAvailableSpellCheckLanguages(): Promise | null { + return null; + } + + protected getSSOCallbackUrl(fragmentAfterLogin: string): URL { + const url = new URL(window.location.href); + url.hash = fragmentAfterLogin || ""; + return url; + } + + /** + * Begin Single Sign On flows. + * @param {MatrixClient} mxClient the matrix client using which we should start the flow + * @param {"sso"|"cas"} loginType the type of SSO it is, CAS/SSO. + * @param {string} fragmentAfterLogin the hash to pass to the app during sso callback. + * @param {string} idpId The ID of the Identity Provider being targeted, optional. + */ + startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string, idpId?: string) { + // persist hs url and is url for when the user is returned to the app with the login token + localStorage.setItem(SSO_HOMESERVER_URL_KEY, mxClient.getHomeserverUrl()); + if (mxClient.getIdentityServerUrl()) { + localStorage.setItem(SSO_ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl()); + } + if (idpId) { + localStorage.setItem(SSO_IDP_ID_KEY, idpId); + } + const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin); + window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType, idpId); // redirect to SSO + } + + onKeyDown(ev: KeyboardEvent): boolean { + return false; // no shortcuts implemented + } + + /** + * Get a previously stored pickle key. The pickle key is used for + * encrypting libolm objects. + * @param {string} userId the user ID for the user that the pickle key is for. + * @param {string} userId the device ID that the pickle key is for. + * @returns {string|null} the previously stored pickle key, or null if no + * pickle key has been stored. + */ + async getPickleKey(userId: string, deviceId: string): Promise { + if (!window.crypto || !window.crypto.subtle) { + return null; + } + let data; + try { + data = await idbLoad("pickleKey", [userId, deviceId]); + } catch (e) {} + if (!data) { + return null; + } + if (!data.encrypted || !data.iv || !data.cryptoKey) { + console.error("Badly formatted pickle key"); + return null; + } + + const additionalData = new Uint8Array(userId.length + deviceId.length + 1); + for (let i = 0; i < userId.length; i++) { + additionalData[i] = userId.charCodeAt(i); + } + additionalData[userId.length] = 124; // "|" + for (let i = 0; i < deviceId.length; i++) { + additionalData[userId.length + 1 + i] = deviceId.charCodeAt(i); + } + + try { + const key = await crypto.subtle.decrypt( + { name: "AES-GCM", iv: data.iv, additionalData }, data.cryptoKey, + data.encrypted, + ); + return encodeUnpaddedBase64(key); + } catch (e) { + console.error("Error decrypting pickle key"); + return null; + } + } + + /** + * Create and store a pickle key for encrypting libolm objects. + * @param {string} userId the user ID for the user that the pickle key is for. + * @param {string} deviceId the device ID that the pickle key is for. + * @returns {string|null} the pickle key, or null if the platform does not + * support storing pickle keys. + */ + async createPickleKey(userId: string, deviceId: string): Promise { + if (!window.crypto || !window.crypto.subtle) { + return null; + } + const crypto = window.crypto; + const randomArray = new Uint8Array(32); + crypto.getRandomValues(randomArray); + const cryptoKey = await crypto.subtle.generateKey( + { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"], + ); + const iv = new Uint8Array(32); + crypto.getRandomValues(iv); + + const additionalData = new Uint8Array(userId.length + deviceId.length + 1); + for (let i = 0; i < userId.length; i++) { + additionalData[i] = userId.charCodeAt(i); + } + additionalData[userId.length] = 124; // "|" + for (let i = 0; i < deviceId.length; i++) { + additionalData[userId.length + 1 + i] = deviceId.charCodeAt(i); + } + + const encrypted = await crypto.subtle.encrypt( + { name: "AES-GCM", iv, additionalData }, cryptoKey, randomArray, + ); + + try { + await idbSave("pickleKey", [userId, deviceId], { encrypted, iv, cryptoKey }); + } catch (e) { + return null; + } + return encodeUnpaddedBase64(randomArray); + } + + /** + * Delete a previously stored pickle key from storage. + * @param {string} userId the user ID for the user that the pickle key is for. + * @param {string} userId the device ID that the pickle key is for. + */ + async destroyPickleKey(userId: string, deviceId: string): Promise { + try { + await idbDelete("pickleKey", [userId, deviceId]); + } catch (e) {} + } +} diff --git a/src/BlurhashEncoder.ts b/src/BlurhashEncoder.ts new file mode 100644 index 0000000000..2aee370fe9 --- /dev/null +++ b/src/BlurhashEncoder.ts @@ -0,0 +1,60 @@ +/* +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 { defer, IDeferred } from "matrix-js-sdk/src/utils"; + +// @ts-ignore - `.ts` is needed here to make TS happy +import BlurhashWorker from "./workers/blurhash.worker.ts"; + +interface IBlurhashWorkerResponse { + seq: number; + blurhash: string; +} + +export class BlurhashEncoder { + private static internalInstance = new BlurhashEncoder(); + + public static get instance(): BlurhashEncoder { + return BlurhashEncoder.internalInstance; + } + + private readonly worker: Worker; + private seq = 0; + private pendingDeferredMap = new Map>(); + + constructor() { + this.worker = new BlurhashWorker(); + this.worker.onmessage = this.onMessage; + } + + private onMessage = (ev: MessageEvent) => { + const { seq, blurhash } = ev.data; + const deferred = this.pendingDeferredMap.get(seq); + if (deferred) { + this.pendingDeferredMap.delete(seq); + deferred.resolve(blurhash); + } + }; + + public getBlurhash(imageData: ImageData): Promise { + const seq = this.seq++; + const deferred = defer(); + this.pendingDeferredMap.set(seq, deferred); + this.worker.postMessage({ seq, imageData }); + return deferred.promise; + } +} + diff --git a/src/CallHandler.js b/src/CallHandler.js deleted file mode 100644 index c63bfe309a..0000000000 --- a/src/CallHandler.js +++ /dev/null @@ -1,551 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017, 2018 New Vector Ltd -Copyright 2019 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. -*/ - -/* - * Manages a list of all the currently active calls. - * - * This handler dispatches when voip calls are added/updated/removed from this list: - * { - * action: 'call_state' - * room_id: - * } - * - * To know the state of the call, this handler exposes a getter to - * obtain the call for a room: - * var call = CallHandler.getCall(roomId) - * var state = call.call_state; // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing - * - * This handler listens for and handles the following actions: - * { - * action: 'place_call', - * type: 'voice|video', - * room_id: - * } - * - * { - * action: 'incoming_call' - * call: MatrixCall - * } - * - * { - * action: 'hangup' - * room_id: - * } - * - * { - * action: 'answer' - * room_id: - * } - */ - -import {MatrixClientPeg} from './MatrixClientPeg'; -import PlatformPeg from './PlatformPeg'; -import Modal from './Modal'; -import * as sdk from './index'; -import { _t } from './languageHandler'; -import Matrix from 'matrix-js-sdk'; -import dis from './dispatcher'; -import { showUnknownDeviceDialogForCalls } from './cryptodevices'; -import WidgetUtils from './utils/WidgetUtils'; -import WidgetEchoStore from './stores/WidgetEchoStore'; -import SettingsStore, { SettingLevel } from './settings/SettingsStore'; -import {generateHumanReadableId} from "./utils/NamingUtils"; -import {Jitsi} from "./widgets/Jitsi"; - -global.mxCalls = { - //room_id: MatrixCall -}; -const calls = global.mxCalls; -let ConferenceHandler = null; - -const audioPromises = {}; - -function play(audioId) { - // TODO: Attach an invisible element for this instead - // which listens? - const audio = document.getElementById(audioId); - if (audio) { - const playAudio = async () => { - try { - // This still causes the chrome debugger to break on promise rejection if - // the promise is rejected, even though we're catching the exception. - await audio.play(); - } catch (e) { - // This is usually because the user hasn't interacted with the document, - // or chrome doesn't think so and is denying the request. Not sure what - // we can really do here... - // https://github.com/vector-im/riot-web/issues/7657 - console.log("Unable to play audio clip", e); - } - }; - if (audioPromises[audioId]) { - audioPromises[audioId] = audioPromises[audioId].then(()=>{ - audio.load(); - return playAudio(); - }); - } else { - audioPromises[audioId] = playAudio(); - } - } -} - -function pause(audioId) { - // TODO: Attach an invisible element for this instead - // which listens? - const audio = document.getElementById(audioId); - if (audio) { - if (audioPromises[audioId]) { - audioPromises[audioId] = audioPromises[audioId].then(()=>audio.pause()); - } else { - // pause doesn't actually return a promise, but might as well do this for symmetry with play(); - audioPromises[audioId] = audio.pause(); - } - } -} - -function _reAttemptCall(call) { - if (call.direction === 'outbound') { - dis.dispatch({ - action: 'place_call', - room_id: call.roomId, - type: call.type, - }); - } else { - call.answer(); - } -} - -function _setCallListeners(call) { - call.on("error", function(err) { - console.error("Call error:", err); - if (err.code === 'unknown_devices') { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - - Modal.createTrackedDialog('Call Failed', '', QuestionDialog, { - title: _t('Call Failed'), - description: _t( - "There are unknown sessions in this room: "+ - "if you proceed without verifying them, it will be "+ - "possible for someone to eavesdrop on your call.", - ), - button: _t('Review Sessions'), - onFinished: function(confirmed) { - if (confirmed) { - const room = MatrixClientPeg.get().getRoom(call.roomId); - showUnknownDeviceDialogForCalls( - MatrixClientPeg.get(), - room, - () => { - _reAttemptCall(call); - }, - call.direction === 'outbound' ? _t("Call Anyway") : _t("Answer Anyway"), - call.direction === 'outbound' ? _t("Call") : _t("Answer"), - ); - } - }, - }); - } else { - if ( - MatrixClientPeg.get().getTurnServers().length === 0 && - SettingsStore.getValue("fallbackICEServerAllowed") === null - ) { - _showICEFallbackPrompt(); - return; - } - - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Call Failed', '', ErrorDialog, { - title: _t('Call Failed'), - description: err.message, - }); - } - }); - call.on("hangup", function() { - _setCallState(undefined, call.roomId, "ended"); - }); - // map web rtc states to dummy UI state - // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing - call.on("state", function(newState, oldState) { - if (newState === "ringing") { - _setCallState(call, call.roomId, "ringing"); - pause("ringbackAudio"); - } else if (newState === "invite_sent") { - _setCallState(call, call.roomId, "ringback"); - play("ringbackAudio"); - } else if (newState === "ended" && oldState === "connected") { - _setCallState(undefined, call.roomId, "ended"); - pause("ringbackAudio"); - play("callendAudio"); - } else if (newState === "ended" && oldState === "invite_sent" && - (call.hangupParty === "remote" || - (call.hangupParty === "local" && call.hangupReason === "invite_timeout") - )) { - _setCallState(call, call.roomId, "busy"); - pause("ringbackAudio"); - play("busyAudio"); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, { - title: _t('Call Timeout'), - description: _t('The remote side failed to pick up') + '.', - }); - } else if (oldState === "invite_sent") { - _setCallState(call, call.roomId, "stop_ringback"); - pause("ringbackAudio"); - } else if (oldState === "ringing") { - _setCallState(call, call.roomId, "stop_ringing"); - pause("ringbackAudio"); - } else if (newState === "connected") { - _setCallState(call, call.roomId, "connected"); - pause("ringbackAudio"); - } - }); -} - -function _setCallState(call, roomId, status) { - console.log( - `Call state in ${roomId} changed to ${status} (${call ? call.call_state : "-"})`, - ); - calls[roomId] = call; - - if (status === "ringing") { - play("ringAudio"); - } else if (call && call.call_state === "ringing") { - pause("ringAudio"); - } - - if (call) { - call.call_state = status; - } - dis.dispatch({ - action: 'call_state', - room_id: roomId, - state: status, - }); -} - -function _showICEFallbackPrompt() { - const cli = MatrixClientPeg.get(); - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const code = sub => {sub}; - Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, { - title: _t("Call failed due to misconfigured server"), - description:
-

{_t( - "Please ask the administrator of your homeserver " + - "(%(homeserverDomain)s) to configure a TURN server in " + - "order for calls to work reliably.", - { homeserverDomain: cli.getDomain() }, { code }, - )}

-

{_t( - "Alternatively, you can try to use the public server at " + - "turn.matrix.org, but this will not be as reliable, and " + - "it will share your IP address with that server. You can also manage " + - "this in Settings.", - null, { code }, - )}

-
, - button: _t('Try using turn.matrix.org'), - cancelButton: _t('OK'), - onFinished: (allow) => { - SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow); - cli.setFallbackICEServerAllowed(allow); - }, - }, null, true); -} - -function _onAction(payload) { - function placeCall(newCall) { - _setCallListeners(newCall); - if (payload.type === 'voice') { - newCall.placeVoiceCall(); - } else if (payload.type === 'video') { - newCall.placeVideoCall( - payload.remote_element, - payload.local_element, - ); - } else if (payload.type === 'screensharing') { - const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString(); - if (screenCapErrorString) { - _setCallState(undefined, newCall.roomId, "ended"); - console.log("Can't capture screen: " + screenCapErrorString); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, { - title: _t('Unable to capture screen'), - description: screenCapErrorString, - }); - return; - } - newCall.placeScreenSharingCall( - payload.remote_element, - payload.local_element, - ); - } else { - console.error("Unknown conf call type: %s", payload.type); - } - } - - switch (payload.action) { - case 'place_call': - { - if (callHandler.getAnyActiveCall()) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, { - title: _t('Existing Call'), - description: _t('You are already in a call.'), - }); - return; // don't allow >1 call to be placed. - } - - // if the runtime env doesn't do VoIP, whine. - if (!MatrixClientPeg.get().supportsVoip()) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, { - title: _t('VoIP is unsupported'), - description: _t('You cannot place VoIP calls in this browser.'), - }); - return; - } - - const room = MatrixClientPeg.get().getRoom(payload.room_id); - if (!room) { - console.error("Room %s does not exist.", payload.room_id); - return; - } - - const members = room.getJoinedMembers(); - if (members.length <= 1) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, { - description: _t('You cannot place a call with yourself.'), - }); - return; - } else if (members.length === 2) { - console.info("Place %s call in %s", payload.type, payload.room_id); - const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id); - placeCall(call); - } else { // > 2 - dis.dispatch({ - action: "place_conference_call", - room_id: payload.room_id, - type: payload.type, - remote_element: payload.remote_element, - local_element: payload.local_element, - }); - } - } - break; - case 'place_conference_call': - console.info("Place conference call in %s", payload.room_id); - _startCallApp(payload.room_id, payload.type); - break; - case 'incoming_call': - { - if (callHandler.getAnyActiveCall()) { - // ignore multiple incoming calls. in future, we may want a line-1/line-2 setup. - // we avoid rejecting with "busy" in case the user wants to answer it on a different device. - // in future we could signal a "local busy" as a warning to the caller. - // see https://github.com/vector-im/vector-web/issues/1964 - return; - } - - // if the runtime env doesn't do VoIP, stop here. - if (!MatrixClientPeg.get().supportsVoip()) { - return; - } - - const call = payload.call; - _setCallListeners(call); - _setCallState(call, call.roomId, "ringing"); - } - break; - case 'hangup': - if (!calls[payload.room_id]) { - return; // no call to hangup - } - calls[payload.room_id].hangup(); - _setCallState(null, payload.room_id, "ended"); - break; - case 'answer': - if (!calls[payload.room_id]) { - return; // no call to answer - } - calls[payload.room_id].answer(); - _setCallState(calls[payload.room_id], payload.room_id, "connected"); - dis.dispatch({ - action: "view_room", - room_id: payload.room_id, - }); - break; - } -} - -async function _startCallApp(roomId, type) { - dis.dispatch({ - action: 'appsDrawer', - show: true, - }); - - const room = MatrixClientPeg.get().getRoom(roomId); - const currentRoomWidgets = WidgetUtils.getRoomWidgets(room); - - if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentRoomWidgets, 'jitsi')) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - - Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, { - title: _t('Call in Progress'), - description: _t('A call is currently being placed!'), - }); - return; - } - - const currentJitsiWidgets = currentRoomWidgets.filter((ev) => { - return ev.getContent().type === 'jitsi'; - }); - if (currentJitsiWidgets.length > 0) { - console.warn( - "Refusing to start conference call widget in " + roomId + - " a conference call widget is already present", - ); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - - Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, { - title: _t('Call in Progress'), - description: _t('A call is already in progress!'), - }); - return; - } - - const confId = `JitsiConference${generateHumanReadableId()}`; - const jitsiDomain = Jitsi.getInstance().preferredDomain; - - let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl(); - - // TODO: Remove URL hacks when the mobile clients eventually support v2 widgets - const parsedUrl = new URL(widgetUrl); - parsedUrl.search = ''; // set to empty string to make the URL class use searchParams instead - parsedUrl.searchParams.set('confId', confId); - widgetUrl = parsedUrl.toString(); - - const widgetData = { - conferenceId: confId, - isAudioOnly: type === 'voice', - domain: jitsiDomain, - }; - - const widgetId = ( - 'jitsi_' + - MatrixClientPeg.get().credentials.userId + - '_' + - Date.now() - ); - - WidgetUtils.setRoomWidget(roomId, widgetId, 'jitsi', widgetUrl, 'Jitsi', widgetData).then(() => { - console.log('Jitsi widget added'); - }).catch((e) => { - if (e.errcode === 'M_FORBIDDEN') { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - - Modal.createTrackedDialog('Call Failed', '', ErrorDialog, { - title: _t('Permission Required'), - description: _t("You do not have permission to start a conference call in this room"), - }); - } - console.error(e); - }); -} - -// FIXME: Nasty way of making sure we only register -// with the dispatcher once -if (!global.mxCallHandler) { - dis.register(_onAction); - // add empty handlers for media actions, otherwise the media keys - // end up causing the audio elements with our ring/ringback etc - // audio clips in to play. - if (navigator.mediaSession) { - navigator.mediaSession.setActionHandler('play', function() {}); - navigator.mediaSession.setActionHandler('pause', function() {}); - navigator.mediaSession.setActionHandler('seekbackward', function() {}); - navigator.mediaSession.setActionHandler('seekforward', function() {}); - navigator.mediaSession.setActionHandler('previoustrack', function() {}); - navigator.mediaSession.setActionHandler('nexttrack', function() {}); - } -} - -const callHandler = { - getCallForRoom: function(roomId) { - let call = callHandler.getCall(roomId); - if (call) return call; - - if (ConferenceHandler) { - call = ConferenceHandler.getConferenceCallForRoom(roomId); - } - if (call) return call; - - return null; - }, - - getCall: function(roomId) { - return calls[roomId] || null; - }, - - getAnyActiveCall: function() { - const roomsWithCalls = Object.keys(calls); - for (let i = 0; i < roomsWithCalls.length; i++) { - if (calls[roomsWithCalls[i]] && - calls[roomsWithCalls[i]].call_state !== "ended") { - return calls[roomsWithCalls[i]]; - } - } - return null; - }, - - /** - * The conference handler is a module that deals with implementation-specific - * multi-party calling implementations. Riot passes in its own which creates - * a one-to-one call with a freeswitch conference bridge. As of July 2018, - * the de-facto way of conference calling is a Jitsi widget, so this is - * deprecated. It reamins here for two reasons: - * 1. So Riot still supports joining existing freeswitch conference calls - * (but doesn't support creating them). After a transition period, we can - * remove support for joining them too. - * 2. To hide the one-to-one rooms that old-style conferencing creates. This - * is much harder to remove: probably either we make Riot leave & forget these - * rooms after we remove support for joining freeswitch conferences, or we - * accept that random rooms with cryptic users will suddently appear for - * anyone who's ever used conference calling, or we are stuck with this - * code forever. - * - * @param {object} confHandler The conference handler object - */ - setConferenceHandler: function(confHandler) { - ConferenceHandler = confHandler; - }, - - getConferenceHandler: function() { - return ConferenceHandler; - }, -}; -// Only things in here which actually need to be global are the -// calls list (done separately) and making sure we only register -// with the dispatcher once (which uses this mechanism but checks -// separately). This could be tidied up. -if (global.mxCallHandler === undefined) { - global.mxCallHandler = callHandler; -} - -export default global.mxCallHandler; diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx new file mode 100644 index 0000000000..f90854ee64 --- /dev/null +++ b/src/CallHandler.tsx @@ -0,0 +1,1086 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017, 2018 New Vector Ltd +Copyright 2019, 2020 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. +*/ + +/* + * Manages a list of all the currently active calls. + * + * This handler dispatches when voip calls are added/updated/removed from this list: + * { + * action: 'call_state' + * room_id: + * } + * + * To know the state of the call, this handler exposes a getter to + * obtain the call for a room: + * var call = CallHandler.getCall(roomId) + * var state = call.call_state; // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing + * + * This handler listens for and handles the following actions: + * { + * action: 'place_call', + * type: 'voice|video', + * room_id: + * } + * + * { + * action: 'incoming_call' + * call: MatrixCall + * } + * + * { + * action: 'hangup' + * room_id: + * } + * + * { + * action: 'answer' + * room_id: + * } + */ + +import React from 'react'; + +import { MatrixClientPeg } from './MatrixClientPeg'; +import PlatformPeg from './PlatformPeg'; +import Modal from './Modal'; +import { _t } from './languageHandler'; +import dis from './dispatcher/dispatcher'; +import WidgetUtils from './utils/WidgetUtils'; +import WidgetEchoStore from './stores/WidgetEchoStore'; +import SettingsStore from './settings/SettingsStore'; +import { Jitsi } from "./widgets/Jitsi"; +import { WidgetType } from "./widgets/WidgetType"; +import { SettingLevel } from "./settings/SettingLevel"; +import { ActionPayload } from "./dispatcher/payloads"; +import { base32 } from "rfc4648"; + +import QuestionDialog from "./components/views/dialogs/QuestionDialog"; +import ErrorDialog from "./components/views/dialogs/ErrorDialog"; +import WidgetStore from "./stores/WidgetStore"; +import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore"; +import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions"; +import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType } from "matrix-js-sdk/src/webrtc/call"; +import Analytics from './Analytics'; +import CountlyAnalytics from "./CountlyAnalytics"; +import { UIFeature } from "./settings/UIFeature"; +import { CallError } from "matrix-js-sdk/src/webrtc/call"; +import { logger } from 'matrix-js-sdk/src/logger'; +import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker"; +import { Action } from './dispatcher/actions'; +import VoipUserMapper from './VoipUserMapper'; +import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid'; +import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring"; +import EventEmitter from 'events'; +import SdkConfig from './SdkConfig'; +import { ensureDMExists, findDMForUser } from './createRoom'; + +export const PROTOCOL_PSTN = 'm.protocol.pstn'; +export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn'; +export const PROTOCOL_SIP_NATIVE = 'im.vector.protocol.sip_native'; +export const PROTOCOL_SIP_VIRTUAL = 'im.vector.protocol.sip_virtual'; + +const CHECK_PROTOCOLS_ATTEMPTS = 3; +// Event type for room account data and room creation content used to mark rooms as virtual rooms +// (and store the ID of their native room) +export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room'; + +export enum AudioID { + Ring = 'ringAudio', + Ringback = 'ringbackAudio', + CallEnd = 'callendAudio', + Busy = 'busyAudio', +} + +interface ThirdpartyLookupResponseFields { + /* eslint-disable camelcase */ + + // im.vector.sip_native + virtual_mxid?: string; + is_virtual?: boolean; + + // im.vector.sip_virtual + native_mxid?: string; + is_native?: boolean; + + // common + lookup_success?: boolean; + + /* eslint-enable camelcase */ +} + +interface ThirdpartyLookupResponse { + userid: string; + protocol: string; + fields: ThirdpartyLookupResponseFields; +} + +// Unlike 'CallType' in js-sdk, this one includes screen sharing +// (because a screen sharing call is only a screen sharing call to the caller, +// to the callee it's just a video call, at least as far as the current impl +// is concerned). +export enum PlaceCallType { + Voice = 'voice', + Video = 'video', + ScreenSharing = 'screensharing', +} + +export enum CallHandlerEvent { + CallsChanged = "calls_changed", + CallChangeRoom = "call_change_room", +} + +export default class CallHandler extends EventEmitter { + private calls = new Map(); // roomId -> call + // Calls started as an attended transfer, ie. with the intention of transferring another + // call with a different party to this one. + private transferees = new Map(); // callId (target) -> call (transferee) + private audioPromises = new Map>(); + private dispatcherRef: string = null; + private supportsPstnProtocol = null; + private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol + private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native + private pstnSupportCheckTimer: number; + // For rooms we've been invited to, true if they're from virtual user, false if we've checked and they aren't. + private invitedRoomsAreVirtual = new Map(); + private invitedRoomCheckInProgress = false; + + // Map of the asserted identity users after we've looked them up using the API. + // We need to be be able to determine the mapped room synchronously, so we + // do the async lookup when we get new information and then store these mappings here + private assertedIdentityNativeUsers = new Map(); + + static sharedInstance() { + if (!window.mxCallHandler) { + window.mxCallHandler = new CallHandler(); + } + + return window.mxCallHandler; + } + + /* + * Gets the user-facing room associated with a call (call.roomId may be the call "virtual room" + * if a voip_mxid_translate_pattern is set in the config) + */ + public roomIdForCall(call: MatrixCall): string { + if (!call) return null; + + const voipConfig = SdkConfig.get()['voip']; + + if (voipConfig && voipConfig.obeyAssertedIdentity) { + const nativeUser = this.assertedIdentityNativeUsers[call.callId]; + if (nativeUser) { + const room = findDMForUser(MatrixClientPeg.get(), nativeUser); + if (room) return room.roomId; + } + } + + return VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(call.roomId) || call.roomId; + } + + start() { + this.dispatcherRef = dis.register(this.onAction); + // add empty handlers for media actions, otherwise the media keys + // end up causing the audio elements with our ring/ringback etc + // audio clips in to play. + if (navigator.mediaSession) { + navigator.mediaSession.setActionHandler('play', function() {}); + navigator.mediaSession.setActionHandler('pause', function() {}); + navigator.mediaSession.setActionHandler('seekbackward', function() {}); + navigator.mediaSession.setActionHandler('seekforward', function() {}); + navigator.mediaSession.setActionHandler('previoustrack', function() {}); + navigator.mediaSession.setActionHandler('nexttrack', function() {}); + } + + if (SettingsStore.getValue(UIFeature.Voip)) { + MatrixClientPeg.get().on('Call.incoming', this.onCallIncoming); + } + + this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS); + } + + stop() { + const cli = MatrixClientPeg.get(); + if (cli) { + cli.removeListener('Call.incoming', this.onCallIncoming); + } + if (this.dispatcherRef !== null) { + dis.unregister(this.dispatcherRef); + this.dispatcherRef = null; + } + } + + private async checkProtocols(maxTries) { + try { + const protocols = await MatrixClientPeg.get().getThirdpartyProtocols(); + + if (protocols[PROTOCOL_PSTN] !== undefined) { + this.supportsPstnProtocol = Boolean(protocols[PROTOCOL_PSTN]); + if (this.supportsPstnProtocol) this.pstnSupportPrefixed = false; + } else if (protocols[PROTOCOL_PSTN_PREFIXED] !== undefined) { + this.supportsPstnProtocol = Boolean(protocols[PROTOCOL_PSTN_PREFIXED]); + if (this.supportsPstnProtocol) this.pstnSupportPrefixed = true; + } else { + this.supportsPstnProtocol = null; + } + + dis.dispatch({ action: Action.PstnSupportUpdated }); + + if (protocols[PROTOCOL_SIP_NATIVE] !== undefined && protocols[PROTOCOL_SIP_VIRTUAL] !== undefined) { + this.supportsSipNativeVirtual = Boolean( + protocols[PROTOCOL_SIP_NATIVE] && protocols[PROTOCOL_SIP_VIRTUAL], + ); + } + + dis.dispatch({ action: Action.VirtualRoomSupportUpdated }); + } catch (e) { + if (maxTries === 1) { + console.log("Failed to check for protocol support and no retries remain: assuming no support", e); + } else { + console.log("Failed to check for protocol support: will retry", e); + this.pstnSupportCheckTimer = setTimeout(() => { + this.checkProtocols(maxTries - 1); + }, 10000); + } + } + } + + public getSupportsPstnProtocol() { + return this.supportsPstnProtocol; + } + + public getSupportsVirtualRooms() { + return this.supportsSipNativeVirtual; + } + + public pstnLookup(phoneNumber: string): Promise { + return MatrixClientPeg.get().getThirdpartyUser( + this.pstnSupportPrefixed ? PROTOCOL_PSTN_PREFIXED : PROTOCOL_PSTN, { + 'm.id.phone': phoneNumber, + }, + ); + } + + public sipVirtualLookup(nativeMxid: string): Promise { + return MatrixClientPeg.get().getThirdpartyUser( + PROTOCOL_SIP_VIRTUAL, { + 'native_mxid': nativeMxid, + }, + ); + } + + public sipNativeLookup(virtualMxid: string): Promise { + return MatrixClientPeg.get().getThirdpartyUser( + PROTOCOL_SIP_NATIVE, { + 'virtual_mxid': virtualMxid, + }, + ); + } + + private onCallIncoming = (call) => { + // we dispatch this synchronously to make sure that the event + // handlers on the call are set up immediately (so that if + // we get an immediate hangup, we don't get a stuck call) + dis.dispatch({ + action: 'incoming_call', + call: call, + }, true); + }; + + getCallForRoom(roomId: string): MatrixCall { + return this.calls.get(roomId) || null; + } + + getAnyActiveCall() { + for (const call of this.calls.values()) { + if (call.state !== CallState.Ended) { + return call; + } + } + return null; + } + + getAllActiveCalls() { + const activeCalls = []; + + for (const call of this.calls.values()) { + if (call.state !== CallState.Ended && call.state !== CallState.Ringing) { + activeCalls.push(call); + } + } + return activeCalls; + } + + getAllActiveCallsNotInRoom(notInThisRoomId) { + const callsNotInThatRoom = []; + + for (const [roomId, call] of this.calls.entries()) { + if (roomId !== notInThisRoomId && call.state !== CallState.Ended) { + callsNotInThatRoom.push(call); + } + } + return callsNotInThatRoom; + } + + getTransfereeForCallId(callId: string): MatrixCall { + return this.transferees[callId]; + } + + play(audioId: AudioID) { + // TODO: Attach an invisible element for this instead + // which listens? + const audio = document.getElementById(audioId) as HTMLMediaElement; + if (audio) { + const playAudio = async () => { + try { + // This still causes the chrome debugger to break on promise rejection if + // the promise is rejected, even though we're catching the exception. + await audio.play(); + } catch (e) { + // This is usually because the user hasn't interacted with the document, + // or chrome doesn't think so and is denying the request. Not sure what + // we can really do here... + // https://github.com/vector-im/element-web/issues/7657 + console.log("Unable to play audio clip", e); + } + }; + if (this.audioPromises.has(audioId)) { + this.audioPromises.set(audioId, this.audioPromises.get(audioId).then(() => { + audio.load(); + return playAudio(); + })); + } else { + this.audioPromises.set(audioId, playAudio()); + } + } + } + + pause(audioId: AudioID) { + // TODO: Attach an invisible element for this instead + // which listens? + const audio = document.getElementById(audioId) as HTMLMediaElement; + if (audio) { + if (this.audioPromises.has(audioId)) { + this.audioPromises.set(audioId, this.audioPromises.get(audioId).then(() => audio.pause())); + } else { + // pause doesn't return a promise, so just do it + audio.pause(); + } + } + } + + private matchesCallForThisRoom(call: MatrixCall) { + // We don't allow placing more than one call per room, but that doesn't mean there + // can't be more than one, eg. in a glare situation. This checks that the given call + // is the call we consider 'the' call for its room. + const mappedRoomId = this.roomIdForCall(call); + + const callForThisRoom = this.getCallForRoom(mappedRoomId); + return callForThisRoom && call.callId === callForThisRoom.callId; + } + + private setCallListeners(call: MatrixCall) { + let mappedRoomId = this.roomIdForCall(call); + + call.on(CallEvent.Error, (err: CallError) => { + if (!this.matchesCallForThisRoom(call)) return; + + Analytics.trackEvent('voip', 'callError', 'error', err.toString()); + console.error("Call error:", err); + + if (err.code === CallErrorCode.NoUserMedia) { + this.showMediaCaptureError(call); + return; + } + + if ( + MatrixClientPeg.get().getTurnServers().length === 0 && + SettingsStore.getValue("fallbackICEServerAllowed") === null + ) { + this.showICEFallbackPrompt(); + return; + } + + Modal.createTrackedDialog('Call Failed', '', ErrorDialog, { + title: _t('Call Failed'), + description: err.message, + }); + }); + call.on(CallEvent.Hangup, () => { + if (!this.matchesCallForThisRoom(call)) return; + + Analytics.trackEvent('voip', 'callHangup'); + + this.removeCallForRoom(mappedRoomId); + }); + call.on(CallEvent.State, (newState: CallState, oldState: CallState) => { + if (!this.matchesCallForThisRoom(call)) return; + + this.setCallState(call, newState); + + switch (oldState) { + case CallState.Ringing: + this.pause(AudioID.Ring); + break; + case CallState.InviteSent: + this.pause(AudioID.Ringback); + break; + } + + switch (newState) { + case CallState.Ringing: + this.play(AudioID.Ring); + break; + case CallState.InviteSent: + this.play(AudioID.Ringback); + break; + case CallState.Ended: + { + Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason); + this.removeCallForRoom(mappedRoomId); + if (oldState === CallState.InviteSent && ( + call.hangupParty === CallParty.Remote || + (call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout) + )) { + this.play(AudioID.Busy); + let title; + let description; + if (call.hangupReason === CallErrorCode.UserHangup) { + title = _t("Call Declined"); + description = _t("The other party declined the call."); + } else if (call.hangupReason === CallErrorCode.UserBusy) { + title = _t("User Busy"); + description = _t("The user you called is busy."); + } else if (call.hangupReason === CallErrorCode.InviteTimeout) { + title = _t("Call Failed"); + // XXX: full stop appended as some relic here, but these + // strings need proper input from design anyway, so let's + // not change this string until we have a proper one. + description = _t('The remote side failed to pick up') + '.'; + } else { + title = _t("Call Failed"); + description = _t("The call could not be established"); + } + + Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { + title, description, + }); + } else if ( + call.hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting + ) { + Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { + title: _t("Answered Elsewhere"), + description: _t("The call was answered on another device."), + }); + } else if (oldState !== CallState.Fledgling && oldState !== CallState.Ringing) { + // don't play the end-call sound for calls that never got off the ground + this.play(AudioID.CallEnd); + } + + this.logCallStats(call, mappedRoomId); + break; + } + } + }); + call.on(CallEvent.Replaced, (newCall: MatrixCall) => { + if (!this.matchesCallForThisRoom(call)) return; + + console.log(`Call ID ${call.callId} is being replaced by call ID ${newCall.callId}`); + + if (call.state === CallState.Ringing) { + this.pause(AudioID.Ring); + } else if (call.state === CallState.InviteSent) { + this.pause(AudioID.Ringback); + } + + this.calls.set(mappedRoomId, newCall); + this.emit(CallHandlerEvent.CallsChanged, this.calls); + this.setCallListeners(newCall); + this.setCallState(newCall, newCall.state); + }); + call.on(CallEvent.AssertedIdentityChanged, async () => { + if (!this.matchesCallForThisRoom(call)) return; + + console.log(`Call ID ${call.callId} got new asserted identity:`, call.getRemoteAssertedIdentity()); + + const newAssertedIdentity = call.getRemoteAssertedIdentity().id; + let newNativeAssertedIdentity = newAssertedIdentity; + if (newAssertedIdentity) { + const response = await this.sipNativeLookup(newAssertedIdentity); + if (response.length && response[0].fields.lookup_success) { + newNativeAssertedIdentity = response[0].userid; + } + } + console.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`); + + if (newNativeAssertedIdentity) { + this.assertedIdentityNativeUsers[call.callId] = newNativeAssertedIdentity; + + // If we don't already have a room with this user, make one. This will be slightly odd + // if they called us because we'll be inviting them, but there's not much we can do about + // this if we want the actual, native room to exist (which we do). This is why it's + // important to only obey asserted identity in trusted environments, since anyone you're + // on a call with can cause you to send a room invite to someone. + await ensureDMExists(MatrixClientPeg.get(), newNativeAssertedIdentity); + + const newMappedRoomId = this.roomIdForCall(call); + console.log(`Old room ID: ${mappedRoomId}, new room ID: ${newMappedRoomId}`); + if (newMappedRoomId !== mappedRoomId) { + this.removeCallForRoom(mappedRoomId); + mappedRoomId = newMappedRoomId; + console.log("Moving call to room " + mappedRoomId); + this.calls.set(mappedRoomId, call); + this.emit(CallHandlerEvent.CallChangeRoom, call); + } + } + }); + } + + private async logCallStats(call: MatrixCall, mappedRoomId: string) { + const stats = await call.getCurrentCallStats(); + logger.debug( + `Call completed. Call ID: ${call.callId}, virtual room ID: ${call.roomId}, ` + + `user-facing room ID: ${mappedRoomId}, direction: ${call.direction}, ` + + `our Party ID: ${call.ourPartyId}, hangup party: ${call.hangupParty}, ` + + `hangup reason: ${call.hangupReason}`, + ); + if (!stats) { + logger.debug( + "Call statistics are undefined. The call has " + + "probably failed before a peerConn was established", + ); + return; + } + logger.debug("Local candidates:"); + for (const cand of stats.filter(item => item.type === 'local-candidate')) { + const address = cand.address || cand.ip; // firefox uses 'address', chrome uses 'ip' + logger.debug( + `${cand.id} - type: ${cand.candidateType}, address: ${address}, port: ${cand.port}, ` + + `protocol: ${cand.protocol}, relay protocol: ${cand.relayProtocol}, network type: ${cand.networkType}`, + ); + } + logger.debug("Remote candidates:"); + for (const cand of stats.filter(item => item.type === 'remote-candidate')) { + const address = cand.address || cand.ip; // firefox uses 'address', chrome uses 'ip' + logger.debug( + `${cand.id} - type: ${cand.candidateType}, address: ${address}, port: ${cand.port}, ` + + `protocol: ${cand.protocol}`, + ); + } + logger.debug("Candidate pairs:"); + for (const pair of stats.filter(item => item.type === 'candidate-pair')) { + logger.debug( + `${pair.localCandidateId} / ${pair.remoteCandidateId} - state: ${pair.state}, ` + + `nominated: ${pair.nominated}, ` + + `requests sent ${pair.requestsSent}, requests received ${pair.requestsReceived}, ` + + `responses received: ${pair.responsesReceived}, responses sent: ${pair.responsesSent}, ` + + `bytes received: ${pair.bytesReceived}, bytes sent: ${pair.bytesSent}, `, + ); + } + } + + private setCallState(call: MatrixCall, status: CallState) { + const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); + + console.log( + `Call state in ${mappedRoomId} changed to ${status}`, + ); + + dis.dispatch({ + action: 'call_state', + room_id: mappedRoomId, + state: status, + }); + } + + private removeCallForRoom(roomId: string) { + console.log("Removing call for room ", roomId); + this.calls.delete(roomId); + this.emit(CallHandlerEvent.CallsChanged, this.calls); + } + + private showICEFallbackPrompt() { + const cli = MatrixClientPeg.get(); + const code = sub => {sub}; + Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, { + title: _t("Call failed due to misconfigured server"), + description:
+

{_t( + "Please ask the administrator of your homeserver " + + "(%(homeserverDomain)s) to configure a TURN server in " + + "order for calls to work reliably.", + { homeserverDomain: cli.getDomain() }, { code }, + )}

+

{_t( + "Alternatively, you can try to use the public server at " + + "turn.matrix.org, but this will not be as reliable, and " + + "it will share your IP address with that server. You can also manage " + + "this in Settings.", + null, { code }, + )}

+
, + button: _t('Try using turn.matrix.org'), + cancelButton: _t('OK'), + onFinished: (allow) => { + SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow); + cli.setFallbackICEServerAllowed(allow); + }, + }, null, true); + } + + private showMediaCaptureError(call: MatrixCall) { + let title; + let description; + + if (call.type === CallType.Voice) { + title = _t("Unable to access microphone"); + description =
+ {_t( + "Call failed because microphone could not be accessed. " + + "Check that a microphone is plugged in and set up correctly.", + )} +
; + } else if (call.type === CallType.Video) { + title = _t("Unable to access webcam / microphone"); + description =
+ {_t("Call failed because webcam or microphone could not be accessed. Check that:")} +
    +
  • {_t("A microphone and webcam are plugged in and set up correctly")}
  • +
  • {_t("Permission is granted to use the webcam")}
  • +
  • {_t("No other application is using the webcam")}
  • +
+
; + } + + Modal.createTrackedDialog('Media capture failed', '', ErrorDialog, { + title, description, + }, null, true); + } + + private async placeCall(roomId: string, type: PlaceCallType, transferee: MatrixCall) { + Analytics.trackEvent('voip', 'placeCall', 'type', type); + CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false); + + const mappedRoomId = (await VoipUserMapper.sharedInstance().getOrCreateVirtualRoomForRoom(roomId)) || roomId; + logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId); + + const timeUntilTurnCresExpire = MatrixClientPeg.get().getTurnServersExpiry() - Date.now(); + console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms"); + const call = MatrixClientPeg.get().createCall(mappedRoomId); + + console.log("Adding call for room ", roomId); + this.calls.set(roomId, call); + this.emit(CallHandlerEvent.CallsChanged, this.calls); + if (transferee) { + this.transferees[call.callId] = transferee; + } + + this.setCallListeners(call); + + this.setActiveCallRoomId(roomId); + + if (type === PlaceCallType.Voice) { + call.placeVoiceCall(); + } else if (type === 'video') { + call.placeVideoCall(); + } else if (type === PlaceCallType.ScreenSharing) { + const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString(); + if (screenCapErrorString) { + this.removeCallForRoom(roomId); + console.log("Can't capture screen: " + screenCapErrorString); + Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, { + title: _t('Unable to capture screen'), + description: screenCapErrorString, + }); + return; + } + + call.placeScreenSharingCall( + async (): Promise => { + const { finished } = Modal.createDialog(DesktopCapturerSourcePicker); + const [source] = await finished; + return source; + }, + ); + } else { + console.error("Unknown conf call type: " + type); + } + } + + private onAction = (payload: ActionPayload) => { + switch (payload.action) { + case 'place_call': + { + // We might be using managed hybrid widgets + if (isManagedHybridWidgetEnabled()) { + addManagedHybridWidget(payload.room_id); + return; + } + + // if the runtime env doesn't do VoIP, whine. + if (!MatrixClientPeg.get().supportsVoip()) { + Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, { + title: _t('VoIP is unsupported'), + description: _t('You cannot place VoIP calls in this browser.'), + }); + return; + } + + // don't allow > 2 calls to be placed. + if (this.getAllActiveCalls().length > 1) { + Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, { + title: _t('Too Many Calls'), + description: _t("You've reached the maximum number of simultaneous calls."), + }); + return; + } + + const room = MatrixClientPeg.get().getRoom(payload.room_id); + if (!room) { + console.error(`Room ${payload.room_id} does not exist.`); + return; + } + + if (this.getCallForRoom(room.roomId)) { + Modal.createTrackedDialog('Call Handler', 'Existing Call with user', ErrorDialog, { + title: _t('Already in call'), + description: _t("You're already in a call with this person."), + }); + return; + } + + const members = room.getJoinedMembers(); + if (members.length <= 1) { + Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, { + description: _t('You cannot place a call with yourself.'), + }); + return; + } else if (members.length === 2) { + console.info(`Place ${payload.type} call in ${payload.room_id}`); + + this.placeCall(payload.room_id, payload.type, payload.transferee); + } else { // > 2 + dis.dispatch({ + action: "place_conference_call", + room_id: payload.room_id, + type: payload.type, + }); + } + } + break; + case 'place_conference_call': + console.info("Place conference call in " + payload.room_id); + Analytics.trackEvent('voip', 'placeConferenceCall'); + CountlyAnalytics.instance.trackStartCall(payload.room_id, payload.type === PlaceCallType.Video, true); + this.startCallApp(payload.room_id, payload.type); + break; + case 'end_conference': + console.info("Terminating conference call in " + payload.room_id); + this.terminateCallApp(payload.room_id); + break; + case 'hangup_conference': + console.info("Leaving conference call in "+ payload.room_id); + this.hangupCallApp(payload.room_id); + break; + case 'incoming_call': + { + // if the runtime env doesn't do VoIP, stop here. + if (!MatrixClientPeg.get().supportsVoip()) { + return; + } + + const call = payload.call as MatrixCall; + + const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); + if (this.getCallForRoom(mappedRoomId)) { + console.log( + "Got incoming call for room " + mappedRoomId + + " but there's already a call for this room: ignoring", + ); + return; + } + + Analytics.trackEvent('voip', 'receiveCall', 'type', call.type); + console.log("Adding call for room ", mappedRoomId); + this.calls.set(mappedRoomId, call); + this.emit(CallHandlerEvent.CallsChanged, this.calls); + this.setCallListeners(call); + + // get ready to send encrypted events in the room, so if the user does answer + // the call, we'll be ready to send. NB. This is the protocol-level room ID not + // the mapped one: that's where we'll send the events. + const cli = MatrixClientPeg.get(); + cli.prepareToEncrypt(cli.getRoom(call.roomId)); + } + break; + case 'hangup': + case 'reject': + if (!this.calls.get(payload.room_id)) { + return; // no call to hangup + } + if (payload.action === 'reject') { + this.calls.get(payload.room_id).reject(); + } else { + this.calls.get(payload.room_id).hangup(CallErrorCode.UserHangup, false); + } + // don't remove the call yet: let the hangup event handler do it (otherwise it will throw + // the hangup event away) + break; + case 'hangup_all': + for (const call of this.calls.values()) { + call.hangup(CallErrorCode.UserHangup, false); + } + break; + case 'answer': { + if (!this.calls.has(payload.room_id)) { + return; // no call to answer + } + + if (this.getAllActiveCalls().length > 1) { + Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, { + title: _t('Too Many Calls'), + description: _t("You've reached the maximum number of simultaneous calls."), + }); + return; + } + + const call = this.calls.get(payload.room_id); + call.answer(); + this.setActiveCallRoomId(payload.room_id); + CountlyAnalytics.instance.trackJoinCall(payload.room_id, call.type === CallType.Video, false); + dis.dispatch({ + action: "view_room", + room_id: payload.room_id, + }); + break; + } + case Action.DialNumber: + this.dialNumber(payload.number); + break; + case Action.TransferCallToMatrixID: + this.startTransferToMatrixID(payload.call, payload.destination, payload.consultFirst); + break; + case Action.TransferCallToPhoneNumber: + this.startTransferToPhoneNumber(payload.call, payload.destination, payload.consultFirst); + break; + } + }; + + private async dialNumber(number: string) { + const results = await this.pstnLookup(number); + if (!results || results.length === 0 || !results[0].userid) { + Modal.createTrackedDialog('', '', ErrorDialog, { + title: _t("Unable to look up phone number"), + description: _t("There was an error looking up the phone number"), + }); + return; + } + const userId = results[0].userid; + + // Now check to see if this is a virtual user, in which case we should find the + // native user + let nativeUserId; + if (this.getSupportsVirtualRooms()) { + const nativeLookupResults = await this.sipNativeLookup(userId); + const lookupSuccess = nativeLookupResults.length > 0 && nativeLookupResults[0].fields.lookup_success; + nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId; + console.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId); + } else { + nativeUserId = userId; + } + + const roomId = await ensureDMExists(MatrixClientPeg.get(), nativeUserId); + + dis.dispatch({ + action: 'view_room', + room_id: roomId, + }); + } + + private async startTransferToPhoneNumber(call: MatrixCall, destination: string, consultFirst: boolean) { + const results = await this.pstnLookup(destination); + if (!results || results.length === 0 || !results[0].userid) { + Modal.createTrackedDialog('', '', ErrorDialog, { + title: _t("Unable to transfer call"), + description: _t("There was an error looking up the phone number"), + }); + return; + } + + await this.startTransferToMatrixID(call, results[0].userid, consultFirst); + } + + private async startTransferToMatrixID(call: MatrixCall, destination: string, consultFirst: boolean) { + if (consultFirst) { + const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), destination); + + dis.dispatch({ + action: 'place_call', + type: call.type, + room_id: dmRoomId, + transferee: call, + }); + dis.dispatch({ + action: 'view_room', + room_id: dmRoomId, + should_peek: false, + joining: false, + }); + } else { + try { + await call.transfer(destination); + } catch (e) { + console.log("Failed to transfer call", e); + Modal.createTrackedDialog('Failed to transfer call', '', ErrorDialog, { + title: _t('Transfer Failed'), + description: _t('Failed to transfer call'), + }); + } + } + } + + setActiveCallRoomId(activeCallRoomId: string) { + logger.info("Setting call in room " + activeCallRoomId + " active"); + + for (const [roomId, call] of this.calls.entries()) { + if (call.state === CallState.Ended) continue; + + if (roomId === activeCallRoomId) { + call.setRemoteOnHold(false); + } else { + logger.info("Holding call in room " + roomId + " because another call is being set active"); + call.setRemoteOnHold(true); + } + } + } + + /** + * @returns true if we are currently in any call where we haven't put the remote party on hold + */ + hasAnyUnheldCall() { + for (const call of this.calls.values()) { + if (call.state === CallState.Ended) continue; + if (!call.isRemoteOnHold()) return true; + } + + return false; + } + + private async startCallApp(roomId: string, type: string) { + dis.dispatch({ + action: 'appsDrawer', + show: true, + }); + + // prevent double clicking the call button + const room = MatrixClientPeg.get().getRoom(roomId); + const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI); + const hasJitsi = currentJitsiWidgets.length > 0 + || WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI); + if (hasJitsi) { + Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, { + title: _t('Call in Progress'), + description: _t('A call is currently being placed!'), + }); + return; + } + + const jitsiDomain = Jitsi.getInstance().preferredDomain; + const jitsiAuth = await Jitsi.getInstance().getJitsiAuth(); + let confId; + if (jitsiAuth === 'openidtoken-jwt') { + // Create conference ID from room ID + // For compatibility with Jitsi, use base32 without padding. + // More details here: + // https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification + confId = base32.stringify(Buffer.from(roomId), { pad: false }); + } else { + // Create a random conference ID + const random = randomUppercaseString(1) + randomLowercaseString(23); + confId = 'Jitsi' + random; + } + + let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl({ auth: jitsiAuth }); + + // TODO: Remove URL hacks when the mobile clients eventually support v2 widgets + const parsedUrl = new URL(widgetUrl); + parsedUrl.search = ''; // set to empty string to make the URL class use searchParams instead + parsedUrl.searchParams.set('confId', confId); + widgetUrl = parsedUrl.toString(); + + const widgetData = { + conferenceId: confId, + isAudioOnly: type === 'voice', + domain: jitsiDomain, + auth: jitsiAuth, + roomName: room.name, + }; + + const widgetId = ( + 'jitsi_' + + MatrixClientPeg.get().credentials.userId + + '_' + + Date.now() + ); + + WidgetUtils.setRoomWidget(roomId, widgetId, WidgetType.JITSI, widgetUrl, 'Jitsi', widgetData).then(() => { + console.log('Jitsi widget added'); + }).catch((e) => { + if (e.errcode === 'M_FORBIDDEN') { + Modal.createTrackedDialog('Call Failed', '', ErrorDialog, { + title: _t('Permission Required'), + description: _t("You do not have permission to start a conference call in this room"), + }); + } + console.error(e); + }); + } + + private terminateCallApp(roomId: string) { + Modal.createTrackedDialog('Confirm Jitsi Terminate', '', QuestionDialog, { + hasCancelButton: true, + title: _t("End conference"), + description: _t("This will end the conference for everyone. Continue?"), + button: _t("End conference"), + onFinished: (proceed) => { + if (!proceed) return; + + // We'll just obliterate them all. There should only ever be one, but might as well + // be safe. + const roomInfo = WidgetStore.instance.getRoom(roomId); + const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type)); + jitsiWidgets.forEach(w => { + // setting invalid content removes it + WidgetUtils.setRoomWidget(roomId, w.id); + }); + }, + }); + } + + private hangupCallApp(roomId: string) { + const roomInfo = WidgetStore.instance.getRoom(roomId); + if (!roomInfo) return; // "should never happen" clauses go here + + const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type)); + jitsiWidgets.forEach(w => { + const messaging = WidgetMessagingStore.instance.getMessagingForId(w.id); + if (!messaging) return; // more "should never happen" words + + messaging.transport.send(ElementWidgetActions.HangupCall, {}); + }); + } +} diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js deleted file mode 100644 index a0364f798a..0000000000 --- a/src/CallMediaHandler.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> - - 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 * as Matrix from 'matrix-js-sdk'; -import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; - -export default { - hasAnyLabeledDevices: async function() { - const devices = await navigator.mediaDevices.enumerateDevices(); - return devices.some(d => !!d.label); - }, - - getDevices: function() { - // Only needed for Electron atm, though should work in modern browsers - // once permission has been granted to the webapp - return navigator.mediaDevices.enumerateDevices().then(function(devices) { - const audiooutput = []; - const audioinput = []; - const videoinput = []; - - devices.forEach((device) => { - switch (device.kind) { - case 'audiooutput': audiooutput.push(device); break; - case 'audioinput': audioinput.push(device); break; - case 'videoinput': videoinput.push(device); break; - } - }); - - // console.log("Loaded WebRTC Devices", mediaDevices); - return { - audiooutput, - audioinput, - videoinput, - }; - }, (error) => { console.log('Unable to refresh WebRTC Devices: ', error); }); - }, - - loadDevices: function() { - const audioOutDeviceId = SettingsStore.getValue("webrtc_audiooutput"); - const audioDeviceId = SettingsStore.getValue("webrtc_audioinput"); - const videoDeviceId = SettingsStore.getValue("webrtc_videoinput"); - - Matrix.setMatrixCallAudioOutput(audioOutDeviceId); - Matrix.setMatrixCallAudioInput(audioDeviceId); - Matrix.setMatrixCallVideoInput(videoDeviceId); - }, - - setAudioOutput: function(deviceId) { - SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId); - Matrix.setMatrixCallAudioOutput(deviceId); - }, - - setAudioInput: function(deviceId) { - SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId); - Matrix.setMatrixCallAudioInput(deviceId); - }, - - setVideoInput: function(deviceId) { - SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId); - Matrix.setMatrixCallVideoInput(deviceId); - }, - - getAudioOutput: function() { - return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput"); - }, - - getAudioInput: function() { - return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput"); - }, - - getVideoInput: function() { - return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput"); - }, -}; diff --git a/src/ContentMessages.js b/src/ContentMessages.js deleted file mode 100644 index 34379c029b..0000000000 --- a/src/ContentMessages.js +++ /dev/null @@ -1,588 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -'use strict'; - -import extend from './extend'; -import dis from './dispatcher'; -import {MatrixClientPeg} from './MatrixClientPeg'; -import * as sdk from './index'; -import { _t } from './languageHandler'; -import Modal from './Modal'; -import RoomViewStore from './stores/RoomViewStore'; -import encrypt from "browser-encrypt-attachment"; -import extractPngChunks from "png-chunks-extract"; - -// Polyfill for Canvas.toBlob API using Canvas.toDataURL -import "blueimp-canvas-to-blob"; - -const MAX_WIDTH = 800; -const MAX_HEIGHT = 600; - -// scraped out of a macOS hidpi (5660ppm) screenshot png -// 5669 px (x-axis) , 5669 px (y-axis) , per metre -const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01]; - -export class UploadCanceledError extends Error {} - -/** - * Create a thumbnail for a image DOM element. - * The image will be smaller than MAX_WIDTH and MAX_HEIGHT. - * The thumbnail will have the same aspect ratio as the original. - * Draws the element into a canvas using CanvasRenderingContext2D.drawImage - * Then calls Canvas.toBlob to get a blob object for the image data. - * - * Since it needs to calculate the dimensions of the source image and the - * thumbnailed image it returns an info object filled out with information - * about the original image and the thumbnail. - * - * @param {HTMLElement} element The element to thumbnail. - * @param {integer} inputWidth The width of the image in the input element. - * @param {integer} inputHeight the width of the image in the input element. - * @param {String} mimeType The mimeType to save the blob as. - * @return {Promise} A promise that resolves with an object with an info key - * and a thumbnail key. - */ -function createThumbnail(element, inputWidth, inputHeight, mimeType) { - return new Promise((resolve) => { - let targetWidth = inputWidth; - let targetHeight = inputHeight; - if (targetHeight > MAX_HEIGHT) { - targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); - targetHeight = MAX_HEIGHT; - } - if (targetWidth > MAX_WIDTH) { - targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); - targetWidth = MAX_WIDTH; - } - - const canvas = document.createElement("canvas"); - canvas.width = targetWidth; - canvas.height = targetHeight; - canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); - canvas.toBlob(function(thumbnail) { - resolve({ - info: { - thumbnail_info: { - w: targetWidth, - h: targetHeight, - mimetype: thumbnail.type, - size: thumbnail.size, - }, - w: inputWidth, - h: inputHeight, - }, - thumbnail: thumbnail, - }); - }, mimeType); - }); -} - -/** - * Load a file into a newly created image element. - * - * @param {File} imageFile The file to load in an image element. - * @return {Promise} A promise that resolves with the html image element. - */ -async function loadImageElement(imageFile) { - // Load the file into an html element - const img = document.createElement("img"); - const objectUrl = URL.createObjectURL(imageFile); - const imgPromise = new Promise((resolve, reject) => { - img.onload = function() { - URL.revokeObjectURL(objectUrl); - resolve(img); - }; - img.onerror = function(e) { - reject(e); - }; - }); - img.src = objectUrl; - - // check for hi-dpi PNGs and fudge display resolution as needed. - // this is mainly needed for macOS screencaps - let parsePromise; - if (imageFile.type === "image/png") { - // in practice macOS happens to order the chunks so they fall in - // the first 0x1000 bytes (thanks to a massive ICC header). - // Thus we could slice the file down to only sniff the first 0x1000 - // bytes (but this makes extractPngChunks choke on the corrupt file) - const headers = imageFile; //.slice(0, 0x1000); - parsePromise = readFileAsArrayBuffer(headers).then(arrayBuffer => { - const buffer = new Uint8Array(arrayBuffer); - const chunks = extractPngChunks(buffer); - for (const chunk of chunks) { - if (chunk.name === 'pHYs') { - if (chunk.data.byteLength !== PHYS_HIDPI.length) return; - const hidpi = chunk.data.every((val, i) => val === PHYS_HIDPI[i]); - return hidpi; - } - } - return false; - }); - } - - const [hidpi] = await Promise.all([parsePromise, imgPromise]); - const width = hidpi ? (img.width >> 1) : img.width; - const height = hidpi ? (img.height >> 1) : img.height; - return {width, height, img}; -} - -/** - * Read the metadata for an image file and create and upload a thumbnail of the image. - * - * @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with. - * @param {String} roomId The ID of the room the image will be uploaded in. - * @param {File} imageFile The image to read and thumbnail. - * @return {Promise} A promise that resolves with the attachment info. - */ -function infoForImageFile(matrixClient, roomId, imageFile) { - let thumbnailType = "image/png"; - if (imageFile.type == "image/jpeg") { - thumbnailType = "image/jpeg"; - } - - let imageInfo; - return loadImageElement(imageFile).then(function(r) { - return createThumbnail(r.img, r.width, r.height, thumbnailType); - }).then(function(result) { - imageInfo = result.info; - return uploadFile(matrixClient, roomId, result.thumbnail); - }).then(function(result) { - imageInfo.thumbnail_url = result.url; - imageInfo.thumbnail_file = result.file; - return imageInfo; - }); -} - -/** - * Load a file into a newly created video element. - * - * @param {File} videoFile The file to load in an video element. - * @return {Promise} A promise that resolves with the video image element. - */ -function loadVideoElement(videoFile) { - return new Promise((resolve, reject) => { - // Load the file into an html element - const video = document.createElement("video"); - - const reader = new FileReader(); - - reader.onload = function(e) { - video.src = e.target.result; - - // Once ready, returns its size - // Wait until we have enough data to thumbnail the first frame. - video.onloadeddata = function() { - resolve(video); - }; - video.onerror = function(e) { - reject(e); - }; - }; - reader.onerror = function(e) { - reject(e); - }; - reader.readAsDataURL(videoFile); - }); -} - -/** - * Read the metadata for a video file and create and upload a thumbnail of the video. - * - * @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with. - * @param {String} roomId The ID of the room the video will be uploaded to. - * @param {File} videoFile The video to read and thumbnail. - * @return {Promise} A promise that resolves with the attachment info. - */ -function infoForVideoFile(matrixClient, roomId, videoFile) { - const thumbnailType = "image/jpeg"; - - let videoInfo; - return loadVideoElement(videoFile).then(function(video) { - return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType); - }).then(function(result) { - videoInfo = result.info; - return uploadFile(matrixClient, roomId, result.thumbnail); - }).then(function(result) { - videoInfo.thumbnail_url = result.url; - videoInfo.thumbnail_file = result.file; - return videoInfo; - }); -} - -/** - * Read the file as an ArrayBuffer. - * @param {File} file The file to read - * @return {Promise} A promise that resolves with an ArrayBuffer when the file - * is read. - */ -function readFileAsArrayBuffer(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = function(e) { - resolve(e.target.result); - }; - reader.onerror = function(e) { - reject(e); - }; - reader.readAsArrayBuffer(file); - }); -} - -/** - * Upload the file to the content repository. - * If the room is encrypted then encrypt the file before uploading. - * - * @param {MatrixClient} matrixClient The matrix client to upload the file with. - * @param {String} roomId The ID of the room being uploaded to. - * @param {File} file The file to upload. - * @param {Function?} progressHandler optional callback to be called when a chunk of - * data is uploaded. - * @return {Promise} A promise that resolves with an object. - * If the file is unencrypted then the object will have a "url" key. - * If the file is encrypted then the object will have a "file" key. - */ -function uploadFile(matrixClient, roomId, file, progressHandler) { - if (matrixClient.isRoomEncrypted(roomId)) { - // If the room is encrypted then encrypt the file before uploading it. - // First read the file into memory. - let canceled = false; - let uploadPromise; - let encryptInfo; - const prom = readFileAsArrayBuffer(file).then(function(data) { - if (canceled) throw new UploadCanceledError(); - // Then encrypt the file. - return encrypt.encryptAttachment(data); - }).then(function(encryptResult) { - if (canceled) throw new UploadCanceledError(); - // Record the information needed to decrypt the attachment. - encryptInfo = encryptResult.info; - // Pass the encrypted data as a Blob to the uploader. - const blob = new Blob([encryptResult.data]); - uploadPromise = matrixClient.uploadContent(blob, { - progressHandler: progressHandler, - includeFilename: false, - }); - - return uploadPromise; - }).then(function(url) { - // If the attachment is encrypted then bundle the URL along - // with the information needed to decrypt the attachment and - // add it under a file key. - encryptInfo.url = url; - if (file.type) { - encryptInfo.mimetype = file.type; - } - return {"file": encryptInfo}; - }); - prom.abort = () => { - canceled = true; - if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise); - }; - return prom; - } else { - const basePromise = matrixClient.uploadContent(file, { - progressHandler: progressHandler, - }); - const promise1 = basePromise.then(function(url) { - // If the attachment isn't encrypted then include the URL directly. - return {"url": url}; - }); - // XXX: copy over the abort method to the new promise - promise1.abort = basePromise.abort; - return promise1; - } -} - -export default class ContentMessages { - constructor() { - this.inprogress = []; - this.nextId = 0; - this._mediaConfig = null; - } - - static sharedInstance() { - if (global.mx_ContentMessages === undefined) { - global.mx_ContentMessages = new ContentMessages(); - } - return global.mx_ContentMessages; - } - - _isFileSizeAcceptable(file) { - if (this._mediaConfig !== null && - this._mediaConfig["m.upload.size"] !== undefined && - file.size > this._mediaConfig["m.upload.size"]) { - return false; - } - return true; - } - - _ensureMediaConfigFetched() { - if (this._mediaConfig !== null) return; - - console.log("[Media Config] Fetching"); - return MatrixClientPeg.get().getMediaConfig().then((config) => { - console.log("[Media Config] Fetched config:", config); - return config; - }).catch(() => { - // Media repo can't or won't report limits, so provide an empty object (no limits). - console.log("[Media Config] Could not fetch config, so not limiting uploads."); - return {}; - }).then((config) => { - this._mediaConfig = config; - }); - } - - sendStickerContentToRoom(url, roomId, info, text, matrixClient) { - return MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => { - console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e); - throw e; - }); - } - - getUploadLimit() { - if (this._mediaConfig !== null && this._mediaConfig["m.upload.size"] !== undefined) { - return this._mediaConfig["m.upload.size"]; - } else { - return null; - } - } - - async sendContentListToRoom(files, roomId, matrixClient) { - if (matrixClient.isGuest()) { - dis.dispatch({action: 'require_registration'}); - return; - } - - const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); - if (isQuoting) { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const shouldUpload = await new Promise((resolve) => { - Modal.createTrackedDialog('Upload Reply Warning', '', QuestionDialog, { - title: _t('Replying With Files'), - description: ( -
{_t( - 'At this time it is not possible to reply with a file. ' + - 'Would you like to upload this file without replying?', - )}
- ), - hasCancelButton: true, - button: _t("Continue"), - onFinished: (shouldUpload) => { - resolve(shouldUpload); - }, - }); - }); - if (!shouldUpload) return; - } - - await this._ensureMediaConfigFetched(); - - const tooBigFiles = []; - const okFiles = []; - - for (let i = 0; i < files.length; ++i) { - if (this._isFileSizeAcceptable(files[i])) { - okFiles.push(files[i]); - } else { - tooBigFiles.push(files[i]); - } - } - - if (tooBigFiles.length > 0) { - const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog"); - const uploadFailureDialogPromise = new Promise((resolve) => { - Modal.createTrackedDialog('Upload Failure', '', UploadFailureDialog, { - badFiles: tooBigFiles, - totalFiles: files.length, - contentMessages: this, - onFinished: (shouldContinue) => { - resolve(shouldContinue); - }, - }); - }); - const shouldContinue = await uploadFailureDialogPromise; - if (!shouldContinue) return; - } - - const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog"); - let uploadAll = false; - // Promise to complete before sending next file into room, used for synchronisation of file-sending - // to match the order the files were specified in - let promBefore = Promise.resolve(); - for (let i = 0; i < okFiles.length; ++i) { - const file = okFiles[i]; - if (!uploadAll) { - const shouldContinue = await new Promise((resolve) => { - Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, { - file, - currentIndex: i, - totalFiles: okFiles.length, - onFinished: (shouldContinue, shouldUploadAll) => { - if (shouldUploadAll) { - uploadAll = true; - } - resolve(shouldContinue); - }, - }); - }); - if (!shouldContinue) break; - } - promBefore = this._sendContentToRoom(file, roomId, matrixClient, promBefore); - } - } - - _sendContentToRoom(file, roomId, matrixClient, promBefore) { - const content = { - body: file.name || 'Attachment', - info: { - size: file.size, - }, - }; - - // if we have a mime type for the file, add it to the message metadata - if (file.type) { - content.info.mimetype = file.type; - } - - const prom = new Promise((resolve) => { - if (file.type.indexOf('image/') == 0) { - content.msgtype = 'm.image'; - infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{ - extend(content.info, imageInfo); - resolve(); - }, (error)=>{ - console.error(error); - content.msgtype = 'm.file'; - resolve(); - }); - } else if (file.type.indexOf('audio/') == 0) { - content.msgtype = 'm.audio'; - resolve(); - } else if (file.type.indexOf('video/') == 0) { - content.msgtype = 'm.video'; - infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{ - extend(content.info, videoInfo); - resolve(); - }, (error)=>{ - content.msgtype = 'm.file'; - resolve(); - }); - } else { - content.msgtype = 'm.file'; - resolve(); - } - }); - - const upload = { - fileName: file.name || 'Attachment', - roomId: roomId, - total: 0, - loaded: 0, - }; - this.inprogress.push(upload); - dis.dispatch({action: 'upload_started'}); - - // Focus the composer view - dis.dispatch({action: 'focus_composer'}); - - let error; - - function onProgress(ev) { - upload.total = ev.total; - upload.loaded = ev.loaded; - dis.dispatch({action: 'upload_progress', upload: upload}); - } - - return prom.then(function() { - // XXX: upload.promise must be the promise that - // is returned by uploadFile as it has an abort() - // method hacked onto it. - upload.promise = uploadFile( - matrixClient, roomId, file, onProgress, - ); - return upload.promise.then(function(result) { - content.file = result.file; - content.url = result.url; - }); - }).then((url) => { - // Await previous message being sent into the room - return promBefore; - }).then(function() { - return matrixClient.sendMessage(roomId, content); - }, function(err) { - error = err; - if (!upload.canceled) { - let desc = _t("The file '%(fileName)s' failed to upload.", {fileName: upload.fileName}); - if (err.http_status == 413) { - desc = _t( - "The file '%(fileName)s' exceeds this homeserver's size limit for uploads", - {fileName: upload.fileName}, - ); - } - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Upload failed', '', ErrorDialog, { - title: _t('Upload Failed'), - description: desc, - }); - } - }).finally(() => { - const inprogressKeys = Object.keys(this.inprogress); - for (let i = 0; i < this.inprogress.length; ++i) { - const k = inprogressKeys[i]; - if (this.inprogress[k].promise === upload.promise) { - this.inprogress.splice(k, 1); - break; - } - } - if (error) { - // 413: File was too big or upset the server in some way: - // clear the media size limit so we fetch it again next time - // we try to upload - if (error && error.http_status === 413) { - this._mediaConfig = null; - } - dis.dispatch({action: 'upload_failed', upload, error}); - } else { - dis.dispatch({action: 'upload_finished', upload}); - dis.dispatch({action: 'message_sent'}); - } - }); - } - - getCurrentUploads() { - return this.inprogress.filter(u => !u.canceled); - } - - cancelUpload(promise) { - const inprogressKeys = Object.keys(this.inprogress); - let upload; - for (let i = 0; i < this.inprogress.length; ++i) { - const k = inprogressKeys[i]; - if (this.inprogress[k].promise === promise) { - upload = this.inprogress[k]; - break; - } - } - if (upload) { - upload.canceled = true; - MatrixClientPeg.get().cancelUpload(upload.promise); - dis.dispatch({action: 'upload_canceled', upload}); - } - } -} diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx new file mode 100644 index 0000000000..0c65a7bd35 --- /dev/null +++ b/src/ContentMessages.tsx @@ -0,0 +1,671 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 New Vector Ltd +Copyright 2020 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 from "react"; +import { MatrixClient } from "matrix-js-sdk/src/client"; + +import dis from './dispatcher/dispatcher'; +import * as sdk from './index'; +import { _t } from './languageHandler'; +import Modal from './Modal'; +import RoomViewStore from './stores/RoomViewStore'; +import encrypt from "browser-encrypt-attachment"; +import extractPngChunks from "png-chunks-extract"; +import Spinner from "./components/views/elements/Spinner"; +import { Action } from "./dispatcher/actions"; +import CountlyAnalytics from "./CountlyAnalytics"; +import { + UploadCanceledPayload, + UploadErrorPayload, + UploadFinishedPayload, + UploadProgressPayload, + UploadStartedPayload, +} from "./dispatcher/payloads/UploadPayload"; +import { IUpload } from "./models/IUpload"; +import { IAbortablePromise, IImageInfo } from "matrix-js-sdk/src/@types/partials"; +import { BlurhashEncoder } from "./BlurhashEncoder"; + +const MAX_WIDTH = 800; +const MAX_HEIGHT = 600; + +// scraped out of a macOS hidpi (5660ppm) screenshot png +// 5669 px (x-axis) , 5669 px (y-axis) , per metre +const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01]; + +export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448 + +export class UploadCanceledError extends Error {} + +type ThumbnailableElement = HTMLImageElement | HTMLVideoElement; + +interface IMediaConfig { + "m.upload.size"?: number; +} + +interface IContent { + body: string; + msgtype: string; + info: { + size: number; + mimetype?: string; + }; + file?: string; + url?: string; +} + +interface IThumbnail { + info: { + // eslint-disable-next-line camelcase + thumbnail_info: { + w: number; + h: number; + mimetype: string; + size: number; + }; + w: number; + h: number; + [BLURHASH_FIELD]: string; + }; + thumbnail: Blob; +} + +/** + * Create a thumbnail for a image DOM element. + * The image will be smaller than MAX_WIDTH and MAX_HEIGHT. + * The thumbnail will have the same aspect ratio as the original. + * Draws the element into a canvas using CanvasRenderingContext2D.drawImage + * Then calls Canvas.toBlob to get a blob object for the image data. + * + * Since it needs to calculate the dimensions of the source image and the + * thumbnailed image it returns an info object filled out with information + * about the original image and the thumbnail. + * + * @param {HTMLElement} element The element to thumbnail. + * @param {number} inputWidth The width of the image in the input element. + * @param {number} inputHeight the width of the image in the input element. + * @param {String} mimeType The mimeType to save the blob as. + * @return {Promise} A promise that resolves with an object with an info key + * and a thumbnail key. + */ +async function createThumbnail( + element: ThumbnailableElement, + inputWidth: number, + inputHeight: number, + mimeType: string, +): Promise { + let targetWidth = inputWidth; + let targetHeight = inputHeight; + if (targetHeight > MAX_HEIGHT) { + targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); + targetHeight = MAX_HEIGHT; + } + if (targetWidth > MAX_WIDTH) { + targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); + targetWidth = MAX_WIDTH; + } + + let canvas: HTMLCanvasElement | OffscreenCanvas; + if (window.OffscreenCanvas) { + canvas = new window.OffscreenCanvas(targetWidth, targetHeight); + } else { + canvas = document.createElement("canvas"); + canvas.width = targetWidth; + canvas.height = targetHeight; + } + + const context = canvas.getContext("2d"); + context.drawImage(element, 0, 0, targetWidth, targetHeight); + + let thumbnailPromise: Promise; + + if (window.OffscreenCanvas) { + thumbnailPromise = (canvas as OffscreenCanvas).convertToBlob({ type: mimeType }); + } else { + thumbnailPromise = new Promise(resolve => (canvas as HTMLCanvasElement).toBlob(resolve, mimeType)); + } + + const imageData = context.getImageData(0, 0, targetWidth, targetHeight); + // thumbnailPromise and blurhash promise are being awaited concurrently + const blurhash = await BlurhashEncoder.instance.getBlurhash(imageData); + const thumbnail = await thumbnailPromise; + + return { + info: { + thumbnail_info: { + w: targetWidth, + h: targetHeight, + mimetype: thumbnail.type, + size: thumbnail.size, + }, + w: inputWidth, + h: inputHeight, + [BLURHASH_FIELD]: blurhash, + }, + thumbnail, + }; +} + +/** + * Load a file into a newly created image element. + * + * @param {File} imageFile The file to load in an image element. + * @return {Promise} A promise that resolves with the html image element. + */ +async function loadImageElement(imageFile: File) { + // Load the file into an html element + const img = document.createElement("img"); + const objectUrl = URL.createObjectURL(imageFile); + const imgPromise = new Promise((resolve, reject) => { + img.onload = function() { + URL.revokeObjectURL(objectUrl); + resolve(img); + }; + img.onerror = function(e) { + reject(e); + }; + }); + img.src = objectUrl; + + // check for hi-dpi PNGs and fudge display resolution as needed. + // this is mainly needed for macOS screencaps + let parsePromise; + if (imageFile.type === "image/png") { + // in practice macOS happens to order the chunks so they fall in + // the first 0x1000 bytes (thanks to a massive ICC header). + // Thus we could slice the file down to only sniff the first 0x1000 + // bytes (but this makes extractPngChunks choke on the corrupt file) + const headers = imageFile; //.slice(0, 0x1000); + parsePromise = readFileAsArrayBuffer(headers).then(arrayBuffer => { + const buffer = new Uint8Array(arrayBuffer); + const chunks = extractPngChunks(buffer); + for (const chunk of chunks) { + if (chunk.name === 'pHYs') { + if (chunk.data.byteLength !== PHYS_HIDPI.length) return; + return chunk.data.every((val, i) => val === PHYS_HIDPI[i]); + } + } + return false; + }); + } + + const [hidpi] = await Promise.all([parsePromise, imgPromise]); + const width = hidpi ? (img.width >> 1) : img.width; + const height = hidpi ? (img.height >> 1) : img.height; + return { width, height, img }; +} + +/** + * Read the metadata for an image file and create and upload a thumbnail of the image. + * + * @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with. + * @param {String} roomId The ID of the room the image will be uploaded in. + * @param {File} imageFile The image to read and thumbnail. + * @return {Promise} A promise that resolves with the attachment info. + */ +function infoForImageFile(matrixClient, roomId, imageFile) { + let thumbnailType = "image/png"; + if (imageFile.type === "image/jpeg") { + thumbnailType = "image/jpeg"; + } + + let imageInfo; + return loadImageElement(imageFile).then((r) => { + return createThumbnail(r.img, r.width, r.height, thumbnailType); + }).then((result) => { + imageInfo = result.info; + return uploadFile(matrixClient, roomId, result.thumbnail); + }).then((result) => { + imageInfo.thumbnail_url = result.url; + imageInfo.thumbnail_file = result.file; + return imageInfo; + }); +} + +/** + * Load a file into a newly created video element and pull some strings + * in an attempt to guarantee the first frame will be showing. + * + * @param {File} videoFile The file to load in an video element. + * @return {Promise} A promise that resolves with the video image element. + */ +function loadVideoElement(videoFile): Promise { + return new Promise((resolve, reject) => { + // Load the file into an html element + const video = document.createElement("video"); + video.preload = "metadata"; + video.playsInline = true; + video.muted = true; + + const reader = new FileReader(); + + reader.onload = function(ev) { + // Wait until we have enough data to thumbnail the first frame. + video.onloadeddata = async function() { + resolve(video); + video.pause(); + }; + video.onerror = function(e) { + reject(e); + }; + + video.src = ev.target.result as string; + video.load(); + video.play(); + }; + reader.onerror = function(e) { + reject(e); + }; + reader.readAsDataURL(videoFile); + }); +} + +/** + * Read the metadata for a video file and create and upload a thumbnail of the video. + * + * @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with. + * @param {String} roomId The ID of the room the video will be uploaded to. + * @param {File} videoFile The video to read and thumbnail. + * @return {Promise} A promise that resolves with the attachment info. + */ +function infoForVideoFile(matrixClient, roomId, videoFile) { + const thumbnailType = "image/jpeg"; + + let videoInfo; + return loadVideoElement(videoFile).then((video) => { + return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType); + }).then((result) => { + videoInfo = result.info; + return uploadFile(matrixClient, roomId, result.thumbnail); + }).then((result) => { + videoInfo.thumbnail_url = result.url; + videoInfo.thumbnail_file = result.file; + return videoInfo; + }); +} + +/** + * Read the file as an ArrayBuffer. + * @param {File} file The file to read + * @return {Promise} A promise that resolves with an ArrayBuffer when the file + * is read. + */ +function readFileAsArrayBuffer(file: File | Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = function(e) { + resolve(e.target.result as ArrayBuffer); + }; + reader.onerror = function(e) { + reject(e); + }; + reader.readAsArrayBuffer(file); + }); +} + +/** + * Upload the file to the content repository. + * If the room is encrypted then encrypt the file before uploading. + * + * @param {MatrixClient} matrixClient The matrix client to upload the file with. + * @param {String} roomId The ID of the room being uploaded to. + * @param {File} file The file to upload. + * @param {Function?} progressHandler optional callback to be called when a chunk of + * data is uploaded. + * @return {Promise} A promise that resolves with an object. + * If the file is unencrypted then the object will have a "url" key. + * If the file is encrypted then the object will have a "file" key. + */ +export function uploadFile( + matrixClient: MatrixClient, + roomId: string, + file: File | Blob, + progressHandler?: any, // TODO: Types +): IAbortablePromise<{url?: string, file?: any}> { // TODO: Types + let canceled = false; + if (matrixClient.isRoomEncrypted(roomId)) { + // If the room is encrypted then encrypt the file before uploading it. + // First read the file into memory. + let uploadPromise; + let encryptInfo; + const prom = readFileAsArrayBuffer(file).then(function(data) { + if (canceled) throw new UploadCanceledError(); + // Then encrypt the file. + return encrypt.encryptAttachment(data); + }).then(function(encryptResult) { + if (canceled) throw new UploadCanceledError(); + // Record the information needed to decrypt the attachment. + encryptInfo = encryptResult.info; + // Pass the encrypted data as a Blob to the uploader. + const blob = new Blob([encryptResult.data]); + uploadPromise = matrixClient.uploadContent(blob, { + progressHandler: progressHandler, + includeFilename: false, + }); + return uploadPromise; + }).then(function(url) { + if (canceled) throw new UploadCanceledError(); + // If the attachment is encrypted then bundle the URL along + // with the information needed to decrypt the attachment and + // add it under a file key. + encryptInfo.url = url; + if (file.type) { + encryptInfo.mimetype = file.type; + } + return { "file": encryptInfo }; + }) as IAbortablePromise<{ file: any }>; + prom.abort = () => { + canceled = true; + if (uploadPromise) matrixClient.cancelUpload(uploadPromise); + }; + return prom; + } else { + const basePromise = matrixClient.uploadContent(file, { + progressHandler: progressHandler, + }); + const promise1 = basePromise.then(function(url) { + if (canceled) throw new UploadCanceledError(); + // If the attachment isn't encrypted then include the URL directly. + return { url }; + }) as IAbortablePromise<{ url: string }>; + promise1.abort = () => { + canceled = true; + matrixClient.cancelUpload(basePromise); + }; + return promise1; + } +} + +export default class ContentMessages { + private inprogress: IUpload[] = []; + private mediaConfig: IMediaConfig = null; + + sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) { + const startTime = CountlyAnalytics.getTimestamp(); + const prom = matrixClient.sendStickerMessage(roomId, url, info, text).catch((e) => { + console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e); + throw e; + }); + CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, false, { msgtype: "m.sticker" }); + return prom; + } + + getUploadLimit() { + if (this.mediaConfig !== null && this.mediaConfig["m.upload.size"] !== undefined) { + return this.mediaConfig["m.upload.size"]; + } else { + return null; + } + } + + async sendContentListToRoom(files: File[], roomId: string, matrixClient: MatrixClient) { + if (matrixClient.isGuest()) { + dis.dispatch({ action: 'require_registration' }); + return; + } + + const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); + if (isQuoting) { + // FIXME: Using an import will result in Element crashing + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, { + title: _t('Replying With Files'), + description: ( +
{_t( + 'At this time it is not possible to reply with a file. ' + + 'Would you like to upload this file without replying?', + )}
+ ), + hasCancelButton: true, + button: _t("Continue"), + }); + const [shouldUpload] = await finished; + if (!shouldUpload) return; + } + + if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to + const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner'); + await this.ensureMediaConfigFetched(matrixClient); + modal.close(); + } + + const tooBigFiles = []; + const okFiles = []; + + for (let i = 0; i < files.length; ++i) { + if (this.isFileSizeAcceptable(files[i])) { + okFiles.push(files[i]); + } else { + tooBigFiles.push(files[i]); + } + } + + if (tooBigFiles.length > 0) { + // FIXME: Using an import will result in Element crashing + const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog"); + const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Failure', '', UploadFailureDialog, { + badFiles: tooBigFiles, + totalFiles: files.length, + contentMessages: this, + }); + const [shouldContinue] = await finished; + if (!shouldContinue) return; + } + + let uploadAll = false; + // Promise to complete before sending next file into room, used for synchronisation of file-sending + // to match the order the files were specified in + let promBefore: Promise = Promise.resolve(); + for (let i = 0; i < okFiles.length; ++i) { + const file = okFiles[i]; + if (!uploadAll) { + // FIXME: Using an import will result in Element crashing + const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog"); + const { finished } = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation', + '', UploadConfirmDialog, { + file, + currentIndex: i, + totalFiles: okFiles.length, + }, + ); + const [shouldContinue, shouldUploadAll] = await finished; + if (!shouldContinue) break; + if (shouldUploadAll) { + uploadAll = true; + } + } + promBefore = this.sendContentToRoom(file, roomId, matrixClient, promBefore); + } + } + + getCurrentUploads() { + return this.inprogress.filter(u => !u.canceled); + } + + cancelUpload(promise: Promise, matrixClient: MatrixClient) { + let upload: IUpload; + for (let i = 0; i < this.inprogress.length; ++i) { + if (this.inprogress[i].promise === promise) { + upload = this.inprogress[i]; + break; + } + } + if (upload) { + upload.canceled = true; + matrixClient.cancelUpload(upload.promise); + dis.dispatch({ action: Action.UploadCanceled, upload }); + } + } + + private sendContentToRoom(file: File, roomId: string, matrixClient: MatrixClient, promBefore: Promise) { + const startTime = CountlyAnalytics.getTimestamp(); + const content: IContent = { + body: file.name || 'Attachment', + info: { + size: file.size, + }, + msgtype: "", // set later + }; + + // if we have a mime type for the file, add it to the message metadata + if (file.type) { + content.info.mimetype = file.type; + } + + const prom = new Promise((resolve) => { + if (file.type.indexOf('image/') === 0) { + content.msgtype = 'm.image'; + infoForImageFile(matrixClient, roomId, file).then((imageInfo) => { + Object.assign(content.info, imageInfo); + resolve(); + }, (e) => { + console.error(e); + content.msgtype = 'm.file'; + resolve(); + }); + } else if (file.type.indexOf('audio/') === 0) { + content.msgtype = 'm.audio'; + resolve(); + } else if (file.type.indexOf('video/') === 0) { + content.msgtype = 'm.video'; + infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => { + Object.assign(content.info, videoInfo); + resolve(); + }, (e) => { + content.msgtype = 'm.file'; + resolve(); + }); + } else { + content.msgtype = 'm.file'; + resolve(); + } + }) as IAbortablePromise; + + // create temporary abort handler for before the actual upload gets passed off to js-sdk + prom.abort = () => { + upload.canceled = true; + }; + + const upload: IUpload = { + fileName: file.name || 'Attachment', + roomId: roomId, + total: file.size, + loaded: 0, + promise: prom, + }; + this.inprogress.push(upload); + dis.dispatch({ action: Action.UploadStarted, upload }); + + // Focus the composer view + dis.fire(Action.FocusSendMessageComposer); + + function onProgress(ev) { + upload.total = ev.total; + upload.loaded = ev.loaded; + dis.dispatch({ action: Action.UploadProgress, upload }); + } + + let error; + return prom.then(function() { + if (upload.canceled) throw new UploadCanceledError(); + // XXX: upload.promise must be the promise that + // is returned by uploadFile as it has an abort() + // method hacked onto it. + upload.promise = uploadFile(matrixClient, roomId, file, onProgress); + return upload.promise.then(function(result) { + content.file = result.file; + content.url = result.url; + }); + }).then(() => { + // Await previous message being sent into the room + return promBefore; + }).then(function() { + if (upload.canceled) throw new UploadCanceledError(); + const prom = matrixClient.sendMessage(roomId, content); + CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, false, content); + return prom; + }, function(err) { + error = err; + if (!upload.canceled) { + let desc = _t("The file '%(fileName)s' failed to upload.", { fileName: upload.fileName }); + if (err.http_status === 413) { + desc = _t( + "The file '%(fileName)s' exceeds this homeserver's size limit for uploads", + { fileName: upload.fileName }, + ); + } + // FIXME: Using an import will result in Element crashing + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Upload failed', '', ErrorDialog, { + title: _t('Upload Failed'), + description: desc, + }); + } + }).finally(() => { + for (let i = 0; i < this.inprogress.length; ++i) { + if (this.inprogress[i].promise === upload.promise) { + this.inprogress.splice(i, 1); + break; + } + } + if (error) { + // 413: File was too big or upset the server in some way: + // clear the media size limit so we fetch it again next time + // we try to upload + if (error && error.http_status === 413) { + this.mediaConfig = null; + } + dis.dispatch({ action: Action.UploadFailed, upload, error }); + } else { + dis.dispatch({ action: Action.UploadFinished, upload }); + dis.dispatch({ action: 'message_sent' }); + } + }); + } + + private isFileSizeAcceptable(file: File) { + if (this.mediaConfig !== null && + this.mediaConfig["m.upload.size"] !== undefined && + file.size > this.mediaConfig["m.upload.size"]) { + return false; + } + return true; + } + + private ensureMediaConfigFetched(matrixClient: MatrixClient) { + if (this.mediaConfig !== null) return; + + console.log("[Media Config] Fetching"); + return matrixClient.getMediaConfig().then((config) => { + console.log("[Media Config] Fetched config:", config); + return config; + }).catch(() => { + // Media repo can't or won't report limits, so provide an empty object (no limits). + console.log("[Media Config] Could not fetch config, so not limiting uploads."); + return {}; + }).then((config) => { + this.mediaConfig = config; + }); + } + + static sharedInstance() { + if (window.mxContentMessages === undefined) { + window.mxContentMessages = new ContentMessages(); + } + return window.mxContentMessages; + } +} diff --git a/src/CountlyAnalytics.ts b/src/CountlyAnalytics.ts new file mode 100644 index 0000000000..72b0462bcd --- /dev/null +++ b/src/CountlyAnalytics.ts @@ -0,0 +1,972 @@ +/* +Copyright 2020 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 { randomString } from "matrix-js-sdk/src/randomstring"; +import { IContent } from "matrix-js-sdk/src/models/event"; +import { sleep } from "matrix-js-sdk/src/utils"; + +import { getCurrentLanguage } from './languageHandler'; +import PlatformPeg from './PlatformPeg'; +import SdkConfig from './SdkConfig'; +import { MatrixClientPeg } from "./MatrixClientPeg"; +import RoomViewStore from "./stores/RoomViewStore"; +import { Action } from "./dispatcher/actions"; + +const INACTIVITY_TIME = 20; // seconds +const HEARTBEAT_INTERVAL = 5_000; // ms +const SESSION_UPDATE_INTERVAL = 60; // seconds +const MAX_PENDING_EVENTS = 1000; + +enum Orientation { + Landscape = "landscape", + Portrait = "portrait", +} + +/* eslint-disable camelcase */ +interface IMetrics { + _resolution?: string; + _app_version?: string; + _density?: number; + _ua?: string; + _locale?: string; +} + +interface IEvent { + key: string; + count: number; + sum?: number; + dur?: number; + segmentation?: Record; + timestamp?: number; // TODO should we use the timestamp when we start or end for the event timestamp + hour?: unknown; + dow?: unknown; +} + +interface IViewEvent extends IEvent { + key: "[CLY]_view"; +} + +interface IOrientationEvent extends IEvent { + key: "[CLY]_orientation"; + segmentation: { + mode: Orientation; + }; +} + +interface IStarRatingEvent extends IEvent { + key: "[CLY]_star_rating"; + segmentation: { + // we just care about collecting feedback, no need to associate with a feedback widget + widget_id?: string; + contactMe?: boolean; + email?: string; + rating: 1 | 2 | 3 | 4 | 5; + comment: string; + }; +} + +type Value = string | number | boolean; + +interface IOperationInc { + "$inc": number; +} +interface IOperationMul { + "$mul": number; +} +interface IOperationMax { + "$max": number; +} +interface IOperationMin { + "$min": number; +} +interface IOperationSetOnce { + "$setOnce": Value; +} +interface IOperationPush { + "$push": Value | Value[]; +} +interface IOperationAddToSet { + "$addToSet": Value | Value[]; +} +interface IOperationPull { + "$pull": Value | Value[]; +} + +type Operation = + IOperationInc | + IOperationMul | + IOperationMax | + IOperationMin | + IOperationSetOnce | + IOperationPush | + IOperationAddToSet | + IOperationPull; + +interface IUserDetails { + name?: string; + username?: string; + email?: string; + organization?: string; + phone?: string; + picture?: string; + gender?: string; + byear?: number; + custom?: Record; // `.` and `$` will be stripped out +} + +interface ICrash { + _resolution?: string; + _app_version: string; + + _ram_current?: number; + _ram_total?: number; + _disk_current?: number; + _disk_total?: number; + _orientation?: Orientation; + + _online?: boolean; + _muted?: boolean; + _background?: boolean; + _view?: string; + + _name?: string; + _error: string; + _nonfatal?: boolean; + _logs?: string; + _run?: number; + + _custom?: Record; +} + +interface IParams { + // APP_KEY of an app for which to report + app_key: string; + // User identifier + device_id: string; + + // Should provide value 1 to indicate session start + begin_session?: number; + // JSON object as string to provide metrics to track with the user + metrics?: string; + // Provides session duration in seconds, can be used as heartbeat to update current sessions duration, recommended time every 60 seconds + session_duration?: number; + // Should provide value 1 to indicate session end + end_session?: number; + + // 10 digit UTC timestamp for recording past data. + timestamp?: number; + // current user local hour (0 - 23) + hour?: number; + // day of the week (0-sunday, 1 - monday, ... 6 - saturday) + dow?: number; + + // JSON array as string containing event objects + events?: string; // IEvent[] + // JSON object as string containing information about users + user_details?: string; + + // provide when changing device ID, so server would merge the data + old_device_id?: string; + + // See ICrash + crash?: string; +} + +interface IRoomSegments extends Record { + room_id: string; // hashed + num_users: number; + is_encrypted: boolean; + is_public: boolean; +} + +interface ISendMessageEvent extends IEvent { + key: "send_message"; + dur: number; // how long it to send (until remote echo) + segmentation: IRoomSegments & { + is_edit: boolean; + is_reply: boolean; + msgtype: string; + format?: string; + }; +} + +interface IRoomDirectoryEvent extends IEvent { + key: "room_directory"; +} + +interface IRoomDirectoryDoneEvent extends IEvent { + key: "room_directory_done"; + dur: number; // time spent in the room directory modal +} + +interface IRoomDirectorySearchEvent extends IEvent { + key: "room_directory_search"; + sum: number; // number of search results + segmentation: { + query_length: number; + query_num_words: number; + }; +} + +interface IStartCallEvent extends IEvent { + key: "start_call"; + segmentation: IRoomSegments & { + is_video: boolean; + is_jitsi: boolean; + }; +} + +interface IJoinCallEvent extends IEvent { + key: "join_call"; + segmentation: IRoomSegments & { + is_video: boolean; + is_jitsi: boolean; + }; +} + +interface IBeginInviteEvent extends IEvent { + key: "begin_invite"; + segmentation: IRoomSegments; +} + +interface ISendInviteEvent extends IEvent { + key: "send_invite"; + sum: number; // quantity that was invited + segmentation: IRoomSegments; +} + +interface ICreateRoomEvent extends IEvent { + key: "create_room"; + dur: number; // how long it took to create (until remote echo) + segmentation: { + room_id: string; // hashed + num_users: number; + is_encrypted: boolean; + is_public: boolean; + }; +} + +interface IJoinRoomEvent extends IEvent { + key: Action.JoinRoom; + dur: number; // how long it took to join (until remote echo) + segmentation: { + room_id: string; // hashed + num_users: number; + is_encrypted: boolean; + is_public: boolean; + type: "room_directory" | "slash_command" | "link" | "invite"; + }; +} +/* eslint-enable camelcase */ + +const hashHex = async (input: string): Promise => { + const buf = new TextEncoder().encode(input); + const digestBuf = await window.crypto.subtle.digest("sha-256", buf); + return [...new Uint8Array(digestBuf)].map((b: number) => b.toString(16).padStart(2, "0")).join(""); +}; + +const knownScreens = new Set([ + "register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory", + "start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group", +]); + +interface IViewData { + name: string; + url: string; + meta: Record; +} + +// Apply fn to all hash path parts after the 1st one +async function getViewData(anonymous = true): Promise { + const rand = randomString(8); + const { origin, hash } = window.location; + let { pathname } = window.location; + + // Redact paths which could contain unexpected PII + if (origin.startsWith('file://')) { + pathname = `//`; // XXX: inject rand because Count.ly doesn't like X->X transitions + } + + let [_, screen, ...parts] = hash.split("/"); + + if (!knownScreens.has(screen)) { + screen = ``; + } + + for (let i = 0; i < parts.length; i++) { + parts[i] = anonymous ? `` : await hashHex(parts[i]); + } + + const hashStr = `${_}/${screen}/${parts.join("/")}`; + const url = origin + pathname + hashStr; + + const meta = {}; + + let name = "$/" + hash; + switch (screen) { + case "room": { + name = "view_room"; + const roomId = RoomViewStore.getRoomId(); + name += " " + parts[0]; // XXX: workaround Count.ly missing X->X transitions + meta["room_id"] = parts[0]; + Object.assign(meta, getRoomStats(roomId)); + break; + } + } + + return { name, url, meta }; +} + +const getRoomStats = (roomId: string) => { + const cli = MatrixClientPeg.get(); + const room = cli?.getRoom(roomId); + + return { + "num_users": room?.getJoinedMemberCount(), + "is_encrypted": cli?.isRoomEncrypted(roomId), + // eslint-disable-next-line camelcase + "is_public": room?.currentState.getStateEvents("m.room.join_rules", "")?.getContent()?.join_rule === "public", + }; +}; + +// async wrapper for regex-powered String.prototype.replace +const strReplaceAsync = async (str: string, regex: RegExp, fn: (...args: string[]) => Promise) => { + const promises: Promise[] = []; + // dry-run to calculate the replace values + str.replace(regex, (...args: string[]) => { + promises.push(fn(...args)); + return ""; + }); + const values = await Promise.all(promises); + return str.replace(regex, () => values.shift()); +}; + +export default class CountlyAnalytics { + private baseUrl: URL = null; + private appKey: string = null; + private userKey: string = null; + private anonymous: boolean; + private appPlatform: string; + private appVersion = "unknown"; + + private initTime = CountlyAnalytics.getTimestamp(); + private firstPage = true; + private heartbeatIntervalId: number; + private activityIntervalId: number; + private trackTime = true; + private lastBeat: number; + private storedDuration = 0; + private lastView: string; + private lastViewTime = 0; + private lastViewStoredDuration = 0; + private sessionStarted = false; + private heartbeatEnabled = false; + private inactivityCounter = 0; + private pendingEvents: IEvent[] = []; + + private static internalInstance = new CountlyAnalytics(); + + public static get instance(): CountlyAnalytics { + return CountlyAnalytics.internalInstance; + } + + public get disabled() { + return !this.baseUrl; + } + + public canEnable() { + const config = SdkConfig.get(); + return Boolean(navigator.doNotTrack !== "1" && config?.countly?.url && config?.countly?.appKey); + } + + private async changeUserKey(userKey: string, merge = false) { + const oldUserKey = this.userKey; + this.userKey = userKey; + if (oldUserKey && merge) { + await this.request({ old_device_id: oldUserKey }); + } + } + + public async enable(anonymous = true) { + if (!this.disabled && this.anonymous === anonymous) return; + if (!this.canEnable()) return; + + if (!this.disabled) { + // flush request queue as our userKey is going to change, no need to await it + this.request(); + } + + const config = SdkConfig.get(); + this.baseUrl = new URL("/i", config.countly.url); + this.appKey = config.countly.appKey; + + this.anonymous = anonymous; + if (anonymous) { + await this.changeUserKey(randomString(64)); + } else { + await this.changeUserKey(await hashHex(MatrixClientPeg.get().getUserId()), true); + } + + const platform = PlatformPeg.get(); + this.appPlatform = platform.getHumanReadableName(); + try { + this.appVersion = await platform.getAppVersion(); + } catch (e) { + console.warn("Failed to get app version, using 'unknown'"); + } + + // start heartbeat + this.heartbeatIntervalId = setInterval(this.heartbeat.bind(this), HEARTBEAT_INTERVAL); + this.trackSessions(); + this.trackErrors(); + } + + public async disable() { + if (this.disabled) return; + await this.track("Opt-Out" ); + this.endSession(); + window.clearInterval(this.heartbeatIntervalId); + window.clearTimeout(this.activityIntervalId); + this.baseUrl = null; + // remove listeners bound in trackSessions() + window.removeEventListener("beforeunload", this.endSession); + window.removeEventListener("unload", this.endSession); + window.removeEventListener("visibilitychange", this.onVisibilityChange); + window.removeEventListener("mousemove", this.onUserActivity); + window.removeEventListener("click", this.onUserActivity); + window.removeEventListener("keydown", this.onUserActivity); + window.removeEventListener("scroll", this.onUserActivity); + } + + public reportFeedback(rating: 1 | 2 | 3 | 4 | 5, comment: string) { + this.track("[CLY]_star_rating", { rating, comment }, null, {}, true); + } + + public trackPageChange(generationTimeMs?: number) { + if (this.disabled) return; + // TODO use generationTimeMs + this.trackPageView(); + } + + private async trackPageView() { + this.reportViewDuration(); + + await sleep(0); // XXX: we sleep here because otherwise we get the old hash and not the new one + const viewData = await getViewData(this.anonymous); + + const page = viewData.name; + this.lastView = page; + this.lastViewTime = CountlyAnalytics.getTimestamp(); + const segments = { + ...viewData.meta, + name: page, + visit: 1, + domain: window.location.hostname, + view: viewData.url, + segment: this.appPlatform, + start: this.firstPage, + }; + + if (this.firstPage) { + this.firstPage = false; + } + + this.track("[CLY]_view", segments); + } + + public static getTimestamp() { + return Math.floor(new Date().getTime() / 1000); + } + + // store the last ms timestamp returned + // we do this to prevent the ts from ever decreasing in the case of system time changing + private lastMsTs = 0; + + private getMsTimestamp() { + const ts = new Date().getTime(); + if (this.lastMsTs >= ts) { + // increment ts as to keep our data points well-ordered + this.lastMsTs++; + } else { + this.lastMsTs = ts; + } + return this.lastMsTs; + } + + public async recordError(err: Error | string, fatal = false) { + if (this.disabled || this.anonymous) return; + + let error = ""; + if (typeof err === "object") { + if (typeof err.stack !== "undefined") { + error = err.stack; + } else { + if (typeof err.name !== "undefined") { + error += err.name + ":"; + } + if (typeof err.message !== "undefined") { + error += err.message + "\n"; + } + if (typeof err.fileName !== "undefined") { + error += "in " + err.fileName + "\n"; + } + if (typeof err.lineNumber !== "undefined") { + error += "on " + err.lineNumber; + } + if (typeof err.columnNumber !== "undefined") { + error += ":" + err.columnNumber; + } + } + } else { + error = err + ""; + } + + // sanitize the error from identifiers + error = await strReplaceAsync(error, /([!@+#]).+?:[\w:.]+/g, async (substring: string, glyph: string) => { + return glyph + await hashHex(substring.substring(1)); + }); + + const metrics = this.getMetrics(); + const ob: ICrash = { + _resolution: metrics?._resolution, + _error: error, + _app_version: this.appVersion, + _run: CountlyAnalytics.getTimestamp() - this.initTime, + _nonfatal: !fatal, + _view: this.lastView, + }; + + if (typeof navigator.onLine !== "undefined") { + ob._online = navigator.onLine; + } + + ob._background = document.hasFocus(); + + this.request({ crash: JSON.stringify(ob) }); + } + + private trackErrors() { + //override global uncaught error handler + window.onerror = (msg, url, line, col, err) => { + if (typeof err !== "undefined") { + this.recordError(err, false); + } else { + let error = ""; + if (typeof msg !== "undefined") { + error += msg + "\n"; + } + if (typeof url !== "undefined") { + error += "at " + url; + } + if (typeof line !== "undefined") { + error += ":" + line; + } + if (typeof col !== "undefined") { + error += ":" + col; + } + error += "\n"; + + try { + const stack = []; + // eslint-disable-next-line no-caller + let f = arguments.callee.caller; + while (f) { + stack.push(f.name); + f = f.caller; + } + error += stack.join("\n"); + } catch (ex) { + //silent error + } + this.recordError(error, false); + } + }; + + window.addEventListener('unhandledrejection', (event) => { + this.recordError(new Error(`Unhandled rejection (reason: ${event.reason?.stack || event.reason}).`), true); + }); + } + + private heartbeat() { + const args: Pick = {}; + + // extend session if needed + if (this.sessionStarted && this.trackTime) { + const last = CountlyAnalytics.getTimestamp(); + if (last - this.lastBeat >= SESSION_UPDATE_INTERVAL) { + args.session_duration = last - this.lastBeat; + this.lastBeat = last; + } + } + + // process event queue + if (this.pendingEvents.length > 0 || args.session_duration) { + this.request(args); + } + } + + private async request( + args: Omit + & Partial> = {}, + ) { + const request: IParams = { + app_key: this.appKey, + device_id: this.userKey, + ...this.getTimeParams(), + ...args, + }; + + if (this.pendingEvents.length > 0) { + const EVENT_BATCH_SIZE = 10; + const events = this.pendingEvents.splice(0, EVENT_BATCH_SIZE); + request.events = JSON.stringify(events); + } + + const params = new URLSearchParams(request as {}); + + try { + await window.fetch(this.baseUrl.toString(), { + method: "POST", + mode: "no-cors", + cache: "no-cache", + redirect: "follow", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: params, + }); + } catch (e) { + console.error("Analytics error: ", e); + } + } + + private getTimeParams(): Pick { + const date = new Date(); + return { + timestamp: this.getMsTimestamp(), + hour: date.getHours(), + dow: date.getDay(), + }; + } + + private queue(args: Omit & Partial>) { + const { count = 1, ...rest } = args; + const ev = { + ...this.getTimeParams(), + ...rest, + count, + platform: this.appPlatform, + app_version: this.appVersion, + }; + + this.pendingEvents.push(ev); + if (this.pendingEvents.length > MAX_PENDING_EVENTS) { + this.pendingEvents.shift(); + } + } + + private getOrientation = (): Orientation => { + return window.matchMedia("(orientation: landscape)").matches + ? Orientation.Landscape + : Orientation.Portrait; + }; + + private reportOrientation = () => { + this.track("[CLY]_orientation", { + mode: this.getOrientation(), + }); + }; + + private startTime() { + if (!this.trackTime) { + this.trackTime = true; + this.lastBeat = CountlyAnalytics.getTimestamp() - this.storedDuration; + this.lastViewTime = CountlyAnalytics.getTimestamp() - this.lastViewStoredDuration; + this.lastViewStoredDuration = 0; + } + } + + private stopTime() { + if (this.trackTime) { + this.trackTime = false; + this.storedDuration = CountlyAnalytics.getTimestamp() - this.lastBeat; + this.lastViewStoredDuration = CountlyAnalytics.getTimestamp() - this.lastViewTime; + } + } + + private getMetrics(): IMetrics { + if (this.anonymous) return undefined; + const metrics: IMetrics = {}; + + // getting app version + metrics._app_version = this.appVersion; + metrics._ua = navigator.userAgent; + + // getting resolution + if (screen.width && screen.height) { + metrics._resolution = `${screen.width}x${screen.height}`; + } + + // getting density ratio + if (window.devicePixelRatio) { + metrics._density = window.devicePixelRatio; + } + + // getting locale + metrics._locale = getCurrentLanguage(); + + return metrics; + } + + private async beginSession(heartbeat = true) { + if (!this.sessionStarted) { + this.reportOrientation(); + window.addEventListener("resize", this.reportOrientation); + + this.lastBeat = CountlyAnalytics.getTimestamp(); + this.sessionStarted = true; + this.heartbeatEnabled = heartbeat; + + const userDetails: IUserDetails = { + custom: { + "home_server": MatrixClientPeg.get() && MatrixClientPeg.getHomeserverName(), // TODO hash? + "anonymous": this.anonymous, + }, + }; + + const request: Parameters[0] = { + begin_session: 1, + user_details: JSON.stringify(userDetails), + }; + + const metrics = this.getMetrics(); + if (metrics) { + request.metrics = JSON.stringify(metrics); + } + + await this.request(request); + } + } + + private reportViewDuration() { + if (this.lastView) { + this.track("[CLY]_view", { + name: this.lastView, + }, null, { + dur: this.trackTime ? CountlyAnalytics.getTimestamp() - this.lastViewTime : this.lastViewStoredDuration, + }); + this.lastView = null; + } + } + + private endSession = () => { + if (this.sessionStarted) { + window.removeEventListener("resize", this.reportOrientation); + + this.reportViewDuration(); + this.request({ + end_session: 1, + session_duration: CountlyAnalytics.getTimestamp() - this.lastBeat, + }); + } + this.sessionStarted = false; + }; + + private onVisibilityChange = () => { + if (document.hidden) { + this.stopTime(); + } else { + this.startTime(); + } + }; + + private onUserActivity = () => { + if (this.inactivityCounter >= INACTIVITY_TIME) { + this.startTime(); + } + this.inactivityCounter = 0; + }; + + private trackSessions() { + this.beginSession(); + this.startTime(); + + window.addEventListener("beforeunload", this.endSession); + window.addEventListener("unload", this.endSession); + window.addEventListener("visibilitychange", this.onVisibilityChange); + window.addEventListener("mousemove", this.onUserActivity); + window.addEventListener("click", this.onUserActivity); + window.addEventListener("keydown", this.onUserActivity); + // Using the passive option to not block the main thread + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners + window.addEventListener("scroll", this.onUserActivity, { passive: true }); + + this.activityIntervalId = setInterval(() => { + this.inactivityCounter++; + if (this.inactivityCounter >= INACTIVITY_TIME) { + this.stopTime(); + } + }, 60_000); + } + + public trackBeginInvite(roomId: string) { + this.track("begin_invite", {}, roomId); + } + + public trackSendInvite(startTime: number, roomId: string, qty: number) { + this.track("send_invite", {}, roomId, { + dur: CountlyAnalytics.getTimestamp() - startTime, + sum: qty, + }); + } + + public async trackRoomCreate(startTime: number, roomId: string) { + if (this.disabled) return; + + let endTime = CountlyAnalytics.getTimestamp(); + const cli = MatrixClientPeg.get(); + if (!cli.getRoom(roomId)) { + await new Promise(resolve => { + const handler = (room) => { + if (room.roomId === roomId) { + cli.off("Room", handler); + resolve(); + } + }; + cli.on("Room", handler); + }); + endTime = CountlyAnalytics.getTimestamp(); + } + + this.track("create_room", {}, roomId, { + dur: endTime - startTime, + }); + } + + public trackRoomJoin(startTime: number, roomId: string, type: IJoinRoomEvent["segmentation"]["type"]) { + this.track(Action.JoinRoom, { type }, roomId, { + dur: CountlyAnalytics.getTimestamp() - startTime, + }); + } + + public async trackSendMessage( + startTime: number, + // eslint-disable-next-line camelcase + sendPromise: Promise<{event_id: string}>, + roomId: string, + isEdit: boolean, + isReply: boolean, + content: IContent, + ) { + if (this.disabled) return; + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(roomId); + + const eventId = (await sendPromise).event_id; + let endTime = CountlyAnalytics.getTimestamp(); + + if (!room.findEventById(eventId)) { + await new Promise(resolve => { + const handler = (ev) => { + if (ev.getId() === eventId) { + room.off("Room.localEchoUpdated", handler); + resolve(); + } + }; + + room.on("Room.localEchoUpdated", handler); + }); + endTime = CountlyAnalytics.getTimestamp(); + } + + this.track("send_message", { + is_edit: isEdit, + is_reply: isReply, + msgtype: content.msgtype, + format: content.format, + }, roomId, { + dur: endTime - startTime, + }); + } + + public trackStartCall(roomId: string, isVideo = false, isJitsi = false) { + this.track("start_call", { + is_video: isVideo, + is_jitsi: isJitsi, + }, roomId); + } + + public trackJoinCall(roomId: string, isVideo = false, isJitsi = false) { + this.track("join_call", { + is_video: isVideo, + is_jitsi: isJitsi, + }, roomId); + } + + public trackRoomDirectoryBegin() { + this.track("room_directory"); + } + + public trackRoomDirectory(startTime: number) { + this.track("room_directory_done", {}, null, { + dur: CountlyAnalytics.getTimestamp() - startTime, + }); + } + + public trackRoomDirectorySearch(numResults: number, query: string) { + this.track("room_directory_search", { + query_length: query.length, + query_num_words: query.split(" ").length, + }, null, { + sum: numResults, + }); + } + + public async track( + key: E["key"], + segments?: Omit, + roomId?: string, + args?: Partial>, + anonymous = false, + ) { + if (this.disabled && !anonymous) return; + + let segmentation = segments || {}; + + if (roomId) { + segmentation = { + room_id: await hashHex(roomId), + ...getRoomStats(roomId), + ...segments, + }; + } + + this.queue({ + key, + count: 1, + segmentation, + ...args, + }); + + // if this event can be sent anonymously and we are disabled then dispatch it right away + if (this.disabled && anonymous) { + await this.request({ device_id: randomString(64) }); + } + } +} + +// expose on window for easy access from the console +window.mxCountlyAnalytics = CountlyAnalytics; diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js deleted file mode 100644 index 1bcf1ba706..0000000000 --- a/src/CrossSigningManager.js +++ /dev/null @@ -1,267 +0,0 @@ -/* -Copyright 2019 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 Modal from './Modal'; -import * as sdk from './index'; -import {MatrixClientPeg} from './MatrixClientPeg'; -import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; -import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; -import { _t } from './languageHandler'; -import SettingsStore from './settings/SettingsStore'; -import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; - -// This stores the secret storage private keys in memory for the JS SDK. This is -// only meant to act as a cache to avoid prompting the user multiple times -// during the same single operation. Use `accessSecretStorage` below to scope a -// single secret storage operation, as it will clear the cached keys once the -// operation ends. -let secretStorageKeys = {}; -let secretStorageBeingAccessed = false; - -function isCachingAllowed() { - return ( - secretStorageBeingAccessed || - SettingsStore.getValue("keepSecretStoragePassphraseForSession") - ); -} - -export class AccessCancelledError extends Error { - constructor() { - super("Secret storage access canceled"); - } -} - -async function confirmToDismiss(name) { - let description; - if (name === "m.cross_signing.user_signing") { - description = _t("If you cancel now, you won't complete verifying the other user."); - } else if (name === "m.cross_signing.self_signing") { - description = _t("If you cancel now, you won't complete verifying your other session."); - } else { - description = _t("If you cancel now, you won't complete your secret storage operation."); - } - - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const [sure] = await Modal.createDialog(QuestionDialog, { - title: _t("Cancel entering passphrase?"), - description, - danger: true, - cancelButton: _t("Enter passphrase"), - button: _t("Cancel"), - }).finished; - return sure; -} - -async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { - const keyInfoEntries = Object.entries(keyInfos); - if (keyInfoEntries.length > 1) { - throw new Error("Multiple storage key requests not implemented"); - } - const [name, info] = keyInfoEntries[0]; - - // Check the in-memory cache - if (isCachingAllowed() && secretStorageKeys[name]) { - return [name, secretStorageKeys[name]]; - } - - const inputToKey = async ({ passphrase, recoveryKey }) => { - if (passphrase) { - return deriveKey( - passphrase, - info.passphrase.salt, - info.passphrase.iterations, - ); - } else { - return decodeRecoveryKey(recoveryKey); - } - }; - const AccessSecretStorageDialog = - sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog"); - const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", - AccessSecretStorageDialog, - /* props= */ - { - keyInfo: info, - checkPrivateKey: async (input) => { - const key = await inputToKey(input); - return await MatrixClientPeg.get().checkSecretStorageKey(key, info); - }, - }, - /* className= */ null, - /* isPriorityModal= */ false, - /* isStaticModal= */ false, - /* options= */ { - onBeforeClose: async (reason) => { - if (reason === "backgroundClick") { - return confirmToDismiss(ssssItemName); - } - return true; - }, - }, - ); - const [input] = await finished; - if (!input) { - throw new AccessCancelledError(); - } - const key = await inputToKey(input); - - // Save to cache to avoid future prompts in the current session - if (isCachingAllowed()) { - secretStorageKeys[name] = key; - } - - return [name, key]; -} - -const onSecretRequested = async function({ - user_id: userId, - device_id: deviceId, - request_id: requestId, - name, - device_trust: deviceTrust, -}) { - console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust); - const client = MatrixClientPeg.get(); - if (userId !== client.getUserId()) { - return; - } - if (!deviceTrust || !deviceTrust.isVerified()) { - console.log(`CrossSigningManager: Ignoring request from untrusted device ${deviceId}`); - return; - } - if (name.startsWith("m.cross_signing")) { - const callbacks = client.getCrossSigningCacheCallbacks(); - if (!callbacks.getCrossSigningKeyCache) return; - /* Explicit enumeration here is deliberate – never share the master key! */ - if (name === "m.cross_signing.self_signing") { - const key = await callbacks.getCrossSigningKeyCache("self_signing"); - if (!key) { - console.log( - `self_signing requested by ${deviceId}, but not found in cache`, - ); - } - return key && encodeBase64(key); - } else if (name === "m.cross_signing.user_signing") { - const key = await callbacks.getCrossSigningKeyCache("user_signing"); - if (!key) { - console.log( - `user_signing requested by ${deviceId}, but not found in cache`, - ); - } - return key && encodeBase64(key); - } - } else if (name === "m.megolm_backup.v1") { - const key = await client._crypto.getSessionBackupPrivateKey(); - if (!key) { - console.log( - `session backup key requested by ${deviceId}, but not found in cache`, - ); - } - return key && encodeBase64(key); - } - console.warn("onSecretRequested didn't recognise the secret named ", name); -}; - -export const crossSigningCallbacks = { - getSecretStorageKey, - onSecretRequested, -}; - -export async function promptForBackupPassphrase() { - let key; - - const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { - showSummary: false, keyCallback: k => key = k, - }, null, /* priority = */ false, /* static = */ true); - - const success = await finished; - if (!success) throw new Error("Key backup prompt cancelled"); - - return key; -} - -/** - * This helper should be used whenever you need to access secret storage. It - * ensures that secret storage (and also cross-signing since they each depend on - * each other in a cycle of sorts) have been bootstrapped before running the - * provided function. - * - * Bootstrapping secret storage may take one of these paths: - * 1. Create secret storage from a passphrase and store cross-signing keys - * in secret storage. - * 2. Access existing secret storage by requesting passphrase and accessing - * cross-signing keys as needed. - * 3. All keys are loaded and there's nothing to do. - * - * Additionally, the secret storage keys are cached during the scope of this function - * to ensure the user is prompted only once for their secret storage - * passphrase. The cache is then cleared once the provided function completes. - * - * @param {Function} [func] An operation to perform once secret storage has been - * bootstrapped. Optional. - * @param {bool} [forceReset] Reset secret storage even if it's already set up - */ -export async function accessSecretStorage(func = async () => { }, forceReset = false) { - const cli = MatrixClientPeg.get(); - secretStorageBeingAccessed = true; - try { - if (!await cli.hasSecretStorageKey() || forceReset) { - // This dialog calls bootstrap itself after guiding the user through - // passphrase creation. - const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '', - import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"), - { - force: forceReset, - }, - null, /* priority = */ false, /* static = */ true, - ); - const [confirmed] = await finished; - if (!confirmed) { - throw new Error("Secret storage creation canceled"); - } - } else { - const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); - await cli.bootstrapSecretStorage({ - authUploadDeviceSigningKeys: async (makeRequest) => { - const { finished } = Modal.createTrackedDialog( - 'Cross-signing keys dialog', '', InteractiveAuthDialog, - { - title: _t("Setting up keys"), - matrixClient: MatrixClientPeg.get(), - makeRequest, - }, - ); - const [confirmed] = await finished; - if (!confirmed) { - throw new Error("Cross-signing key upload auth canceled"); - } - }, - getBackupPassphrase: promptForBackupPassphrase, - }); - } - - // `return await` needed here to ensure `finally` block runs after the - // inner operation completes. - return await func(); - } finally { - // Clear secret storage key cache now that work is complete - secretStorageBeingAccessed = false; - if (!isCachingAllowed()) { - secretStorageKeys = {}; - } - } -} diff --git a/src/DateUtils.js b/src/DateUtils.js deleted file mode 100644 index 108697238c..0000000000 --- a/src/DateUtils.js +++ /dev/null @@ -1,138 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { _t } from './languageHandler'; - -function getDaysArray() { - return [ - _t('Sun'), - _t('Mon'), - _t('Tue'), - _t('Wed'), - _t('Thu'), - _t('Fri'), - _t('Sat'), - ]; -} - -function getMonthsArray() { - return [ - _t('Jan'), - _t('Feb'), - _t('Mar'), - _t('Apr'), - _t('May'), - _t('Jun'), - _t('Jul'), - _t('Aug'), - _t('Sep'), - _t('Oct'), - _t('Nov'), - _t('Dec'), - ]; -} - -function pad(n) { - return (n < 10 ? '0' : '') + n; -} - -function twelveHourTime(date, showSeconds=false) { - let hours = date.getHours() % 12; - const minutes = pad(date.getMinutes()); - const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM'); - hours = hours ? hours : 12; // convert 0 -> 12 - if (showSeconds) { - const seconds = pad(date.getSeconds()); - return `${hours}:${minutes}:${seconds}${ampm}`; - } - return `${hours}:${minutes}${ampm}`; -} - -export function formatDate(date, showTwelveHour=false) { - const now = new Date(); - const days = getDaysArray(); - const months = getMonthsArray(); - if (date.toDateString() === now.toDateString()) { - return formatTime(date, showTwelveHour); - } else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { - // TODO: use standard date localize function provided in counterpart - return _t('%(weekDayName)s %(time)s', { - weekDayName: days[date.getDay()], - time: formatTime(date, showTwelveHour), - }); - } else if (now.getFullYear() === date.getFullYear()) { - // TODO: use standard date localize function provided in counterpart - return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', { - weekDayName: days[date.getDay()], - monthName: months[date.getMonth()], - day: date.getDate(), - time: formatTime(date, showTwelveHour), - }); - } - return formatFullDate(date, showTwelveHour); -} - -export function formatFullDateNoTime(date) { - const days = getDaysArray(); - const months = getMonthsArray(); - return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s', { - weekDayName: days[date.getDay()], - monthName: months[date.getMonth()], - day: date.getDate(), - fullYear: date.getFullYear(), - }); -} - -export function formatFullDate(date, showTwelveHour=false) { - const days = getDaysArray(); - const months = getMonthsArray(); - return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', { - weekDayName: days[date.getDay()], - monthName: months[date.getMonth()], - day: date.getDate(), - fullYear: date.getFullYear(), - time: formatFullTime(date, showTwelveHour), - }); -} - -export function formatFullTime(date, showTwelveHour=false) { - if (showTwelveHour) { - return twelveHourTime(date, true); - } - return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds()); -} - -export function formatTime(date, showTwelveHour=false) { - if (showTwelveHour) { - return twelveHourTime(date); - } - return pad(date.getHours()) + ':' + pad(date.getMinutes()); -} - -const MILLIS_IN_DAY = 86400000; -export function wantsDateSeparator(prevEventDate, nextEventDate) { - if (!nextEventDate || !prevEventDate) { - return false; - } - // Return early for events that are > 24h apart - if (Math.abs(prevEventDate.getTime() - nextEventDate.getTime()) > MILLIS_IN_DAY) { - return true; - } - - // Compare weekdays - return prevEventDate.getDay() !== nextEventDate.getDay(); -} diff --git a/src/DateUtils.ts b/src/DateUtils.ts new file mode 100644 index 0000000000..e4a1175d88 --- /dev/null +++ b/src/DateUtils.ts @@ -0,0 +1,138 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { _t } from './languageHandler'; + +function getDaysArray(): string[] { + return [ + _t('Sun'), + _t('Mon'), + _t('Tue'), + _t('Wed'), + _t('Thu'), + _t('Fri'), + _t('Sat'), + ]; +} + +function getMonthsArray(): string[] { + return [ + _t('Jan'), + _t('Feb'), + _t('Mar'), + _t('Apr'), + _t('May'), + _t('Jun'), + _t('Jul'), + _t('Aug'), + _t('Sep'), + _t('Oct'), + _t('Nov'), + _t('Dec'), + ]; +} + +function pad(n: number): string { + return (n < 10 ? '0' : '') + n; +} + +function twelveHourTime(date: Date, showSeconds = false): string { + let hours = date.getHours() % 12; + const minutes = pad(date.getMinutes()); + const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM'); + hours = hours ? hours : 12; // convert 0 -> 12 + if (showSeconds) { + const seconds = pad(date.getSeconds()); + return `${hours}:${minutes}:${seconds}${ampm}`; + } + return `${hours}:${minutes}${ampm}`; +} + +export function formatDate(date: Date, showTwelveHour = false): string { + const now = new Date(); + const days = getDaysArray(); + const months = getMonthsArray(); + if (date.toDateString() === now.toDateString()) { + return formatTime(date, showTwelveHour); + } else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { + // TODO: use standard date localize function provided in counterpart + return _t('%(weekDayName)s %(time)s', { + weekDayName: days[date.getDay()], + time: formatTime(date, showTwelveHour), + }); + } else if (now.getFullYear() === date.getFullYear()) { + // TODO: use standard date localize function provided in counterpart + return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', { + weekDayName: days[date.getDay()], + monthName: months[date.getMonth()], + day: date.getDate(), + time: formatTime(date, showTwelveHour), + }); + } + return formatFullDate(date, showTwelveHour); +} + +export function formatFullDateNoTime(date: Date): string { + const days = getDaysArray(); + const months = getMonthsArray(); + return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s', { + weekDayName: days[date.getDay()], + monthName: months[date.getMonth()], + day: date.getDate(), + fullYear: date.getFullYear(), + }); +} + +export function formatFullDate(date: Date, showTwelveHour = false, showSeconds = true): string { + const days = getDaysArray(); + const months = getMonthsArray(); + return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', { + weekDayName: days[date.getDay()], + monthName: months[date.getMonth()], + day: date.getDate(), + fullYear: date.getFullYear(), + time: showSeconds ? formatFullTime(date, showTwelveHour) : formatTime(date, showTwelveHour), + }); +} + +export function formatFullTime(date: Date, showTwelveHour = false): string { + if (showTwelveHour) { + return twelveHourTime(date, true); + } + return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds()); +} + +export function formatTime(date: Date, showTwelveHour = false): string { + if (showTwelveHour) { + return twelveHourTime(date); + } + return pad(date.getHours()) + ':' + pad(date.getMinutes()); +} + +const MILLIS_IN_DAY = 86400000; +export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean { + if (!nextEventDate || !prevEventDate) { + return false; + } + // Return early for events that are > 24h apart + if (Math.abs(prevEventDate.getTime() - nextEventDate.getTime()) > MILLIS_IN_DAY) { + return true; + } + + // Compare weekdays + return prevEventDate.getDay() !== nextEventDate.getDay(); +} diff --git a/src/DecryptionFailureTracker.js b/src/DecryptionFailureTracker.js deleted file mode 100644 index b02a5e937b..0000000000 --- a/src/DecryptionFailureTracker.js +++ /dev/null @@ -1,202 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -export class DecryptionFailure { - constructor(failedEventId, errorCode) { - this.failedEventId = failedEventId; - this.errorCode = errorCode; - this.ts = Date.now(); - } -} - -export class DecryptionFailureTracker { - // Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list - // is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did - // are accumulated in `failureCounts`. - failures = []; - - // A histogram of the number of failures that will be tracked at the next tracking - // interval, split by failure error code. - failureCounts = { - // [errorCode]: 42 - }; - - // Event IDs of failures that were tracked previously - trackedEventHashMap = { - // [eventId]: true - }; - - // Set to an interval ID when `start` is called - checkInterval = null; - trackInterval = null; - - // Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`. - static TRACK_INTERVAL_MS = 60000; - - // Call `checkFailures` every `CHECK_INTERVAL_MS`. - static CHECK_INTERVAL_MS = 5000; - - // Give events a chance to be decrypted by waiting `GRACE_PERIOD_MS` before counting - // the failure in `failureCounts`. - static GRACE_PERIOD_MS = 60000; - - /** - * Create a new DecryptionFailureTracker. - * - * Call `eventDecrypted(event, err)` on this instance when an event is decrypted. - * - * Call `start()` to start the tracker, and `stop()` to stop tracking. - * - * @param {function} fn The tracking function, which will be called when failures - * are tracked. The function should have a signature `(count, trackedErrorCode) => {...}`, - * where `count` is the number of failures and `errorCode` matches the `.code` of - * provided DecryptionError errors (by default, unless `errorCodeMapFn` is specified. - * @param {function?} errorCodeMapFn The function used to map error codes to the - * trackedErrorCode. If not provided, the `.code` of errors will be used. - */ - constructor(fn, errorCodeMapFn) { - if (!fn || typeof fn !== 'function') { - throw new Error('DecryptionFailureTracker requires tracking function'); - } - - if (errorCodeMapFn && typeof errorCodeMapFn !== 'function') { - throw new Error('DecryptionFailureTracker second constructor argument should be a function'); - } - - this._trackDecryptionFailure = fn; - this._mapErrorCode = errorCodeMapFn; - } - - // loadTrackedEventHashMap() { - // this.trackedEventHashMap = JSON.parse(localStorage.getItem('mx-decryption-failure-event-id-hashes')) || {}; - // } - - // saveTrackedEventHashMap() { - // localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap)); - // } - - eventDecrypted(e, err) { - if (err) { - this.addDecryptionFailure(new DecryptionFailure(e.getId(), err.code)); - } else { - // Could be an event in the failures, remove it - this.removeDecryptionFailuresForEvent(e); - } - } - - addDecryptionFailure(failure) { - this.failures.push(failure); - } - - removeDecryptionFailuresForEvent(e) { - this.failures = this.failures.filter((f) => f.failedEventId !== e.getId()); - } - - /** - * Start checking for and tracking failures. - */ - start() { - this.checkInterval = setInterval( - () => this.checkFailures(Date.now()), - DecryptionFailureTracker.CHECK_INTERVAL_MS, - ); - - this.trackInterval = setInterval( - () => this.trackFailures(), - DecryptionFailureTracker.TRACK_INTERVAL_MS, - ); - } - - /** - * Clear state and stop checking for and tracking failures. - */ - stop() { - clearInterval(this.checkInterval); - clearInterval(this.trackInterval); - - this.failures = []; - this.failureCounts = {}; - } - - /** - * Mark failures that occured before nowTs - GRACE_PERIOD_MS as failures that should be - * tracked. Only mark one failure per event ID. - * @param {number} nowTs the timestamp that represents the time now. - */ - checkFailures(nowTs) { - const failuresGivenGrace = []; - const failuresNotReady = []; - while (this.failures.length > 0) { - const f = this.failures.shift(); - if (nowTs > f.ts + DecryptionFailureTracker.GRACE_PERIOD_MS) { - failuresGivenGrace.push(f); - } else { - failuresNotReady.push(f); - } - } - this.failures = failuresNotReady; - - // Only track one failure per event - const dedupedFailuresMap = failuresGivenGrace.reduce( - (map, failure) => { - if (!this.trackedEventHashMap[failure.failedEventId]) { - return map.set(failure.failedEventId, failure); - } else { - return map; - } - }, - // Use a map to preseve key ordering - new Map(), - ); - - const trackedEventIds = [...dedupedFailuresMap.keys()]; - - this.trackedEventHashMap = trackedEventIds.reduce( - (result, eventId) => ({...result, [eventId]: true}), - this.trackedEventHashMap, - ); - - // Commented out for now for expediency, we need to consider unbound nature of storing - // this in localStorage - // this.saveTrackedEventHashMap(); - - const dedupedFailures = dedupedFailuresMap.values(); - - this._aggregateFailures(dedupedFailures); - } - - _aggregateFailures(failures) { - for (const failure of failures) { - const errorCode = failure.errorCode; - this.failureCounts[errorCode] = (this.failureCounts[errorCode] || 0) + 1; - } - } - - /** - * If there are failures that should be tracked, call the given trackDecryptionFailure - * function with the number of failures that should be tracked. - */ - trackFailures() { - for (const errorCode of Object.keys(this.failureCounts)) { - if (this.failureCounts[errorCode] > 0) { - const trackedErrorCode = this._mapErrorCode ? this._mapErrorCode(errorCode) : errorCode; - - this._trackDecryptionFailure(this.failureCounts[errorCode], trackedErrorCode); - this.failureCounts[errorCode] = 0; - } - } - } -} diff --git a/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts new file mode 100644 index 0000000000..df306a54f5 --- /dev/null +++ b/src/DecryptionFailureTracker.ts @@ -0,0 +1,205 @@ +/* +Copyright 2018 - 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 { MatrixError } from "matrix-js-sdk/src/http-api"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + +export class DecryptionFailure { + public readonly ts: number; + + constructor(public readonly failedEventId: string, public readonly errorCode: string) { + this.ts = Date.now(); + } +} + +type TrackingFn = (count: number, trackedErrCode: string) => void; +type ErrCodeMapFn = (errcode: string) => string; + +export class DecryptionFailureTracker { + // Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list + // is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did + // are accumulated in `failureCounts`. + public failures: DecryptionFailure[] = []; + + // A histogram of the number of failures that will be tracked at the next tracking + // interval, split by failure error code. + public failureCounts: Record = { + // [errorCode]: 42 + }; + + // Event IDs of failures that were tracked previously + public trackedEventHashMap: Record = { + // [eventId]: true + }; + + // Set to an interval ID when `start` is called + public checkInterval: number = null; + public trackInterval: number = null; + + // Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`. + static TRACK_INTERVAL_MS = 60000; + + // Call `checkFailures` every `CHECK_INTERVAL_MS`. + static CHECK_INTERVAL_MS = 5000; + + // Give events a chance to be decrypted by waiting `GRACE_PERIOD_MS` before counting + // the failure in `failureCounts`. + static GRACE_PERIOD_MS = 60000; + + /** + * Create a new DecryptionFailureTracker. + * + * Call `eventDecrypted(event, err)` on this instance when an event is decrypted. + * + * Call `start()` to start the tracker, and `stop()` to stop tracking. + * + * @param {function} fn The tracking function, which will be called when failures + * are tracked. The function should have a signature `(count, trackedErrorCode) => {...}`, + * where `count` is the number of failures and `errorCode` matches the `.code` of + * provided DecryptionError errors (by default, unless `errorCodeMapFn` is specified. + * @param {function?} errorCodeMapFn The function used to map error codes to the + * trackedErrorCode. If not provided, the `.code` of errors will be used. + */ + constructor(private readonly fn: TrackingFn, private readonly errorCodeMapFn?: ErrCodeMapFn) { + if (!fn || typeof fn !== 'function') { + throw new Error('DecryptionFailureTracker requires tracking function'); + } + + if (errorCodeMapFn && typeof errorCodeMapFn !== 'function') { + throw new Error('DecryptionFailureTracker second constructor argument should be a function'); + } + } + + // loadTrackedEventHashMap() { + // this.trackedEventHashMap = JSON.parse(localStorage.getItem('mx-decryption-failure-event-id-hashes')) || {}; + // } + + // saveTrackedEventHashMap() { + // localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap)); + // } + + public eventDecrypted(e: MatrixEvent, err: MatrixError | Error): void { + if (err) { + this.addDecryptionFailure(new DecryptionFailure(e.getId(), err.code)); + } else { + // Could be an event in the failures, remove it + this.removeDecryptionFailuresForEvent(e); + } + } + + public addDecryptionFailure(failure: DecryptionFailure): void { + this.failures.push(failure); + } + + public removeDecryptionFailuresForEvent(e: MatrixEvent): void { + this.failures = this.failures.filter((f) => f.failedEventId !== e.getId()); + } + + /** + * Start checking for and tracking failures. + */ + public start(): void { + this.checkInterval = setInterval( + () => this.checkFailures(Date.now()), + DecryptionFailureTracker.CHECK_INTERVAL_MS, + ); + + this.trackInterval = setInterval( + () => this.trackFailures(), + DecryptionFailureTracker.TRACK_INTERVAL_MS, + ); + } + + /** + * Clear state and stop checking for and tracking failures. + */ + public stop(): void { + clearInterval(this.checkInterval); + clearInterval(this.trackInterval); + + this.failures = []; + this.failureCounts = {}; + } + + /** + * Mark failures that occurred before nowTs - GRACE_PERIOD_MS as failures that should be + * tracked. Only mark one failure per event ID. + * @param {number} nowTs the timestamp that represents the time now. + */ + public checkFailures(nowTs: number): void { + const failuresGivenGrace = []; + const failuresNotReady = []; + while (this.failures.length > 0) { + const f = this.failures.shift(); + if (nowTs > f.ts + DecryptionFailureTracker.GRACE_PERIOD_MS) { + failuresGivenGrace.push(f); + } else { + failuresNotReady.push(f); + } + } + this.failures = failuresNotReady; + + // Only track one failure per event + const dedupedFailuresMap = failuresGivenGrace.reduce( + (map, failure) => { + if (!this.trackedEventHashMap[failure.failedEventId]) { + return map.set(failure.failedEventId, failure); + } else { + return map; + } + }, + // Use a map to preseve key ordering + new Map(), + ); + + const trackedEventIds = [...dedupedFailuresMap.keys()]; + + this.trackedEventHashMap = trackedEventIds.reduce( + (result, eventId) => ({ ...result, [eventId]: true }), + this.trackedEventHashMap, + ); + + // Commented out for now for expediency, we need to consider unbound nature of storing + // this in localStorage + // this.saveTrackedEventHashMap(); + + const dedupedFailures = dedupedFailuresMap.values(); + + this.aggregateFailures(dedupedFailures); + } + + private aggregateFailures(failures: DecryptionFailure[]): void { + for (const failure of failures) { + const errorCode = failure.errorCode; + this.failureCounts[errorCode] = (this.failureCounts[errorCode] || 0) + 1; + } + } + + /** + * If there are failures that should be tracked, call the given trackDecryptionFailure + * function with the number of failures that should be tracked. + */ + public trackFailures(): void { + for (const errorCode of Object.keys(this.failureCounts)) { + if (this.failureCounts[errorCode] > 0) { + const trackedErrorCode = this.errorCodeMapFn ? this.errorCodeMapFn(errorCode) : errorCode; + + this.fn(this.failureCounts[errorCode], trackedErrorCode); + this.failureCounts[errorCode] = 0; + } + } + } +} diff --git a/src/DeviceListener.js b/src/DeviceListener.js deleted file mode 100644 index 21c844e11c..0000000000 --- a/src/DeviceListener.js +++ /dev/null @@ -1,219 +0,0 @@ -/* -Copyright 2020 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 { MatrixClientPeg } from './MatrixClientPeg'; -import SettingsStore from './settings/SettingsStore'; -import * as sdk from './index'; -import { _t } from './languageHandler'; -import ToastStore from './stores/ToastStore'; - -function toastKey(deviceId) { - return 'unverified_session_' + deviceId; -} - -const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; -const THIS_DEVICE_TOAST_KEY = 'setupencryption'; - -export default class DeviceListener { - static sharedInstance() { - if (!global.mx_DeviceListener) global.mx_DeviceListener = new DeviceListener(); - return global.mx_DeviceListener; - } - - constructor() { - // set of device IDs we're currently showing toasts for - this._activeNagToasts = new Set(); - // device IDs for which the user has dismissed the verify toast ('Later') - this._dismissed = new Set(); - // has the user dismissed any of the various nag toasts to setup encryption on this device? - this._dismissedThisDeviceToast = false; - - // cache of the key backup info - this._keyBackupInfo = null; - this._keyBackupFetchedAt = null; - } - - start() { - MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated); - MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged); - MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged); - MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged); - MatrixClientPeg.get().on('accountData', this._onAccountData); - this._recheck(); - } - - stop() { - if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated); - MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged); - MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged); - MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this._onCrossSingingKeysChanged); - MatrixClientPeg.get().removeListener('accountData', this._onAccountData); - } - this._dismissed.clear(); - } - - dismissVerification(deviceId) { - this._dismissed.add(deviceId); - this._recheck(); - } - - dismissEncryptionSetup() { - this._dismissedThisDeviceToast = true; - this._recheck(); - } - - _onDevicesUpdated = (users) => { - if (!users.includes(MatrixClientPeg.get().getUserId())) return; - this._recheck(); - } - - _onDeviceVerificationChanged = (userId) => { - if (userId !== MatrixClientPeg.get().getUserId()) return; - this._recheck(); - } - - _onUserTrustStatusChanged = (userId, trustLevel) => { - if (userId !== MatrixClientPeg.get().getUserId()) return; - this._recheck(); - } - - _onCrossSingingKeysChanged = () => { - this._recheck(); - } - - _onAccountData = (ev) => { - // User may have: - // * migrated SSSS to symmetric - // * uploaded keys to secret storage - // * completed secret storage creation - // which result in account data changes affecting checks below. - if ( - ev.getType().startsWith('m.secret_storage.') || - ev.getType().startsWith('m.cross_signing.') - ) { - this._recheck(); - } - } - - // The server doesn't tell us when key backup is set up, so we poll - // & cache the result - async _getKeyBackupInfo() { - const now = (new Date()).getTime(); - if (!this._keyBackupInfo || this._keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) { - this._keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); - this._keyBackupFetchedAt = now; - } - return this._keyBackupInfo; - } - - async _recheck() { - const cli = MatrixClientPeg.get(); - - if ( - !SettingsStore.isFeatureEnabled("feature_cross_signing") || - !await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing") - ) return; - - if (!cli.isCryptoEnabled()) return; - - const crossSigningReady = await cli.isCrossSigningReady(); - - if (this._dismissedThisDeviceToast) { - ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); - } else { - if (!crossSigningReady) { - // cross signing isn't enabled - nag to enable it - // There are 3 different toasts for: - if (cli.getStoredCrossSigningForUser(cli.getUserId())) { - // Cross-signing on account but this device doesn't trust the master key (verify this session) - ToastStore.sharedInstance().addOrReplaceToast({ - key: THIS_DEVICE_TOAST_KEY, - title: _t("Verify this session"), - icon: "verification_warning", - props: {kind: 'verify_this_session'}, - component: sdk.getComponent("toasts.SetupEncryptionToast"), - }); - } else { - const backupInfo = await this._getKeyBackupInfo(); - if (backupInfo) { - // No cross-signing on account but key backup available (upgrade encryption) - ToastStore.sharedInstance().addOrReplaceToast({ - key: THIS_DEVICE_TOAST_KEY, - title: _t("Encryption upgrade available"), - icon: "verification_warning", - props: {kind: 'upgrade_encryption'}, - component: sdk.getComponent("toasts.SetupEncryptionToast"), - }); - } else { - // No cross-signing or key backup on account (set up encryption) - ToastStore.sharedInstance().addOrReplaceToast({ - key: THIS_DEVICE_TOAST_KEY, - title: _t("Set up encryption"), - icon: "verification_warning", - props: {kind: 'set_up_encryption'}, - component: sdk.getComponent("toasts.SetupEncryptionToast"), - }); - } - } - return; - } else if (await cli.secretStorageKeyNeedsUpgrade()) { - ToastStore.sharedInstance().addOrReplaceToast({ - key: THIS_DEVICE_TOAST_KEY, - title: _t("Encryption upgrade available"), - icon: "verification_warning", - props: {kind: 'upgrade_ssss'}, - component: sdk.getComponent("toasts.SetupEncryptionToast"), - }); - } else { - // cross-signing is ready, and we don't need to upgrade encryption - ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); - } - } - - // as long as cross-signing isn't ready, - // you can't see or dismiss any device toasts - if (crossSigningReady) { - const newActiveToasts = new Set(); - - const devices = await cli.getStoredDevicesForUser(cli.getUserId()); - for (const device of devices) { - if (device.deviceId == cli.deviceId) continue; - - const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId); - if (deviceTrust.isCrossSigningVerified() || this._dismissed.has(device.deviceId)) { - ToastStore.sharedInstance().dismissToast(toastKey(device.deviceId)); - } else { - this._activeNagToasts.add(device.deviceId); - ToastStore.sharedInstance().addOrReplaceToast({ - key: toastKey(device.deviceId), - title: _t("Unverified login. Was this you?"), - icon: "verification_warning", - props: { device }, - component: sdk.getComponent("toasts.UnverifiedSessionToast"), - }); - newActiveToasts.add(device.deviceId); - } - } - - // clear any other outstanding toasts (eg. logged out devices) - for (const deviceId of this._activeNagToasts) { - if (!newActiveToasts.has(deviceId)) ToastStore.sharedInstance().dismissToast(toastKey(deviceId)); - } - this._activeNagToasts = newActiveToasts; - } - } -} diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts new file mode 100644 index 0000000000..d033063677 --- /dev/null +++ b/src/DeviceListener.ts @@ -0,0 +1,308 @@ +/* +Copyright 2020 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 { MatrixClientPeg } from './MatrixClientPeg'; +import dis from "./dispatcher/dispatcher"; +import { + hideToast as hideBulkUnverifiedSessionsToast, + showToast as showBulkUnverifiedSessionsToast, +} from "./toasts/BulkUnverifiedSessionsToast"; +import { + hideToast as hideSetupEncryptionToast, + Kind as SetupKind, + showToast as showSetupEncryptionToast, +} from "./toasts/SetupEncryptionToast"; +import { + hideToast as hideUnverifiedSessionsToast, + showToast as showUnverifiedSessionsToast, +} from "./toasts/UnverifiedSessionToast"; +import { isSecretStorageBeingAccessed, accessSecretStorage } from "./SecurityManager"; +import { isSecureBackupRequired } from './utils/WellKnownUtils'; +import { isLoggedIn } from './components/structures/MatrixChat'; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + +const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; + +export default class DeviceListener { + private dispatcherRef: string; + // device IDs for which the user has dismissed the verify toast ('Later') + private dismissed = new Set(); + // has the user dismissed any of the various nag toasts to setup encryption on this device? + private dismissedThisDeviceToast = false; + // cache of the key backup info + private keyBackupInfo: object = null; + private keyBackupFetchedAt: number = null; + // We keep a list of our own device IDs so we can batch ones that were already + // there the last time the app launched into a single toast, but display new + // ones in their own toasts. + private ourDeviceIdsAtStart: Set = null; + // The set of device IDs we're currently displaying toasts for + private displayingToastsForDeviceIds = new Set(); + + static sharedInstance() { + if (!window.mxDeviceListener) window.mxDeviceListener = new DeviceListener(); + return window.mxDeviceListener; + } + + start() { + MatrixClientPeg.get().on('crypto.willUpdateDevices', this._onWillUpdateDevices); + MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated); + MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged); + MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged); + MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged); + MatrixClientPeg.get().on('accountData', this._onAccountData); + MatrixClientPeg.get().on('sync', this._onSync); + MatrixClientPeg.get().on('RoomState.events', this._onRoomStateEvents); + this.dispatcherRef = dis.register(this._onAction); + this._recheck(); + } + + stop() { + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener('crypto.willUpdateDevices', this._onWillUpdateDevices); + MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated); + MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged); + MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged); + MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this._onCrossSingingKeysChanged); + MatrixClientPeg.get().removeListener('accountData', this._onAccountData); + MatrixClientPeg.get().removeListener('sync', this._onSync); + MatrixClientPeg.get().removeListener('RoomState.events', this._onRoomStateEvents); + } + if (this.dispatcherRef) { + dis.unregister(this.dispatcherRef); + this.dispatcherRef = null; + } + this.dismissed.clear(); + this.dismissedThisDeviceToast = false; + this.keyBackupInfo = null; + this.keyBackupFetchedAt = null; + this.ourDeviceIdsAtStart = null; + this.displayingToastsForDeviceIds = new Set(); + } + + /** + * Dismiss notifications about our own unverified devices + * + * @param {String[]} deviceIds List of device IDs to dismiss notifications for + */ + async dismissUnverifiedSessions(deviceIds: Iterable) { + for (const d of deviceIds) { + this.dismissed.add(d); + } + + this._recheck(); + } + + dismissEncryptionSetup() { + this.dismissedThisDeviceToast = true; + this._recheck(); + } + + _ensureDeviceIdsAtStartPopulated() { + if (this.ourDeviceIdsAtStart === null) { + const cli = MatrixClientPeg.get(); + this.ourDeviceIdsAtStart = new Set( + cli.getStoredDevicesForUser(cli.getUserId()).map(d => d.deviceId), + ); + } + } + + _onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => { + // If we didn't know about *any* devices before (ie. it's fresh login), + // then they are all pre-existing devices, so ignore this and set the + // devicesAtStart list to the devices that we see after the fetch. + if (initialFetch) return; + + const myUserId = MatrixClientPeg.get().getUserId(); + if (users.includes(myUserId)) this._ensureDeviceIdsAtStartPopulated(); + + // No need to do a recheck here: we just need to get a snapshot of our devices + // before we download any new ones. + }; + + _onDevicesUpdated = (users: string[]) => { + if (!users.includes(MatrixClientPeg.get().getUserId())) return; + this._recheck(); + }; + + _onDeviceVerificationChanged = (userId: string) => { + if (userId !== MatrixClientPeg.get().getUserId()) return; + this._recheck(); + }; + + _onUserTrustStatusChanged = (userId: string) => { + if (userId !== MatrixClientPeg.get().getUserId()) return; + this._recheck(); + }; + + _onCrossSingingKeysChanged = () => { + this._recheck(); + }; + + _onAccountData = (ev) => { + // User may have: + // * migrated SSSS to symmetric + // * uploaded keys to secret storage + // * completed secret storage creation + // which result in account data changes affecting checks below. + if ( + ev.getType().startsWith('m.secret_storage.') || + ev.getType().startsWith('m.cross_signing.') || + ev.getType() === 'm.megolm_backup.v1' + ) { + this._recheck(); + } + }; + + _onSync = (state, prevState) => { + if (state === 'PREPARED' && prevState === null) this._recheck(); + }; + + _onRoomStateEvents = (ev: MatrixEvent) => { + if (ev.getType() !== "m.room.encryption") { + return; + } + + // If a room changes to encrypted, re-check as it may be our first + // encrypted room. This also catches encrypted room creation as well. + this._recheck(); + }; + + _onAction = ({ action }) => { + if (action !== "on_logged_in") return; + this._recheck(); + }; + + // The server doesn't tell us when key backup is set up, so we poll + // & cache the result + async _getKeyBackupInfo() { + const now = (new Date()).getTime(); + if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) { + this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); + this.keyBackupFetchedAt = now; + } + return this.keyBackupInfo; + } + + private shouldShowSetupEncryptionToast() { + // If we're in the middle of a secret storage operation, we're likely + // modifying the state involved here, so don't add new toasts to setup. + if (isSecretStorageBeingAccessed()) return false; + // Show setup toasts once the user is in at least one encrypted room. + const cli = MatrixClientPeg.get(); + return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId)); + } + + async _recheck() { + const cli = MatrixClientPeg.get(); + + if (!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) return; + + if (!cli.isCryptoEnabled()) return; + // don't recheck until the initial sync is complete: lots of account data events will fire + // while the initial sync is processing and we don't need to recheck on each one of them + // (we add a listener on sync to do once check after the initial sync is done) + if (!cli.isInitialSyncComplete()) return; + + const crossSigningReady = await cli.isCrossSigningReady(); + const secretStorageReady = await cli.isSecretStorageReady(); + const allSystemsReady = crossSigningReady && secretStorageReady; + + if (this.dismissedThisDeviceToast || allSystemsReady) { + hideSetupEncryptionToast(); + } else if (this.shouldShowSetupEncryptionToast()) { + // make sure our keys are finished downloading + await cli.downloadKeys([cli.getUserId()]); + // cross signing isn't enabled - nag to enable it + // There are 3 different toasts for: + if ( + !cli.getCrossSigningId() && + cli.getStoredCrossSigningForUser(cli.getUserId()) + ) { + // Cross-signing on account but this device doesn't trust the master key (verify this session) + showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION); + } else { + const backupInfo = await this._getKeyBackupInfo(); + if (backupInfo) { + // No cross-signing on account but key backup available (upgrade encryption) + showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION); + } else { + // No cross-signing or key backup on account (set up encryption) + await cli.waitForClientWellKnown(); + if (isSecureBackupRequired() && isLoggedIn()) { + // If we're meant to set up, and Secure Backup is required, + // trigger the flow directly without a toast once logged in. + hideSetupEncryptionToast(); + accessSecretStorage(); + } else { + showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION); + } + } + } + } + + // This needs to be done after awaiting on downloadKeys() above, so + // we make sure we get the devices after the fetch is done. + this._ensureDeviceIdsAtStartPopulated(); + + // Unverified devices that were there last time the app ran + // (technically could just be a boolean: we don't actually + // need to remember the device IDs, but for the sake of + // symmetry...). + const oldUnverifiedDeviceIds = new Set(); + // Unverified devices that have appeared since then + const newUnverifiedDeviceIds = new Set(); + + // as long as cross-signing isn't ready, + // you can't see or dismiss any device toasts + if (crossSigningReady) { + const devices = cli.getStoredDevicesForUser(cli.getUserId()); + for (const device of devices) { + if (device.deviceId === cli.deviceId) continue; + + const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId); + if (!deviceTrust.isCrossSigningVerified() && !this.dismissed.has(device.deviceId)) { + if (this.ourDeviceIdsAtStart.has(device.deviceId)) { + oldUnverifiedDeviceIds.add(device.deviceId); + } else { + newUnverifiedDeviceIds.add(device.deviceId); + } + } + } + } + + // Display or hide the batch toast for old unverified sessions + if (oldUnverifiedDeviceIds.size > 0) { + showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds); + } else { + hideBulkUnverifiedSessionsToast(); + } + + // Show toasts for new unverified devices if they aren't already there + for (const deviceId of newUnverifiedDeviceIds) { + showUnverifiedSessionsToast(deviceId); + } + + // ...and hide any we don't need any more + for (const deviceId of this.displayingToastsForDeviceIds) { + if (!newUnverifiedDeviceIds.has(deviceId)) { + hideUnverifiedSessionsToast(deviceId); + } + } + + this.displayingToastsForDeviceIds = newUnverifiedDeviceIds; + } +} diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js deleted file mode 100644 index c9793d40f7..0000000000 --- a/src/FromWidgetPostMessageApi.js +++ /dev/null @@ -1,274 +0,0 @@ -/* -Copyright 2018 New Vector Ltd -Copyright 2019 Travis Ralston -Copyright 2019 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 URL from 'url'; -import dis from './dispatcher'; -import WidgetMessagingEndpoint from './WidgetMessagingEndpoint'; -import ActiveWidgetStore from './stores/ActiveWidgetStore'; -import {MatrixClientPeg} from "./MatrixClientPeg"; -import RoomViewStore from "./stores/RoomViewStore"; -import {IntegrationManagers} from "./integrations/IntegrationManagers"; -import SettingsStore from "./settings/SettingsStore"; -import {Capability} from "./widgets/WidgetApi"; - -const WIDGET_API_VERSION = '0.0.2'; // Current API version -const SUPPORTED_WIDGET_API_VERSIONS = [ - '0.0.1', - '0.0.2', -]; -const INBOUND_API_NAME = 'fromWidget'; - -// Listen for and handle incoming requests using the 'fromWidget' postMessage -// API and initiate responses -export default class FromWidgetPostMessageApi { - constructor() { - this.widgetMessagingEndpoints = []; - this.widgetListeners = {}; // {action: func[]} - - this.start = this.start.bind(this); - this.stop = this.stop.bind(this); - this.onPostMessage = this.onPostMessage.bind(this); - } - - start() { - window.addEventListener('message', this.onPostMessage); - } - - stop() { - window.removeEventListener('message', this.onPostMessage); - } - - /** - * Adds a listener for a given action - * @param {string} action The action to listen for. - * @param {Function} callbackFn A callback function to be called when the action is - * encountered. Called with two parameters: the interesting request information and - * the raw event received from the postMessage API. The raw event is meant to be used - * for sendResponse and similar functions. - */ - addListener(action, callbackFn) { - if (!this.widgetListeners[action]) this.widgetListeners[action] = []; - this.widgetListeners[action].push(callbackFn); - } - - /** - * Removes a listener for a given action. - * @param {string} action The action that was subscribed to. - * @param {Function} callbackFn The original callback function that was used to subscribe - * to updates. - */ - removeListener(action, callbackFn) { - if (!this.widgetListeners[action]) return; - - const idx = this.widgetListeners[action].indexOf(callbackFn); - if (idx !== -1) this.widgetListeners[action].splice(idx, 1); - } - - /** - * Register a widget endpoint for trusted postMessage communication - * @param {string} widgetId Unique widget identifier - * @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host) - */ - addEndpoint(widgetId, endpointUrl) { - const u = URL.parse(endpointUrl); - if (!u || !u.protocol || !u.host) { - console.warn('Add FromWidgetPostMessageApi endpoint - Invalid origin:', endpointUrl); - return; - } - - const origin = u.protocol + '//' + u.host; - const endpoint = new WidgetMessagingEndpoint(widgetId, origin); - if (this.widgetMessagingEndpoints.some(function(ep) { - return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl); - })) { - // Message endpoint already registered - console.warn('Add FromWidgetPostMessageApi - Endpoint already registered'); - return; - } else { - console.log(`Adding fromWidget messaging endpoint for ${widgetId}`, endpoint); - this.widgetMessagingEndpoints.push(endpoint); - } - } - - /** - * De-register a widget endpoint from trusted communication sources - * @param {string} widgetId Unique widget identifier - * @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host) - * @return {boolean} True if endpoint was successfully removed - */ - removeEndpoint(widgetId, endpointUrl) { - const u = URL.parse(endpointUrl); - if (!u || !u.protocol || !u.host) { - console.warn('Remove widget messaging endpoint - Invalid origin'); - return; - } - - const origin = u.protocol + '//' + u.host; - if (this.widgetMessagingEndpoints && this.widgetMessagingEndpoints.length > 0) { - const length = this.widgetMessagingEndpoints.length; - this.widgetMessagingEndpoints = this.widgetMessagingEndpoints - .filter((endpoint) => endpoint.widgetId !== widgetId || endpoint.endpointUrl !== origin); - return (length > this.widgetMessagingEndpoints.length); - } - return false; - } - - /** - * Handle widget postMessage events - * Messages are only handled where a valid, registered messaging endpoints - * @param {Event} event Event to handle - * @return {undefined} - */ - onPostMessage(event) { - if (!event.origin) { // Handle chrome - event.origin = event.originalEvent.origin; - } - - // Event origin is empty string if undefined - if ( - event.origin.length === 0 || - !this.trustedEndpoint(event.origin) || - event.data.api !== INBOUND_API_NAME || - !event.data.widgetId - ) { - return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise - } - - // Call any listeners we have registered - if (this.widgetListeners[event.data.action]) { - for (const fn of this.widgetListeners[event.data.action]) { - fn(event.data, event); - } - } - - // Although the requestId is required, we don't use it. We'll be nice and process the message - // if the property is missing, but with a warning for widget developers. - if (!event.data.requestId) { - console.warn("fromWidget action '" + event.data.action + "' does not have a requestId"); - } - - const action = event.data.action; - const widgetId = event.data.widgetId; - if (action === 'content_loaded') { - console.log('Widget reported content loaded for', widgetId); - dis.dispatch({ - action: 'widget_content_loaded', - widgetId: widgetId, - }); - this.sendResponse(event, {success: true}); - } else if (action === 'supported_api_versions') { - this.sendResponse(event, { - api: INBOUND_API_NAME, - supported_versions: SUPPORTED_WIDGET_API_VERSIONS, - }); - } else if (action === 'api_version') { - this.sendResponse(event, { - api: INBOUND_API_NAME, - version: WIDGET_API_VERSION, - }); - } else if (action === 'm.sticker') { - // console.warn('Got sticker message from widget', widgetId); - // NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually - const data = event.data.data || event.data.widgetData; - dis.dispatch({action: 'm.sticker', data: data, widgetId: event.data.widgetId}); - } else if (action === 'integration_manager_open') { - // Close the stickerpicker - dis.dispatch({action: 'stickerpicker_close'}); - // Open the integration manager - // NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually - const data = event.data.data || event.data.widgetData; - const integType = (data && data.integType) ? data.integType : null; - const integId = (data && data.integId) ? data.integId : null; - - // TODO: Open the right integration manager for the widget - if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) { - IntegrationManagers.sharedInstance().openAll( - MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), - `type_${integType}`, - integId, - ); - } else { - IntegrationManagers.sharedInstance().getPrimaryManager().open( - MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), - `type_${integType}`, - integId, - ); - } - } else if (action === 'set_always_on_screen') { - // This is a new message: there is no reason to support the deprecated widgetData here - const data = event.data.data; - const val = data.value; - - if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) { - ActiveWidgetStore.setWidgetPersistence(widgetId, val); - } - } else if (action === 'get_openid') { - // Handled by caller - } else { - console.warn('Widget postMessage event unhandled'); - this.sendError(event, {message: 'The postMessage was unhandled'}); - } - } - - /** - * Check if message origin is registered as trusted - * @param {string} origin PostMessage origin to check - * @return {boolean} True if trusted - */ - trustedEndpoint(origin) { - if (!origin) { - return false; - } - - return this.widgetMessagingEndpoints.some((endpoint) => { - // TODO / FIXME -- Should this also check the widgetId? - return endpoint.endpointUrl === origin; - }); - } - - /** - * Send a postmessage response to a postMessage request - * @param {Event} event The original postMessage request event - * @param {Object} res Response data - */ - sendResponse(event, res) { - const data = JSON.parse(JSON.stringify(event.data)); - data.response = res; - event.source.postMessage(data, event.origin); - } - - /** - * Send an error response to a postMessage request - * @param {Event} event The original postMessage request event - * @param {string} msg Error message - * @param {Error} nestedError Nested error event (optional) - */ - sendError(event, msg, nestedError) { - console.error('Action:' + event.data.action + ' failed with message: ' + msg); - const data = JSON.parse(JSON.stringify(event.data)); - data.response = { - error: { - message: msg, - }, - }; - if (nestedError) { - data.response.error._error = nestedError; - } - event.source.postMessage(data, event.origin); - } -} diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index 9131a89e5d..ea1813876c 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -19,9 +19,9 @@ import Modal from './Modal'; import * as sdk from './'; import MultiInviter from './utils/MultiInviter'; import { _t } from './languageHandler'; -import {MatrixClientPeg} from './MatrixClientPeg'; +import { MatrixClientPeg } from './MatrixClientPeg'; import GroupStore from './stores/GroupStore'; -import {allSettled} from "./utils/promise"; +import StyledCheckbox from './components/views/elements/StyledCheckbox'; export function showGroupInviteDialog(groupId) { return new Promise((resolve, reject) => { @@ -61,19 +61,19 @@ export function showGroupAddRoomDialog(groupId) {
{ _t("Which rooms would you like to add to this community?") }
; - const checkboxContainer = ; + const checkboxContainer = + { _t("Show these rooms to non-members on the community page and room list?") } + ; const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, { title: _t("Add rooms to the community"), description: description, extraNode: checkboxContainer, - placeholder: _t("Room name or alias"), + placeholder: _t("Room name or address"), button: _t("Add to community"), pickerType: 'room', validAddressTypes: ['mx-room-id'], @@ -103,7 +103,7 @@ function _onGroupInviteFinished(groupId, addrs) { if (errorList.length > 0) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to invite the following users to the group', '', ErrorDialog, { - title: _t("Failed to invite the following users to %(groupId)s:", {groupId: groupId}), + title: _t("Failed to invite the following users to %(groupId)s:", { groupId: groupId }), description: errorList.join(", "), }); } @@ -111,7 +111,7 @@ function _onGroupInviteFinished(groupId, addrs) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to invite users to group', '', ErrorDialog, { title: _t("Failed to invite users to community"), - description: _t("Failed to invite users to %(groupId)s", {groupId: groupId}), + description: _t("Failed to invite users to %(groupId)s", { groupId: groupId }), }); }); } @@ -119,7 +119,7 @@ function _onGroupInviteFinished(groupId, addrs) { function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) { const matrixClient = MatrixClientPeg.get(); const errorList = []; - return allSettled(addrs.map((addr) => { + return Promise.allSettled(addrs.map((addr) => { return GroupStore .addRoomToGroup(groupId, addr.address, addRoomsPublicly) .catch(() => { errorList.push(addr.address); }) @@ -137,7 +137,7 @@ function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) { // Add this group as related if (!groups.includes(groupId)) { groups.push(groupId); - return MatrixClientPeg.get().sendStateEvent(roomId, 'm.room.related_groups', {groups}, ''); + return MatrixClientPeg.get().sendStateEvent(roomId, 'm.room.related_groups', { groups }, ''); } }); })).then(() => { @@ -147,13 +147,15 @@ function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog( 'Failed to add the following room to the group', - '', ErrorDialog, - { - title: _t( - "Failed to add the following rooms to %(groupId)s:", - {groupId}, - ), - description: errorList.join(", "), - }); + '', + ErrorDialog, + { + title: _t( + "Failed to add the following rooms to %(groupId)s:", + { groupId }, + ), + description: errorList.join(", "), + }, + ); }); } diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js deleted file mode 100644 index a58ea25c8a..0000000000 --- a/src/HtmlUtils.js +++ /dev/null @@ -1,532 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017, 2018 New Vector Ltd -Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019 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. -*/ - -'use strict'; - -import ReplyThread from "./components/views/elements/ReplyThread"; - -import React from 'react'; -import sanitizeHtml from 'sanitize-html'; -import * as linkify from 'linkifyjs'; -import linkifyMatrix from './linkify-matrix'; -import _linkifyElement from 'linkifyjs/element'; -import _linkifyString from 'linkifyjs/string'; -import classNames from 'classnames'; -import {MatrixClientPeg} from './MatrixClientPeg'; -import url from 'url'; - -import EMOJIBASE_REGEX from 'emojibase-regex'; -import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; -import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji"; - -linkifyMatrix(linkify); - -// Anything outside the basic multilingual plane will be a surrogate pair -const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/; -// And there a bunch more symbol characters that emojibase has within the -// BMP, so this includes the ranges from 'letterlike symbols' to -// 'miscellaneous symbols and arrows' which should catch all of them -// (with plenty of false positives, but that's OK) -const SYMBOL_PATTERN = /([\u2100-\u2bff])/; - -// Regex pattern for Zero-Width joiner unicode characters -const ZWJ_REGEX = new RegExp("\u200D|\u2003", "g"); - -// Regex pattern for whitespace characters -const WHITESPACE_REGEX = new RegExp("\\s", "g"); - -const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i'); - -const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; - -const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet']; - -/* - * Return true if the given string contains emoji - * Uses a much, much simpler regex than emojibase's so will give false - * positives, but useful for fast-path testing strings to see if they - * need emojification. - * unicodeToImage uses this function. - */ -function mightContainEmoji(str) { - return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str); -} - -/** - * Returns the shortcode for an emoji character. - * - * @param {String} char The emoji character - * @return {String} The shortcode (such as :thumbup:) - */ -export function unicodeToShortcode(char) { - const data = getEmojiFromUnicode(char); - return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : ''); -} - -/** - * Returns the unicode character for an emoji shortcode - * - * @param {String} shortcode The shortcode (such as :thumbup:) - * @return {String} The emoji character; null if none exists - */ -export function shortcodeToUnicode(shortcode) { - shortcode = shortcode.slice(1, shortcode.length - 1); - const data = SHORTCODE_TO_EMOJI.get(shortcode); - return data ? data.unicode : null; -} - -export function processHtmlForSending(html: string): string { - const contentDiv = document.createElement('div'); - contentDiv.innerHTML = html; - - if (contentDiv.children.length === 0) { - return contentDiv.innerHTML; - } - - let contentHTML = ""; - for (let i=0; i < contentDiv.children.length; i++) { - const element = contentDiv.children[i]; - if (element.tagName.toLowerCase() === 'p') { - contentHTML += element.innerHTML; - // Don't add a
for the last

- if (i !== contentDiv.children.length - 1) { - contentHTML += '
'; - } - } else { - const temp = document.createElement('div'); - temp.appendChild(element.cloneNode(true)); - contentHTML += temp.innerHTML; - } - } - - return contentHTML; -} - -/* - * Given an untrusted HTML string, return a React node with an sanitized version - * of that HTML. - */ -export function sanitizedHtmlNode(insaneHtml) { - const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams); - - return

; -} - -/** - * Tests if a URL from an untrusted source may be safely put into the DOM - * The biggest threat here is javascript: URIs. - * Note that the HTML sanitiser library has its own internal logic for - * doing this, to which we pass the same list of schemes. This is used in - * other places we need to sanitise URLs. - * @return true if permitted, otherwise false - */ -export function isUrlPermitted(inputUrl) { - try { - const parsed = url.parse(inputUrl); - if (!parsed.protocol) return false; - // URL parser protocol includes the trailing colon - return PERMITTED_URL_SCHEMES.includes(parsed.protocol.slice(0, -1)); - } catch (e) { - return false; - } -} - -const transformTags = { // custom to matrix - // add blank targets to all hyperlinks except vector URLs - 'a': function(tagName, attribs) { - if (attribs.href) { - attribs.target = '_blank'; // by default - - const transformed = tryTransformPermalinkToLocalHref(attribs.href); - if (transformed !== attribs.href || attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN)) { - attribs.href = transformed; - delete attribs.target; - } - } - attribs.rel = 'noreferrer noopener'; // https://mathiasbynens.github.io/rel-noopener/ - return { tagName, attribs }; - }, - 'img': function(tagName, attribs) { - // Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag - // because transformTags is used _before_ we filter by allowedSchemesByTag and - // we don't want to allow images with `https?` `src`s. - if (!attribs.src || !attribs.src.startsWith('mxc://')) { - return { tagName, attribs: {}}; - } - attribs.src = MatrixClientPeg.get().mxcUrlToHttp( - attribs.src, - attribs.width || 800, - attribs.height || 600, - ); - return { tagName, attribs }; - }, - 'code': function(tagName, attribs) { - if (typeof attribs.class !== 'undefined') { - // Filter out all classes other than ones starting with language- for syntax highlighting. - const classes = attribs.class.split(/\s/).filter(function(cl) { - return cl.startsWith('language-'); - }); - attribs.class = classes.join(' '); - } - return { tagName, attribs }; - }, - '*': function(tagName, attribs) { - // Delete any style previously assigned, style is an allowedTag for font and span - // because attributes are stripped after transforming - delete attribs.style; - - // Sanitise and transform data-mx-color and data-mx-bg-color to their CSS - // equivalents - const customCSSMapper = { - 'data-mx-color': 'color', - 'data-mx-bg-color': 'background-color', - // $customAttributeKey: $cssAttributeKey - }; - - let style = ""; - Object.keys(customCSSMapper).forEach((customAttributeKey) => { - const cssAttributeKey = customCSSMapper[customAttributeKey]; - const customAttributeValue = attribs[customAttributeKey]; - if (customAttributeValue && - typeof customAttributeValue === 'string' && - COLOR_REGEX.test(customAttributeValue) - ) { - style += cssAttributeKey + ":" + customAttributeValue + ";"; - delete attribs[customAttributeKey]; - } - }); - - if (style) { - attribs.style = style; - } - - return { tagName, attribs }; - }, -}; - -const sanitizeHtmlParams = { - allowedTags: [ - 'font', // custom to matrix for IRC-style font coloring - 'del', // for markdown - 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub', - 'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', - 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img', - ], - allowedAttributes: { - // custom ones first: - font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix - span: ['data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix - a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix - img: ['src', 'width', 'height', 'alt', 'title'], - ol: ['start'], - code: ['class'], // We don't actually allow all classes, we filter them in transformTags - }, - // Lots of these won't come up by default because we don't allow them - selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'], - // URL schemes we permit - allowedSchemes: PERMITTED_URL_SCHEMES, - - allowProtocolRelative: false, - transformTags, -}; - -// this is the same as the above except with less rewriting -const composerSanitizeHtmlParams = Object.assign({}, sanitizeHtmlParams); -composerSanitizeHtmlParams.transformTags = { - 'code': transformTags['code'], - '*': transformTags['*'], -}; - -class BaseHighlighter { - constructor(highlightClass, highlightLink) { - this.highlightClass = highlightClass; - this.highlightLink = highlightLink; - } - - /** - * apply the highlights to a section of text - * - * @param {string} safeSnippet The snippet of text to apply the highlights - * to. - * @param {string[]} safeHighlights A list of substrings to highlight, - * sorted by descending length. - * - * returns a list of results (strings for HtmlHighligher, react nodes for - * TextHighlighter). - */ - applyHighlights(safeSnippet, safeHighlights) { - let lastOffset = 0; - let offset; - let nodes = []; - - const safeHighlight = safeHighlights[0]; - while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) { - // handle preamble - if (offset > lastOffset) { - var subSnippet = safeSnippet.substring(lastOffset, offset); - nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights)); - } - - // do highlight. use the original string rather than safeHighlight - // to preserve the original casing. - const endOffset = offset + safeHighlight.length; - nodes.push(this._processSnippet(safeSnippet.substring(offset, endOffset), true)); - - lastOffset = endOffset; - } - - // handle postamble - if (lastOffset !== safeSnippet.length) { - subSnippet = safeSnippet.substring(lastOffset, undefined); - nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights)); - } - return nodes; - } - - _applySubHighlights(safeSnippet, safeHighlights) { - if (safeHighlights[1]) { - // recurse into this range to check for the next set of highlight matches - return this.applyHighlights(safeSnippet, safeHighlights.slice(1)); - } else { - // no more highlights to be found, just return the unhighlighted string - return [this._processSnippet(safeSnippet, false)]; - } - } -} - -class HtmlHighlighter extends BaseHighlighter { - /* highlight the given snippet if required - * - * snippet: content of the span; must have been sanitised - * highlight: true to highlight as a search match - * - * returns an HTML string - */ - _processSnippet(snippet, highlight) { - if (!highlight) { - // nothing required here - return snippet; - } - - let span = "" - + snippet + ""; - - if (this.highlightLink) { - span = "" - +span+""; - } - return span; - } -} - -class TextHighlighter extends BaseHighlighter { - constructor(highlightClass, highlightLink) { - super(highlightClass, highlightLink); - this._key = 0; - } - - /* create a node to hold the given content - * - * snippet: content of the span - * highlight: true to highlight as a search match - * - * returns a React node - */ - _processSnippet(snippet, highlight) { - const key = this._key++; - - let node = - - { snippet } - ; - - if (highlight && this.highlightLink) { - node = { node }; - } - - return node; - } -} - - -/* turn a matrix event body into html - * - * content: 'content' of the MatrixEvent - * - * highlights: optional list of words to highlight, ordered by longest word first - * - * opts.highlightLink: optional href to add to highlighted words - * opts.disableBigEmoji: optional argument to disable the big emoji class. - * opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing - * opts.returnString: return an HTML string rather than JSX elements - * opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer - * opts.ref: React ref to attach to any React components returned (not compatible with opts.returnString) - */ -export function bodyToHtml(content, highlights, opts={}) { - const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body; - let bodyHasEmoji = false; - - let sanitizeParams = sanitizeHtmlParams; - if (opts.forComposerQuote) { - sanitizeParams = composerSanitizeHtmlParams; - } - - let strippedBody; - let safeBody; - let isDisplayedWithHtml; - // XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying - // to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which - // are interrupted by HTML tags (not that we did before) - e.g. foobar won't get highlighted - // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either - try { - if (highlights && highlights.length > 0) { - const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink); - const safeHighlights = highlights.map(function(highlight) { - return sanitizeHtml(highlight, sanitizeParams); - }); - // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. - sanitizeParams.textFilter = function(safeText) { - return highlighter.applyHighlights(safeText, safeHighlights).join(''); - }; - } - - let formattedBody = typeof content.formatted_body === 'string' ? content.formatted_body : null; - const plainBody = typeof content.body === 'string' ? content.body : null; - - if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody); - strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(plainBody) : plainBody; - - bodyHasEmoji = mightContainEmoji(isHtmlMessage ? formattedBody : plainBody); - - // Only generate safeBody if the message was sent as org.matrix.custom.html - if (isHtmlMessage) { - isDisplayedWithHtml = true; - safeBody = sanitizeHtml(formattedBody, sanitizeParams); - } - } finally { - delete sanitizeParams.textFilter; - } - - if (opts.returnString) { - return isDisplayedWithHtml ? safeBody : strippedBody; - } - - let emojiBody = false; - if (!opts.disableBigEmoji && bodyHasEmoji) { - let contentBodyTrimmed = strippedBody !== undefined ? strippedBody.trim() : ''; - - // Ignore spaces in body text. Emojis with spaces in between should - // still be counted as purely emoji messages. - contentBodyTrimmed = contentBodyTrimmed.replace(WHITESPACE_REGEX, ''); - - // Remove zero width joiner characters from emoji messages. This ensures - // that emojis that are made up of multiple unicode characters are still - // presented as large. - contentBodyTrimmed = contentBodyTrimmed.replace(ZWJ_REGEX, ''); - - const match = BIGEMOJI_REGEX.exec(contentBodyTrimmed); - emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length && - // Prevent user pills expanding for users with only emoji in - // their username. Permalinks (links in pills) can be any URL - // now, so we just check for an HTTP-looking thing. - ( - content.formatted_body == undefined || - (!content.formatted_body.includes("http:") && - !content.formatted_body.includes("https:")) - ); - } - - const className = classNames({ - 'mx_EventTile_body': true, - 'mx_EventTile_bigEmoji': emojiBody, - 'markdown-body': isHtmlMessage && !emojiBody, - }); - - return isDisplayedWithHtml ? - : - { strippedBody }; -} - -/** - * Linkifies the given string. This is a wrapper around 'linkifyjs/string'. - * - * @param {string} str string to linkify - * @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options - * @returns {string} Linkified string - */ -export function linkifyString(str, options = linkifyMatrix.options) { - return _linkifyString(str, options); -} - -/** - * Linkifies the given DOM element. This is a wrapper around 'linkifyjs/element'. - * - * @param {object} element DOM element to linkify - * @param {object} [options] Options for linkifyElement. Default: linkifyMatrix.options - * @returns {object} - */ -export function linkifyElement(element, options = linkifyMatrix.options) { - return _linkifyElement(element, options); -} - -/** - * Linkify the given string and sanitize the HTML afterwards. - * - * @param {string} dirtyHtml The HTML string to sanitize and linkify - * @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options - * @returns {string} - */ -export function linkifyAndSanitizeHtml(dirtyHtml, options = linkifyMatrix.options) { - return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams); -} - -/** - * Returns if a node is a block element or not. - * Only takes html nodes into account that are allowed in matrix messages. - * - * @param {Node} node - * @returns {bool} - */ -export function checkBlockNode(node) { - switch (node.nodeName) { - case "H1": - case "H2": - case "H3": - case "H4": - case "H5": - case "H6": - case "PRE": - case "BLOCKQUOTE": - case "DIV": - case "P": - case "UL": - case "OL": - case "LI": - case "HR": - case "TABLE": - case "THEAD": - case "TBODY": - case "TR": - case "TH": - case "TD": - return true; - default: - return false; - } -} diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx new file mode 100644 index 0000000000..5e83fdc2a0 --- /dev/null +++ b/src/HtmlUtils.tsx @@ -0,0 +1,589 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017, 2018 New Vector Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019 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, { ReactNode } from 'react'; +import sanitizeHtml from 'sanitize-html'; +import cheerio from 'cheerio'; +import * as linkify from 'linkifyjs'; +import _linkifyElement from 'linkifyjs/element'; +import _linkifyString from 'linkifyjs/string'; +import classNames from 'classnames'; +import EMOJIBASE_REGEX from 'emojibase-regex'; +import url from 'url'; +import katex from 'katex'; +import { AllHtmlEntities } from 'html-entities'; +import { IContent } from 'matrix-js-sdk/src/models/event'; + +import { IExtendedSanitizeOptions } from './@types/sanitize-html'; +import linkifyMatrix from './linkify-matrix'; +import SettingsStore from './settings/SettingsStore'; +import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks"; +import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji"; +import ReplyThread from "./components/views/elements/ReplyThread"; +import { mediaFromMxc } from "./customisations/Media"; + +linkifyMatrix(linkify); + +// Anything outside the basic multilingual plane will be a surrogate pair +const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/; +// And there a bunch more symbol characters that emojibase has within the +// BMP, so this includes the ranges from 'letterlike symbols' to +// 'miscellaneous symbols and arrows' which should catch all of them +// (with plenty of false positives, but that's OK) +const SYMBOL_PATTERN = /([\u2100-\u2bff])/; + +// Regex pattern for Zero-Width joiner unicode characters +const ZWJ_REGEX = new RegExp("\u200D|\u2003", "g"); + +// Regex pattern for whitespace characters +const WHITESPACE_REGEX = new RegExp("\\s", "g"); + +const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i'); + +const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; + +export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet']; + +const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/; + +/* + * Return true if the given string contains emoji + * Uses a much, much simpler regex than emojibase's so will give false + * positives, but useful for fast-path testing strings to see if they + * need emojification. + * unicodeToImage uses this function. + */ +function mightContainEmoji(str: string): boolean { + return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str); +} + +/** + * Returns the shortcode for an emoji character. + * + * @param {String} char The emoji character + * @return {String} The shortcode (such as :thumbup:) + */ +export function unicodeToShortcode(char: string): string { + const data = getEmojiFromUnicode(char); + return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : ''); +} + +/** + * Returns the unicode character for an emoji shortcode + * + * @param {String} shortcode The shortcode (such as :thumbup:) + * @return {String} The emoji character; null if none exists + */ +export function shortcodeToUnicode(shortcode: string): string { + shortcode = shortcode.slice(1, shortcode.length - 1); + const data = SHORTCODE_TO_EMOJI.get(shortcode); + return data ? data.unicode : null; +} + +export function processHtmlForSending(html: string): string { + const contentDiv = document.createElement('div'); + contentDiv.innerHTML = html; + + if (contentDiv.children.length === 0) { + return contentDiv.innerHTML; + } + + let contentHTML = ""; + for (let i = 0; i < contentDiv.children.length; i++) { + const element = contentDiv.children[i]; + if (element.tagName.toLowerCase() === 'p') { + contentHTML += element.innerHTML; + // Don't add a
for the last

+ if (i !== contentDiv.children.length - 1) { + contentHTML += '
'; + } + } else { + const temp = document.createElement('div'); + temp.appendChild(element.cloneNode(true)); + contentHTML += temp.innerHTML; + } + } + + return contentHTML; +} + +/* + * Given an untrusted HTML string, return a React node with an sanitized version + * of that HTML. + */ +export function sanitizedHtmlNode(insaneHtml: string): ReactNode { + const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams); + + return

; +} + +export function getHtmlText(insaneHtml: string): string { + return sanitizeHtml(insaneHtml, { + allowedTags: [], + allowedAttributes: {}, + selfClosing: [], + allowedSchemes: [], + disallowedTagsMode: 'discard', + }); +} + +/** + * Tests if a URL from an untrusted source may be safely put into the DOM + * The biggest threat here is javascript: URIs. + * Note that the HTML sanitiser library has its own internal logic for + * doing this, to which we pass the same list of schemes. This is used in + * other places we need to sanitise URLs. + * @return true if permitted, otherwise false + */ +export function isUrlPermitted(inputUrl: string): boolean { + try { + const parsed = url.parse(inputUrl); + if (!parsed.protocol) return false; + // URL parser protocol includes the trailing colon + return PERMITTED_URL_SCHEMES.includes(parsed.protocol.slice(0, -1)); + } catch (e) { + return false; + } +} + +const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to matrix + // add blank targets to all hyperlinks except vector URLs + 'a': function(tagName: string, attribs: sanitizeHtml.Attributes) { + if (attribs.href) { + attribs.target = '_blank'; // by default + + const transformed = tryTransformPermalinkToLocalHref(attribs.href); + if (transformed !== attribs.href || attribs.href.match(linkifyMatrix.ELEMENT_URL_PATTERN)) { + attribs.href = transformed; + delete attribs.target; + } + } + attribs.rel = 'noreferrer noopener'; // https://mathiasbynens.github.io/rel-noopener/ + return { tagName, attribs }; + }, + 'img': function(tagName: string, attribs: sanitizeHtml.Attributes) { + let src = attribs.src; + // Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag + // because transformTags is used _before_ we filter by allowedSchemesByTag and + // we don't want to allow images with `https?` `src`s. + // We also drop inline images (as if they were not present at all) when the "show + // images" preference is disabled. Future work might expose some UI to reveal them + // like standalone image events have. + if (!src || !SettingsStore.getValue("showImages")) { + return { tagName, attribs: {} }; + } + + if (!src.startsWith("mxc://")) { + const match = MEDIA_API_MXC_REGEX.exec(src); + if (match) { + src = `mxc://${match[1]}/${match[2]}`; + } + } + + if (!src.startsWith("mxc://")) { + return { tagName, attribs: {} }; + } + + const width = Number(attribs.width) || 800; + const height = Number(attribs.height) || 600; + attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height); + return { tagName, attribs }; + }, + 'code': function(tagName: string, attribs: sanitizeHtml.Attributes) { + if (typeof attribs.class !== 'undefined') { + // Filter out all classes other than ones starting with language- for syntax highlighting. + const classes = attribs.class.split(/\s/).filter(function(cl) { + return cl.startsWith('language-') && !cl.startsWith('language-_'); + }); + attribs.class = classes.join(' '); + } + 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; + + // Sanitise and transform data-mx-color and data-mx-bg-color to their CSS + // equivalents + const customCSSMapper = { + 'data-mx-color': 'color', + 'data-mx-bg-color': 'background-color', + // $customAttributeKey: $cssAttributeKey + }; + + let style = ""; + Object.keys(customCSSMapper).forEach((customAttributeKey) => { + const cssAttributeKey = customCSSMapper[customAttributeKey]; + const customAttributeValue = attribs[customAttributeKey]; + if (customAttributeValue && + typeof customAttributeValue === 'string' && + COLOR_REGEX.test(customAttributeValue) + ) { + style += cssAttributeKey + ":" + customAttributeValue + ";"; + delete attribs[customAttributeKey]; + } + }); + + if (style) { + attribs.style = style; + } + + return { tagName, attribs }; + }, +}; + +const sanitizeHtmlParams: IExtendedSanitizeOptions = { + allowedTags: [ + 'font', // custom to matrix for IRC-style font coloring + 'del', // for markdown + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub', + 'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', + 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img', + 'details', 'summary', + ], + allowedAttributes: { + // 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'], + ol: ['start'], + code: ['class'], // We don't actually allow all classes, we filter them in transformTags + }, + // Lots of these won't come up by default because we don't allow them + selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'], + // URL schemes we permit + allowedSchemes: PERMITTED_URL_SCHEMES, + allowProtocolRelative: false, + transformTags, + // 50 levels deep "should be enough for anyone" + nestingLimit: 50, +}; + +// this is the same as the above except with less rewriting +const composerSanitizeHtmlParams: IExtendedSanitizeOptions = { + ...sanitizeHtmlParams, + transformTags: { + 'code': transformTags['code'], + '*': transformTags['*'], + }, +}; + +abstract class BaseHighlighter { + constructor(public highlightClass: string, public highlightLink: string) { + } + + /** + * apply the highlights to a section of text + * + * @param {string} safeSnippet The snippet of text to apply the highlights + * to. + * @param {string[]} safeHighlights A list of substrings to highlight, + * sorted by descending length. + * + * returns a list of results (strings for HtmlHighligher, react nodes for + * TextHighlighter). + */ + public applyHighlights(safeSnippet: string, safeHighlights: string[]): T[] { + let lastOffset = 0; + let offset; + let nodes: T[] = []; + + const safeHighlight = safeHighlights[0]; + while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) { + // handle preamble + if (offset > lastOffset) { + const subSnippet = safeSnippet.substring(lastOffset, offset); + nodes = nodes.concat(this.applySubHighlights(subSnippet, safeHighlights)); + } + + // do highlight. use the original string rather than safeHighlight + // to preserve the original casing. + const endOffset = offset + safeHighlight.length; + nodes.push(this.processSnippet(safeSnippet.substring(offset, endOffset), true)); + + lastOffset = endOffset; + } + + // handle postamble + if (lastOffset !== safeSnippet.length) { + const subSnippet = safeSnippet.substring(lastOffset, undefined); + nodes = nodes.concat(this.applySubHighlights(subSnippet, safeHighlights)); + } + return nodes; + } + + private applySubHighlights(safeSnippet: string, safeHighlights: string[]): T[] { + if (safeHighlights[1]) { + // recurse into this range to check for the next set of highlight matches + return this.applyHighlights(safeSnippet, safeHighlights.slice(1)); + } else { + // no more highlights to be found, just return the unhighlighted string + return [this.processSnippet(safeSnippet, false)]; + } + } + + protected abstract processSnippet(snippet: string, highlight: boolean): T; +} + +class HtmlHighlighter extends BaseHighlighter { + /* highlight the given snippet if required + * + * snippet: content of the span; must have been sanitised + * highlight: true to highlight as a search match + * + * returns an HTML string + */ + protected processSnippet(snippet: string, highlight: boolean): string { + if (!highlight) { + // nothing required here + return snippet; + } + + let span = `${snippet}`; + + if (this.highlightLink) { + span = `${span}`; + } + return span; + } +} + +interface IOpts { + highlightLink?: string; + disableBigEmoji?: boolean; + stripReplyFallback?: boolean; + returnString?: boolean; + forComposerQuote?: boolean; + ref?: React.Ref; +} + +export interface IOptsReturnNode extends IOpts { + returnString: false | undefined; +} + +export interface IOptsReturnString extends IOpts { + returnString: true; +} + +/* turn a matrix event body into html + * + * content: 'content' of the MatrixEvent + * + * highlights: optional list of words to highlight, ordered by longest word first + * + * opts.highlightLink: optional href to add to highlighted words + * opts.disableBigEmoji: optional argument to disable the big emoji class. + * opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing + * opts.returnString: return an HTML string rather than JSX elements + * opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer + * opts.ref: React ref to attach to any React components returned (not compatible with opts.returnString) + */ +export function bodyToHtml(content: IContent, highlights: string[], opts: IOptsReturnString): string; +export function bodyToHtml(content: IContent, highlights: string[], opts: IOptsReturnNode): ReactNode; +export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts = {}) { + const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body; + let bodyHasEmoji = false; + + let sanitizeParams = sanitizeHtmlParams; + if (opts.forComposerQuote) { + sanitizeParams = composerSanitizeHtmlParams; + } + + let strippedBody: string; + let safeBody: string; + let isDisplayedWithHtml: boolean; + // XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying + // to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which + // are interrupted by HTML tags (not that we did before) - e.g. foobar won't get highlighted + // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either + try { + if (highlights && highlights.length > 0) { + const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink); + const safeHighlights = highlights + // sanitizeHtml can hang if an unclosed HTML tag is thrown at it + // A search for ` !highlight.includes("<")) + .map((highlight: string): string => sanitizeHtml(highlight, sanitizeParams)); + // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. + sanitizeParams.textFilter = function(safeText) { + return highlighter.applyHighlights(safeText, safeHighlights).join(''); + }; + } + + let formattedBody = typeof content.formatted_body === 'string' ? content.formatted_body : null; + const plainBody = typeof content.body === 'string' ? content.body : ""; + + if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody); + strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(plainBody) : plainBody; + + bodyHasEmoji = mightContainEmoji(isHtmlMessage ? formattedBody : plainBody); + + // Only generate safeBody if the message was sent as org.matrix.custom.html + if (isHtmlMessage) { + isDisplayedWithHtml = true; + safeBody = sanitizeHtml(formattedBody, sanitizeParams); + + if (SettingsStore.getValue("feature_latex_maths")) { + const phtml = cheerio.load(safeBody, { + // @ts-ignore: The `_useHtmlParser2` internal option is the + // simplest way to both parse and render using `htmlparser2`. + _useHtmlParser2: true, + decodeEntities: false, + }); + // @ts-ignore - The types for `replaceWith` wrongly expect + // Cheerio instance to be returned. + phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) { + return katex.renderToString( + AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')), + { + throwOnError: false, + // @ts-ignore - `e` can be an Element, not just a Node + displayMode: e.name == 'div', + output: "htmlAndMathml", + }); + }); + safeBody = phtml.html(); + } + } + } finally { + delete sanitizeParams.textFilter; + } + + const contentBody = isDisplayedWithHtml ? safeBody : strippedBody; + if (opts.returnString) { + return contentBody; + } + + let emojiBody = false; + if (!opts.disableBigEmoji && bodyHasEmoji) { + let contentBodyTrimmed = contentBody !== undefined ? contentBody.trim() : ''; + + // Ignore spaces in body text. Emojis with spaces in between should + // still be counted as purely emoji messages. + contentBodyTrimmed = contentBodyTrimmed.replace(WHITESPACE_REGEX, ''); + + // Remove zero width joiner characters from emoji messages. This ensures + // that emojis that are made up of multiple unicode characters are still + // presented as large. + contentBodyTrimmed = contentBodyTrimmed.replace(ZWJ_REGEX, ''); + + const match = BIGEMOJI_REGEX.exec(contentBodyTrimmed); + emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length && + // Prevent user pills expanding for users with only emoji in + // their username. Permalinks (links in pills) can be any URL + // now, so we just check for an HTTP-looking thing. + ( + strippedBody === safeBody || // replies have the html fallbacks, account for that here + content.formatted_body === undefined || + (!content.formatted_body.includes("http:") && + !content.formatted_body.includes("https:")) + ); + } + + const className = classNames({ + 'mx_EventTile_body': true, + 'mx_EventTile_bigEmoji': emojiBody, + 'markdown-body': isHtmlMessage && !emojiBody, + }); + + return isDisplayedWithHtml ? + : { strippedBody }; +} + +/** + * Linkifies the given string. This is a wrapper around 'linkifyjs/string'. + * + * @param {string} str string to linkify + * @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options + * @returns {string} Linkified string + */ +export function linkifyString(str: string, options = linkifyMatrix.options): string { + return _linkifyString(str, options); +} + +/** + * Linkifies the given DOM element. This is a wrapper around 'linkifyjs/element'. + * + * @param {object} element DOM element to linkify + * @param {object} [options] Options for linkifyElement. Default: linkifyMatrix.options + * @returns {object} + */ +export function linkifyElement(element: HTMLElement, options = linkifyMatrix.options): HTMLElement { + return _linkifyElement(element, options); +} + +/** + * Linkify the given string and sanitize the HTML afterwards. + * + * @param {string} dirtyHtml The HTML string to sanitize and linkify + * @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options + * @returns {string} + */ +export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatrix.options): string { + return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams); +} + +/** + * Returns if a node is a block element or not. + * Only takes html nodes into account that are allowed in matrix messages. + * + * @param {Node} node + * @returns {bool} + */ +export function checkBlockNode(node: Node): boolean { + switch (node.nodeName) { + case "H1": + case "H2": + case "H3": + case "H4": + case "H5": + case "H6": + case "PRE": + case "BLOCKQUOTE": + case "P": + case "UL": + case "OL": + case "LI": + case "HR": + case "TABLE": + case "THEAD": + case "TBODY": + case "TR": + case "TH": + case "TD": + return true; + case "DIV": + // don't treat math nodes as block nodes for deserializing + return !(node as HTMLElement).hasAttribute("data-mx-maths"); + default: + return false; + } +} diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 4a830d6506..447c5edd30 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -14,9 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { createClient, SERVICE_TYPES } from 'matrix-js-sdk'; +import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types'; +import { createClient } from 'matrix-js-sdk/src/matrix'; -import {MatrixClientPeg} from './MatrixClientPeg'; +import { MatrixClientPeg } from './MatrixClientPeg'; import Modal from './Modal'; import * as sdk from './index'; import { _t } from './languageHandler'; @@ -126,7 +127,7 @@ export default class IdentityAuthClient { await this._matrixClient.getIdentityAccount(token); } catch (e) { if (e.errcode === "M_TERMS_NOT_SIGNED") { - console.log("Identity Server requires new terms to be agreed to"); + console.log("Identity server requires new terms to be agreed to"); await startTermsFlow([new Service( SERVICE_TYPES.IS, identityServerUrl, @@ -162,9 +163,10 @@ export default class IdentityAuthClient {
), button: _t("Trust"), - }); + }); const [confirmed] = await finished; if (confirmed) { + // eslint-disable-next-line react-hooks/rules-of-hooks useDefaultIdentityServer(); } else { throw new AbortedIdentityActionError( @@ -177,7 +179,7 @@ export default class IdentityAuthClient { // appropriately. We already clear storage on sign out, but we'll need // additional clearing when changing ISes in settings as part of future // privacy work. - // See also https://github.com/vector-im/riot-web/issues/10455. + // See also https://github.com/vector-im/element-web/issues/10455. } async registerForToken(check=true) { diff --git a/src/ImageUtils.js b/src/ImageUtils.js deleted file mode 100644 index c0f7b94b81..0000000000 --- a/src/ImageUtils.js +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -'use strict'; - -/** - * Returns the actual height that an image of dimensions (fullWidth, fullHeight) - * will occupy if resized to fit inside a thumbnail bounding box of size - * (thumbWidth, thumbHeight). - * - * If the aspect ratio of the source image is taller than the aspect ratio of - * the thumbnail bounding box, then we return the thumbHeight parameter unchanged. - * Otherwise we return the thumbHeight parameter scaled down appropriately to - * reflect the actual height the scaled thumbnail occupies. - * - * This is very useful for calculating how much height a thumbnail will actually - * consume in the timeline, when performing scroll offset calcuations - * (e.g. scroll locking) - */ -export function thumbHeight(fullWidth, fullHeight, thumbWidth, thumbHeight) { - if (!fullWidth || !fullHeight) { - // Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even - // log this because it's spammy - return undefined; - } - if (fullWidth < thumbWidth && fullHeight < thumbHeight) { - // no scaling needs to be applied - return fullHeight; - } - const widthMulti = thumbWidth / fullWidth; - const heightMulti = thumbHeight / fullHeight; - if (widthMulti < heightMulti) { - // width is the dominant dimension so scaling will be fixed on that - return Math.floor(widthMulti * fullHeight); - } else { - // height is the dominant dimension so scaling will be fixed on that - return Math.floor(heightMulti * fullHeight); - } -} - diff --git a/src/ImageUtils.ts b/src/ImageUtils.ts new file mode 100644 index 0000000000..9bfab37193 --- /dev/null +++ b/src/ImageUtils.ts @@ -0,0 +1,51 @@ +/* +Copyright 2015, 2016, 2020 Copyright 2020 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. +*/ + +/** + * Returns the actual height that an image of dimensions (fullWidth, fullHeight) + * will occupy if resized to fit inside a thumbnail bounding box of size + * (thumbWidth, thumbHeight). + * + * If the aspect ratio of the source image is taller than the aspect ratio of + * the thumbnail bounding box, then we return the thumbHeight parameter unchanged. + * Otherwise we return the thumbHeight parameter scaled down appropriately to + * reflect the actual height the scaled thumbnail occupies. + * + * This is very useful for calculating how much height a thumbnail will actually + * consume in the timeline, when performing scroll offset calcuations + * (e.g. scroll locking) + */ +export function thumbHeight(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number) { + if (!fullWidth || !fullHeight) { + // Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even + // log this because it's spammy + return null; + } + if (fullWidth < thumbWidth && fullHeight < thumbHeight) { + // no scaling needs to be applied + return fullHeight; + } + const widthMulti = thumbWidth / fullWidth; + const heightMulti = thumbHeight / fullHeight; + if (widthMulti < heightMulti) { + // width is the dominant dimension so scaling will be fixed on that + return Math.floor(widthMulti * fullHeight); + } else { + // height is the dominant dimension so scaling will be fixed on that + return Math.floor(heightMulti * fullHeight); + } +} + diff --git a/src/KeyBindingsDefaults.ts b/src/KeyBindingsDefaults.ts new file mode 100644 index 0000000000..b2f70abff7 --- /dev/null +++ b/src/KeyBindingsDefaults.ts @@ -0,0 +1,407 @@ +/* +Copyright 2021 Clemens Zeidler + +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 { AutocompleteAction, IKeyBindingsProvider, KeyBinding, MessageComposerAction, NavigationAction, RoomAction, + RoomListAction } from "./KeyBindingsManager"; +import { isMac, Key } from "./Keyboard"; +import SettingsStore from "./settings/SettingsStore"; + +const messageComposerBindings = (): KeyBinding[] => { + const bindings: KeyBinding[] = [ + { + action: MessageComposerAction.SelectPrevSendHistory, + keyCombo: { + key: Key.ARROW_UP, + altKey: true, + ctrlKey: true, + }, + }, + { + action: MessageComposerAction.SelectNextSendHistory, + keyCombo: { + key: Key.ARROW_DOWN, + altKey: true, + ctrlKey: true, + }, + }, + { + action: MessageComposerAction.EditPrevMessage, + keyCombo: { + key: Key.ARROW_UP, + }, + }, + { + action: MessageComposerAction.EditNextMessage, + keyCombo: { + key: Key.ARROW_DOWN, + }, + }, + { + action: MessageComposerAction.CancelEditing, + keyCombo: { + key: Key.ESCAPE, + }, + }, + { + action: MessageComposerAction.FormatBold, + keyCombo: { + key: Key.B, + ctrlOrCmd: true, + }, + }, + { + action: MessageComposerAction.FormatItalics, + keyCombo: { + key: Key.I, + ctrlOrCmd: true, + }, + }, + { + action: MessageComposerAction.FormatQuote, + keyCombo: { + key: Key.GREATER_THAN, + ctrlOrCmd: true, + shiftKey: true, + }, + }, + { + action: MessageComposerAction.EditUndo, + keyCombo: { + key: Key.Z, + ctrlOrCmd: true, + }, + }, + { + action: MessageComposerAction.MoveCursorToStart, + keyCombo: { + key: Key.HOME, + ctrlOrCmd: true, + }, + }, + { + action: MessageComposerAction.MoveCursorToEnd, + keyCombo: { + key: Key.END, + ctrlOrCmd: true, + }, + }, + ]; + if (isMac) { + bindings.push({ + action: MessageComposerAction.EditRedo, + keyCombo: { + key: Key.Z, + ctrlOrCmd: true, + shiftKey: true, + }, + }); + } else { + bindings.push({ + action: MessageComposerAction.EditRedo, + keyCombo: { + key: Key.Y, + ctrlOrCmd: true, + }, + }); + } + if (SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend')) { + bindings.push({ + action: MessageComposerAction.Send, + keyCombo: { + key: Key.ENTER, + ctrlOrCmd: true, + }, + }); + bindings.push({ + action: MessageComposerAction.NewLine, + keyCombo: { + key: Key.ENTER, + }, + }); + } else { + bindings.push({ + action: MessageComposerAction.Send, + keyCombo: { + key: Key.ENTER, + }, + }); + bindings.push({ + action: MessageComposerAction.NewLine, + keyCombo: { + key: Key.ENTER, + shiftKey: true, + }, + }); + if (isMac) { + bindings.push({ + action: MessageComposerAction.NewLine, + keyCombo: { + key: Key.ENTER, + altKey: true, + }, + }); + } + } + return bindings; +}; + +const autocompleteBindings = (): KeyBinding[] => { + return [ + { + action: AutocompleteAction.CompleteOrNextSelection, + keyCombo: { + key: Key.TAB, + }, + }, + { + action: AutocompleteAction.CompleteOrNextSelection, + keyCombo: { + key: Key.TAB, + ctrlKey: true, + }, + }, + { + action: AutocompleteAction.CompleteOrPrevSelection, + keyCombo: { + key: Key.TAB, + shiftKey: true, + }, + }, + { + action: AutocompleteAction.CompleteOrPrevSelection, + keyCombo: { + key: Key.TAB, + ctrlKey: true, + shiftKey: true, + }, + }, + { + action: AutocompleteAction.Cancel, + keyCombo: { + key: Key.ESCAPE, + }, + }, + { + action: AutocompleteAction.PrevSelection, + keyCombo: { + key: Key.ARROW_UP, + }, + }, + { + action: AutocompleteAction.NextSelection, + keyCombo: { + key: Key.ARROW_DOWN, + }, + }, + ]; +}; + +const roomListBindings = (): KeyBinding[] => { + return [ + { + action: RoomListAction.ClearSearch, + keyCombo: { + key: Key.ESCAPE, + }, + }, + { + action: RoomListAction.PrevRoom, + keyCombo: { + key: Key.ARROW_UP, + }, + }, + { + action: RoomListAction.NextRoom, + keyCombo: { + key: Key.ARROW_DOWN, + }, + }, + { + action: RoomListAction.SelectRoom, + keyCombo: { + key: Key.ENTER, + }, + }, + { + action: RoomListAction.CollapseSection, + keyCombo: { + key: Key.ARROW_LEFT, + }, + }, + { + action: RoomListAction.ExpandSection, + keyCombo: { + key: Key.ARROW_RIGHT, + }, + }, + ]; +}; + +const roomBindings = (): KeyBinding[] => { + const bindings: KeyBinding[] = [ + { + action: RoomAction.ScrollUp, + keyCombo: { + key: Key.PAGE_UP, + }, + }, + { + action: RoomAction.RoomScrollDown, + keyCombo: { + key: Key.PAGE_DOWN, + }, + }, + { + action: RoomAction.DismissReadMarker, + keyCombo: { + key: Key.ESCAPE, + }, + }, + { + action: RoomAction.JumpToOldestUnread, + keyCombo: { + key: Key.PAGE_UP, + shiftKey: true, + }, + }, + { + action: RoomAction.UploadFile, + keyCombo: { + key: Key.U, + ctrlOrCmd: true, + shiftKey: true, + }, + }, + { + action: RoomAction.JumpToFirstMessage, + keyCombo: { + key: Key.HOME, + ctrlKey: true, + }, + }, + { + action: RoomAction.JumpToLatestMessage, + keyCombo: { + key: Key.END, + ctrlKey: true, + }, + }, + ]; + + if (SettingsStore.getValue('ctrlFForSearch')) { + bindings.push({ + action: RoomAction.FocusSearch, + keyCombo: { + key: Key.F, + ctrlOrCmd: true, + }, + }); + } + + return bindings; +}; + +const navigationBindings = (): KeyBinding[] => { + return [ + { + action: NavigationAction.FocusRoomSearch, + keyCombo: { + key: Key.K, + ctrlOrCmd: true, + }, + }, + { + action: NavigationAction.ToggleRoomSidePanel, + keyCombo: { + key: Key.PERIOD, + ctrlOrCmd: true, + }, + }, + { + action: NavigationAction.ToggleUserMenu, + // Ideally this would be CTRL+P for "Profile", but that's + // taken by the print dialog. CTRL+I for "Information" + // was previously chosen but conflicted with italics in + // composer, so CTRL+` it is + keyCombo: { + key: Key.BACKTICK, + ctrlOrCmd: true, + }, + }, + { + action: NavigationAction.ToggleShortCutDialog, + keyCombo: { + key: Key.SLASH, + ctrlOrCmd: true, + }, + }, + { + action: NavigationAction.ToggleShortCutDialog, + keyCombo: { + key: Key.SLASH, + ctrlOrCmd: true, + shiftKey: true, + }, + }, + { + action: NavigationAction.GoToHome, + keyCombo: { + key: Key.H, + ctrlKey: true, + altKey: !isMac, + shiftKey: isMac, + }, + }, + { + action: NavigationAction.SelectPrevRoom, + keyCombo: { + key: Key.ARROW_UP, + altKey: true, + }, + }, + { + action: NavigationAction.SelectNextRoom, + keyCombo: { + key: Key.ARROW_DOWN, + altKey: true, + }, + }, + { + action: NavigationAction.SelectPrevUnreadRoom, + keyCombo: { + key: Key.ARROW_UP, + altKey: true, + shiftKey: true, + }, + }, + { + action: NavigationAction.SelectNextUnreadRoom, + keyCombo: { + key: Key.ARROW_DOWN, + altKey: true, + shiftKey: true, + }, + }, + ]; +}; + +export const defaultBindingsProvider: IKeyBindingsProvider = { + getMessageComposerBindings: messageComposerBindings, + getAutocompleteBindings: autocompleteBindings, + getRoomListBindings: roomListBindings, + getRoomBindings: roomBindings, + getNavigationBindings: navigationBindings, +}; diff --git a/src/KeyBindingsManager.ts b/src/KeyBindingsManager.ts new file mode 100644 index 0000000000..4225d2f449 --- /dev/null +++ b/src/KeyBindingsManager.ts @@ -0,0 +1,273 @@ +/* +Copyright 2021 Clemens Zeidler + +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 { defaultBindingsProvider } from './KeyBindingsDefaults'; +import { isMac } from './Keyboard'; + +/** Actions for the chat message composer component */ +export enum MessageComposerAction { + /** Send a message */ + Send = 'Send', + /** Go backwards through the send history and use the message in composer view */ + SelectPrevSendHistory = 'SelectPrevSendHistory', + /** Go forwards through the send history */ + SelectNextSendHistory = 'SelectNextSendHistory', + /** Start editing the user's last sent message */ + EditPrevMessage = 'EditPrevMessage', + /** Start editing the user's next sent message */ + EditNextMessage = 'EditNextMessage', + /** Cancel editing a message or cancel replying to a message */ + CancelEditing = 'CancelEditing', + + /** Set bold format the current selection */ + FormatBold = 'FormatBold', + /** Set italics format the current selection */ + FormatItalics = 'FormatItalics', + /** Format the current selection as quote */ + FormatQuote = 'FormatQuote', + /** Undo the last editing */ + EditUndo = 'EditUndo', + /** Redo editing */ + EditRedo = 'EditRedo', + /** Insert new line */ + NewLine = 'NewLine', + /** Move the cursor to the start of the message */ + MoveCursorToStart = 'MoveCursorToStart', + /** Move the cursor to the end of the message */ + MoveCursorToEnd = 'MoveCursorToEnd', +} + +/** Actions for text editing autocompletion */ +export enum AutocompleteAction { + /** + * Select previous selection or, if the autocompletion window is not shown, open the window and select the first + * selection. + */ + CompleteOrPrevSelection = 'ApplySelection', + /** Select next selection or, if the autocompletion window is not shown, open it and select the first selection */ + CompleteOrNextSelection = 'CompleteOrNextSelection', + /** Move to the previous autocomplete selection */ + PrevSelection = 'PrevSelection', + /** Move to the next autocomplete selection */ + NextSelection = 'NextSelection', + /** Close the autocompletion window */ + Cancel = 'Cancel', +} + +/** Actions for the room list sidebar */ +export enum RoomListAction { + /** Clear room list filter field */ + ClearSearch = 'ClearSearch', + /** Navigate up/down in the room list */ + PrevRoom = 'PrevRoom', + /** Navigate down in the room list */ + NextRoom = 'NextRoom', + /** Select room from the room list */ + SelectRoom = 'SelectRoom', + /** Collapse room list section */ + CollapseSection = 'CollapseSection', + /** Expand room list section, if already expanded, jump to first room in the selection */ + ExpandSection = 'ExpandSection', +} + +/** Actions for the current room view */ +export enum RoomAction { + /** Scroll up in the timeline */ + ScrollUp = 'ScrollUp', + /** Scroll down in the timeline */ + RoomScrollDown = 'RoomScrollDown', + /** Dismiss read marker and jump to bottom */ + DismissReadMarker = 'DismissReadMarker', + /** Jump to oldest unread message */ + JumpToOldestUnread = 'JumpToOldestUnread', + /** Upload a file */ + UploadFile = 'UploadFile', + /** Focus search message in a room (must be enabled) */ + FocusSearch = 'FocusSearch', + /** Jump to the first (downloaded) message in the room */ + JumpToFirstMessage = 'JumpToFirstMessage', + /** Jump to the latest message in the room */ + JumpToLatestMessage = 'JumpToLatestMessage', +} + +/** Actions for navigating do various menus, dialogs or screens */ +export enum NavigationAction { + /** Jump to room search (search for a room) */ + FocusRoomSearch = 'FocusRoomSearch', + /** Toggle the room side panel */ + ToggleRoomSidePanel = 'ToggleRoomSidePanel', + /** Toggle the user menu */ + ToggleUserMenu = 'ToggleUserMenu', + /** Toggle the short cut help dialog */ + ToggleShortCutDialog = 'ToggleShortCutDialog', + /** Got to the Element home screen */ + GoToHome = 'GoToHome', + /** Select prev room */ + SelectPrevRoom = 'SelectPrevRoom', + /** Select next room */ + SelectNextRoom = 'SelectNextRoom', + /** Select prev room with unread messages */ + SelectPrevUnreadRoom = 'SelectPrevUnreadRoom', + /** Select next room with unread messages */ + SelectNextUnreadRoom = 'SelectNextUnreadRoom', +} + +/** + * Represent a key combination. + * + * The combo is evaluated strictly, i.e. the KeyboardEvent must match exactly what is specified in the KeyCombo. + */ +export type KeyCombo = { + key?: string; + + /** On PC: ctrl is pressed; on Mac: meta is pressed */ + ctrlOrCmd?: boolean; + + altKey?: boolean; + ctrlKey?: boolean; + metaKey?: boolean; + shiftKey?: boolean; +}; + +export type KeyBinding = { + action: T; + keyCombo: KeyCombo; +}; + +/** + * Helper method to check if a KeyboardEvent matches a KeyCombo + * + * Note, this method is only exported for testing. + */ +export function isKeyComboMatch(ev: KeyboardEvent | React.KeyboardEvent, combo: KeyCombo, onMac: boolean): boolean { + if (combo.key !== undefined) { + // When shift is pressed, letters are returned as upper case chars. In this case do a lower case comparison. + // This works for letter combos such as shift + U as well for none letter combos such as shift + Escape. + // If shift is not pressed, the toLowerCase conversion can be avoided. + if (ev.shiftKey) { + if (ev.key.toLowerCase() !== combo.key.toLowerCase()) { + return false; + } + } else if (ev.key !== combo.key) { + return false; + } + } + + const comboCtrl = combo.ctrlKey ?? false; + const comboAlt = combo.altKey ?? false; + const comboShift = combo.shiftKey ?? false; + const comboMeta = combo.metaKey ?? false; + // Tests mock events may keep the modifiers undefined; convert them to booleans + const evCtrl = ev.ctrlKey ?? false; + const evAlt = ev.altKey ?? false; + const evShift = ev.shiftKey ?? false; + const evMeta = ev.metaKey ?? false; + // When ctrlOrCmd is set, the keys need do evaluated differently on PC and Mac + if (combo.ctrlOrCmd) { + if (onMac) { + if (!evMeta + || evCtrl !== comboCtrl + || evAlt !== comboAlt + || evShift !== comboShift) { + return false; + } + } else { + if (!evCtrl + || evMeta !== comboMeta + || evAlt !== comboAlt + || evShift !== comboShift) { + return false; + } + } + return true; + } + + if (evMeta !== comboMeta + || evCtrl !== comboCtrl + || evAlt !== comboAlt + || evShift !== comboShift) { + return false; + } + + return true; +} + +export type KeyBindingGetter = () => KeyBinding[]; + +export interface IKeyBindingsProvider { + getMessageComposerBindings: KeyBindingGetter; + getAutocompleteBindings: KeyBindingGetter; + getRoomListBindings: KeyBindingGetter; + getRoomBindings: KeyBindingGetter; + getNavigationBindings: KeyBindingGetter; +} + +export class KeyBindingsManager { + /** + * List of key bindings providers. + * + * Key bindings from the first provider(s) in the list will have precedence over key bindings from later providers. + * + * To overwrite the default key bindings add a new providers before the default provider, e.g. a provider for + * customized key bindings. + */ + bindingsProviders: IKeyBindingsProvider[] = [ + defaultBindingsProvider, + ]; + + /** + * Finds a matching KeyAction for a given KeyboardEvent + */ + private getAction( + getters: KeyBindingGetter[], + ev: KeyboardEvent | React.KeyboardEvent, + ): T | undefined { + for (const getter of getters) { + const bindings = getter(); + const binding = bindings.find(it => isKeyComboMatch(ev, it.keyCombo, isMac)); + if (binding) { + return binding.action; + } + } + return undefined; + } + + getMessageComposerAction(ev: KeyboardEvent | React.KeyboardEvent): MessageComposerAction | undefined { + return this.getAction(this.bindingsProviders.map(it => it.getMessageComposerBindings), ev); + } + + getAutocompleteAction(ev: KeyboardEvent | React.KeyboardEvent): AutocompleteAction | undefined { + return this.getAction(this.bindingsProviders.map(it => it.getAutocompleteBindings), ev); + } + + getRoomListAction(ev: KeyboardEvent | React.KeyboardEvent): RoomListAction | undefined { + return this.getAction(this.bindingsProviders.map(it => it.getRoomListBindings), ev); + } + + getRoomAction(ev: KeyboardEvent | React.KeyboardEvent): RoomAction | undefined { + return this.getAction(this.bindingsProviders.map(it => it.getRoomBindings), ev); + } + + getNavigationAction(ev: KeyboardEvent | React.KeyboardEvent): NavigationAction | undefined { + return this.getAction(this.bindingsProviders.map(it => it.getNavigationBindings), ev); + } +} + +const manager = new KeyBindingsManager(); + +export function getKeyBindingsManager(): KeyBindingsManager { + return manager; +} diff --git a/src/KeyRequestHandler.js b/src/KeyRequestHandler.js deleted file mode 100644 index 30f3b7d50e..0000000000 --- a/src/KeyRequestHandler.js +++ /dev/null @@ -1,158 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd -Copyright 2020 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 * as sdk from './index'; -import Modal from './Modal'; -import SettingsStore from './settings/SettingsStore'; - -// TODO: We can remove this once cross-signing is the only way. -// https://github.com/vector-im/riot-web/issues/11908 -export default class KeyRequestHandler { - constructor(matrixClient) { - this._matrixClient = matrixClient; - - // the user/device for which we currently have a dialog open - this._currentUser = null; - this._currentDevice = null; - - // userId -> deviceId -> [keyRequest] - this._pendingKeyRequests = Object.create(null); - } - - handleKeyRequest(keyRequest) { - // Ignore own device key requests if cross-signing lab enabled - if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { - return; - } - - const userId = keyRequest.userId; - const deviceId = keyRequest.deviceId; - const requestId = keyRequest.requestId; - - if (!this._pendingKeyRequests[userId]) { - this._pendingKeyRequests[userId] = Object.create(null); - } - if (!this._pendingKeyRequests[userId][deviceId]) { - this._pendingKeyRequests[userId][deviceId] = []; - } - - // check if we already have this request - const requests = this._pendingKeyRequests[userId][deviceId]; - if (requests.find((r) => r.requestId === requestId)) { - console.log("Already have this key request, ignoring"); - return; - } - - requests.push(keyRequest); - - if (this._currentUser) { - // ignore for now - console.log("Key request, but we already have a dialog open"); - return; - } - - this._processNextRequest(); - } - - handleKeyRequestCancellation(cancellation) { - // Ignore own device key requests if cross-signing lab enabled - if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { - return; - } - - // see if we can find the request in the queue - const userId = cancellation.userId; - const deviceId = cancellation.deviceId; - const requestId = cancellation.requestId; - - if (userId === this._currentUser && deviceId === this._currentDevice) { - console.log( - "room key request cancellation for the user we currently have a" - + " dialog open for", - ); - // TODO: update the dialog. For now, we just ignore the - // cancellation. - return; - } - - if (!this._pendingKeyRequests[userId]) { - return; - } - const requests = this._pendingKeyRequests[userId][deviceId]; - if (!requests) { - return; - } - const idx = requests.findIndex((r) => r.requestId === requestId); - if (idx < 0) { - return; - } - console.log("Forgetting room key request"); - requests.splice(idx, 1); - if (requests.length === 0) { - delete this._pendingKeyRequests[userId][deviceId]; - if (Object.keys(this._pendingKeyRequests[userId]).length === 0) { - delete this._pendingKeyRequests[userId]; - } - } - } - - _processNextRequest() { - const userId = Object.keys(this._pendingKeyRequests)[0]; - if (!userId) { - return; - } - const deviceId = Object.keys(this._pendingKeyRequests[userId])[0]; - if (!deviceId) { - return; - } - console.log(`Starting KeyShareDialog for ${userId}:${deviceId}`); - - const finished = (r) => { - this._currentUser = null; - this._currentDevice = null; - - if (!this._pendingKeyRequests[userId] || !this._pendingKeyRequests[userId][deviceId]) { - // request was removed in the time the dialog was displayed - this._processNextRequest(); - return; - } - - if (r) { - for (const req of this._pendingKeyRequests[userId][deviceId]) { - req.share(); - } - } - delete this._pendingKeyRequests[userId][deviceId]; - if (Object.keys(this._pendingKeyRequests[userId]).length === 0) { - delete this._pendingKeyRequests[userId]; - } - - this._processNextRequest(); - }; - - const KeyShareDialog = sdk.getComponent("dialogs.KeyShareDialog"); - Modal.appendTrackedDialog('Key Share', 'Process Next Request', KeyShareDialog, { - matrixClient: this._matrixClient, - userId: userId, - deviceId: deviceId, - onFinished: finished, - }); - this._currentUser = userId; - this._currentDevice = deviceId; - } -} - diff --git a/src/Keyboard.ts b/src/Keyboard.ts index 23e2bbf0d6..7040898872 100644 --- a/src/Keyboard.ts +++ b/src/Keyboard.ts @@ -43,6 +43,8 @@ export const Key = { BACKTICK: "`", SPACE: " ", SLASH: "/", + SQUARE_BRACKET_LEFT: "[", + SQUARE_BRACKET_RIGHT: "]", A: "a", B: "b", C: "c", diff --git a/src/Lifecycle.js b/src/Lifecycle.js deleted file mode 100644 index 1baa6c8e0c..0000000000 --- a/src/Lifecycle.js +++ /dev/null @@ -1,686 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd -Copyright 2019, 2020 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 Matrix from 'matrix-js-sdk'; - -import {MatrixClientPeg} from './MatrixClientPeg'; -import EventIndexPeg from './indexing/EventIndexPeg'; -import createMatrixClient from './utils/createMatrixClient'; -import Analytics from './Analytics'; -import Notifier from './Notifier'; -import UserActivity from './UserActivity'; -import Presence from './Presence'; -import dis from './dispatcher'; -import DMRoomMap from './utils/DMRoomMap'; -import Modal from './Modal'; -import * as sdk from './index'; -import ActiveWidgetStore from './stores/ActiveWidgetStore'; -import PlatformPeg from "./PlatformPeg"; -import { sendLoginRequest } from "./Login"; -import * as StorageManager from './utils/StorageManager'; -import SettingsStore from "./settings/SettingsStore"; -import TypingStore from "./stores/TypingStore"; -import ToastStore from "./stores/ToastStore"; -import {IntegrationManagers} from "./integrations/IntegrationManagers"; -import {Mjolnir} from "./mjolnir/Mjolnir"; -import DeviceListener from "./DeviceListener"; -import {Jitsi} from "./widgets/Jitsi"; - -/** - * Called at startup, to attempt to build a logged-in Matrix session. It tries - * a number of things: - * - * - * 1. if we have a guest access token in the fragment query params, it uses - * that. - * - * 2. if an access token is stored in local storage (from a previous session), - * it uses that. - * - * 3. it attempts to auto-register as a guest user. - * - * If any of steps 1-4 are successful, it will call {_doSetLoggedIn}, which in - * turn will raise on_logged_in and will_start_client events. - * - * @param {object} opts - * - * @param {object} opts.fragmentQueryParams: string->string map of the - * query-parameters extracted from the #-fragment of the starting URI. - * - * @param {boolean} opts.enableGuest: set to true to enable guest access tokens - * and auto-guest registrations. - * - * @params {string} opts.guestHsUrl: homeserver URL. Only used if enableGuest is - * true; defines the HS to register against. - * - * @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is - * true; defines the IS to use. - * - * @params {bool} opts.ignoreGuest: If the stored session is a guest account, ignore - * it and don't load it. - * - * @returns {Promise} a promise which resolves when the above process completes. - * Resolves to `true` if we ended up starting a session, or `false` if we - * failed. - */ -export async function loadSession(opts) { - try { - let enableGuest = opts.enableGuest || false; - const guestHsUrl = opts.guestHsUrl; - const guestIsUrl = opts.guestIsUrl; - const fragmentQueryParams = opts.fragmentQueryParams || {}; - const defaultDeviceDisplayName = opts.defaultDeviceDisplayName; - - if (enableGuest && !guestHsUrl) { - console.warn("Cannot enable guest access: can't determine HS URL to use"); - enableGuest = false; - } - - if (enableGuest && - fragmentQueryParams.guest_user_id && - fragmentQueryParams.guest_access_token - ) { - console.log("Using guest access credentials"); - return _doSetLoggedIn({ - userId: fragmentQueryParams.guest_user_id, - accessToken: fragmentQueryParams.guest_access_token, - homeserverUrl: guestHsUrl, - identityServerUrl: guestIsUrl, - guest: true, - }, true).then(() => true); - } - const success = await _restoreFromLocalStorage({ - ignoreGuest: Boolean(opts.ignoreGuest), - }); - if (success) { - return true; - } - - if (enableGuest) { - return _registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName); - } - - // fall back to welcome screen - return false; - } catch (e) { - if (e instanceof AbortLoginAndRebuildStorage) { - // If we're aborting login because of a storage inconsistency, we don't - // need to show the general failure dialog. Instead, just go back to welcome. - return false; - } - return _handleLoadSessionFailure(e); - } -} - -/** - * Gets the user ID of the persisted session, if one exists. This does not validate - * that the user's credentials still work, just that they exist and that a user ID - * is associated with them. The session is not loaded. - * @returns {String} The persisted session's owner, if an owner exists. Null otherwise. - */ -export function getStoredSessionOwner() { - const {hsUrl, userId, accessToken} = getLocalStorageSessionVars(); - return hsUrl && userId && accessToken ? userId : null; -} - -/** - * @returns {bool} True if the stored session is for a guest user or false if it is - * for a real user. If there is no stored session, return null. - */ -export function getStoredSessionIsGuest() { - const sessVars = getLocalStorageSessionVars(); - return sessVars.hsUrl && sessVars.userId && sessVars.accessToken ? sessVars.isGuest : null; -} - -/** - * @param {Object} queryParams string->string map of the - * query-parameters extracted from the real query-string of the starting - * URI. - * - * @param {String} defaultDeviceDisplayName - * - * @returns {Promise} promise which resolves to true if we completed the token - * login, else false - */ -export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) { - if (!queryParams.loginToken) { - return Promise.resolve(false); - } - - if (!queryParams.homeserver) { - console.warn("Cannot log in with token: can't determine HS URL to use"); - return Promise.resolve(false); - } - - return sendLoginRequest( - queryParams.homeserver, - queryParams.identityServer, - "m.login.token", { - token: queryParams.loginToken, - initial_device_display_name: defaultDeviceDisplayName, - }, - ).then(function(creds) { - console.log("Logged in with token"); - return _clearStorage().then(() => { - _persistCredentialsToLocalStorage(creds); - return true; - }); - }).catch((err) => { - console.error("Failed to log in with login token: " + err + " " + - err.data); - return false; - }); -} - -export function handleInvalidStoreError(e) { - if (e.reason === Matrix.InvalidStoreError.TOGGLED_LAZY_LOADING) { - return Promise.resolve().then(() => { - const lazyLoadEnabled = e.value; - if (lazyLoadEnabled) { - const LazyLoadingResyncDialog = - sdk.getComponent("views.dialogs.LazyLoadingResyncDialog"); - return new Promise((resolve) => { - Modal.createDialog(LazyLoadingResyncDialog, { - onFinished: resolve, - }); - }); - } else { - // show warning about simultaneous use - // between LL/non-LL version on same host. - // as disabling LL when previously enabled - // is a strong indicator of this (/develop & /app) - const LazyLoadingDisabledDialog = - sdk.getComponent("views.dialogs.LazyLoadingDisabledDialog"); - return new Promise((resolve) => { - Modal.createDialog(LazyLoadingDisabledDialog, { - onFinished: resolve, - host: window.location.host, - }); - }); - } - }).then(() => { - return MatrixClientPeg.get().store.deleteAllData(); - }).then(() => { - PlatformPeg.get().reload(); - }); - } -} - -function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { - console.log(`Doing guest login on ${hsUrl}`); - - // create a temporary MatrixClient to do the login - const client = Matrix.createClient({ - baseUrl: hsUrl, - }); - - return client.registerGuest({ - body: { - initial_device_display_name: defaultDeviceDisplayName, - }, - }).then((creds) => { - console.log(`Registered as guest: ${creds.user_id}`); - return _doSetLoggedIn({ - userId: creds.user_id, - deviceId: creds.device_id, - accessToken: creds.access_token, - homeserverUrl: hsUrl, - identityServerUrl: isUrl, - guest: true, - }, true).then(() => true); - }, (err) => { - console.error("Failed to register as guest", err); - return false; - }); -} - -/** - * Retrieves information about the stored session in localstorage. The session - * may not be valid, as it is not tested for consistency here. - * @returns {Object} Information about the session - see implementation for variables. - */ -export function getLocalStorageSessionVars() { - const hsUrl = localStorage.getItem("mx_hs_url"); - const isUrl = localStorage.getItem("mx_is_url"); - const accessToken = localStorage.getItem("mx_access_token"); - const userId = localStorage.getItem("mx_user_id"); - const deviceId = localStorage.getItem("mx_device_id"); - - let isGuest; - if (localStorage.getItem("mx_is_guest") !== null) { - isGuest = localStorage.getItem("mx_is_guest") === "true"; - } else { - // legacy key name - isGuest = localStorage.getItem("matrix-is-guest") === "true"; - } - - return {hsUrl, isUrl, accessToken, userId, deviceId, isGuest}; -} - -// returns a promise which resolves to true if a session is found in -// localstorage -// -// N.B. Lifecycle.js should not maintain any further localStorage state, we -// are moving towards using SessionStore to keep track of state related -// to the current session (which is typically backed by localStorage). -// -// The plan is to gradually move the localStorage access done here into -// SessionStore to avoid bugs where the view becomes out-of-sync with -// localStorage (e.g. isGuest etc.) -async function _restoreFromLocalStorage(opts) { - const ignoreGuest = opts.ignoreGuest; - - if (!localStorage) { - return false; - } - - const {hsUrl, isUrl, accessToken, userId, deviceId, isGuest} = getLocalStorageSessionVars(); - - if (accessToken && userId && hsUrl) { - if (ignoreGuest && isGuest) { - console.log("Ignoring stored guest account: " + userId); - return false; - } - - console.log(`Restoring session for ${userId}`); - await _doSetLoggedIn({ - userId: userId, - deviceId: deviceId, - accessToken: accessToken, - homeserverUrl: hsUrl, - identityServerUrl: isUrl, - guest: isGuest, - }, false); - return true; - } else { - console.log("No previous session found."); - return false; - } -} - -async function _handleLoadSessionFailure(e) { - console.error("Unable to load session", e); - - const SessionRestoreErrorDialog = - sdk.getComponent('views.dialogs.SessionRestoreErrorDialog'); - - const modal = Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { - error: e.message, - }); - - const [success] = await modal.finished; - if (success) { - // user clicked continue. - await _clearStorage(); - return false; - } - - // try, try again - return loadSession(); -} - -/** - * Transitions to a logged-in state using the given credentials. - * - * Starts the matrix client and all other react-sdk services that - * listen for events while a session is logged in. - * - * Also stops the old MatrixClient and clears old credentials/etc out of - * storage before starting the new client. - * - * @param {MatrixClientCreds} credentials The credentials to use - * - * @returns {Promise} promise which resolves to the new MatrixClient once it has been started - */ -export function setLoggedIn(credentials) { - stopMatrixClient(); - return _doSetLoggedIn(credentials, true); -} - -/** - * Hydrates an existing session by using the credentials provided. This will - * not clear any local storage, unlike setLoggedIn(). - * - * Stops the existing Matrix client (without clearing its data) and starts a - * new one in its place. This additionally starts all other react-sdk services - * which use the new Matrix client. - * - * If the credentials belong to a different user from the session already stored, - * the old session will be cleared automatically. - * - * @param {MatrixClientCreds} credentials The credentials to use - * - * @returns {Promise} promise which resolves to the new MatrixClient once it has been started - */ -export function hydrateSession(credentials) { - const oldUserId = MatrixClientPeg.get().getUserId(); - const oldDeviceId = MatrixClientPeg.get().getDeviceId(); - - stopMatrixClient(); // unsets MatrixClientPeg.get() - localStorage.removeItem("mx_soft_logout"); - _isLoggingOut = false; - - const overwrite = credentials.userId !== oldUserId || credentials.deviceId !== oldDeviceId; - if (overwrite) { - console.warn("Clearing all data: Old session belongs to a different user/session"); - } - - return _doSetLoggedIn(credentials, overwrite); -} - -/** - * fires on_logging_in, optionally clears localstorage, persists new credentials - * to localstorage, starts the new client. - * - * @param {MatrixClientCreds} credentials - * @param {Boolean} clearStorage - * - * @returns {Promise} promise which resolves to the new MatrixClient once it has been started - */ -async function _doSetLoggedIn(credentials, clearStorage) { - credentials.guest = Boolean(credentials.guest); - - const softLogout = isSoftLogout(); - - console.log( - "setLoggedIn: mxid: " + credentials.userId + - " deviceId: " + credentials.deviceId + - " guest: " + credentials.guest + - " hs: " + credentials.homeserverUrl + - " softLogout: " + softLogout, - ); - - // This is dispatched to indicate that the user is still in the process of logging in - // because async code may take some time to resolve, breaking the assumption that - // `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms - // later than MatrixChat might assume. - // - // we fire it *synchronously* to make sure it fires before on_logged_in. - // (dis.dispatch uses `setTimeout`, which does not guarantee ordering.) - dis.dispatch({action: 'on_logging_in'}, true); - - if (clearStorage) { - await _clearStorage(); - } - - const results = await StorageManager.checkConsistency(); - // If there's an inconsistency between account data in local storage and the - // crypto store, we'll be generally confused when handling encrypted data. - // Show a modal recommending a full reset of storage. - if (results.dataInLocalStorage && results.cryptoInited && !results.dataInCryptoStore) { - const signOut = await _showStorageEvictedDialog(); - if (signOut) { - await _clearStorage(); - // This error feels a bit clunky, but we want to make sure we don't go any - // further and instead head back to sign in. - throw new AbortLoginAndRebuildStorage( - "Aborting login in progress because of storage inconsistency", - ); - } - } - - Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl); - - if (localStorage) { - try { - _persistCredentialsToLocalStorage(credentials); - - // The user registered as a PWLU (PassWord-Less User), the generated password - // is cached here such that the user can change it at a later time. - if (credentials.password) { - // Update SessionStore - dis.dispatch({ - action: 'cached_password', - cachedPassword: credentials.password, - }); - } - } catch (e) { - console.warn("Error using local storage: can't persist session!", e); - } - } else { - console.warn("No local storage available: can't persist session!"); - } - - MatrixClientPeg.replaceUsingCreds(credentials); - - dis.dispatch({ action: 'on_logged_in' }); - - await startMatrixClient(/*startSyncing=*/!softLogout); - return MatrixClientPeg.get(); -} - -function _showStorageEvictedDialog() { - const StorageEvictedDialog = sdk.getComponent('views.dialogs.StorageEvictedDialog'); - return new Promise(resolve => { - Modal.createTrackedDialog('Storage evicted', '', StorageEvictedDialog, { - onFinished: resolve, - }); - }); -} - -// Note: Babel 6 requires the `transform-builtin-extend` plugin for this to satisfy -// `instanceof`. Babel 7 supports this natively in their class handling. -class AbortLoginAndRebuildStorage extends Error { } - -function _persistCredentialsToLocalStorage(credentials) { - localStorage.setItem("mx_hs_url", credentials.homeserverUrl); - if (credentials.identityServerUrl) { - localStorage.setItem("mx_is_url", credentials.identityServerUrl); - } - localStorage.setItem("mx_user_id", credentials.userId); - localStorage.setItem("mx_access_token", credentials.accessToken); - localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest)); - - // if we didn't get a deviceId from the login, leave mx_device_id unset, - // rather than setting it to "undefined". - // - // (in this case MatrixClient doesn't bother with the crypto stuff - // - that's fine for us). - if (credentials.deviceId) { - localStorage.setItem("mx_device_id", credentials.deviceId); - } - - console.log(`Session persisted for ${credentials.userId}`); -} - -let _isLoggingOut = false; - -/** - * Logs the current session out and transitions to the logged-out state - */ -export function logout() { - if (!MatrixClientPeg.get()) return; - - if (MatrixClientPeg.get().isGuest()) { - // logout doesn't work for guest sessions - // Also we sometimes want to re-log in a guest session - // if we abort the login - onLoggedOut(); - return; - } - - _isLoggingOut = true; - MatrixClientPeg.get().logout().then(onLoggedOut, - (err) => { - // Just throwing an error here is going to be very unhelpful - // if you're trying to log out because your server's down and - // you want to log into a different server, so just forget the - // access token. It's annoying that this will leave the access - // token still valid, but we should fix this by having access - // tokens expire (and if you really think you've been compromised, - // change your password). - console.log("Failed to call logout API: token will not be invalidated"); - onLoggedOut(); - }, - ); -} - -export function softLogout() { - if (!MatrixClientPeg.get()) return; - - // Track that we've detected and trapped a soft logout. This helps prevent other - // parts of the app from starting if there's no point (ie: don't sync if we've - // been soft logged out, despite having credentials and data for a MatrixClient). - localStorage.setItem("mx_soft_logout", "true"); - - // Dev note: please keep this log line around. It can be useful for track down - // random clients stopping in the middle of the logs. - console.log("Soft logout initiated"); - _isLoggingOut = true; // to avoid repeated flags - // Ensure that we dispatch a view change **before** stopping the client so - // so that React components unmount first. This avoids React soft crashes - // that can occur when components try to use a null client. - dis.dispatch({action: 'on_client_not_viable'}); // generic version of on_logged_out - stopMatrixClient(/*unsetClient=*/false); - - // DO NOT CALL LOGOUT. A soft logout preserves data, logout does not. -} - -export function isSoftLogout() { - return localStorage.getItem("mx_soft_logout") === "true"; -} - -export function isLoggingOut() { - return _isLoggingOut; -} - -/** - * Starts the matrix client and all other react-sdk services that - * listen for events while a session is logged in. - * @param {boolean} startSyncing True (default) to actually start - * syncing the client. - */ -async function startMatrixClient(startSyncing=true) { - console.log(`Lifecycle: Starting MatrixClient`); - - // dispatch this before starting the matrix client: it's used - // to add listeners for the 'sync' event so otherwise we'd have - // a race condition (and we need to dispatch synchronously for this - // to work). - dis.dispatch({action: 'will_start_client'}, true); - - Notifier.start(); - UserActivity.sharedInstance().start(); - TypingStore.sharedInstance().reset(); // just in case - ToastStore.sharedInstance().reset(); - DMRoomMap.makeShared().start(); - IntegrationManagers.sharedInstance().startWatching(); - ActiveWidgetStore.start(); - - // Start Mjolnir even though we haven't checked the feature flag yet. Starting - // the thing just wastes CPU cycles, but should result in no actual functionality - // being exposed to the user. - Mjolnir.sharedInstance().start(); - - if (startSyncing) { - // The client might want to populate some views with events from the - // index (e.g. the FilePanel), therefore initialize the event index - // before the client. - await EventIndexPeg.init(); - await MatrixClientPeg.start(); - } else { - console.warn("Caller requested only auxiliary services be started"); - await MatrixClientPeg.assign(); - } - - // This needs to be started after crypto is set up - DeviceListener.sharedInstance().start(); - // Similarly, don't start sending presence updates until we've started - // the client - if (!SettingsStore.getValue("lowBandwidth")) { - Presence.start(); - } - - // Now that we have a MatrixClientPeg, update the Jitsi info - await Jitsi.getInstance().update(); - - // dispatch that we finished starting up to wire up any other bits - // of the matrix client that cannot be set prior to starting up. - dis.dispatch({action: 'client_started'}); - - if (isSoftLogout()) { - softLogout(); - } -} - -/* - * Stops a running client and all related services, and clears persistent - * storage. Used after a session has been logged out. - */ -export async function onLoggedOut() { - _isLoggingOut = false; - // Ensure that we dispatch a view change **before** stopping the client so - // so that React components unmount first. This avoids React soft crashes - // that can occur when components try to use a null client. - dis.dispatch({action: 'on_logged_out'}, true); - stopMatrixClient(); - await _clearStorage(); -} - -/** - * @returns {Promise} promise which resolves once the stores have been cleared - */ -async function _clearStorage() { - Analytics.disable(); - - if (window.localStorage) { - window.localStorage.clear(); - } - - if (window.sessionStorage) { - window.sessionStorage.clear(); - } - - // create a temporary client to clear out the persistent stores. - const cli = createMatrixClient({ - // we'll never make any requests, so can pass a bogus HS URL - baseUrl: "", - }); - - await EventIndexPeg.deleteEventIndex(); - await cli.clearStores(); -} - -/** - * Stop all the background processes related to the current client. - * @param {boolean} unsetClient True (default) to abandon the client - * on MatrixClientPeg after stopping. - */ -export function stopMatrixClient(unsetClient=true) { - Notifier.stop(); - UserActivity.sharedInstance().stop(); - TypingStore.sharedInstance().reset(); - Presence.stop(); - ActiveWidgetStore.stop(); - IntegrationManagers.sharedInstance().stopWatching(); - Mjolnir.sharedInstance().stop(); - DeviceListener.sharedInstance().stop(); - if (DMRoomMap.shared()) DMRoomMap.shared().stop(); - EventIndexPeg.stop(); - const cli = MatrixClientPeg.get(); - if (cli) { - cli.stopClient(); - cli.removeAllListeners(); - - if (unsetClient) { - MatrixClientPeg.unset(); - EventIndexPeg.unset(); - } - } -} diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts new file mode 100644 index 0000000000..61ded93833 --- /dev/null +++ b/src/Lifecycle.ts @@ -0,0 +1,902 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd +Copyright 2019, 2020 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 { createClient } from 'matrix-js-sdk/src/matrix'; +import { InvalidStoreError } from "matrix-js-sdk/src/errors"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes"; + +import { IMatrixClientCreds, MatrixClientPeg } from './MatrixClientPeg'; +import SecurityCustomisations from "./customisations/Security"; +import EventIndexPeg from './indexing/EventIndexPeg'; +import createMatrixClient from './utils/createMatrixClient'; +import Analytics from './Analytics'; +import Notifier from './Notifier'; +import UserActivity from './UserActivity'; +import Presence from './Presence'; +import dis from './dispatcher/dispatcher'; +import DMRoomMap from './utils/DMRoomMap'; +import Modal from './Modal'; +import ActiveWidgetStore from './stores/ActiveWidgetStore'; +import PlatformPeg from "./PlatformPeg"; +import { sendLoginRequest } from "./Login"; +import * as StorageManager from './utils/StorageManager'; +import SettingsStore from "./settings/SettingsStore"; +import TypingStore from "./stores/TypingStore"; +import ToastStore from "./stores/ToastStore"; +import { IntegrationManagers } from "./integrations/IntegrationManagers"; +import { Mjolnir } from "./mjolnir/Mjolnir"; +import DeviceListener from "./DeviceListener"; +import { Jitsi } from "./widgets/Jitsi"; +import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY } from "./BasePlatform"; +import ThreepidInviteStore from "./stores/ThreepidInviteStore"; +import CountlyAnalytics from "./CountlyAnalytics"; +import CallHandler from './CallHandler'; +import LifecycleCustomisations from "./customisations/Lifecycle"; +import ErrorDialog from "./components/views/dialogs/ErrorDialog"; +import { _t } from "./languageHandler"; +import LazyLoadingResyncDialog from "./components/views/dialogs/LazyLoadingResyncDialog"; +import LazyLoadingDisabledDialog from "./components/views/dialogs/LazyLoadingDisabledDialog"; +import SessionRestoreErrorDialog from "./components/views/dialogs/SessionRestoreErrorDialog"; +import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog"; + +const HOMESERVER_URL_KEY = "mx_hs_url"; +const ID_SERVER_URL_KEY = "mx_is_url"; + +interface ILoadSessionOpts { + enableGuest?: boolean; + guestHsUrl?: string; + guestIsUrl?: string; + ignoreGuest?: boolean; + defaultDeviceDisplayName?: string; + fragmentQueryParams?: Record; +} + +/** + * Called at startup, to attempt to build a logged-in Matrix session. It tries + * a number of things: + * + * 1. if we have a guest access token in the fragment query params, it uses + * that. + * 2. if an access token is stored in local storage (from a previous session), + * it uses that. + * 3. it attempts to auto-register as a guest user. + * + * If any of steps 1-4 are successful, it will call {_doSetLoggedIn}, which in + * turn will raise on_logged_in and will_start_client events. + * + * @param {object} [opts] + * @param {object} [opts.fragmentQueryParams]: string->string map of the + * query-parameters extracted from the #-fragment of the starting URI. + * @param {boolean} [opts.enableGuest]: set to true to enable guest access + * tokens and auto-guest registrations. + * @param {string} [opts.guestHsUrl]: homeserver URL. Only used if enableGuest + * is true; defines the HS to register against. + * @param {string} [opts.guestIsUrl]: homeserver URL. Only used if enableGuest + * is true; defines the IS to use. + * @param {bool} [opts.ignoreGuest]: If the stored session is a guest account, + * ignore it and don't load it. + * @param {string} [opts.defaultDeviceDisplayName]: Default display name to use + * when registering as a guest. + * @returns {Promise} a promise which resolves when the above process completes. + * Resolves to `true` if we ended up starting a session, or `false` if we + * failed. + */ +export async function loadSession(opts: ILoadSessionOpts = {}): Promise { + try { + let enableGuest = opts.enableGuest || false; + const guestHsUrl = opts.guestHsUrl; + const guestIsUrl = opts.guestIsUrl; + const fragmentQueryParams = opts.fragmentQueryParams || {}; + const defaultDeviceDisplayName = opts.defaultDeviceDisplayName; + + if (enableGuest && !guestHsUrl) { + console.warn("Cannot enable guest access: can't determine HS URL to use"); + enableGuest = false; + } + + if ( + enableGuest && + fragmentQueryParams.guest_user_id && + fragmentQueryParams.guest_access_token + ) { + console.log("Using guest access credentials"); + return doSetLoggedIn({ + userId: fragmentQueryParams.guest_user_id, + accessToken: fragmentQueryParams.guest_access_token, + homeserverUrl: guestHsUrl, + identityServerUrl: guestIsUrl, + guest: true, + }, true).then(() => true); + } + const success = await restoreFromLocalStorage({ + ignoreGuest: Boolean(opts.ignoreGuest), + }); + if (success) { + return true; + } + + if (enableGuest) { + return registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName); + } + + // fall back to welcome screen + return false; + } catch (e) { + if (e instanceof AbortLoginAndRebuildStorage) { + // If we're aborting login because of a storage inconsistency, we don't + // need to show the general failure dialog. Instead, just go back to welcome. + return false; + } + return handleLoadSessionFailure(e); + } +} + +/** + * Gets the user ID of the persisted session, if one exists. This does not validate + * that the user's credentials still work, just that they exist and that a user ID + * is associated with them. The session is not loaded. + * @returns {[String, bool]} The persisted session's owner and whether the stored + * session is for a guest user, if an owner exists. If there is no stored session, + * return [null, null]. + */ +export async function getStoredSessionOwner(): Promise<[string, boolean]> { + const { hsUrl, userId, hasAccessToken, isGuest } = await getStoredSessionVars(); + return hsUrl && userId && hasAccessToken ? [userId, isGuest] : [null, null]; +} + +/** + * @param {Object} queryParams string->string map of the + * query-parameters extracted from the real query-string of the starting + * URI. + * + * @param {string} defaultDeviceDisplayName + * @param {string} fragmentAfterLogin path to go to after a successful login, only used for "Try again" + * + * @returns {Promise} promise which resolves to true if we completed the token + * login, else false + */ +export function attemptTokenLogin( + queryParams: Record, + defaultDeviceDisplayName?: string, + fragmentAfterLogin?: string, +): Promise { + if (!queryParams.loginToken) { + return Promise.resolve(false); + } + + const homeserver = localStorage.getItem(SSO_HOMESERVER_URL_KEY); + const identityServer = localStorage.getItem(SSO_ID_SERVER_URL_KEY); + if (!homeserver) { + console.warn("Cannot log in with token: can't determine HS URL to use"); + Modal.createTrackedDialog("SSO", "Unknown HS", ErrorDialog, { + title: _t("We couldn't log you in"), + description: _t("We asked the browser to remember which homeserver you use to let you sign in, " + + "but unfortunately your browser has forgotten it. Go to the sign in page and try again."), + button: _t("Try again"), + }); + return Promise.resolve(false); + } + + return sendLoginRequest( + homeserver, + identityServer, + "m.login.token", { + token: queryParams.loginToken, + initial_device_display_name: defaultDeviceDisplayName, + }, + ).then(function(creds) { + console.log("Logged in with token"); + return clearStorage().then(async () => { + await persistCredentials(creds); + // remember that we just logged in + sessionStorage.setItem("mx_fresh_login", String(true)); + return true; + }); + }).catch((err) => { + Modal.createTrackedDialog("SSO", "Token Rejected", ErrorDialog, { + title: _t("We couldn't log you in"), + description: err.name === "ConnectionError" + ? _t("Your homeserver was unreachable and was not able to log you in. Please try again. " + + "If this continues, please contact your homeserver administrator.") + : _t("Your homeserver rejected your log in attempt. " + + "This could be due to things just taking too long. Please try again. " + + "If this continues, please contact your homeserver administrator."), + button: _t("Try again"), + onFinished: tryAgain => { + if (tryAgain) { + const cli = createClient({ + baseUrl: homeserver, + idBaseUrl: identityServer, + }); + const idpId = localStorage.getItem(SSO_IDP_ID_KEY) || undefined; + PlatformPeg.get().startSingleSignOn(cli, "sso", fragmentAfterLogin, idpId); + } + }, + }); + console.error("Failed to log in with login token:"); + console.error(err); + return false; + }); +} + +export function handleInvalidStoreError(e: InvalidStoreError): Promise { + if (e.reason === InvalidStoreError.TOGGLED_LAZY_LOADING) { + return Promise.resolve().then(() => { + const lazyLoadEnabled = e.value; + if (lazyLoadEnabled) { + return new Promise((resolve) => { + Modal.createDialog(LazyLoadingResyncDialog, { + onFinished: resolve, + }); + }); + } else { + // show warning about simultaneous use + // between LL/non-LL version on same host. + // as disabling LL when previously enabled + // is a strong indicator of this (/develop & /app) + return new Promise((resolve) => { + Modal.createDialog(LazyLoadingDisabledDialog, { + onFinished: resolve, + host: window.location.host, + }); + }); + } + }).then(() => { + return MatrixClientPeg.get().store.deleteAllData(); + }).then(() => { + PlatformPeg.get().reload(); + }); + } +} + +function registerAsGuest( + hsUrl: string, + isUrl: string, + defaultDeviceDisplayName: string, +): Promise { + console.log(`Doing guest login on ${hsUrl}`); + + // create a temporary MatrixClient to do the login + const client = createClient({ + baseUrl: hsUrl, + }); + + return client.registerGuest({ + body: { + initial_device_display_name: defaultDeviceDisplayName, + }, + }).then((creds) => { + console.log(`Registered as guest: ${creds.user_id}`); + return doSetLoggedIn({ + userId: creds.user_id, + deviceId: creds.device_id, + accessToken: creds.access_token, + homeserverUrl: hsUrl, + identityServerUrl: isUrl, + guest: true, + }, true).then(() => true); + }, (err) => { + console.error("Failed to register as guest", err); + return false; + }); +} + +export interface IStoredSession { + hsUrl: string; + isUrl: string; + hasAccessToken: boolean; + accessToken: string | IEncryptedPayload; + userId: string; + deviceId: string; + isGuest: boolean; +} + +/** + * Retrieves information about the stored session from the browser's storage. The session + * may not be valid, as it is not tested for consistency here. + * @returns {Object} Information about the session - see implementation for variables. + */ +export async function getStoredSessionVars(): Promise { + const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY); + const isUrl = localStorage.getItem(ID_SERVER_URL_KEY); + let accessToken; + try { + accessToken = await StorageManager.idbLoad("account", "mx_access_token"); + } catch (e) {} + if (!accessToken) { + accessToken = localStorage.getItem("mx_access_token"); + if (accessToken) { + try { + // try to migrate access token to IndexedDB if we can + await StorageManager.idbSave("account", "mx_access_token", accessToken); + localStorage.removeItem("mx_access_token"); + } catch (e) {} + } + } + // if we pre-date storing "mx_has_access_token", but we retrieved an access + // token, then we should say we have an access token + const hasAccessToken = + (localStorage.getItem("mx_has_access_token") === "true") || !!accessToken; + const userId = localStorage.getItem("mx_user_id"); + const deviceId = localStorage.getItem("mx_device_id"); + + let isGuest; + if (localStorage.getItem("mx_is_guest") !== null) { + isGuest = localStorage.getItem("mx_is_guest") === "true"; + } else { + // legacy key name + isGuest = localStorage.getItem("matrix-is-guest") === "true"; + } + + return { hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest }; +} + +// The pickle key is a string of unspecified length and format. For AES, we +// need a 256-bit Uint8Array. So we HKDF the pickle key to generate the AES +// key. The AES key should be zeroed after it is used. +async function pickleKeyToAesKey(pickleKey: string): Promise { + const pickleKeyBuffer = new Uint8Array(pickleKey.length); + for (let i = 0; i < pickleKey.length; i++) { + pickleKeyBuffer[i] = pickleKey.charCodeAt(i); + } + const hkdfKey = await window.crypto.subtle.importKey( + "raw", pickleKeyBuffer, "HKDF", false, ["deriveBits"], + ); + pickleKeyBuffer.fill(0); + return new Uint8Array(await window.crypto.subtle.deriveBits( + { + name: "HKDF", hash: "SHA-256", + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879 + salt: new Uint8Array(32), info: new Uint8Array(0), + }, + hkdfKey, + 256, + )); +} + +async function abortLogin() { + const signOut = await showStorageEvictedDialog(); + if (signOut) { + await clearStorage(); + // This error feels a bit clunky, but we want to make sure we don't go any + // further and instead head back to sign in. + throw new AbortLoginAndRebuildStorage( + "Aborting login in progress because of storage inconsistency", + ); + } +} + +// returns a promise which resolves to true if a session is found in +// localstorage +// +// N.B. Lifecycle.js should not maintain any further localStorage state, we +// are moving towards using SessionStore to keep track of state related +// to the current session (which is typically backed by localStorage). +// +// The plan is to gradually move the localStorage access done here into +// SessionStore to avoid bugs where the view becomes out-of-sync with +// localStorage (e.g. isGuest etc.) +export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promise { + const ignoreGuest = opts?.ignoreGuest; + + if (!localStorage) { + return false; + } + + const { hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest } = await getStoredSessionVars(); + + if (hasAccessToken && !accessToken) { + abortLogin(); + } + + if (accessToken && userId && hsUrl) { + if (ignoreGuest && isGuest) { + console.log("Ignoring stored guest account: " + userId); + return false; + } + + let decryptedAccessToken = accessToken; + const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId); + if (pickleKey) { + console.log("Got pickle key"); + if (typeof accessToken !== "string") { + const encrKey = await pickleKeyToAesKey(pickleKey); + decryptedAccessToken = await decryptAES(accessToken, encrKey, "access_token"); + encrKey.fill(0); + } + } else { + console.log("No pickle key available"); + } + + const freshLogin = sessionStorage.getItem("mx_fresh_login") === "true"; + sessionStorage.removeItem("mx_fresh_login"); + + console.log(`Restoring session for ${userId}`); + await doSetLoggedIn({ + userId: userId, + deviceId: deviceId, + accessToken: decryptedAccessToken as string, + homeserverUrl: hsUrl, + identityServerUrl: isUrl, + guest: isGuest, + pickleKey: pickleKey, + freshLogin: freshLogin, + }, false); + return true; + } else { + console.log("No previous session found."); + return false; + } +} + +async function handleLoadSessionFailure(e: Error): Promise { + console.error("Unable to load session", e); + + const modal = Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { + error: e.message, + }); + + const [success] = await modal.finished; + if (success) { + // user clicked continue. + await clearStorage(); + return false; + } + + // try, try again + return loadSession(); +} + +/** + * Transitions to a logged-in state using the given credentials. + * + * Starts the matrix client and all other react-sdk services that + * listen for events while a session is logged in. + * + * Also stops the old MatrixClient and clears old credentials/etc out of + * storage before starting the new client. + * + * @param {MatrixClientCreds} credentials The credentials to use + * + * @returns {Promise} promise which resolves to the new MatrixClient once it has been started + */ +export async function setLoggedIn(credentials: IMatrixClientCreds): Promise { + credentials.freshLogin = true; + stopMatrixClient(); + const pickleKey = credentials.userId && credentials.deviceId + ? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId) + : null; + + if (pickleKey) { + console.log("Created pickle key"); + } else { + console.log("Pickle key not created"); + } + + return doSetLoggedIn(Object.assign({}, credentials, { pickleKey }), true); +} + +/** + * Hydrates an existing session by using the credentials provided. This will + * not clear any local storage, unlike setLoggedIn(). + * + * Stops the existing Matrix client (without clearing its data) and starts a + * new one in its place. This additionally starts all other react-sdk services + * which use the new Matrix client. + * + * If the credentials belong to a different user from the session already stored, + * the old session will be cleared automatically. + * + * @param {MatrixClientCreds} credentials The credentials to use + * + * @returns {Promise} promise which resolves to the new MatrixClient once it has been started + */ +export function hydrateSession(credentials: IMatrixClientCreds): Promise { + const oldUserId = MatrixClientPeg.get().getUserId(); + const oldDeviceId = MatrixClientPeg.get().getDeviceId(); + + stopMatrixClient(); // unsets MatrixClientPeg.get() + localStorage.removeItem("mx_soft_logout"); + _isLoggingOut = false; + + const overwrite = credentials.userId !== oldUserId || credentials.deviceId !== oldDeviceId; + if (overwrite) { + console.warn("Clearing all data: Old session belongs to a different user/session"); + } + + return doSetLoggedIn(credentials, overwrite); +} + +/** + * fires on_logging_in, optionally clears localstorage, persists new credentials + * to localstorage, starts the new client. + * + * @param {MatrixClientCreds} credentials + * @param {Boolean} clearStorage + * + * @returns {Promise} promise which resolves to the new MatrixClient once it has been started + */ +async function doSetLoggedIn( + credentials: IMatrixClientCreds, + clearStorageEnabled: boolean, +): Promise { + credentials.guest = Boolean(credentials.guest); + + const softLogout = isSoftLogout(); + + console.log( + "setLoggedIn: mxid: " + credentials.userId + + " deviceId: " + credentials.deviceId + + " guest: " + credentials.guest + + " hs: " + credentials.homeserverUrl + + " softLogout: " + softLogout, + " freshLogin: " + credentials.freshLogin, + ); + + // This is dispatched to indicate that the user is still in the process of logging in + // because async code may take some time to resolve, breaking the assumption that + // `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms + // later than MatrixChat might assume. + // + // we fire it *synchronously* to make sure it fires before on_logged_in. + // (dis.dispatch uses `setTimeout`, which does not guarantee ordering.) + dis.dispatch({ action: 'on_logging_in' }, true); + + if (clearStorageEnabled) { + await clearStorage(); + } + + const results = await StorageManager.checkConsistency(); + // If there's an inconsistency between account data in local storage and the + // crypto store, we'll be generally confused when handling encrypted data. + // Show a modal recommending a full reset of storage. + if (results.dataInLocalStorage && results.cryptoInited && !results.dataInCryptoStore) { + await abortLogin(); + } + + Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl); + + MatrixClientPeg.replaceUsingCreds(credentials); + const client = MatrixClientPeg.get(); + + if (credentials.freshLogin && SettingsStore.getValue("feature_dehydration")) { + // If we just logged in, try to rehydrate a device instead of using a + // new device. If it succeeds, we'll get a new device ID, so make sure + // we persist that ID to localStorage + const newDeviceId = await client.rehydrateDevice(); + if (newDeviceId) { + credentials.deviceId = newDeviceId; + } + + delete credentials.freshLogin; + } + + if (localStorage) { + try { + await persistCredentials(credentials); + // make sure we don't think that it's a fresh login any more + sessionStorage.removeItem("mx_fresh_login"); + } catch (e) { + console.warn("Error using local storage: can't persist session!", e); + } + } else { + console.warn("No local storage available: can't persist session!"); + } + + dis.dispatch({ action: 'on_logged_in' }); + + await startMatrixClient(/*startSyncing=*/!softLogout); + return client; +} + +function showStorageEvictedDialog(): Promise { + return new Promise(resolve => { + Modal.createTrackedDialog('Storage evicted', '', StorageEvictedDialog, { + onFinished: resolve, + }); + }); +} + +// Note: Babel 6 requires the `transform-builtin-extend` plugin for this to satisfy +// `instanceof`. Babel 7 supports this natively in their class handling. +class AbortLoginAndRebuildStorage extends Error { } + +async function persistCredentials(credentials: IMatrixClientCreds): Promise { + localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl); + if (credentials.identityServerUrl) { + localStorage.setItem(ID_SERVER_URL_KEY, credentials.identityServerUrl); + } + localStorage.setItem("mx_user_id", credentials.userId); + localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest)); + + // store whether we expect to find an access token, to detect the case + // where IndexedDB is blown away + if (credentials.accessToken) { + localStorage.setItem("mx_has_access_token", "true"); + } else { + localStorage.deleteItem("mx_has_access_token"); + } + + if (credentials.pickleKey) { + let encryptedAccessToken; + try { + // try to encrypt the access token using the pickle key + const encrKey = await pickleKeyToAesKey(credentials.pickleKey); + encryptedAccessToken = await encryptAES(credentials.accessToken, encrKey, "access_token"); + encrKey.fill(0); + } catch (e) { + console.warn("Could not encrypt access token", e); + } + try { + // save either the encrypted access token, or the plain access + // token if we were unable to encrypt (e.g. if the browser doesn't + // have WebCrypto). + await StorageManager.idbSave( + "account", "mx_access_token", + encryptedAccessToken || credentials.accessToken, + ); + } catch (e) { + // if we couldn't save to indexedDB, fall back to localStorage. We + // store the access token unencrypted since localStorage only saves + // strings. + localStorage.setItem("mx_access_token", credentials.accessToken); + } + localStorage.setItem("mx_has_pickle_key", String(true)); + } else { + try { + await StorageManager.idbSave( + "account", "mx_access_token", credentials.accessToken, + ); + } catch (e) { + localStorage.setItem("mx_access_token", credentials.accessToken); + } + if (localStorage.getItem("mx_has_pickle_key")) { + console.error("Expected a pickle key, but none provided. Encryption may not work."); + } + } + + // if we didn't get a deviceId from the login, leave mx_device_id unset, + // rather than setting it to "undefined". + // + // (in this case MatrixClient doesn't bother with the crypto stuff + // - that's fine for us). + if (credentials.deviceId) { + localStorage.setItem("mx_device_id", credentials.deviceId); + } + + SecurityCustomisations.persistCredentials?.(credentials); + + console.log(`Session persisted for ${credentials.userId}`); +} + +let _isLoggingOut = false; + +/** + * Logs the current session out and transitions to the logged-out state + */ +export function logout(): void { + if (!MatrixClientPeg.get()) return; + if (!CountlyAnalytics.instance.disabled) { + // user has logged out, fall back to anonymous + CountlyAnalytics.instance.enable(/* anonymous = */ true); + } + + if (MatrixClientPeg.get().isGuest()) { + // logout doesn't work for guest sessions + // Also we sometimes want to re-log in a guest session if we abort the login. + // defer until next tick because it calls a synchronous dispatch and we are likely here from a dispatch. + setImmediate(() => onLoggedOut()); + return; + } + + _isLoggingOut = true; + const client = MatrixClientPeg.get(); + PlatformPeg.get().destroyPickleKey(client.getUserId(), client.getDeviceId()); + client.logout().then(onLoggedOut, + (err) => { + // Just throwing an error here is going to be very unhelpful + // if you're trying to log out because your server's down and + // you want to log into a different server, so just forget the + // access token. It's annoying that this will leave the access + // token still valid, but we should fix this by having access + // tokens expire (and if you really think you've been compromised, + // change your password). + console.log("Failed to call logout API: token will not be invalidated"); + onLoggedOut(); + }, + ); +} + +export function softLogout(): void { + if (!MatrixClientPeg.get()) return; + + // Track that we've detected and trapped a soft logout. This helps prevent other + // parts of the app from starting if there's no point (ie: don't sync if we've + // been soft logged out, despite having credentials and data for a MatrixClient). + localStorage.setItem("mx_soft_logout", "true"); + + // Dev note: please keep this log line around. It can be useful for track down + // random clients stopping in the middle of the logs. + console.log("Soft logout initiated"); + _isLoggingOut = true; // to avoid repeated flags + // Ensure that we dispatch a view change **before** stopping the client so + // so that React components unmount first. This avoids React soft crashes + // that can occur when components try to use a null client. + dis.dispatch({ action: 'on_client_not_viable' }); // generic version of on_logged_out + stopMatrixClient(/*unsetClient=*/false); + + // DO NOT CALL LOGOUT. A soft logout preserves data, logout does not. +} + +export function isSoftLogout(): boolean { + return localStorage.getItem("mx_soft_logout") === "true"; +} + +export function isLoggingOut(): boolean { + return _isLoggingOut; +} + +/** + * Starts the matrix client and all other react-sdk services that + * listen for events while a session is logged in. + * @param {boolean} startSyncing True (default) to actually start + * syncing the client. + */ +async function startMatrixClient(startSyncing = true): Promise { + console.log(`Lifecycle: Starting MatrixClient`); + + // dispatch this before starting the matrix client: it's used + // to add listeners for the 'sync' event so otherwise we'd have + // a race condition (and we need to dispatch synchronously for this + // to work). + dis.dispatch({ action: 'will_start_client' }, true); + + // reset things first just in case + TypingStore.sharedInstance().reset(); + ToastStore.sharedInstance().reset(); + + Notifier.start(); + UserActivity.sharedInstance().start(); + DMRoomMap.makeShared().start(); + IntegrationManagers.sharedInstance().startWatching(); + ActiveWidgetStore.start(); + CallHandler.sharedInstance().start(); + + // Start Mjolnir even though we haven't checked the feature flag yet. Starting + // the thing just wastes CPU cycles, but should result in no actual functionality + // being exposed to the user. + Mjolnir.sharedInstance().start(); + + if (startSyncing) { + // The client might want to populate some views with events from the + // index (e.g. the FilePanel), therefore initialize the event index + // before the client. + await EventIndexPeg.init(); + await MatrixClientPeg.start(); + } else { + console.warn("Caller requested only auxiliary services be started"); + await MatrixClientPeg.assign(); + } + + // This needs to be started after crypto is set up + DeviceListener.sharedInstance().start(); + // Similarly, don't start sending presence updates until we've started + // the client + if (!SettingsStore.getValue("lowBandwidth")) { + Presence.start(); + } + + // Now that we have a MatrixClientPeg, update the Jitsi info + await Jitsi.getInstance().start(); + + // dispatch that we finished starting up to wire up any other bits + // of the matrix client that cannot be set prior to starting up. + dis.dispatch({ action: 'client_started' }); + + if (isSoftLogout()) { + softLogout(); + } +} + +/* + * Stops a running client and all related services, and clears persistent + * storage. Used after a session has been logged out. + */ +export async function onLoggedOut(): Promise { + _isLoggingOut = false; + // Ensure that we dispatch a view change **before** stopping the client so + // so that React components unmount first. This avoids React soft crashes + // that can occur when components try to use a null client. + dis.dispatch({ action: 'on_logged_out' }, true); + stopMatrixClient(); + await clearStorage({ deleteEverything: true }); + LifecycleCustomisations.onLoggedOutAndStorageCleared?.(); +} + +/** + * @param {object} opts Options for how to clear storage. + * @returns {Promise} promise which resolves once the stores have been cleared + */ +async function clearStorage(opts?: { deleteEverything?: boolean }): Promise { + Analytics.disable(); + + if (window.localStorage) { + // try to save any 3pid invites from being obliterated + const pendingInvites = ThreepidInviteStore.instance.getWireInvites(); + + window.localStorage.clear(); + + try { + await StorageManager.idbDelete("account", "mx_access_token"); + } catch (e) {} + + // now restore those invites + if (!opts?.deleteEverything) { + pendingInvites.forEach(i => { + const roomId = i.roomId; + delete i.roomId; // delete to avoid confusing the store + ThreepidInviteStore.instance.storeInvite(roomId, i); + }); + } + } + + if (window.sessionStorage) { + window.sessionStorage.clear(); + } + + // create a temporary client to clear out the persistent stores. + const cli = createMatrixClient({ + // we'll never make any requests, so can pass a bogus HS URL + baseUrl: "", + }); + + await EventIndexPeg.deleteEventIndex(); + await cli.clearStores(); +} + +/** + * Stop all the background processes related to the current client. + * @param {boolean} unsetClient True (default) to abandon the client + * on MatrixClientPeg after stopping. + */ +export function stopMatrixClient(unsetClient = true): void { + Notifier.stop(); + CallHandler.sharedInstance().stop(); + UserActivity.sharedInstance().stop(); + TypingStore.sharedInstance().reset(); + Presence.stop(); + ActiveWidgetStore.stop(); + IntegrationManagers.sharedInstance().stopWatching(); + Mjolnir.sharedInstance().stop(); + DeviceListener.sharedInstance().stop(); + if (DMRoomMap.shared()) DMRoomMap.shared().stop(); + EventIndexPeg.stop(); + const cli = MatrixClientPeg.get(); + if (cli) { + cli.stopClient(); + cli.removeAllListeners(); + + if (unsetClient) { + MatrixClientPeg.unset(); + EventIndexPeg.unset(); + } + } +} diff --git a/src/Livestream.ts b/src/Livestream.ts new file mode 100644 index 0000000000..2389132762 --- /dev/null +++ b/src/Livestream.ts @@ -0,0 +1,55 @@ +/* +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 { ClientWidgetApi } from "matrix-widget-api"; +import { MatrixClientPeg } from "./MatrixClientPeg"; +import SdkConfig from "./SdkConfig"; +import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions"; + +export function getConfigLivestreamUrl() { + return SdkConfig.get()["audioStreamUrl"]; +} + +// Dummy rtmp URL used to signal that we want a special audio-only stream +const AUDIOSTREAM_DUMMY_URL = 'rtmp://audiostream.dummy/'; + +async function createLiveStream(roomId: string) { + const openIdToken = await MatrixClientPeg.get().getOpenIdToken(); + + const url = getConfigLivestreamUrl() + "/createStream"; + + const response = await window.fetch(url, { + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + room_id: roomId, + openid_token: openIdToken, + }), + }); + + const respBody = await response.json(); + return respBody['stream_id']; +} + +export async function startJitsiAudioLivestream(widgetMessaging: ClientWidgetApi, roomId: string) { + const streamId = await createLiveStream(roomId); + + await widgetMessaging.transport.send(ElementWidgetActions.StartLiveStream, { + rtmpStreamKey: AUDIOSTREAM_DUMMY_URL + streamId, + }); +} diff --git a/src/Login.js b/src/Login.js deleted file mode 100644 index 1590e5ac28..0000000000 --- a/src/Login.js +++ /dev/null @@ -1,187 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd -Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2020 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 Matrix from "matrix-js-sdk"; - -export default class Login { - constructor(hsUrl, isUrl, fallbackHsUrl, opts) { - this._hsUrl = hsUrl; - this._isUrl = isUrl; - this._fallbackHsUrl = fallbackHsUrl; - this._currentFlowIndex = 0; - this._flows = []; - this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName; - this._tempClient = null; // memoize - } - - getHomeserverUrl() { - return this._hsUrl; - } - - getIdentityServerUrl() { - return this._isUrl; - } - - setHomeserverUrl(hsUrl) { - this._tempClient = null; // clear memoization - this._hsUrl = hsUrl; - } - - setIdentityServerUrl(isUrl) { - this._tempClient = null; // clear memoization - this._isUrl = isUrl; - } - - /** - * Get a temporary MatrixClient, which can be used for login or register - * requests. - * @returns {MatrixClient} - */ - createTemporaryClient() { - if (this._tempClient) return this._tempClient; // use memoization - return this._tempClient = Matrix.createClient({ - baseUrl: this._hsUrl, - idBaseUrl: this._isUrl, - }); - } - - getFlows() { - const self = this; - const client = this.createTemporaryClient(); - return client.loginFlows().then(function(result) { - self._flows = result.flows; - self._currentFlowIndex = 0; - // technically the UI should display options for all flows for the - // user to then choose one, so return all the flows here. - return self._flows; - }); - } - - chooseFlow(flowIndex) { - this._currentFlowIndex = flowIndex; - } - - getCurrentFlowStep() { - // technically the flow can have multiple steps, but no one does this - // for login so we can ignore it. - const flowStep = this._flows[this._currentFlowIndex]; - return flowStep ? flowStep.type : null; - } - - loginViaPassword(username, phoneCountry, phoneNumber, pass) { - const self = this; - - const isEmail = username.indexOf("@") > 0; - - let identifier; - if (phoneCountry && phoneNumber) { - identifier = { - type: 'm.id.phone', - country: phoneCountry, - number: phoneNumber, - }; - } else if (isEmail) { - identifier = { - type: 'm.id.thirdparty', - medium: 'email', - address: username, - }; - } else { - identifier = { - type: 'm.id.user', - user: username, - }; - } - - const loginParams = { - password: pass, - identifier: identifier, - initial_device_display_name: this._defaultDeviceDisplayName, - }; - - const tryFallbackHs = (originalError) => { - return sendLoginRequest( - self._fallbackHsUrl, this._isUrl, 'm.login.password', loginParams, - ).catch((fallbackError) => { - console.log("fallback HS login failed", fallbackError); - // throw the original error - throw originalError; - }); - }; - - let originalLoginError = null; - return sendLoginRequest( - self._hsUrl, self._isUrl, 'm.login.password', loginParams, - ).catch((error) => { - originalLoginError = error; - if (error.httpStatus === 403) { - if (self._fallbackHsUrl) { - return tryFallbackHs(originalLoginError); - } - } - throw originalLoginError; - }).catch((error) => { - console.log("Login failed", error); - throw error; - }); - } -} - - -/** - * Send a login request to the given server, and format the response - * as a MatrixClientCreds - * - * @param {string} hsUrl the base url of the Homeserver used to log in. - * @param {string} isUrl the base url of the default identity server - * @param {string} loginType the type of login to do - * @param {object} loginParams the parameters for the login - * - * @returns {MatrixClientCreds} - */ -export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { - const client = Matrix.createClient({ - baseUrl: hsUrl, - idBaseUrl: isUrl, - }); - - const data = await client.login(loginType, loginParams); - - const wellknown = data.well_known; - if (wellknown) { - if (wellknown["m.homeserver"] && wellknown["m.homeserver"]["base_url"]) { - hsUrl = wellknown["m.homeserver"]["base_url"]; - console.log(`Overrode homeserver setting with ${hsUrl} from login response`); - } - if (wellknown["m.identity_server"] && wellknown["m.identity_server"]["base_url"]) { - // TODO: should we prompt here? - isUrl = wellknown["m.identity_server"]["base_url"]; - console.log(`Overrode IS setting with ${isUrl} from login response`); - } - } - - return { - homeserverUrl: hsUrl, - identityServerUrl: isUrl, - userId: data.user_id, - deviceId: data.device_id, - accessToken: data.access_token, - }; -} diff --git a/src/Login.ts b/src/Login.ts new file mode 100644 index 0000000000..a8848210be --- /dev/null +++ b/src/Login.ts @@ -0,0 +1,241 @@ +/* +Copyright 2015-2021 The Matrix.org Foundation C.I.C. +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> + +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. +*/ + +// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising +import { createClient } from "matrix-js-sdk/src/matrix"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { IMatrixClientCreds } from "./MatrixClientPeg"; +import SecurityCustomisations from "./customisations/Security"; + +interface ILoginOptions { + defaultDeviceDisplayName?: string; +} + +// TODO: Move this to JS SDK +interface IPasswordFlow { + type: "m.login.password"; +} + +export enum IdentityProviderBrand { + Gitlab = "gitlab", + Github = "github", + Apple = "apple", + Google = "google", + Facebook = "facebook", + Twitter = "twitter", +} + +export interface IIdentityProvider { + id: string; + name: string; + icon?: string; + brand?: IdentityProviderBrand | string; +} + +export interface ISSOFlow { + type: "m.login.sso" | "m.login.cas"; + // eslint-disable-next-line camelcase + identity_providers: IIdentityProvider[]; +} + +export type LoginFlow = ISSOFlow | IPasswordFlow; + +// TODO: Move this to JS SDK +/* eslint-disable camelcase */ +interface ILoginParams { + identifier?: object; + password?: string; + token?: string; + device_id?: string; + initial_device_display_name?: string; +} +/* eslint-enable camelcase */ + +export default class Login { + private hsUrl: string; + private isUrl: string; + private fallbackHsUrl: string; + // TODO: Flows need a type in JS SDK + private flows: Array; + private defaultDeviceDisplayName: string; + private tempClient: MatrixClient; + + constructor( + hsUrl: string, + isUrl: string, + fallbackHsUrl?: string, + opts?: ILoginOptions, + ) { + this.hsUrl = hsUrl; + this.isUrl = isUrl; + this.fallbackHsUrl = fallbackHsUrl; + this.flows = []; + this.defaultDeviceDisplayName = opts.defaultDeviceDisplayName; + this.tempClient = null; // memoize + } + + public getHomeserverUrl(): string { + return this.hsUrl; + } + + public getIdentityServerUrl(): string { + return this.isUrl; + } + + public setHomeserverUrl(hsUrl: string): void { + this.tempClient = null; // clear memoization + this.hsUrl = hsUrl; + } + + public setIdentityServerUrl(isUrl: string): void { + this.tempClient = null; // clear memoization + this.isUrl = isUrl; + } + + /** + * Get a temporary MatrixClient, which can be used for login or register + * requests. + * @returns {MatrixClient} + */ + public createTemporaryClient(): MatrixClient { + if (this.tempClient) return this.tempClient; // use memoization + return this.tempClient = createClient({ + baseUrl: this.hsUrl, + idBaseUrl: this.isUrl, + }); + } + + public async getFlows(): Promise> { + const client = this.createTemporaryClient(); + const { flows } = await client.loginFlows(); + this.flows = flows; + return this.flows; + } + + public loginViaPassword( + username: string, + phoneCountry: string, + phoneNumber: string, + password: string, + ): Promise { + const isEmail = username.indexOf("@") > 0; + + let identifier; + if (phoneCountry && phoneNumber) { + identifier = { + type: 'm.id.phone', + country: phoneCountry, + phone: phoneNumber, + // XXX: Synapse historically wanted `number` and not `phone` + number: phoneNumber, + }; + } else if (isEmail) { + identifier = { + type: 'm.id.thirdparty', + medium: 'email', + address: username, + }; + } else { + identifier = { + type: 'm.id.user', + user: username, + }; + } + + const loginParams = { + password, + identifier, + initial_device_display_name: this.defaultDeviceDisplayName, + }; + + const tryFallbackHs = (originalError) => { + return sendLoginRequest( + this.fallbackHsUrl, this.isUrl, 'm.login.password', loginParams, + ).catch((fallbackError) => { + console.log("fallback HS login failed", fallbackError); + // throw the original error + throw originalError; + }); + }; + + let originalLoginError = null; + return sendLoginRequest( + this.hsUrl, this.isUrl, 'm.login.password', loginParams, + ).catch((error) => { + originalLoginError = error; + if (error.httpStatus === 403) { + if (this.fallbackHsUrl) { + return tryFallbackHs(originalLoginError); + } + } + throw originalLoginError; + }).catch((error) => { + console.log("Login failed", error); + throw error; + }); + } +} + +/** + * Send a login request to the given server, and format the response + * as a MatrixClientCreds + * + * @param {string} hsUrl the base url of the Homeserver used to log in. + * @param {string} isUrl the base url of the default identity server + * @param {string} loginType the type of login to do + * @param {ILoginParams} loginParams the parameters for the login + * + * @returns {MatrixClientCreds} + */ +export async function sendLoginRequest( + hsUrl: string, + isUrl: string, + loginType: string, + loginParams: ILoginParams, +): Promise { + const client = createClient({ + baseUrl: hsUrl, + idBaseUrl: isUrl, + }); + + const data = await client.login(loginType, loginParams); + + const wellknown = data.well_known; + if (wellknown) { + if (wellknown["m.homeserver"] && wellknown["m.homeserver"]["base_url"]) { + hsUrl = wellknown["m.homeserver"]["base_url"]; + console.log(`Overrode homeserver setting with ${hsUrl} from login response`); + } + if (wellknown["m.identity_server"] && wellknown["m.identity_server"]["base_url"]) { + // TODO: should we prompt here? + isUrl = wellknown["m.identity_server"]["base_url"]; + console.log(`Overrode IS setting with ${isUrl} from login response`); + } + } + + const creds: IMatrixClientCreds = { + homeserverUrl: hsUrl, + identityServerUrl: isUrl, + userId: data.user_id, + deviceId: data.device_id, + accessToken: data.access_token, + }; + + SecurityCustomisations.examineLoginResponse?.(data, creds); + + return creds; +} diff --git a/src/Markdown.js b/src/Markdown.js deleted file mode 100644 index fb1f8bf0ea..0000000000 --- a/src/Markdown.js +++ /dev/null @@ -1,203 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import commonmark from 'commonmark'; -import escape from 'lodash/escape'; - -const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; - -// These types of node are definitely text -const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document']; - -function is_allowed_html_tag(node) { - // Regex won't work for tags with attrs, but we only - // allow anyway. - const matches = /^<\/?(.*)>$/.exec(node.literal); - if (matches && matches.length == 2) { - const tag = matches[1]; - return ALLOWED_HTML_TAGS.indexOf(tag) > -1; - } - return false; -} - -function html_if_tag_allowed(node) { - if (is_allowed_html_tag(node)) { - this.lit(node.literal); - return; - } else { - this.lit(escape(node.literal)); - } -} - -/* - * Returns true if the parse output containing the node - * comprises multiple block level elements (ie. lines), - * or false if it is only a single line. - */ -function is_multi_line(node) { - let par = node; - while (par.parent) { - par = par.parent; - } - return par.firstChild != par.lastChild; -} - -/** - * Class that wraps commonmark, adding the ability to see whether - * a given message actually uses any markdown syntax or whether - * it's plain text. - */ -export default class Markdown { - constructor(input) { - this.input = input; - - const parser = new commonmark.Parser(); - this.parsed = parser.parse(this.input); - } - - isPlainText() { - const walker = this.parsed.walker(); - - let ev; - while ( (ev = walker.next()) ) { - const node = ev.node; - if (TEXT_NODES.indexOf(node.type) > -1) { - // definitely text - continue; - } else if (node.type == 'html_inline' || node.type == 'html_block') { - // if it's an allowed html tag, we need to render it and therefore - // we will need to use HTML. If it's not allowed, it's not HTML since - // we'll just be treating it as text. - if (is_allowed_html_tag(node)) { - return false; - } - } else { - return false; - } - } - return true; - } - - toHTML({ externalLinks = false } = {}) { - const renderer = new commonmark.HtmlRenderer({ - safe: false, - - // Set soft breaks to hard HTML breaks: commonmark - // puts softbreaks in for multiple lines in a blockquote, - // so if these are just newline characters then the - // block quote ends up all on one line - // (https://github.com/vector-im/riot-web/issues/3154) - softbreak: '
', - }); - - // Trying to strip out the wrapping

causes a lot more complication - // than it's worth, i think. For instance, this code will go and strip - // out any

tag (no matter where it is in the tree) which doesn't - // contain \n's. - // On the flip side,

s are quite opionated and restricted on where - // you can nest them. - // - // Let's try sending with

s anyway for now, though. - - const real_paragraph = renderer.paragraph; - - renderer.paragraph = function(node, entering) { - // If there is only one top level node, just return the - // bare text: it's a single line of text and so should be - // 'inline', rather than unnecessarily wrapped in its own - // p tag. If, however, we have multiple nodes, each gets - // its own p tag to keep them as separate paragraphs. - if (is_multi_line(node)) { - real_paragraph.call(this, node, entering); - } - }; - - renderer.link = function(node, entering) { - const attrs = this.attrs(node); - if (entering) { - attrs.push(['href', this.esc(node.destination)]); - if (node.title) { - attrs.push(['title', this.esc(node.title)]); - } - // Modified link behaviour to treat them all as external and - // thus opening in a new tab. - if (externalLinks) { - attrs.push(['target', '_blank']); - attrs.push(['rel', 'noreferrer noopener']); - } - this.tag('a', attrs); - } else { - this.tag('/a'); - } - }; - - renderer.html_inline = html_if_tag_allowed; - - renderer.html_block = function(node) { -/* - // as with `paragraph`, we only insert line breaks - // if there are multiple lines in the markdown. - const isMultiLine = is_multi_line(node); - if (isMultiLine) this.cr(); -*/ - html_if_tag_allowed.call(this, node); -/* - if (isMultiLine) this.cr(); -*/ - }; - - return renderer.render(this.parsed); - } - - /* - * Render the markdown message to plain text. That is, essentially - * just remove any backslashes escaping what would otherwise be - * markdown syntax - * (to fix https://github.com/vector-im/riot-web/issues/2870). - * - * N.B. this does **NOT** render arbitrary MD to plain text - only MD - * which has no formatting. Otherwise it emits HTML(!). - */ - toPlaintext() { - const renderer = new commonmark.HtmlRenderer({safe: false}); - const real_paragraph = renderer.paragraph; - - // The default `out` function only sends the input through an XML - // escaping function, which causes messages to be entity encoded, - // which we don't want in this case. - renderer.out = function(s) { - // The `lit` function adds a string literal to the output buffer. - this.lit(s); - }; - - renderer.paragraph = function(node, entering) { - // as with toHTML, only append lines to paragraphs if there are - // multiple paragraphs - if (is_multi_line(node)) { - if (!entering && node.next) { - this.lit('\n\n'); - } - } - }; - - renderer.html_block = function(node) { - this.lit(node.literal); - if (is_multi_line(node) && node.next) this.lit('\n\n'); - }; - - return renderer.render(this.parsed); - } -} diff --git a/src/Markdown.ts b/src/Markdown.ts new file mode 100644 index 0000000000..96169d4011 --- /dev/null +++ b/src/Markdown.ts @@ -0,0 +1,210 @@ +/* +Copyright 2016 OpenMarket Ltd +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 * as commonmark from 'commonmark'; +import { escape } from "lodash"; + +const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; + +// These types of node are definitely text +const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document']; + +// As far as @types/commonmark is concerned, these are not public, so add them +interface CommonmarkHtmlRendererInternal extends commonmark.HtmlRenderer { + paragraph: (node: commonmark.Node, entering: boolean) => void; + link: (node: commonmark.Node, entering: boolean) => void; + html_inline: (node: commonmark.Node) => void; // eslint-disable-line camelcase + html_block: (node: commonmark.Node) => void; // eslint-disable-line camelcase +} + +function isAllowedHtmlTag(node: commonmark.Node): boolean { + if (node.literal != null && + node.literal.match('^<((div|span) data-mx-maths="[^"]*"|/(div|span))>$') != null) { + return true; + } + + // Regex won't work for tags with attrs, but we only + // allow anyway. + const matches = /^<\/?(.*)>$/.exec(node.literal); + if (matches && matches.length == 2) { + const tag = matches[1]; + return ALLOWED_HTML_TAGS.indexOf(tag) > -1; + } + + return false; +} + +/* + * Returns true if the parse output containing the node + * comprises multiple block level elements (ie. lines), + * or false if it is only a single line. + */ +function isMultiLine(node: commonmark.Node): boolean { + let par = node; + while (par.parent) { + par = par.parent; + } + return par.firstChild != par.lastChild; +} + +/** + * Class that wraps commonmark, adding the ability to see whether + * a given message actually uses any markdown syntax or whether + * it's plain text. + */ +export default class Markdown { + private input: string; + private parsed: commonmark.Node; + + constructor(input) { + this.input = input; + + const parser = new commonmark.Parser(); + this.parsed = parser.parse(this.input); + } + + isPlainText(): boolean { + const walker = this.parsed.walker(); + + let ev; + while ( (ev = walker.next()) ) { + const node = ev.node; + if (TEXT_NODES.indexOf(node.type) > -1) { + // definitely text + continue; + } else if (node.type == 'html_inline' || node.type == 'html_block') { + // if it's an allowed html tag, we need to render it and therefore + // we will need to use HTML. If it's not allowed, it's not HTML since + // we'll just be treating it as text. + if (isAllowedHtmlTag(node)) { + return false; + } + } else { + return false; + } + } + return true; + } + + toHTML({ externalLinks = false } = {}): string { + const renderer = new commonmark.HtmlRenderer({ + safe: false, + + // Set soft breaks to hard HTML breaks: commonmark + // puts softbreaks in for multiple lines in a blockquote, + // so if these are just newline characters then the + // block quote ends up all on one line + // (https://github.com/vector-im/element-web/issues/3154) + softbreak: '
', + }) as CommonmarkHtmlRendererInternal; + + // Trying to strip out the wrapping

causes a lot more complication + // than it's worth, i think. For instance, this code will go and strip + // out any

tag (no matter where it is in the tree) which doesn't + // contain \n's. + // On the flip side,

s are quite opionated and restricted on where + // you can nest them. + // + // Let's try sending with

s anyway for now, though. + + const realParagraph = renderer.paragraph; + + renderer.paragraph = function(node: commonmark.Node, entering: boolean) { + // If there is only one top level node, just return the + // bare text: it's a single line of text and so should be + // 'inline', rather than unnecessarily wrapped in its own + // p tag. If, however, we have multiple nodes, each gets + // its own p tag to keep them as separate paragraphs. + if (isMultiLine(node)) { + realParagraph.call(this, node, entering); + } + }; + + renderer.link = function(node, entering) { + const attrs = this.attrs(node); + if (entering) { + attrs.push(['href', this.esc(node.destination)]); + if (node.title) { + attrs.push(['title', this.esc(node.title)]); + } + // Modified link behaviour to treat them all as external and + // thus opening in a new tab. + if (externalLinks) { + attrs.push(['target', '_blank']); + attrs.push(['rel', 'noreferrer noopener']); + } + this.tag('a', attrs); + } else { + this.tag('/a'); + } + }; + + renderer.html_inline = function(node: commonmark.Node) { + if (isAllowedHtmlTag(node)) { + this.lit(node.literal); + return; + } else { + this.lit(escape(node.literal)); + } + }; + + renderer.html_block = function(node: commonmark.Node) { + /* + // as with `paragraph`, we only insert line breaks + // if there are multiple lines in the markdown. + const isMultiLine = is_multi_line(node); + if (isMultiLine) this.cr(); + */ + renderer.html_inline(node); + /* + if (isMultiLine) this.cr(); + */ + }; + + return renderer.render(this.parsed); + } + + /* + * Render the markdown message to plain text. That is, essentially + * just remove any backslashes escaping what would otherwise be + * markdown syntax + * (to fix https://github.com/vector-im/element-web/issues/2870). + * + * N.B. this does **NOT** render arbitrary MD to plain text - only MD + * which has no formatting. Otherwise it emits HTML(!). + */ + toPlaintext(): string { + const renderer = new commonmark.HtmlRenderer({ safe: false }) as CommonmarkHtmlRendererInternal; + + renderer.paragraph = function(node: commonmark.Node, entering: boolean) { + // as with toHTML, only append lines to paragraphs if there are + // multiple paragraphs + if (isMultiLine(node)) { + if (!entering && node.next) { + this.lit('\n\n'); + } + } + }; + + renderer.html_block = function(node: commonmark.Node) { + this.lit(node.literal); + if (isMultiLine(node) && node.next) this.lit('\n\n'); + }; + + return renderer.render(this.parsed); + } +} diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js deleted file mode 100644 index 21f05b9759..0000000000 --- a/src/MatrixClientPeg.js +++ /dev/null @@ -1,260 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd. -Copyright 2017, 2018, 2019 New Vector Ltd -Copyright 2019, 2020 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 {MatrixClient, MemoryStore} from 'matrix-js-sdk'; - -import * as utils from 'matrix-js-sdk/src/utils'; -import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline'; -import {EventTimelineSet} from 'matrix-js-sdk/src/models/event-timeline-set'; -import * as sdk from './index'; -import createMatrixClient from './utils/createMatrixClient'; -import SettingsStore from './settings/SettingsStore'; -import MatrixActionCreators from './actions/MatrixActionCreators'; -import Modal from './Modal'; -import {verificationMethods} from 'matrix-js-sdk/src/crypto'; -import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; -import * as StorageManager from './utils/StorageManager'; -import IdentityAuthClient from './IdentityAuthClient'; -import { crossSigningCallbacks } from './CrossSigningManager'; -import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; - -interface MatrixClientCreds { - homeserverUrl: string, - identityServerUrl: string, - userId: string, - deviceId: string, - accessToken: string, - guest: boolean, -} - -/** - * Wrapper object for handling the js-sdk Matrix Client object in the react-sdk - * Handles the creation/initialisation of client objects. - * This module provides a singleton instance of this class so the 'current' - * Matrix Client object is available easily. - */ -class _MatrixClientPeg { - constructor() { - this.matrixClient = null; - this._justRegisteredUserId = null; - - // These are the default options used when when the - // client is started in 'start'. These can be altered - // at any time up to after the 'will_start_client' - // event is finished processing. - this.opts = { - initialSyncLimit: 20, - }; - // the credentials used to init the current client object. - // used if we tear it down & recreate it with a different store - this._currentClientCreds = null; - } - - /** - * Sets the script href passed to the IndexedDB web worker - * If set, a separate web worker will be started to run the IndexedDB - * queries on. - * - * @param {string} script href to the script to be passed to the web worker - */ - setIndexedDbWorkerScript(script) { - createMatrixClient.indexedDbWorkerScript = script; - } - - get(): MatrixClient { - return this.matrixClient; - } - - unset() { - this.matrixClient = null; - - MatrixActionCreators.stop(); - } - - /** - * If we've registered a user ID we set this to the ID of the - * user we've just registered. If they then go & log in, we - * can send them to the welcome user (obviously this doesn't - * guarentee they'll get a chat with the welcome user). - * - * @param {string} uid The user ID of the user we've just registered - */ - setJustRegisteredUserId(uid) { - this._justRegisteredUserId = uid; - } - - /** - * Returns true if the current user has just been registered by this - * client as determined by setJustRegisteredUserId() - * - * @returns {bool} True if user has just been registered - */ - currentUserIsJustRegistered() { - return ( - this.matrixClient && - this.matrixClient.credentials.userId === this._justRegisteredUserId - ); - } - - /* - * Replace this MatrixClientPeg's client with a client instance that has - * homeserver / identity server URLs and active credentials - */ - replaceUsingCreds(creds: MatrixClientCreds) { - this._currentClientCreds = creds; - this._createClient(creds); - } - - async assign() { - for (const dbType of ['indexeddb', 'memory']) { - try { - const promise = this.matrixClient.store.startup(); - console.log("MatrixClientPeg: waiting for MatrixClient store to initialise"); - await promise; - break; - } catch (err) { - if (dbType === 'indexeddb') { - console.error('Error starting matrixclient store - falling back to memory store', err); - this.matrixClient.store = new MemoryStore({ - localStorage: global.localStorage, - }); - } else { - console.error('Failed to start memory store!', err); - throw err; - } - } - } - - StorageManager.trackStores(this.matrixClient); - - // try to initialise e2e on the new client - try { - // check that we have a version of the js-sdk which includes initCrypto - if (!SettingsStore.getValue("lowBandwidth") && this.matrixClient.initCrypto) { - await this.matrixClient.initCrypto(); - this.matrixClient.setCryptoTrustCrossSignedDevices( - !SettingsStore.getValue('e2ee.manuallyVerifyAllSessions'), - ); - StorageManager.setCryptoInitialised(true); - } - } catch (e) { - if (e && e.name === 'InvalidCryptoStoreError') { - // The js-sdk found a crypto DB too new for it to use - const CryptoStoreTooNewDialog = - sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog"); - Modal.createDialog(CryptoStoreTooNewDialog, { - host: window.location.host, - }); - } - // this can happen for a number of reasons, the most likely being - // that the olm library was missing. It's not fatal. - console.warn("Unable to initialise e2e", e); - } - - const opts = utils.deepCopy(this.opts); - // the react sdk doesn't work without this, so don't allow - opts.pendingEventOrdering = "detached"; - opts.lazyLoadMembers = true; - - // Connect the matrix client to the dispatcher and setting handlers - MatrixActionCreators.start(this.matrixClient); - MatrixClientBackedSettingsHandler.matrixClient = this.matrixClient; - - return opts; - } - - async start() { - const opts = await this.assign(); - - console.log(`MatrixClientPeg: really starting MatrixClient`); - await this.get().startClient(opts); - console.log(`MatrixClientPeg: MatrixClient started`); - } - - getCredentials(): MatrixClientCreds { - return { - homeserverUrl: this.matrixClient.baseUrl, - identityServerUrl: this.matrixClient.idBaseUrl, - userId: this.matrixClient.credentials.userId, - deviceId: this.matrixClient.getDeviceId(), - accessToken: this.matrixClient.getAccessToken(), - guest: this.matrixClient.isGuest(), - }; - } - - /* - * Return the server name of the user's homeserver - * Throws an error if unable to deduce the homeserver name - * (eg. if the user is not logged in) - */ - getHomeserverName() { - const matches = /^@.+:(.+)$/.exec(this.matrixClient.credentials.userId); - if (matches === null || matches.length < 1) { - throw new Error("Failed to derive homeserver name from user ID!"); - } - return matches[1]; - } - - _createClient(creds: MatrixClientCreds) { - const opts = { - baseUrl: creds.homeserverUrl, - idBaseUrl: creds.identityServerUrl, - accessToken: creds.accessToken, - userId: creds.userId, - deviceId: creds.deviceId, - timelineSupport: true, - forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false), - fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), - verificationMethods: [ - verificationMethods.SAS, - SHOW_QR_CODE_METHOD, - verificationMethods.RECIPROCATE_QR_CODE, - ], - unstableClientRelationAggregation: true, - identityServer: new IdentityAuthClient(), - }; - - opts.cryptoCallbacks = {}; - // These are always installed regardless of the labs flag so that - // cross-signing features can toggle on without reloading and also be - // accessed immediately after login. - Object.assign(opts.cryptoCallbacks, crossSigningCallbacks); - - this.matrixClient = createMatrixClient(opts); - - // we're going to add eventlisteners for each matrix event tile, so the - // potential number of event listeners is quite high. - this.matrixClient.setMaxListeners(500); - - this.matrixClient.setGuest(Boolean(creds.guest)); - - const notifTimelineSet = new EventTimelineSet(null, { - timelineSupport: true, - }); - // XXX: what is our initial pagination token?! it somehow needs to be synchronised with /sync. - notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS); - this.matrixClient.setNotifTimelineSet(notifTimelineSet); - } -} - -if (!global.mxMatrixClientPeg) { - global.mxMatrixClientPeg = new _MatrixClientPeg(); -} - -export const MatrixClientPeg = global.mxMatrixClientPeg; diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts new file mode 100644 index 0000000000..e9364b1b47 --- /dev/null +++ b/src/MatrixClientPeg.ts @@ -0,0 +1,306 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd. +Copyright 2017, 2018, 2019 New Vector Ltd +Copyright 2019, 2020 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 { ICreateClientOpts, PendingEventOrdering } from 'matrix-js-sdk/src/matrix'; +import { IStartClientOpts, MatrixClient } from 'matrix-js-sdk/src/client'; +import { MemoryStore } from 'matrix-js-sdk/src/store/memory'; +import * as utils from 'matrix-js-sdk/src/utils'; +import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; +import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set'; +import * as sdk from './index'; +import createMatrixClient from './utils/createMatrixClient'; +import SettingsStore from './settings/SettingsStore'; +import MatrixActionCreators from './actions/MatrixActionCreators'; +import Modal from './Modal'; +import { verificationMethods } from 'matrix-js-sdk/src/crypto'; +import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; +import * as StorageManager from './utils/StorageManager'; +import IdentityAuthClient from './IdentityAuthClient'; +import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager'; +import { SHOW_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode"; +import SecurityCustomisations from "./customisations/Security"; + +export interface IMatrixClientCreds { + homeserverUrl: string; + identityServerUrl: string; + userId: string; + deviceId?: string; + accessToken: string; + guest?: boolean; + pickleKey?: string; + freshLogin?: boolean; +} + +export interface IMatrixClientPeg { + opts: IStartClientOpts; + + /** + * Return the server name of the user's homeserver + * Throws an error if unable to deduce the homeserver name + * (eg. if the user is not logged in) + * + * @returns {string} The homeserver name, if present. + */ + getHomeserverName(): string; + + get(): MatrixClient; + unset(): void; + assign(): Promise; + start(): Promise; + + getCredentials(): IMatrixClientCreds; + + /** + * If we've registered a user ID we set this to the ID of the + * user we've just registered. If they then go & log in, we + * can send them to the welcome user (obviously this doesn't + * guarentee they'll get a chat with the welcome user). + * + * @param {string} uid The user ID of the user we've just registered + */ + setJustRegisteredUserId(uid: string): void; + + /** + * Returns true if the current user has just been registered by this + * client as determined by setJustRegisteredUserId() + * + * @returns {bool} True if user has just been registered + */ + currentUserIsJustRegistered(): boolean; + + /** + * If the current user has been registered by this device then this + * returns a boolean of whether it was within the last N hours given. + */ + userRegisteredWithinLastHours(hours: number): boolean; + + /** + * Replace this MatrixClientPeg's client with a client instance that has + * homeserver / identity server URLs and active credentials + * + * @param {IMatrixClientCreds} creds The new credentials to use. + */ + replaceUsingCreds(creds: IMatrixClientCreds): void; +} + +/** + * Wrapper object for handling the js-sdk Matrix Client object in the react-sdk + * Handles the creation/initialisation of client objects. + * This module provides a singleton instance of this class so the 'current' + * Matrix Client object is available easily. + */ +class _MatrixClientPeg implements IMatrixClientPeg { + // These are the default options used when when the + // client is started in 'start'. These can be altered + // at any time up to after the 'will_start_client' + // event is finished processing. + public opts: IStartClientOpts = { + initialSyncLimit: 20, + }; + + private matrixClient: MatrixClient = null; + private justRegisteredUserId: string; + + // the credentials used to init the current client object. + // used if we tear it down & recreate it with a different store + private currentClientCreds: IMatrixClientCreds; + + constructor() { + } + + public get(): MatrixClient { + return this.matrixClient; + } + + public unset(): void { + this.matrixClient = null; + + MatrixActionCreators.stop(); + } + + public setJustRegisteredUserId(uid: string): void { + this.justRegisteredUserId = uid; + if (uid) { + window.localStorage.setItem("mx_registration_time", String(new Date().getTime())); + } + } + + public currentUserIsJustRegistered(): boolean { + return ( + this.matrixClient && + this.matrixClient.credentials.userId === this.justRegisteredUserId + ); + } + + public userRegisteredWithinLastHours(hours: number): boolean { + try { + const date = new Date(window.localStorage.getItem("mx_registration_time")); + return ((new Date().getTime() - date.getTime()) / 36e5) <= hours; + } catch (e) { + return false; + } + } + + public replaceUsingCreds(creds: IMatrixClientCreds): void { + this.currentClientCreds = creds; + this.createClient(creds); + } + + public async assign(): Promise { + for (const dbType of ['indexeddb', 'memory']) { + try { + const promise = this.matrixClient.store.startup(); + console.log("MatrixClientPeg: waiting for MatrixClient store to initialise"); + await promise; + break; + } catch (err) { + if (dbType === 'indexeddb') { + console.error('Error starting matrixclient store - falling back to memory store', err); + this.matrixClient.store = new MemoryStore({ + localStorage: localStorage, + }); + } else { + console.error('Failed to start memory store!', err); + throw err; + } + } + } + + StorageManager.trackStores(this.matrixClient); + + // try to initialise e2e on the new client + try { + // check that we have a version of the js-sdk which includes initCrypto + if (!SettingsStore.getValue("lowBandwidth") && this.matrixClient.initCrypto) { + await this.matrixClient.initCrypto(); + this.matrixClient.setCryptoTrustCrossSignedDevices( + !SettingsStore.getValue('e2ee.manuallyVerifyAllSessions'), + ); + await tryToUnlockSecretStorageWithDehydrationKey(this.matrixClient); + StorageManager.setCryptoInitialised(true); + } + } catch (e) { + if (e && e.name === 'InvalidCryptoStoreError') { + // The js-sdk found a crypto DB too new for it to use + // FIXME: Using an import will result in test failures + const CryptoStoreTooNewDialog = + sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog"); + Modal.createDialog(CryptoStoreTooNewDialog); + } + // this can happen for a number of reasons, the most likely being + // that the olm library was missing. It's not fatal. + console.warn("Unable to initialise e2e", e); + } + + const opts = utils.deepCopy(this.opts); + // the react sdk doesn't work without this, so don't allow + opts.pendingEventOrdering = PendingEventOrdering.Detached; + opts.lazyLoadMembers = true; + opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours + + // Connect the matrix client to the dispatcher and setting handlers + MatrixActionCreators.start(this.matrixClient); + MatrixClientBackedSettingsHandler.matrixClient = this.matrixClient; + + return opts; + } + + public async start(): Promise { + const opts = await this.assign(); + + console.log(`MatrixClientPeg: really starting MatrixClient`); + await this.get().startClient(opts); + console.log(`MatrixClientPeg: MatrixClient started`); + } + + public getCredentials(): IMatrixClientCreds { + return { + homeserverUrl: this.matrixClient.baseUrl, + identityServerUrl: this.matrixClient.idBaseUrl, + userId: this.matrixClient.credentials.userId, + deviceId: this.matrixClient.getDeviceId(), + accessToken: this.matrixClient.getAccessToken(), + guest: this.matrixClient.isGuest(), + }; + } + + public getHomeserverName(): string { + const matches = /^@[^:]+:(.+)$/.exec(this.matrixClient.credentials.userId); + if (matches === null || matches.length < 1) { + throw new Error("Failed to derive homeserver name from user ID!"); + } + return matches[1]; + } + + private createClient(creds: IMatrixClientCreds): void { + const opts: ICreateClientOpts = { + baseUrl: creds.homeserverUrl, + idBaseUrl: creds.identityServerUrl, + accessToken: creds.accessToken, + userId: creds.userId, + deviceId: creds.deviceId, + pickleKey: creds.pickleKey, + timelineSupport: true, + forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer'), + fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), + // Gather up to 20 ICE candidates when a call arrives: this should be more than we'd + // ever normally need, so effectively this should make all the gathering happen when + // the call arrives. + iceCandidatePoolSize: 20, + verificationMethods: [ + verificationMethods.SAS, + SHOW_QR_CODE_METHOD, + verificationMethods.RECIPROCATE_QR_CODE, + ], + unstableClientRelationAggregation: true, + identityServer: new IdentityAuthClient(), + cryptoCallbacks: {}, + }; + + // These are always installed regardless of the labs flag so that + // cross-signing features can toggle on without reloading and also be + // accessed immediately after login. + Object.assign(opts.cryptoCallbacks, crossSigningCallbacks); + if (SecurityCustomisations.getDehydrationKey) { + opts.cryptoCallbacks.getDehydrationKey = + SecurityCustomisations.getDehydrationKey; + } + + this.matrixClient = createMatrixClient(opts); + + // we're going to add eventlisteners for each matrix event tile, so the + // potential number of event listeners is quite high. + this.matrixClient.setMaxListeners(500); + + this.matrixClient.setGuest(Boolean(creds.guest)); + + const notifTimelineSet = new EventTimelineSet(null, { + timelineSupport: true, + }); + // XXX: what is our initial pagination token?! it somehow needs to be synchronised with /sync. + notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS); + this.matrixClient.setNotifTimelineSet(notifTimelineSet); + } +} + +if (!window.mxMatrixClientPeg) { + window.mxMatrixClientPeg = new _MatrixClientPeg(); +} + +export const MatrixClientPeg = window.mxMatrixClientPeg; diff --git a/src/MediaDeviceHandler.ts b/src/MediaDeviceHandler.ts new file mode 100644 index 0000000000..073f24523d --- /dev/null +++ b/src/MediaDeviceHandler.ts @@ -0,0 +1,125 @@ +/* +Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2021 Šimon Brandner + +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 SettingsStore from "./settings/SettingsStore"; +import { SettingLevel } from "./settings/SettingLevel"; +import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix"; +import EventEmitter from 'events'; + +// XXX: MediaDeviceKind is a union type, so we make our own enum +export enum MediaDeviceKindEnum { + AudioOutput = "audiooutput", + AudioInput = "audioinput", + VideoInput = "videoinput", +} + +export type IMediaDevices = Record>; + +export enum MediaDeviceHandlerEvent { + AudioOutputChanged = "audio_output_changed", +} + +export default class MediaDeviceHandler extends EventEmitter { + private static internalInstance; + + public static get instance(): MediaDeviceHandler { + if (!MediaDeviceHandler.internalInstance) { + MediaDeviceHandler.internalInstance = new MediaDeviceHandler(); + } + return MediaDeviceHandler.internalInstance; + } + + public static async hasAnyLabeledDevices(): Promise { + const devices = await navigator.mediaDevices.enumerateDevices(); + return devices.some(d => Boolean(d.label)); + } + + public static async getDevices(): Promise { + // Only needed for Electron atm, though should work in modern browsers + // once permission has been granted to the webapp + + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + const output = { + [MediaDeviceKindEnum.AudioOutput]: [], + [MediaDeviceKindEnum.AudioInput]: [], + [MediaDeviceKindEnum.VideoInput]: [], + }; + + devices.forEach((device) => output[device.kind].push(device)); + return output; + } catch (error) { + console.warn('Unable to refresh WebRTC Devices: ', error); + } + } + + /** + * Retrieves devices from the SettingsStore and tells the js-sdk to use them + */ + public static loadDevices(): void { + const audioDeviceId = SettingsStore.getValue("webrtc_audioinput"); + const videoDeviceId = SettingsStore.getValue("webrtc_videoinput"); + + setMatrixCallAudioInput(audioDeviceId); + setMatrixCallVideoInput(videoDeviceId); + } + + public setAudioOutput(deviceId: string): void { + SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId); + this.emit(MediaDeviceHandlerEvent.AudioOutputChanged, deviceId); + } + + /** + * This will not change the device that a potential call uses. The call will + * need to be ended and started again for this change to take effect + * @param {string} deviceId + */ + public setAudioInput(deviceId: string): void { + SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId); + setMatrixCallAudioInput(deviceId); + } + + /** + * This will not change the device that a potential call uses. The call will + * need to be ended and started again for this change to take effect + * @param {string} deviceId + */ + public setVideoInput(deviceId: string): void { + SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId); + setMatrixCallVideoInput(deviceId); + } + + public setDevice(deviceId: string, kind: MediaDeviceKindEnum): void { + switch (kind) { + case MediaDeviceKindEnum.AudioOutput: this.setAudioOutput(deviceId); break; + case MediaDeviceKindEnum.AudioInput: this.setAudioInput(deviceId); break; + case MediaDeviceKindEnum.VideoInput: this.setVideoInput(deviceId); break; + } + } + + public static getAudioOutput(): string { + return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput"); + } + + public static getAudioInput(): string { + return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput"); + } + + public static getVideoInput(): string { + return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput"); + } +} diff --git a/src/Modal.js b/src/Modal.js deleted file mode 100644 index de441740f1..0000000000 --- a/src/Modal.js +++ /dev/null @@ -1,322 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - - -import React from 'react'; -import ReactDOM from 'react-dom'; -import Analytics from './Analytics'; -import dis from './dispatcher'; -import {defer} from './utils/promise'; -import AsyncWrapper from './AsyncWrapper'; - -const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; -const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; - -class ModalManager { - constructor() { - this._counter = 0; - - // The modal to prioritise over all others. If this is set, only show - // this modal. Remove all other modals from the stack when this modal - // is closed. - this._priorityModal = null; - // The modal to keep open underneath other modals if possible. Useful - // for cases like Settings where the modal should remain open while the - // user is prompted for more information/errors. - this._staticModal = null; - // A list of the modals we have stacked up, with the most recent at [0] - // Neither the static nor priority modal will be in this list. - this._modals = [ - /* { - elem: React component for this dialog - onFinished: caller-supplied onFinished callback - className: CSS class for the dialog wrapper div - } */ - ]; - - this.onBackgroundClick = this.onBackgroundClick.bind(this); - } - - hasDialogs() { - return this._priorityModal || this._staticModal || this._modals.length > 0; - } - - getOrCreateContainer() { - let container = document.getElementById(DIALOG_CONTAINER_ID); - - if (!container) { - container = document.createElement("div"); - container.id = DIALOG_CONTAINER_ID; - document.body.appendChild(container); - } - - return container; - } - - getOrCreateStaticContainer() { - let container = document.getElementById(STATIC_DIALOG_CONTAINER_ID); - - if (!container) { - container = document.createElement("div"); - container.id = STATIC_DIALOG_CONTAINER_ID; - document.body.appendChild(container); - } - - return container; - } - - createTrackedDialog(analyticsAction, analyticsInfo, ...rest) { - Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); - return this.createDialog(...rest); - } - - appendTrackedDialog(analyticsAction, analyticsInfo, ...rest) { - Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); - return this.appendDialog(...rest); - } - - createDialog(Element, ...rest) { - return this.createDialogAsync(Promise.resolve(Element), ...rest); - } - - appendDialog(Element, ...rest) { - return this.appendDialogAsync(Promise.resolve(Element), ...rest); - } - - createTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) { - Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); - return this.createDialogAsync(...rest); - } - - appendTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) { - Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); - return this.appendDialogAsync(...rest); - } - - _buildModal(prom, props, className, options) { - const modal = {}; - - // never call this from onFinished() otherwise it will loop - const [closeDialog, onFinishedProm] = this._getCloseFn(modal, props); - - // don't attempt to reuse the same AsyncWrapper for different dialogs, - // otherwise we'll get confused. - const modalCount = this._counter++; - - // FIXME: If a dialog uses getDefaultProps it clobbers the onFinished - // property set here so you can't close the dialog from a button click! - modal.elem = ( - - ); - modal.onFinished = props ? props.onFinished : null; - modal.className = className; - modal.onBeforeClose = options.onBeforeClose; - modal.beforeClosePromise = null; - modal.close = closeDialog; - modal.closeReason = null; - - return {modal, closeDialog, onFinishedProm}; - } - - _getCloseFn(modal, props) { - const deferred = defer(); - return [async (...args) => { - if (modal.beforeClosePromise) { - await modal.beforeClosePromise; - } else if (modal.onBeforeClose) { - modal.beforeClosePromise = modal.onBeforeClose(modal.closeReason); - const shouldClose = await modal.beforeClosePromise; - modal.beforeClosePromise = null; - if (!shouldClose) { - return; - } - } - deferred.resolve(args); - if (props && props.onFinished) props.onFinished.apply(null, args); - const i = this._modals.indexOf(modal); - if (i >= 0) { - this._modals.splice(i, 1); - } - - if (this._priorityModal === modal) { - this._priorityModal = null; - - // XXX: This is destructive - this._modals = []; - } - - if (this._staticModal === modal) { - this._staticModal = null; - - // XXX: This is destructive - this._modals = []; - } - - this._reRender(); - }, deferred.promise]; - } - - /** - * @callback onBeforeClose - * @param {string?} reason either "backgroundClick" or null - * @return {Promise} whether the dialog should close - */ - - /** - * Open a modal view. - * - * This can be used to display a react component which is loaded as an asynchronous - * webpack component. To do this, set 'loader' as: - * - * (cb) => { - * require([''], cb); - * } - * - * @param {Promise} prom a promise which resolves with a React component - * which will be displayed as the modal view. - * - * @param {Object} props properties to pass to the displayed - * component. (We will also pass an 'onFinished' property.) - * - * @param {String} className CSS class to apply to the modal wrapper - * - * @param {boolean} isPriorityModal if true, this modal will be displayed regardless - * of other modals that are currently in the stack. - * Also, when closed, all modals will be removed - * from the stack. - * @param {boolean} isStaticModal if true, this modal will be displayed under other - * modals in the stack. When closed, all modals will - * also be removed from the stack. This is not compatible - * with being a priority modal. Only one modal can be - * static at a time. - * @param {Object} options? extra options for the dialog - * @param {onBeforeClose} options.onBeforeClose a callback to decide whether to close the dialog - * @returns {object} Object with 'close' parameter being a function that will close the dialog - */ - createDialogAsync(prom, props, className, isPriorityModal, isStaticModal, options = {}) { - const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, options); - if (isPriorityModal) { - // XXX: This is destructive - this._priorityModal = modal; - } else if (isStaticModal) { - // This is intentionally destructive - this._staticModal = modal; - } else { - this._modals.unshift(modal); - } - - this._reRender(); - return { - close: closeDialog, - finished: onFinishedProm, - }; - } - - appendDialogAsync(prom, props, className) { - const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, {}); - - this._modals.push(modal); - this._reRender(); - return { - close: closeDialog, - finished: onFinishedProm, - }; - } - - onBackgroundClick() { - const modal = this._getCurrentModal(); - if (!modal) { - return; - } - // we want to pass a reason to the onBeforeClose - // callback, but close is currently defined to - // pass all number of arguments to the onFinished callback - // so, pass the reason to close through a member variable - modal.closeReason = "backgroundClick"; - modal.close(); - modal.closeReason = null; - } - - _getCurrentModal() { - return this._priorityModal ? this._priorityModal : (this._modals[0] || this._staticModal); - } - - _reRender() { - if (this._modals.length === 0 && !this._priorityModal && !this._staticModal) { - // If there is no modal to render, make all of Riot available - // to screen reader users again - dis.dispatch({ - action: 'aria_unhide_main_app', - }); - ReactDOM.unmountComponentAtNode(this.getOrCreateContainer()); - ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer()); - return; - } - - // Hide the content outside the modal to screen reader users - // so they won't be able to navigate into it and act on it using - // screen reader specific features - dis.dispatch({ - action: 'aria_hide_main_app', - }); - - if (this._staticModal) { - const classes = "mx_Dialog_wrapper mx_Dialog_staticWrapper " - + (this._staticModal.className ? this._staticModal.className : ''); - - const staticDialog = ( -

-
- { this._staticModal.elem } -
-
-
- ); - - ReactDOM.render(staticDialog, this.getOrCreateStaticContainer()); - } else { - // This is safe to call repeatedly if we happen to do that - ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer()); - } - - const modal = this._getCurrentModal(); - if (modal !== this._staticModal) { - const classes = "mx_Dialog_wrapper " - + (this._staticModal ? "mx_Dialog_wrapperWithStaticUnder " : '') - + (modal.className ? modal.className : ''); - - const dialog = ( -
-
- {modal.elem} -
-
-
- ); - - ReactDOM.render(dialog, this.getOrCreateContainer()); - } else { - // This is safe to call repeatedly if we happen to do that - ReactDOM.unmountComponentAtNode(this.getOrCreateContainer()); - } - } -} - -if (!global.singletonModalManager) { - global.singletonModalManager = new ModalManager(); -} -export default global.singletonModalManager; diff --git a/src/Modal.tsx b/src/Modal.tsx new file mode 100644 index 0000000000..55fc871d67 --- /dev/null +++ b/src/Modal.tsx @@ -0,0 +1,398 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 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 from 'react'; +import ReactDOM from 'react-dom'; +import classNames from 'classnames'; +import { defer } from "matrix-js-sdk/src/utils"; + +import Analytics from './Analytics'; +import dis from './dispatcher/dispatcher'; +import AsyncWrapper from './AsyncWrapper'; + +const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; +const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; + +export interface IModal { + elem: React.ReactNode; + className?: string; + beforeClosePromise?: Promise; + closeReason?: string; + onBeforeClose?(reason?: string): Promise; + onFinished(...args: T): void; + close(...args: T): void; + hidden?: boolean; +} + +export interface IHandle { + finished: Promise; + close(...args: T): void; +} + +interface IProps { + onFinished?(...args: T): void; + // TODO improve typing here once all Modals are TS and we can exhaustively check the props + [key: string]: any; +} + +interface IOptions { + onBeforeClose?: IModal["onBeforeClose"]; +} + +type ParametersWithoutFirst any> = T extends (a: any, ...args: infer P) => any ? P : never; + +export class ModalManager { + private counter = 0; + // The modal to prioritise over all others. If this is set, only show + // this modal. Remove all other modals from the stack when this modal + // is closed. + private priorityModal: IModal = null; + // The modal to keep open underneath other modals if possible. Useful + // for cases like Settings where the modal should remain open while the + // user is prompted for more information/errors. + private staticModal: IModal = null; + // A list of the modals we have stacked up, with the most recent at [0] + // Neither the static nor priority modal will be in this list. + private modals: IModal[] = []; + + private static getOrCreateContainer() { + let container = document.getElementById(DIALOG_CONTAINER_ID); + + if (!container) { + container = document.createElement("div"); + container.id = DIALOG_CONTAINER_ID; + document.body.appendChild(container); + } + + return container; + } + + private static getOrCreateStaticContainer() { + let container = document.getElementById(STATIC_DIALOG_CONTAINER_ID); + + if (!container) { + container = document.createElement("div"); + container.id = STATIC_DIALOG_CONTAINER_ID; + document.body.appendChild(container); + } + + return container; + } + + public toggleCurrentDialogVisibility() { + const modal = this.getCurrentModal(); + if (!modal) return; + modal.hidden = !modal.hidden; + } + + public hasDialogs() { + return this.priorityModal || this.staticModal || this.modals.length > 0; + } + + public createTrackedDialog( + analyticsAction: string, + analyticsInfo: string, + ...rest: Parameters + ) { + Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); + return this.createDialog(...rest); + } + + public appendTrackedDialog( + analyticsAction: string, + analyticsInfo: string, + ...rest: Parameters + ) { + Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); + return this.appendDialog(...rest); + } + + public createDialog( + Element: React.ComponentType, + ...rest: ParametersWithoutFirst + ) { + return this.createDialogAsync(Promise.resolve(Element), ...rest); + } + + public appendDialog( + Element: React.ComponentType, + ...rest: ParametersWithoutFirst + ) { + return this.appendDialogAsync(Promise.resolve(Element), ...rest); + } + + public createTrackedDialogAsync( + analyticsAction: string, + analyticsInfo: string, + ...rest: Parameters + ) { + Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); + return this.createDialogAsync(...rest); + } + + public appendTrackedDialogAsync( + analyticsAction: string, + analyticsInfo: string, + ...rest: Parameters + ) { + Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); + return this.appendDialogAsync(...rest); + } + + public closeCurrentModal(reason: string) { + const modal = this.getCurrentModal(); + if (!modal) { + return; + } + modal.closeReason = reason; + modal.close(); + } + + private buildModal( + prom: Promise, + props?: IProps, + className?: string, + options?: IOptions, + ) { + const modal: IModal = { + onFinished: props ? props.onFinished : null, + onBeforeClose: options.onBeforeClose, + beforeClosePromise: null, + closeReason: null, + className, + + // these will be set below but we need an object reference to pass to getCloseFn before we can do that + elem: null, + close: null, + }; + + // never call this from onFinished() otherwise it will loop + const [closeDialog, onFinishedProm] = this.getCloseFn(modal, props); + + // don't attempt to reuse the same AsyncWrapper for different dialogs, + // otherwise we'll get confused. + const modalCount = this.counter++; + + // FIXME: If a dialog uses getDefaultProps it clobbers the onFinished + // property set here so you can't close the dialog from a button click! + modal.elem = ; + modal.close = closeDialog; + + return { modal, closeDialog, onFinishedProm }; + } + + private getCloseFn( + modal: IModal, + props: IProps, + ): [IHandle["close"], IHandle["finished"]] { + const deferred = defer(); + return [async (...args: T) => { + if (modal.beforeClosePromise) { + await modal.beforeClosePromise; + } else if (modal.onBeforeClose) { + modal.beforeClosePromise = modal.onBeforeClose(modal.closeReason); + const shouldClose = await modal.beforeClosePromise; + modal.beforeClosePromise = null; + if (!shouldClose) { + return; + } + } + deferred.resolve(args); + if (props && props.onFinished) props.onFinished.apply(null, args); + const i = this.modals.indexOf(modal); + if (i >= 0) { + this.modals.splice(i, 1); + } + + if (this.priorityModal === modal) { + this.priorityModal = null; + + // XXX: This is destructive + this.modals = []; + } + + if (this.staticModal === modal) { + this.staticModal = null; + + // XXX: This is destructive + this.modals = []; + } + + this.reRender(); + }, deferred.promise]; + } + + /** + * @callback onBeforeClose + * @param {string?} reason either "backgroundClick" or null + * @return {Promise} whether the dialog should close + */ + + /** + * Open a modal view. + * + * This can be used to display a react component which is loaded as an asynchronous + * webpack component. To do this, set 'loader' as: + * + * (cb) => { + * require([''], cb); + * } + * + * @param {Promise} prom a promise which resolves with a React component + * which will be displayed as the modal view. + * + * @param {Object} props properties to pass to the displayed + * component. (We will also pass an 'onFinished' property.) + * + * @param {String} className CSS class to apply to the modal wrapper + * + * @param {boolean} isPriorityModal if true, this modal will be displayed regardless + * of other modals that are currently in the stack. + * Also, when closed, all modals will be removed + * from the stack. + * @param {boolean} isStaticModal if true, this modal will be displayed under other + * modals in the stack. When closed, all modals will + * also be removed from the stack. This is not compatible + * with being a priority modal. Only one modal can be + * static at a time. + * @param {Object} options? extra options for the dialog + * @param {onBeforeClose} options.onBeforeClose a callback to decide whether to close the dialog + * @returns {object} Object with 'close' parameter being a function that will close the dialog + */ + private createDialogAsync( + prom: Promise, + props?: IProps, + className?: string, + isPriorityModal = false, + isStaticModal = false, + options: IOptions = {}, + ): IHandle { + const { modal, closeDialog, onFinishedProm } = this.buildModal(prom, props, className, options); + if (isPriorityModal) { + // XXX: This is destructive + this.priorityModal = modal; + } else if (isStaticModal) { + // This is intentionally destructive + this.staticModal = modal; + } else { + this.modals.unshift(modal); + } + + this.reRender(); + return { + close: closeDialog, + finished: onFinishedProm, + }; + } + + private appendDialogAsync( + prom: Promise, + props?: IProps, + className?: string, + ): IHandle { + const { modal, closeDialog, onFinishedProm } = this.buildModal(prom, props, className, {}); + + this.modals.push(modal); + this.reRender(); + return { + close: closeDialog, + finished: onFinishedProm, + }; + } + + private onBackgroundClick = () => { + const modal = this.getCurrentModal(); + if (!modal) { + return; + } + // we want to pass a reason to the onBeforeClose + // callback, but close is currently defined to + // pass all number of arguments to the onFinished callback + // so, pass the reason to close through a member variable + modal.closeReason = "backgroundClick"; + modal.close(); + modal.closeReason = null; + }; + + private getCurrentModal(): IModal { + return this.priorityModal ? this.priorityModal : (this.modals[0] || this.staticModal); + } + + private reRender() { + if (this.modals.length === 0 && !this.priorityModal && !this.staticModal) { + // If there is no modal to render, make all of Element available + // to screen reader users again + dis.dispatch({ + action: 'aria_unhide_main_app', + }); + ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer()); + ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer()); + return; + } + + // Hide the content outside the modal to screen reader users + // so they won't be able to navigate into it and act on it using + // screen reader specific features + dis.dispatch({ + action: 'aria_hide_main_app', + }); + + if (this.staticModal) { + const classes = classNames("mx_Dialog_wrapper mx_Dialog_staticWrapper", this.staticModal.className); + + const staticDialog = ( +
+
+ { this.staticModal.elem } +
+
+
+ ); + + ReactDOM.render(staticDialog, ModalManager.getOrCreateStaticContainer()); + } else { + // This is safe to call repeatedly if we happen to do that + ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer()); + } + + const modal = this.getCurrentModal(); + if (modal !== this.staticModal && !modal.hidden) { + const classes = classNames("mx_Dialog_wrapper", modal.className, { + mx_Dialog_wrapperWithStaticUnder: this.staticModal, + }); + + const dialog = ( +
+
+ {modal.elem} +
+
+
+ ); + + setImmediate(() => ReactDOM.render(dialog, ModalManager.getOrCreateContainer())); + } else { + // This is safe to call repeatedly if we happen to do that + ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer()); + } + } +} + +if (!window.singletonModalManager) { + window.singletonModalManager = new ModalManager(); +} +export default window.singletonModalManager; diff --git a/src/NodeAnimator.js b/src/NodeAnimator.js new file mode 100644 index 0000000000..8456e6e9fd --- /dev/null +++ b/src/NodeAnimator.js @@ -0,0 +1,121 @@ +import React from "react"; +import ReactDom from "react-dom"; +import PropTypes from 'prop-types'; + +/** + * The NodeAnimator contains components and animates transitions. + * It will only pick up direct changes to properties ('left', currently), and so + * will not work for animating positional changes where the position is implicit + * from DOM order. This makes it a lot simpler and lighter: if you need fully + * automatic positional animation, look at react-shuffle or similar libraries. + */ +export default class NodeAnimator extends React.Component { + static propTypes = { + // either a list of child nodes, or a single child. + children: PropTypes.any, + + // optional transition information for changing existing children + transition: PropTypes.object, + + // a list of state objects to apply to each child node in turn + startStyles: PropTypes.array, + }; + + static defaultProps = { + startStyles: [], + }; + + constructor(props) { + super(props); + + this.nodes = {}; + this._updateChildren(this.props.children); + } + + componentDidUpdate() { + this._updateChildren(this.props.children); + } + + /** + * + * @param {HTMLElement} node element to apply styles to + * @param {object} styles a key/value pair of CSS properties + * @returns {void} + */ + _applyStyles(node, styles) { + Object.entries(styles).forEach(([property, value]) => { + node.style[property] = value; + }); + } + + _updateChildren(newChildren) { + const oldChildren = this.children || {}; + this.children = {}; + React.Children.toArray(newChildren).forEach((c) => { + if (oldChildren[c.key]) { + const old = oldChildren[c.key]; + const oldNode = ReactDom.findDOMNode(this.nodes[old.key]); + + if (oldNode && oldNode.style.left !== c.props.style.left) { + this._applyStyles(oldNode, { left: c.props.style.left }); + // console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left); + } + // clone the old element with the props (and children) of the new element + // so prop updates are still received by the children. + this.children[c.key] = React.cloneElement(old, c.props, c.props.children); + } else { + // new element. If we have a startStyle, use that as the style and go through + // the enter animations + const newProps = {}; + const restingStyle = c.props.style; + + const startStyles = this.props.startStyles; + if (startStyles.length > 0) { + const startStyle = startStyles[0]; + newProps.style = startStyle; + // console.log("mounted@startstyle0: "+JSON.stringify(startStyle)); + } + + newProps.ref = ((n) => this._collectNode( + c.key, n, restingStyle, + )); + + this.children[c.key] = React.cloneElement(c, newProps); + } + }); + } + + _collectNode(k, node, restingStyle) { + if ( + node && + this.nodes[k] === undefined && + this.props.startStyles.length > 0 + ) { + const startStyles = this.props.startStyles; + const domNode = ReactDom.findDOMNode(node); + // start from startStyle 1: 0 is the one we gave it + // to start with, so now we animate 1 etc. + for (let i = 1; i < startStyles.length; ++i) { + this._applyStyles(domNode, startStyles[i]); + // console.log("start:" + // JSON.stringify(startStyles[i]), + // ); + } + + // and then we animate to the resting state + setTimeout(() => { + this._applyStyles(domNode, restingStyle); + }, 0); + + // console.log("enter:", + // JSON.stringify(restingStyle)); + } + this.nodes[k] = node; + } + + render() { + return ( + <>{ Object.values(this.children) } + ); + } +} diff --git a/src/Notifier.js b/src/Notifier.js deleted file mode 100644 index ec92840998..0000000000 --- a/src/Notifier.js +++ /dev/null @@ -1,388 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2017 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import {MatrixClientPeg} from './MatrixClientPeg'; -import PlatformPeg from './PlatformPeg'; -import * as TextForEvent from './TextForEvent'; -import Analytics from './Analytics'; -import * as Avatar from './Avatar'; -import dis from './dispatcher'; -import * as sdk from './index'; -import { _t } from './languageHandler'; -import Modal from './Modal'; -import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; - -/* - * Dispatches: - * { - * action: "notifier_enabled", - * value: boolean - * } - */ - -const MAX_PENDING_ENCRYPTED = 20; - -/* -Override both the content body and the TextForEvent handler for specific msgtypes, in notifications. -This is useful when the content body contains fallback text that would explain that the client can't handle a particular -type of tile. -*/ -const typehandlers = { - "m.key.verification.request": (event) => { - const name = (event.sender || {}).name; - return _t("%(name)s is requesting verification", { name }); - }, -}; - -const Notifier = { - notifsByRoom: {}, - - // A list of event IDs that we've received but need to wait until - // they're decrypted until we decide whether to notify for them - // or not - pendingEncryptedEventIds: [], - - notificationMessageForEvent: function(ev) { - if (typehandlers.hasOwnProperty(ev.getContent().msgtype)) { - return typehandlers[ev.getContent().msgtype](ev); - } - return TextForEvent.textForEvent(ev); - }, - - _displayPopupNotification: function(ev, room) { - const plaf = PlatformPeg.get(); - if (!plaf) { - return; - } - if (!plaf.supportsNotifications() || !plaf.maySendNotifications()) { - return; - } - if (global.document.hasFocus()) { - return; - } - - let msg = this.notificationMessageForEvent(ev); - if (!msg) return; - - let title; - if (!ev.sender || room.name === ev.sender.name) { - title = room.name; - // notificationMessageForEvent includes sender, - // but we already have the sender here - if (ev.getContent().body && !typehandlers.hasOwnProperty(ev.getContent().msgtype)) { - msg = ev.getContent().body; - } - } else if (ev.getType() === 'm.room.member') { - // context is all in the message here, we don't need - // to display sender info - title = room.name; - } else if (ev.sender) { - title = ev.sender.name + " (" + room.name + ")"; - // notificationMessageForEvent includes sender, - // but we've just out sender in the title - if (ev.getContent().body && !typehandlers.hasOwnProperty(ev.getContent().msgtype)) { - msg = ev.getContent().body; - } - } - - if (!this.isBodyEnabled()) { - msg = ''; - } - - let avatarUrl = null; - if (ev.sender && !SettingsStore.getValue("lowBandwidth")) { - avatarUrl = Avatar.avatarUrlForMember(ev.sender, 40, 40, 'crop'); - } - - const notif = plaf.displayNotification(title, msg, avatarUrl, room); - - // if displayNotification returns non-null, the platform supports - // clearing notifications later, so keep track of this. - if (notif) { - if (this.notifsByRoom[ev.getRoomId()] === undefined) this.notifsByRoom[ev.getRoomId()] = []; - this.notifsByRoom[ev.getRoomId()].push(notif); - } - }, - - getSoundForRoom: async function(roomId) { - // We do no caching here because the SDK caches setting - // and the browser will cache the sound. - const content = SettingsStore.getValue("notificationSound", roomId); - if (!content) { - return null; - } - - if (!content.url) { - console.warn(`${roomId} has custom notification sound event, but no url key`); - return null; - } - - if (!content.url.startsWith("mxc://")) { - console.warn(`${roomId} has custom notification sound event, but url is not a mxc url`); - return null; - } - - // Ideally in here we could use MSC1310 to detect the type of file, and reject it. - - return { - url: MatrixClientPeg.get().mxcUrlToHttp(content.url), - name: content.name, - type: content.type, - size: content.size, - }; - }, - - _playAudioNotification: async function(ev, room) { - const sound = await this.getSoundForRoom(room.roomId); - console.log(`Got sound ${sound && sound.name || "default"} for ${room.roomId}`); - - try { - const selector = document.querySelector(sound ? `audio[src='${sound.url}']` : "#messageAudio"); - let audioElement = selector; - if (!selector) { - if (!sound) { - console.error("No audio element or sound to play for notification"); - return; - } - audioElement = new Audio(sound.url); - if (sound.type) { - audioElement.type = sound.type; - } - document.body.appendChild(audioElement); - } - await audioElement.play(); - } catch (ex) { - console.warn("Caught error when trying to fetch room notification sound:", ex); - } - }, - - start: function() { - // do not re-bind in the case of repeated call - this.boundOnEvent = this.boundOnEvent || this.onEvent.bind(this); - this.boundOnSyncStateChange = this.boundOnSyncStateChange || this.onSyncStateChange.bind(this); - this.boundOnRoomReceipt = this.boundOnRoomReceipt || this.onRoomReceipt.bind(this); - this.boundOnEventDecrypted = this.boundOnEventDecrypted || this.onEventDecrypted.bind(this); - - MatrixClientPeg.get().on('event', this.boundOnEvent); - MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt); - MatrixClientPeg.get().on('Event.decrypted', this.boundOnEventDecrypted); - MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange); - this.toolbarHidden = false; - this.isSyncing = false; - }, - - stop: function() { - if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener('Event', this.boundOnEvent); - MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt); - MatrixClientPeg.get().removeListener('Event.decrypted', this.boundOnEventDecrypted); - MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange); - } - this.isSyncing = false; - }, - - supportsDesktopNotifications: function() { - const plaf = PlatformPeg.get(); - return plaf && plaf.supportsNotifications(); - }, - - setEnabled: function(enable, callback) { - const plaf = PlatformPeg.get(); - if (!plaf) return; - - // Dev note: We don't set the "notificationsEnabled" setting to true here because it is a - // calculated value. It is determined based upon whether or not the master rule is enabled - // and other flags. Setting it here would cause a circular reference. - - Analytics.trackEvent('Notifier', 'Set Enabled', enable); - - // make sure that we persist the current setting audio_enabled setting - // before changing anything - if (SettingsStore.isLevelSupported(SettingLevel.DEVICE)) { - SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, this.isEnabled()); - } - - if (enable) { - // Attempt to get permission from user - plaf.requestNotificationPermission().then((result) => { - if (result !== 'granted') { - // The permission request was dismissed or denied - // TODO: Support alternative branding in messaging - const description = result === 'denied' - ? _t('Riot does not have permission to send you notifications - ' + - 'please check your browser settings') - : _t('Riot was not given permission to send notifications - please try again'); - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); - Modal.createTrackedDialog('Unable to enable Notifications', result, ErrorDialog, { - title: _t('Unable to enable Notifications'), - description, - }); - return; - } - - if (callback) callback(); - dis.dispatch({ - action: "notifier_enabled", - value: true, - }); - }); - } else { - dis.dispatch({ - action: "notifier_enabled", - value: false, - }); - } - // set the notifications_hidden flag, as the user has knowingly interacted - // with the setting we shouldn't nag them any further - this.setToolbarHidden(true); - }, - - isEnabled: function() { - return this.isPossible() && SettingsStore.getValue("notificationsEnabled"); - }, - - isPossible: function() { - const plaf = PlatformPeg.get(); - if (!plaf) return false; - if (!plaf.supportsNotifications()) return false; - if (!plaf.maySendNotifications()) return false; - - return true; // possible, but not necessarily enabled - }, - - isBodyEnabled: function() { - return this.isEnabled() && SettingsStore.getValue("notificationBodyEnabled"); - }, - - isAudioEnabled: function() { - return this.isEnabled() && SettingsStore.getValue("audioNotificationsEnabled"); - }, - - setToolbarHidden: function(hidden, persistent = true) { - this.toolbarHidden = hidden; - - Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden); - - // XXX: why are we dispatching this here? - // this is nothing to do with notifier_enabled - dis.dispatch({ - action: "notifier_enabled", - value: this.isEnabled(), - }); - - // update the info to localStorage for persistent settings - if (persistent && global.localStorage) { - global.localStorage.setItem("notifications_hidden", hidden); - } - }, - - shouldShowToolbar: function() { - const client = MatrixClientPeg.get(); - if (!client) { - return false; - } - const isGuest = client.isGuest(); - return !isGuest && this.supportsDesktopNotifications() && - !this.isEnabled() && !this._isToolbarHidden(); - }, - - _isToolbarHidden: function() { - // Check localStorage for any such meta data - if (global.localStorage) { - return global.localStorage.getItem("notifications_hidden") === "true"; - } - - return this.toolbarHidden; - }, - - onSyncStateChange: function(state) { - if (state === "SYNCING") { - this.isSyncing = true; - } else if (state === "STOPPED" || state === "ERROR") { - this.isSyncing = false; - } - }, - - onEvent: function(ev) { - if (!this.isSyncing) return; // don't alert for any messages initially - if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return; - - // If it's an encrypted event and the type is still 'm.room.encrypted', - // it hasn't yet been decrypted, so wait until it is. - if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { - this.pendingEncryptedEventIds.push(ev.getId()); - // don't let the list fill up indefinitely - while (this.pendingEncryptedEventIds.length > MAX_PENDING_ENCRYPTED) { - this.pendingEncryptedEventIds.shift(); - } - return; - } - - this._evaluateEvent(ev); - }, - - onEventDecrypted: function(ev) { - // 'decrypted' means the decryption process has finished: it may have failed, - // in which case it might decrypt soon if the keys arrive - if (ev.isDecryptionFailure()) return; - - const idx = this.pendingEncryptedEventIds.indexOf(ev.getId()); - if (idx === -1) return; - - this.pendingEncryptedEventIds.splice(idx, 1); - this._evaluateEvent(ev); - }, - - onRoomReceipt: function(ev, room) { - if (room.getUnreadNotificationCount() === 0) { - // ideally we would clear each notification when it was read, - // but we have no way, given a read receipt, to know whether - // the receipt comes before or after an event, so we can't - // do this. Instead, clear all notifications for a room once - // there are no notifs left in that room., which is not quite - // as good but it's something. - const plaf = PlatformPeg.get(); - if (!plaf) return; - if (this.notifsByRoom[room.roomId] === undefined) return; - for (const notif of this.notifsByRoom[room.roomId]) { - plaf.clearNotification(notif); - } - delete this.notifsByRoom[room.roomId]; - } - }, - - _evaluateEvent: function(ev) { - const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); - const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); - if (actions && actions.notify) { - if (this.isEnabled()) { - this._displayPopupNotification(ev, room); - } - if (actions.tweaks.sound && this.isAudioEnabled()) { - PlatformPeg.get().loudNotification(ev, room); - this._playAudioNotification(ev, room); - } - } - }, -}; - -if (!global.mxNotifier) { - global.mxNotifier = Notifier; -} - -export default global.mxNotifier; diff --git a/src/Notifier.ts b/src/Notifier.ts new file mode 100644 index 0000000000..1137e44aec --- /dev/null +++ b/src/Notifier.ts @@ -0,0 +1,407 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2017 New Vector Ltd +Copyright 2020 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 { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { Room } from "matrix-js-sdk/src/models/room"; + +import { MatrixClientPeg } from './MatrixClientPeg'; +import SdkConfig from './SdkConfig'; +import PlatformPeg from './PlatformPeg'; +import * as TextForEvent from './TextForEvent'; +import Analytics from './Analytics'; +import * as Avatar from './Avatar'; +import dis from './dispatcher/dispatcher'; +import { _t } from './languageHandler'; +import Modal from './Modal'; +import SettingsStore from "./settings/SettingsStore"; +import { hideToast as hideNotificationsToast } from "./toasts/DesktopNotificationsToast"; +import { SettingLevel } from "./settings/SettingLevel"; +import { isPushNotifyDisabled } from "./settings/controllers/NotificationControllers"; +import RoomViewStore from "./stores/RoomViewStore"; +import UserActivity from "./UserActivity"; +import { mediaFromMxc } from "./customisations/Media"; +import ErrorDialog from "./components/views/dialogs/ErrorDialog"; + +/* + * Dispatches: + * { + * action: "notifier_enabled", + * value: boolean + * } + */ + +const MAX_PENDING_ENCRYPTED = 20; + +/* +Override both the content body and the TextForEvent handler for specific msgtypes, in notifications. +This is useful when the content body contains fallback text that would explain that the client can't handle a particular +type of tile. +*/ +const typehandlers = { + "m.key.verification.request": (event) => { + const name = (event.sender || {}).name; + return _t("%(name)s is requesting verification", { name }); + }, +}; + +export const Notifier = { + notifsByRoom: {}, + + // A list of event IDs that we've received but need to wait until + // they're decrypted until we decide whether to notify for them + // or not + pendingEncryptedEventIds: [], + + notificationMessageForEvent: function(ev: MatrixEvent): string { + if (typehandlers.hasOwnProperty(ev.getContent().msgtype)) { + return typehandlers[ev.getContent().msgtype](ev); + } + return TextForEvent.textForEvent(ev); + }, + + _displayPopupNotification: function(ev: MatrixEvent, room: Room) { + const plaf = PlatformPeg.get(); + if (!plaf) { + return; + } + if (!plaf.supportsNotifications() || !plaf.maySendNotifications()) { + return; + } + if (global.document.hasFocus()) { + return; + } + + let msg = this.notificationMessageForEvent(ev); + if (!msg) return; + + let title; + if (!ev.sender || room.name === ev.sender.name) { + title = room.name; + // notificationMessageForEvent includes sender, + // but we already have the sender here + if (ev.getContent().body && !typehandlers.hasOwnProperty(ev.getContent().msgtype)) { + msg = ev.getContent().body; + } + } else if (ev.getType() === 'm.room.member') { + // context is all in the message here, we don't need + // to display sender info + title = room.name; + } else if (ev.sender) { + title = ev.sender.name + " (" + room.name + ")"; + // notificationMessageForEvent includes sender, + // but we've just out sender in the title + if (ev.getContent().body && !typehandlers.hasOwnProperty(ev.getContent().msgtype)) { + msg = ev.getContent().body; + } + } + + if (!this.isBodyEnabled()) { + msg = ''; + } + + let avatarUrl = null; + if (ev.sender && !SettingsStore.getValue("lowBandwidth")) { + avatarUrl = Avatar.avatarUrlForMember(ev.sender, 40, 40, 'crop'); + } + + const notif = plaf.displayNotification(title, msg, avatarUrl, room); + + // if displayNotification returns non-null, the platform supports + // clearing notifications later, so keep track of this. + if (notif) { + if (this.notifsByRoom[ev.getRoomId()] === undefined) this.notifsByRoom[ev.getRoomId()] = []; + this.notifsByRoom[ev.getRoomId()].push(notif); + } + }, + + getSoundForRoom: function(roomId: string) { + // We do no caching here because the SDK caches setting + // and the browser will cache the sound. + const content = SettingsStore.getValue("notificationSound", roomId); + if (!content) { + return null; + } + + if (!content.url) { + console.warn(`${roomId} has custom notification sound event, but no url key`); + return null; + } + + if (!content.url.startsWith("mxc://")) { + console.warn(`${roomId} has custom notification sound event, but url is not a mxc url`); + return null; + } + + // Ideally in here we could use MSC1310 to detect the type of file, and reject it. + + return { + url: mediaFromMxc(content.url).srcHttp, + name: content.name, + type: content.type, + size: content.size, + }; + }, + + _playAudioNotification: async function(ev: MatrixEvent, room: Room) { + const sound = this.getSoundForRoom(room.roomId); + console.log(`Got sound ${sound && sound.name || "default"} for ${room.roomId}`); + + try { + const selector = + document.querySelector(sound ? `audio[src='${sound.url}']` : "#messageAudio"); + let audioElement = selector; + if (!selector) { + if (!sound) { + console.error("No audio element or sound to play for notification"); + return; + } + audioElement = new Audio(sound.url); + if (sound.type) { + audioElement.type = sound.type; + } + document.body.appendChild(audioElement); + } + await audioElement.play(); + } catch (ex) { + console.warn("Caught error when trying to fetch room notification sound:", ex); + } + }, + + start: function() { + // do not re-bind in the case of repeated call + this.boundOnEvent = this.boundOnEvent || this.onEvent.bind(this); + this.boundOnSyncStateChange = this.boundOnSyncStateChange || this.onSyncStateChange.bind(this); + this.boundOnRoomReceipt = this.boundOnRoomReceipt || this.onRoomReceipt.bind(this); + this.boundOnEventDecrypted = this.boundOnEventDecrypted || this.onEventDecrypted.bind(this); + + MatrixClientPeg.get().on('event', this.boundOnEvent); + MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt); + MatrixClientPeg.get().on('Event.decrypted', this.boundOnEventDecrypted); + MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange); + this.toolbarHidden = false; + this.isSyncing = false; + }, + + stop: function() { + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener('Event', this.boundOnEvent); + MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt); + MatrixClientPeg.get().removeListener('Event.decrypted', this.boundOnEventDecrypted); + MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange); + } + this.isSyncing = false; + }, + + supportsDesktopNotifications: function() { + const plaf = PlatformPeg.get(); + return plaf && plaf.supportsNotifications(); + }, + + setEnabled: function(enable: boolean, callback?: () => void) { + const plaf = PlatformPeg.get(); + if (!plaf) return; + + // Dev note: We don't set the "notificationsEnabled" setting to true here because it is a + // calculated value. It is determined based upon whether or not the master rule is enabled + // and other flags. Setting it here would cause a circular reference. + + Analytics.trackEvent('Notifier', 'Set Enabled', String(enable)); + + // make sure that we persist the current setting audio_enabled setting + // before changing anything + if (SettingsStore.isLevelSupported(SettingLevel.DEVICE)) { + SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, this.isEnabled()); + } + + if (enable) { + // Attempt to get permission from user + plaf.requestNotificationPermission().then((result) => { + if (result !== 'granted') { + // The permission request was dismissed or denied + // TODO: Support alternative branding in messaging + const brand = SdkConfig.get().brand; + const description = result === 'denied' + ? _t('%(brand)s does not have permission to send you notifications - ' + + 'please check your browser settings', { brand }) + : _t('%(brand)s was not given permission to send notifications - please try again', { brand }); + Modal.createTrackedDialog('Unable to enable Notifications', result, ErrorDialog, { + title: _t('Unable to enable Notifications'), + description, + }); + return; + } + + if (callback) callback(); + dis.dispatch({ + action: "notifier_enabled", + value: true, + }); + }); + } else { + dis.dispatch({ + action: "notifier_enabled", + value: false, + }); + } + // set the notifications_hidden flag, as the user has knowingly interacted + // with the setting we shouldn't nag them any further + this.setPromptHidden(true); + }, + + isEnabled: function() { + return this.isPossible() && SettingsStore.getValue("notificationsEnabled"); + }, + + isPossible: function() { + const plaf = PlatformPeg.get(); + if (!plaf) return false; + if (!plaf.supportsNotifications()) return false; + if (!plaf.maySendNotifications()) return false; + + return true; // possible, but not necessarily enabled + }, + + isBodyEnabled: function() { + return this.isEnabled() && SettingsStore.getValue("notificationBodyEnabled"); + }, + + isAudioEnabled: function() { + // We don't route Audio via the HTML Notifications API so it is possible regardless of other things + return SettingsStore.getValue("audioNotificationsEnabled"); + }, + + setPromptHidden: function(hidden: boolean, persistent = true) { + this.toolbarHidden = hidden; + + Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', String(hidden)); + + hideNotificationsToast(); + + // update the info to localStorage for persistent settings + if (persistent && global.localStorage) { + global.localStorage.setItem("notifications_hidden", String(hidden)); + } + }, + + shouldShowPrompt: function() { + const client = MatrixClientPeg.get(); + if (!client) { + return false; + } + const isGuest = client.isGuest(); + return !isGuest && this.supportsDesktopNotifications() && !isPushNotifyDisabled() && + !this.isEnabled() && !this._isPromptHidden(); + }, + + _isPromptHidden: function() { + // Check localStorage for any such meta data + if (global.localStorage) { + return global.localStorage.getItem("notifications_hidden") === "true"; + } + + return this.toolbarHidden; + }, + + onSyncStateChange: function(state: string) { + if (state === "SYNCING") { + this.isSyncing = true; + } else if (state === "STOPPED" || state === "ERROR") { + this.isSyncing = false; + } + }, + + onEvent: function(ev: MatrixEvent) { + if (!this.isSyncing) return; // don't alert for any messages initially + if (ev.getSender() === MatrixClientPeg.get().credentials.userId) return; + + MatrixClientPeg.get().decryptEventIfNeeded(ev); + + // If it's an encrypted event and the type is still 'm.room.encrypted', + // it hasn't yet been decrypted, so wait until it is. + if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) { + this.pendingEncryptedEventIds.push(ev.getId()); + // don't let the list fill up indefinitely + while (this.pendingEncryptedEventIds.length > MAX_PENDING_ENCRYPTED) { + this.pendingEncryptedEventIds.shift(); + } + return; + } + + this._evaluateEvent(ev); + }, + + onEventDecrypted: function(ev: MatrixEvent) { + // 'decrypted' means the decryption process has finished: it may have failed, + // in which case it might decrypt soon if the keys arrive + if (ev.isDecryptionFailure()) return; + + const idx = this.pendingEncryptedEventIds.indexOf(ev.getId()); + if (idx === -1) return; + + this.pendingEncryptedEventIds.splice(idx, 1); + this._evaluateEvent(ev); + }, + + onRoomReceipt: function(ev: MatrixEvent, room: Room) { + if (room.getUnreadNotificationCount() === 0) { + // ideally we would clear each notification when it was read, + // but we have no way, given a read receipt, to know whether + // the receipt comes before or after an event, so we can't + // do this. Instead, clear all notifications for a room once + // there are no notifs left in that room., which is not quite + // as good but it's something. + const plaf = PlatformPeg.get(); + if (!plaf) return; + if (this.notifsByRoom[room.roomId] === undefined) return; + for (const notif of this.notifsByRoom[room.roomId]) { + plaf.clearNotification(notif); + } + delete this.notifsByRoom[room.roomId]; + } + }, + + _evaluateEvent: function(ev) { + const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); + const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); + if (actions && actions.notify) { + if (RoomViewStore.getRoomId() === room.roomId && UserActivity.sharedInstance().userActiveRecently()) { + // don't bother notifying as user was recently active in this room + return; + } + if (SettingsStore.getValue("doNotDisturb")) { + // Don't bother the user if they didn't ask to be bothered + return; + } + + if (this.isEnabled()) { + this._displayPopupNotification(ev, room); + } + if (actions.tweaks.sound && this.isAudioEnabled()) { + PlatformPeg.get().loudNotification(ev, room); + this._playAudioNotification(ev, room); + } + } + }, +}; + +if (!window.mxNotifier) { + window.mxNotifier = Notifier; +} + +export default window.mxNotifier; diff --git a/src/ObjectUtils.js b/src/ObjectUtils.js deleted file mode 100644 index 24dfe61d68..0000000000 --- a/src/ObjectUtils.js +++ /dev/null @@ -1,113 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd -Copyright 2019 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. -*/ - -/** - * For two objects of the form { key: [val1, val2, val3] }, work out the added/removed - * values. Entirely new keys will result in the entire value array being added. - * @param {Object} before - * @param {Object} after - * @return {Object[]} An array of objects with the form: - * { key: $KEY, val: $VALUE, place: "add|del" } - */ -export function getKeyValueArrayDiffs(before, after) { - const results = []; - const delta = {}; - Object.keys(before).forEach(function(beforeKey) { - delta[beforeKey] = delta[beforeKey] || 0; // init to 0 initially - delta[beforeKey]--; // keys present in the past have -ve values - }); - Object.keys(after).forEach(function(afterKey) { - delta[afterKey] = delta[afterKey] || 0; // init to 0 initially - delta[afterKey]++; // keys present in the future have +ve values - }); - - Object.keys(delta).forEach(function(muxedKey) { - switch (delta[muxedKey]) { - case 1: // A new key in after - after[muxedKey].forEach(function(afterVal) { - results.push({ place: "add", key: muxedKey, val: afterVal }); - }); - break; - case -1: // A before key was removed - before[muxedKey].forEach(function(beforeVal) { - results.push({ place: "del", key: muxedKey, val: beforeVal }); - }); - break; - case 0: {// A mix of added/removed keys - // compare old & new vals - const itemDelta = {}; - before[muxedKey].forEach(function(beforeVal) { - itemDelta[beforeVal] = itemDelta[beforeVal] || 0; - itemDelta[beforeVal]--; - }); - after[muxedKey].forEach(function(afterVal) { - itemDelta[afterVal] = itemDelta[afterVal] || 0; - itemDelta[afterVal]++; - }); - - Object.keys(itemDelta).forEach(function(item) { - if (itemDelta[item] === 1) { - results.push({ place: "add", key: muxedKey, val: item }); - } else if (itemDelta[item] === -1) { - results.push({ place: "del", key: muxedKey, val: item }); - } else { - // itemDelta of 0 means it was unchanged between before/after - } - }); - break; - } - default: - console.error("Calculated key delta of " + delta[muxedKey] + " - this should never happen!"); - break; - } - }); - - return results; -} - -/** - * Shallow-compare two objects for equality: each key and value must be identical - * @param {Object} objA First object to compare against the second - * @param {Object} objB Second object to compare against the first - * @return {boolean} whether the two objects have same key=values - */ -export function shallowEqual(objA, objB) { - if (objA === objB) { - return true; - } - - if (typeof objA !== 'object' || objA === null || - typeof objB !== 'object' || objB === null) { - return false; - } - - const keysA = Object.keys(objA); - const keysB = Object.keys(objB); - - if (keysA.length !== keysB.length) { - return false; - } - - for (let i = 0; i < keysA.length; i++) { - const key = keysA[i]; - if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) { - return false; - } - } - - return true; -} diff --git a/src/PasswordReset.js b/src/PasswordReset.js index 320599f6d9..88ae00d088 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as Matrix from 'matrix-js-sdk'; +import { createClient } from 'matrix-js-sdk/src/matrix'; import { _t } from './languageHandler'; /** @@ -32,7 +32,7 @@ export default class PasswordReset { * @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping. */ constructor(homeserverUrl, identityUrl) { - this.client = Matrix.createClient({ + this.client = createClient({ baseUrl: homeserverUrl, idBaseUrl: identityUrl, }); @@ -40,10 +40,6 @@ export default class PasswordReset { this.identityServerDomain = identityUrl ? identityUrl.split("://")[1] : null; } - doesServerRequireIdServerParam() { - return this.client.doesServerRequireIdServerParam(); - } - /** * Attempt to reset the user's password. This will trigger a side-effect of * sending an email to the provided email address. @@ -58,7 +54,7 @@ export default class PasswordReset { return res; }, function(err) { if (err.errcode === 'M_THREEPID_NOT_FOUND') { - err.message = _t('This email address was not found'); + err.message = _t('This email address was not found'); } else if (err.httpStatus) { err.message = err.message + ` (Status ${err.httpStatus})`; } @@ -78,14 +74,17 @@ export default class PasswordReset { sid: this.sessionId, client_secret: this.clientSecret, }; - if (await this.doesServerRequireIdServerParam()) { - creds.id_server = this.identityServerDomain; - } try { await this.client.setPassword({ + // Note: Though this sounds like a login type for identity servers only, it + // has a dual purpose of being used for homeservers too. type: "m.login.email.identity", + // TODO: Remove `threepid_creds` once servers support proper UIA + // See https://github.com/matrix-org/synapse/issues/5665 + // See https://github.com/matrix-org/matrix-doc/issues/2220 threepid_creds: creds, + threepidCreds: creds, }, this.password); } catch (err) { if (err.httpStatus === 401) { diff --git a/src/PhasedRollOut.js b/src/PhasedRollOut.js deleted file mode 100644 index b17ed37974..0000000000 --- a/src/PhasedRollOut.js +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import SdkConfig from './SdkConfig'; -import {hashCode} from './utils/FormattingUtils'; - -export function phasedRollOutExpiredForUser(username, feature, now, rollOutConfig = SdkConfig.get().phasedRollOut) { - if (!rollOutConfig) { - console.log(`no phased rollout configuration, so enabling ${feature}`); - return true; - } - const featureConfig = rollOutConfig[feature]; - if (!featureConfig) { - console.log(`${feature} doesn't have phased rollout configured, so enabling`); - return true; - } - if (!Number.isFinite(featureConfig.offset) || !Number.isFinite(featureConfig.period)) { - console.error(`phased rollout of ${feature} is misconfigured, ` + - `offset and/or period are not numbers, so disabling`, featureConfig); - return false; - } - - const hash = hashCode(username); - //ms -> min, enable users at minute granularity - const bucketRatio = 1000 * 60; - const bucketCount = featureConfig.period / bucketRatio; - const userBucket = hash % bucketCount; - const userMs = userBucket * bucketRatio; - const enableAt = featureConfig.offset + userMs; - const result = now >= enableAt; - const bucketStr = `(bucket ${userBucket}/${bucketCount})`; - if (result) { - console.log(`${feature} enabled for ${username} ${bucketStr}`); - } else { - console.log(`${feature} will be enabled for ${username} in ${Math.ceil((enableAt - now)/1000)}s ${bucketStr}`); - } - return result; -} diff --git a/src/PlatformPeg.js b/src/PlatformPeg.js deleted file mode 100644 index 34131fde7d..0000000000 --- a/src/PlatformPeg.js +++ /dev/null @@ -1,50 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/* - * Holds the current Platform object used by the code to do anything - * specific to the platform we're running on (eg. web, electron) - * Platforms are provided by the app layer. - * This allows the app layer to set a Platform without necessarily - * having to have a MatrixChat object - */ -class PlatformPeg { - constructor() { - this.platform = null; - } - - /** - * Returns the current Platform object for the application. - * This should be an instance of a class extending BasePlatform. - */ - get() { - return this.platform; - } - - /** - * Sets the current platform handler object to use for the - * application. - * This should be an instance of a class extending BasePlatform. - */ - set(plaf) { - this.platform = plaf; - } -} - -if (!global.mxPlatformPeg) { - global.mxPlatformPeg = new PlatformPeg(); -} -export default global.mxPlatformPeg; diff --git a/src/PlatformPeg.ts b/src/PlatformPeg.ts new file mode 100644 index 0000000000..1d2b813ebc --- /dev/null +++ b/src/PlatformPeg.ts @@ -0,0 +1,51 @@ +/* +Copyright 2016 OpenMarket Ltd +Copyright 2020 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 BasePlatform from "./BasePlatform"; + +/* + * Holds the current Platform object used by the code to do anything + * specific to the platform we're running on (eg. web, electron) + * Platforms are provided by the app layer. + * This allows the app layer to set a Platform without necessarily + * having to have a MatrixChat object + */ +export class PlatformPeg { + platform: BasePlatform = null; + + /** + * Returns the current Platform object for the application. + * This should be an instance of a class extending BasePlatform. + */ + get() { + return this.platform; + } + + /** + * Sets the current platform handler object to use for the + * application. + * This should be an instance of a class extending BasePlatform. + */ + set(plaf: BasePlatform) { + this.platform = plaf; + } +} + +if (!window.mxPlatformPeg) { + window.mxPlatformPeg = new PlatformPeg(); +} +export default window.mxPlatformPeg; diff --git a/src/Presence.js b/src/Presence.js deleted file mode 100644 index 2fc13a090b..0000000000 --- a/src/Presence.js +++ /dev/null @@ -1,108 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd -Copyright 2019 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 {MatrixClientPeg} from "./MatrixClientPeg"; -import dis from "./dispatcher"; -import Timer from './utils/Timer'; - - // Time in ms after that a user is considered as unavailable/away -const UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins -const PRESENCE_STATES = ["online", "offline", "unavailable"]; - -class Presence { - constructor() { - this._activitySignal = null; - this._unavailableTimer = null; - this._onAction = this._onAction.bind(this); - this._dispatcherRef = null; - } - /** - * Start listening the user activity to evaluate his presence state. - * Any state change will be sent to the homeserver. - */ - async start() { - this._unavailableTimer = new Timer(UNAVAILABLE_TIME_MS); - // the user_activity_start action starts the timer - this._dispatcherRef = dis.register(this._onAction); - while (this._unavailableTimer) { - try { - await this._unavailableTimer.finished(); - this.setState("unavailable"); - } catch (e) { /* aborted, stop got called */ } - } - } - - /** - * Stop tracking user activity - */ - stop() { - if (this._dispatcherRef) { - dis.unregister(this._dispatcherRef); - this._dispatcherRef = null; - } - if (this._unavailableTimer) { - this._unavailableTimer.abort(); - this._unavailableTimer = null; - } - } - - /** - * Get the current presence state. - * @returns {string} the presence state (see PRESENCE enum) - */ - getState() { - return this.state; - } - - _onAction(payload) { - if (payload.action === 'user_activity') { - this.setState("online"); - this._unavailableTimer.restart(); - } - } - - /** - * Set the presence state. - * If the state has changed, the homeserver will be notified. - * @param {string} newState the new presence state (see PRESENCE enum) - */ - async setState(newState) { - if (newState === this.state) { - return; - } - if (PRESENCE_STATES.indexOf(newState) === -1) { - throw new Error("Bad presence state: " + newState); - } - const oldState = this.state; - this.state = newState; - - if (MatrixClientPeg.get().isGuest()) { - return; // don't try to set presence when a guest; it won't work. - } - - try { - await MatrixClientPeg.get().setPresence(this.state); - console.info("Presence: %s", newState); - } catch (err) { - console.error("Failed to set presence: %s", err); - this.state = oldState; - } - } -} - -export default new Presence(); diff --git a/src/Presence.ts b/src/Presence.ts new file mode 100644 index 0000000000..af35060363 --- /dev/null +++ b/src/Presence.ts @@ -0,0 +1,110 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 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 { MatrixClientPeg } from "./MatrixClientPeg"; +import dis from "./dispatcher/dispatcher"; +import Timer from './utils/Timer'; +import { ActionPayload } from "./dispatcher/payloads"; + +// Time in ms after that a user is considered as unavailable/away +const UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins + +enum State { + Online = "online", + Offline = "offline", + Unavailable = "unavailable", +} + +class Presence { + private unavailableTimer: Timer = null; + private dispatcherRef: string = null; + private state: State = null; + + /** + * Start listening the user activity to evaluate his presence state. + * Any state change will be sent to the homeserver. + */ + public async start() { + this.unavailableTimer = new Timer(UNAVAILABLE_TIME_MS); + // the user_activity_start action starts the timer + this.dispatcherRef = dis.register(this.onAction); + while (this.unavailableTimer) { + try { + await this.unavailableTimer.finished(); + this.setState(State.Unavailable); + } catch (e) { /* aborted, stop got called */ } + } + } + + /** + * Stop tracking user activity + */ + public stop() { + if (this.dispatcherRef) { + dis.unregister(this.dispatcherRef); + this.dispatcherRef = null; + } + if (this.unavailableTimer) { + this.unavailableTimer.abort(); + this.unavailableTimer = null; + } + } + + /** + * Get the current presence state. + * @returns {string} the presence state (see PRESENCE enum) + */ + public getState() { + return this.state; + } + + private onAction = (payload: ActionPayload) => { + if (payload.action === 'user_activity') { + this.setState(State.Online); + this.unavailableTimer.restart(); + } + }; + + /** + * Set the presence state. + * If the state has changed, the homeserver will be notified. + * @param {string} newState the new presence state (see PRESENCE enum) + */ + private async setState(newState: State) { + if (newState === this.state) { + return; + } + + const oldState = this.state; + this.state = newState; + + if (MatrixClientPeg.get().isGuest()) { + return; // don't try to set presence when a guest; it won't work. + } + + try { + await MatrixClientPeg.get().setPresence({ presence: this.state }); + console.info("Presence:", newState); + } catch (err) { + console.error("Failed to set presence:", err); + this.state = oldState; + } + } +} + +export default new Presence(); diff --git a/src/Registration.js b/src/Registration.js index ca162bac03..70dcd38454 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -20,11 +20,10 @@ limitations under the License. * registration code. */ -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import * as sdk from './index'; import Modal from './Modal'; import { _t } from './languageHandler'; -// import {MatrixClientPeg} from './MatrixClientPeg'; // Regex for what a "safe" or "Matrix-looking" localpart would be. // TODO: Update as needed for https://github.com/matrix-org/matrix-doc/issues/1514 @@ -44,70 +43,27 @@ export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/; */ export async function startAnyRegistrationFlow(options) { if (options === undefined) options = {}; - // look for an ILAG compatible flow. We define this as one - // which has only dummy or recaptcha flows. In practice it - // would support any stage InteractiveAuth supports, just not - // ones like email & msisdn which require the user to supply - // the relevant details in advance. We err on the side of - // caution though. - - // XXX: ILAG is disabled for now, - // see https://github.com/vector-im/riot-web/issues/8222 - - // const flows = await _getRegistrationFlows(); - // const hasIlagFlow = flows.some((flow) => { - // return flow.stages.every((stage) => { - // return ['m.login.dummy', 'm.login.recaptcha', 'm.login.terms'].includes(stage); - // }); - // }); - - // if (hasIlagFlow) { - // dis.dispatch({ - // action: 'view_set_mxid', - // go_home_on_cancel: options.go_home_on_cancel, - // }); - //} else { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const modal = Modal.createTrackedDialog('Registration required', '', QuestionDialog, { - hasCancelButton: true, - quitOnly: true, - title: _t("Sign In or Create Account"), - description: _t("Use your account or create a new one to continue."), - button: _t("Create Account"), - extraButtons: [ - , - ], - onFinished: (proceed) => { - if (proceed) { - dis.dispatch({action: 'start_registration', screenAfterLogin: options.screen_after}); - } else if (options.go_home_on_cancel) { - dis.dispatch({action: 'view_home_page'}); - } else if (options.go_welcome_on_cancel) { - dis.dispatch({action: 'view_welcome_page'}); - } - }, - }); - //} + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const modal = Modal.createTrackedDialog('Registration required', '', QuestionDialog, { + hasCancelButton: true, + quitOnly: true, + title: _t("Sign In or Create Account"), + description: _t("Use your account or create a new one to continue."), + button: _t("Create Account"), + extraButtons: [ + , + ], + onFinished: (proceed) => { + if (proceed) { + dis.dispatch({ action: 'start_registration', screenAfterLogin: options.screen_after }); + } else if (options.go_home_on_cancel) { + dis.dispatch({ action: 'view_home_page' }); + } else if (options.go_welcome_on_cancel) { + dis.dispatch({ action: 'view_welcome_page' }); + } + }, + }); } - -// async function _getRegistrationFlows() { -// try { -// await MatrixClientPeg.get().register( -// null, -// null, -// undefined, -// {}, -// {}, -// ); -// console.log("Register request succeeded when it should have returned 401!"); -// } catch (e) { -// if (e.httpStatus === 401) { -// return e.data.flows; -// } -// throw e; -// } -// throw new Error("Register request succeeded when it should have returned 401!"); -// } diff --git a/src/Resend.js b/src/Resend.js deleted file mode 100644 index 6d6c18cf27..0000000000 --- a/src/Resend.js +++ /dev/null @@ -1,61 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 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 {MatrixClientPeg} from './MatrixClientPeg'; -import dis from './dispatcher'; -import { EventStatus } from 'matrix-js-sdk'; - -export default class Resend { - static resendUnsentEvents(room) { - room.getPendingEvents().filter(function(ev) { - return ev.status === EventStatus.NOT_SENT; - }).forEach(function(event) { - Resend.resend(event); - }); - } - - static cancelUnsentEvents(room) { - room.getPendingEvents().filter(function(ev) { - return ev.status === EventStatus.NOT_SENT; - }).forEach(function(event) { - Resend.removeFromQueue(event); - }); - } - - static resend(event) { - const room = MatrixClientPeg.get().getRoom(event.getRoomId()); - MatrixClientPeg.get().resendEvent(event, room).then(function(res) { - dis.dispatch({ - action: 'message_sent', - event: event, - }); - }, function(err) { - // XXX: temporary logging to try to diagnose - // https://github.com/vector-im/riot-web/issues/3148 - console.log('Resend got send failure: ' + err.name + '(' + err + ')'); - - dis.dispatch({ - action: 'message_send_failed', - event: event, - }); - }); - } - - static removeFromQueue(event) { - MatrixClientPeg.get().cancelPendingEvent(event); - } -} diff --git a/src/Resend.ts b/src/Resend.ts new file mode 100644 index 0000000000..38b84a28e0 --- /dev/null +++ b/src/Resend.ts @@ -0,0 +1,62 @@ +/* +Copyright 2015-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 { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event'; +import { Room } from 'matrix-js-sdk/src/models/room'; + +import { MatrixClientPeg } from './MatrixClientPeg'; +import dis from './dispatcher/dispatcher'; + +export default class Resend { + static resendUnsentEvents(room: Room): Promise { + return Promise.all(room.getPendingEvents().filter(function(ev: MatrixEvent) { + return ev.status === EventStatus.NOT_SENT; + }).map(function(event: MatrixEvent) { + return Resend.resend(event); + })); + } + + static cancelUnsentEvents(room: Room): void { + room.getPendingEvents().filter(function(ev: MatrixEvent) { + return ev.status === EventStatus.NOT_SENT; + }).forEach(function(event: MatrixEvent) { + Resend.removeFromQueue(event); + }); + } + + static resend(event: MatrixEvent): Promise { + const room = MatrixClientPeg.get().getRoom(event.getRoomId()); + return MatrixClientPeg.get().resendEvent(event, room).then(function(res) { + dis.dispatch({ + action: 'message_sent', + event: event, + }); + }, function(err: Error) { + // XXX: temporary logging to try to diagnose + // https://github.com/vector-im/element-web/issues/3148 + console.log('Resend got send failure: ' + err.name + '(' + err + ')'); + + dis.dispatch({ + action: 'message_send_failed', + event: event, + }); + }); + } + + static removeFromQueue(event: MatrixEvent): void { + MatrixClientPeg.get().cancelPendingEvent(event); + } +} diff --git a/src/Roles.js b/src/Roles.js deleted file mode 100644 index 7cc3c880d7..0000000000 --- a/src/Roles.js +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -import { _t } from './languageHandler'; - -export function levelRoleMap(usersDefault) { - return { - undefined: _t('Default'), - 0: _t('Restricted'), - [usersDefault]: _t('Default'), - 50: _t('Moderator'), - 100: _t('Admin'), - }; -} - -export function textualPowerLevel(level, usersDefault) { - const LEVEL_ROLE_MAP = levelRoleMap(usersDefault); - if (LEVEL_ROLE_MAP[level]) { - return LEVEL_ROLE_MAP[level]; - } else { - return _t("Custom (%(level)s)", {level}); - } -} diff --git a/src/Roles.ts b/src/Roles.ts new file mode 100644 index 0000000000..ae0d316d30 --- /dev/null +++ b/src/Roles.ts @@ -0,0 +1,36 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { _t } from './languageHandler'; + +export function levelRoleMap(usersDefault: number) { + return { + undefined: _t('Default'), + 0: _t('Restricted'), + [usersDefault]: _t('Default'), + 50: _t('Moderator'), + 100: _t('Admin'), + }; +} + +export function textualPowerLevel(level: number, usersDefault: number): string { + const LEVEL_ROLE_MAP = levelRoleMap(usersDefault); + if (LEVEL_ROLE_MAP[level]) { + return LEVEL_ROLE_MAP[level]; + } else { + return _t("Custom (%(level)s)", { level }); + } +} diff --git a/src/RoomAliasCache.js b/src/RoomAliasCache.js deleted file mode 100644 index bb511ba4d7..0000000000 --- a/src/RoomAliasCache.js +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2019 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. -*/ - -/** - * This is meant to be a cache of room alias to room ID so that moving between - * rooms happens smoothly (for example using browser back / forward buttons). - * - * For the moment, it's in memory only and so only applies for the current - * session for simplicity, but could be extended further in the future. - * - * A similar thing could also be achieved via `pushState` with a state object, - * but keeping it separate like this seems easier in case we do want to extend. - */ -const aliasToIDMap = new Map(); - -export function storeRoomAliasInCache(alias, id) { - aliasToIDMap.set(alias, id); -} - -export function getCachedRoomIDForAlias(alias) { - return aliasToIDMap.get(alias); -} diff --git a/src/RoomAliasCache.ts b/src/RoomAliasCache.ts new file mode 100644 index 0000000000..c318db2d3f --- /dev/null +++ b/src/RoomAliasCache.ts @@ -0,0 +1,35 @@ +/* +Copyright 2019, 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. +*/ + +/** + * This is meant to be a cache of room alias to room ID so that moving between + * rooms happens smoothly (for example using browser back / forward buttons). + * + * For the moment, it's in memory only and so only applies for the current + * session for simplicity, but could be extended further in the future. + * + * A similar thing could also be achieved via `pushState` with a state object, + * but keeping it separate like this seems easier in case we do want to extend. + */ +const aliasToIDMap = new Map(); + +export function storeRoomAliasInCache(alias: string, id: string): void { + aliasToIDMap.set(alias, id); +} + +export function getCachedRoomIDForAlias(alias: string): string { + return aliasToIDMap.get(alias); +} diff --git a/src/RoomInvite.js b/src/RoomInvite.js deleted file mode 100644 index 839d677069..0000000000 --- a/src/RoomInvite.js +++ /dev/null @@ -1,125 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd -Copyright 2017, 2018 New Vector Ltd -Copyright 2020 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 from 'react'; -import {MatrixClientPeg} from './MatrixClientPeg'; -import MultiInviter from './utils/MultiInviter'; -import Modal from './Modal'; -import * as sdk from './'; -import { _t } from './languageHandler'; -import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog"; - -/** - * Invites multiple addresses to a room - * Simpler interface to utils/MultiInviter but with - * no option to cancel. - * - * @param {string} roomId The ID of the room to invite to - * @param {string[]} addrs Array of strings of addresses to invite. May be matrix IDs or 3pids. - * @returns {Promise} Promise - */ -export function inviteMultipleToRoom(roomId, addrs) { - const inviter = new MultiInviter(roomId); - return inviter.invite(addrs).then(states => Promise.resolve({states, inviter})); -} - -export function showStartChatInviteDialog() { - // This dialog handles the room creation internally - we don't need to worry about it. - const InviteDialog = sdk.getComponent("dialogs.InviteDialog"); - Modal.createTrackedDialog( - 'Start DM', '', InviteDialog, {kind: KIND_DM}, - /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, - ); -} - -export function showRoomInviteDialog(roomId) { - // This dialog handles the room creation internally - we don't need to worry about it. - const InviteDialog = sdk.getComponent("dialogs.InviteDialog"); - Modal.createTrackedDialog( - 'Invite Users', '', InviteDialog, {kind: KIND_INVITE, roomId}, - /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, - ); -} - -/** - * Checks if the given MatrixEvent is a valid 3rd party user invite. - * @param {MatrixEvent} event The event to check - * @returns {boolean} True if valid, false otherwise - */ -export function isValid3pidInvite(event) { - if (!event || event.getType() !== "m.room.third_party_invite") return false; - - // any events without these keys are not valid 3pid invites, so we ignore them - const requiredKeys = ['key_validity_url', 'public_key', 'display_name']; - for (let i = 0; i < requiredKeys.length; ++i) { - if (!event.getContent()[requiredKeys[i]]) return false; - } - - // Valid enough by our standards - return true; -} - -export function inviteUsersToRoom(roomId, userIds) { - return inviteMultipleToRoom(roomId, userIds).then((result) => { - const room = MatrixClientPeg.get().getRoom(roomId); - return _showAnyInviteErrors(result.states, room, result.inviter); - }).catch((err) => { - console.error(err.stack); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { - title: _t("Failed to invite"), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - }); -} - -function _showAnyInviteErrors(addrs, room, inviter) { - // Show user any errors - const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error'); - if (failedUsers.length === 1 && inviter.fatal) { - // Just get the first message because there was a fatal problem on the first - // user. This usually means that no other users were attempted, making it - // pointless for us to list who failed exactly. - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to invite users to the room', '', ErrorDialog, { - title: _t("Failed to invite users to the room:", {roomName: room.name}), - description: inviter.getErrorText(failedUsers[0]), - }); - } else { - const errorList = []; - for (const addr of failedUsers) { - if (addrs[addr] === "error") { - const reason = inviter.getErrorText(addr); - errorList.push(addr + ": " + reason); - } - } - - if (errorList.length > 0) { - // React 16 doesn't let us use `errorList.join(
)` anymore, so this is our solution - const description =
{errorList.map(e =>
{e}
)}
; - - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, { - title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), - description, - }); - } - } - - return addrs; -} diff --git a/src/RoomInvite.tsx b/src/RoomInvite.tsx new file mode 100644 index 0000000000..7d093f4092 --- /dev/null +++ b/src/RoomInvite.tsx @@ -0,0 +1,187 @@ +/* +Copyright 2016 - 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 from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { User } from "matrix-js-sdk/src/models/user"; + +import { MatrixClientPeg } from './MatrixClientPeg'; +import MultiInviter, { CompletionStates } from './utils/MultiInviter'; +import Modal from './Modal'; +import { _t } from './languageHandler'; +import InviteDialog, { KIND_DM, KIND_INVITE, Member } from "./components/views/dialogs/InviteDialog"; +import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog"; +import { CommunityPrototypeStore } from "./stores/CommunityPrototypeStore"; +import BaseAvatar from "./components/views/avatars/BaseAvatar"; +import { mediaFromMxc } from "./customisations/Media"; +import ErrorDialog from "./components/views/dialogs/ErrorDialog"; + +export interface IInviteResult { + states: CompletionStates; + inviter: MultiInviter; +} + +/** + * Invites multiple addresses to a room + * Simpler interface to utils/MultiInviter but with + * no option to cancel. + * + * @param {string} roomId The ID of the room to invite to + * @param {string[]} addresses Array of strings of addresses to invite. May be matrix IDs or 3pids. + * @returns {Promise} Promise + */ +export function inviteMultipleToRoom(roomId: string, addresses: string[]): Promise { + const inviter = new MultiInviter(roomId); + return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter })); +} + +export function showStartChatInviteDialog(initialText = ""): void { + // This dialog handles the room creation internally - we don't need to worry about it. + Modal.createTrackedDialog( + 'Start DM', '', InviteDialog, { kind: KIND_DM, initialText }, + /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, + ); +} + +export function showRoomInviteDialog(roomId: string, initialText = ""): void { + // This dialog handles the room creation internally - we don't need to worry about it. + Modal.createTrackedDialog( + "Invite Users", "", InviteDialog, { + kind: KIND_INVITE, + initialText, + roomId, + }, + /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, + ); +} + +export function showCommunityRoomInviteDialog(roomId: string, communityName: string): void { + Modal.createTrackedDialog( + 'Invite Users to Community', '', CommunityPrototypeInviteDialog, { communityName, roomId }, + /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, + ); +} + +export function showCommunityInviteDialog(communityId: string): void { + const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId); + if (chat) { + const name = CommunityPrototypeStore.instance.getCommunityName(communityId); + showCommunityRoomInviteDialog(chat.roomId, name); + } else { + throw new Error("Failed to locate appropriate room to start an invite in"); + } +} + +/** + * Checks if the given MatrixEvent is a valid 3rd party user invite. + * @param {MatrixEvent} event The event to check + * @returns {boolean} True if valid, false otherwise + */ +export function isValid3pidInvite(event: MatrixEvent): boolean { + if (!event || event.getType() !== "m.room.third_party_invite") return false; + + // any events without these keys are not valid 3pid invites, so we ignore them + const requiredKeys = ['key_validity_url', 'public_key', 'display_name']; + for (let i = 0; i < requiredKeys.length; ++i) { + if (!event.getContent()[requiredKeys[i]]) return false; + } + + // Valid enough by our standards + return true; +} + +export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise { + return inviteMultipleToRoom(roomId, userIds).then((result) => { + const room = MatrixClientPeg.get().getRoom(roomId); + showAnyInviteErrors(result.states, room, result.inviter); + }).catch((err) => { + console.error(err.stack); + Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { + title: _t("Failed to invite"), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + }); +} + +export function showAnyInviteErrors( + states: CompletionStates, + room: Room, + inviter: MultiInviter, + userMap?: Map, +): boolean { + // Show user any errors + const failedUsers = Object.keys(states).filter(a => states[a] === 'error'); + if (failedUsers.length === 1 && inviter.fatal) { + // Just get the first message because there was a fatal problem on the first + // user. This usually means that no other users were attempted, making it + // pointless for us to list who failed exactly. + Modal.createTrackedDialog('Failed to invite users to the room', '', ErrorDialog, { + title: _t("Failed to invite users to the room:", { roomName: room.name }), + description: inviter.getErrorText(failedUsers[0]), + }); + return false; + } else { + const errorList = []; + for (const addr of failedUsers) { + if (states[addr] === "error") { + const reason = inviter.getErrorText(addr); + errorList.push(addr + ": " + reason); + } + } + + const cli = MatrixClientPeg.get(); + if (errorList.length > 0) { + // React 16 doesn't let us use `errorList.join(
)` anymore, so this is our solution + const description =
+

{ _t("We sent the others, but the below people couldn't be invited to ", {}, { + RoomName: () => { room.name }, + }) }

+
+ { failedUsers.map(addr => { + const user = userMap?.get(addr) || cli.getUser(addr); + const name = (user as Member).name || (user as User).rawDisplayName; + const avatarUrl = (user as Member).getMxcAvatarUrl?.() || (user as User).avatarUrl; + return
+
+ + { name } + { user.userId } +
+
+ { inviter.getErrorText(addr) } +
+
; + }) } +
+
; + + Modal.createTrackedDialog("Some invites could not be sent", "", ErrorDialog, { + title: _t("Some invites couldn't be sent"), + description, + }); + return false; + } + } + + return true; +} diff --git a/src/RoomListSorter.js b/src/RoomListSorter.js deleted file mode 100644 index 0ff37a6af2..0000000000 --- a/src/RoomListSorter.js +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -'use strict'; - -function tsOfNewestEvent(room) { - if (room.timeline.length) { - return room.timeline[room.timeline.length - 1].getTs(); - } else { - return Number.MAX_SAFE_INTEGER; - } -} - -export function mostRecentActivityFirst(roomList) { - return roomList.sort(function(a, b) { - return tsOfNewestEvent(b) - tsOfNewestEvent(a); - }); -} diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js index c67acaf314..5d109094af 100644 --- a/src/RoomNotifs.js +++ b/src/RoomNotifs.js @@ -15,8 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClientPeg} from './MatrixClientPeg'; -import {PushProcessor} from 'matrix-js-sdk/src/pushprocessor'; +import { MatrixClientPeg } from './MatrixClientPeg'; +import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor'; export const ALL_MESSAGES_LOUD = 'all_messages_loud'; export const ALL_MESSAGES = 'all_messages'; @@ -34,32 +34,12 @@ export function shouldShowMentionBadge(roomNotifState) { return MENTION_BADGE_STATES.includes(roomNotifState); } -export function countRoomsWithNotif(rooms) { - return rooms.reduce((result, room, index) => { - const roomNotifState = getRoomNotifsState(room.roomId); - const highlight = room.getUnreadNotificationCount('highlight') > 0; - const notificationCount = room.getUnreadNotificationCount(); - - const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState); - const mentionBadges = highlight && shouldShowMentionBadge(roomNotifState); - const isInvite = room.hasMembershipState(MatrixClientPeg.get().credentials.userId, 'invite'); - const badges = notifBadges || mentionBadges || isInvite; - - if (badges) { - result.count++; - if (highlight) { - result.highlight = true; - } - } - return result; - }, {count: 0, highlight: false}); -} - export function aggregateNotificationCount(rooms) { - return rooms.reduce((result, room, index) => { + return rooms.reduce((result, room) => { const roomNotifState = getRoomNotifsState(room.roomId); const highlight = room.getUnreadNotificationCount('highlight') > 0; - const notificationCount = room.getUnreadNotificationCount(); + // use helper method to include highlights in the previous version of the room + const notificationCount = getUnreadNotificationCount(room); const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState); const mentionBadges = highlight && shouldShowMentionBadge(roomNotifState); @@ -72,7 +52,7 @@ export function aggregateNotificationCount(rooms) { } } return result; - }, {count: 0, highlight: false}); + }, { count: 0, highlight: false }); } export function getRoomHasBadge(room) { @@ -222,12 +202,13 @@ function setRoomNotifsStateUnmuted(roomId, newState) { } function findOverrideMuteRule(roomId) { - if (!MatrixClientPeg.get().pushRules || - !MatrixClientPeg.get().pushRules['global'] || - !MatrixClientPeg.get().pushRules['global'].override) { + const cli = MatrixClientPeg.get(); + if (!cli.pushRules || + !cli.pushRules['global'] || + !cli.pushRules['global'].override) { return null; } - for (const rule of MatrixClientPeg.get().pushRules['global'].override) { + for (const rule of cli.pushRules['global'].override) { if (isRuleForRoom(roomId, rule)) { if (isMuteRule(rule) && rule.enabled) { return rule; diff --git a/src/RoomNotifsTypes.ts b/src/RoomNotifsTypes.ts new file mode 100644 index 0000000000..0e7093e434 --- /dev/null +++ b/src/RoomNotifsTypes.ts @@ -0,0 +1,24 @@ +/* +Copyright 2020 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 { + ALL_MESSAGES, + ALL_MESSAGES_LOUD, + MENTIONS_ONLY, + MUTE, +} from "./RoomNotifs"; + +export type Volume = ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE; diff --git a/src/Rooms.js b/src/Rooms.js deleted file mode 100644 index 218e970f35..0000000000 --- a/src/Rooms.js +++ /dev/null @@ -1,193 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import {MatrixClientPeg} from './MatrixClientPeg'; - -/** - * Given a room object, return the alias we should use for it, - * if any. This could be the canonical alias if one exists, otherwise - * an alias selected arbitrarily but deterministically from the list - * of aliases. Otherwise return null; - */ -export function getDisplayAliasForRoom(room) { - return room.getCanonicalAlias() || room.getAltAliases()[0]; -} - -/** - * If the room contains only two members including the logged-in user, - * return the other one. Otherwise, return null. - */ -export function getOnlyOtherMember(room, myUserId) { - if (room.currentState.getJoinedMemberCount() === 2) { - return room.getJoinedMembers().filter(function(m) { - return m.userId !== myUserId; - })[0]; - } - - return null; -} - -function _isConfCallRoom(room, myUserId, conferenceHandler) { - if (!conferenceHandler) return false; - - const myMembership = room.getMyMembership(); - if (myMembership != "join") { - return false; - } - - const otherMember = getOnlyOtherMember(room, myUserId); - if (!otherMember) { - return false; - } - - if (conferenceHandler.isConferenceUser(otherMember.userId)) { - return true; - } - - return false; -} - -// Cache whether a room is a conference call. Assumes that rooms will always -// either will or will not be a conference call room. -const isConfCallRoomCache = { - // $roomId: bool -}; - -export function isConfCallRoom(room, myUserId, conferenceHandler) { - if (isConfCallRoomCache[room.roomId] !== undefined) { - return isConfCallRoomCache[room.roomId]; - } - - const result = _isConfCallRoom(room, myUserId, conferenceHandler); - - isConfCallRoomCache[room.roomId] = result; - - return result; -} - -export function looksLikeDirectMessageRoom(room, myUserId) { - const myMembership = room.getMyMembership(); - const me = room.getMember(myUserId); - - if (myMembership == "join" || myMembership === "ban" || (me && me.isKicked())) { - // Used to split rooms via tags - const tagNames = Object.keys(room.tags); - // Used for 1:1 direct chats - // Show 1:1 chats in seperate "Direct Messages" section as long as they haven't - // been moved to a different tag section - const totalMemberCount = room.currentState.getJoinedMemberCount() + - room.currentState.getInvitedMemberCount(); - if (totalMemberCount === 2 && !tagNames.length) { - return true; - } - } - return false; -} - -export function guessAndSetDMRoom(room, isDirect) { - let newTarget; - if (isDirect) { - const guessedUserId = guessDMRoomTargetId( - room, MatrixClientPeg.get().getUserId(), - ); - newTarget = guessedUserId; - } else { - newTarget = null; - } - - return setDMRoom(room.roomId, newTarget); -} - -/** - * Marks or unmarks the given room as being as a DM room. - * @param {string} roomId The ID of the room to modify - * @param {string} userId The user ID of the desired DM - room target user or null to un-mark - this room as a DM room - * @returns {object} A promise - */ -export function setDMRoom(roomId, userId) { - if (MatrixClientPeg.get().isGuest()) { - return Promise.resolve(); - } - - const mDirectEvent = MatrixClientPeg.get().getAccountData('m.direct'); - let dmRoomMap = {}; - - if (mDirectEvent !== undefined) dmRoomMap = mDirectEvent.getContent(); - - // remove it from the lists of any others users - // (it can only be a DM room for one person) - for (const thisUserId of Object.keys(dmRoomMap)) { - const roomList = dmRoomMap[thisUserId]; - - if (thisUserId != userId) { - const indexOfRoom = roomList.indexOf(roomId); - if (indexOfRoom > -1) { - roomList.splice(indexOfRoom, 1); - } - } - } - - // now add it, if it's not already there - if (userId) { - const roomList = dmRoomMap[userId] || []; - if (roomList.indexOf(roomId) == -1) { - roomList.push(roomId); - } - dmRoomMap[userId] = roomList; - } - - - return MatrixClientPeg.get().setAccountData('m.direct', dmRoomMap); -} - -/** - * Given a room, estimate which of its members is likely to - * be the target if the room were a DM room and return that user. - * - * @param {Object} room Target room - * @param {string} myUserId User ID of the current user - * @returns {string} User ID of the user that the room is probably a DM with - */ -function guessDMRoomTargetId(room, myUserId) { - let oldestTs; - let oldestUser; - - // Pick the joined user who's been here longest (and isn't us), - for (const user of room.getJoinedMembers()) { - if (user.userId == myUserId) continue; - - if (oldestTs === undefined || (user.events.member && user.events.member.getTs() < oldestTs)) { - oldestUser = user; - oldestTs = user.events.member.getTs(); - } - } - if (oldestUser) return oldestUser.userId; - - // if there are no joined members other than us, use the oldest member - for (const user of room.currentState.getMembers()) { - if (user.userId == myUserId) continue; - - if (oldestTs === undefined || (user.events.member && user.events.member.getTs() < oldestTs)) { - oldestUser = user; - oldestTs = user.events.member.getTs(); - } - } - - if (oldestUser === undefined) return myUserId; - return oldestUser.userId; -} diff --git a/src/Rooms.ts b/src/Rooms.ts new file mode 100644 index 0000000000..6e2fd4d3a2 --- /dev/null +++ b/src/Rooms.ts @@ -0,0 +1,155 @@ +/* +Copyright 2015 - 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 { Room } from "matrix-js-sdk/src/models/room"; + +import { MatrixClientPeg } from './MatrixClientPeg'; +import AliasCustomisations from './customisations/Alias'; + +/** + * Given a room object, return the alias we should use for it, + * if any. This could be the canonical alias if one exists, otherwise + * an alias selected arbitrarily but deterministically from the list + * of aliases. Otherwise return null; + * + * @param {Object} room The room object + * @returns {string} A display alias for the given room + */ +export function getDisplayAliasForRoom(room: Room): string { + return getDisplayAliasForAliasSet( + room.getCanonicalAlias(), room.getAltAliases(), + ); +} + +// The various display alias getters should all feed through this one path so +// there's a single place to change the logic. +export function getDisplayAliasForAliasSet(canonicalAlias: string, altAliases: string[]): string { + if (AliasCustomisations.getDisplayAliasForAliasSet) { + return AliasCustomisations.getDisplayAliasForAliasSet(canonicalAlias, altAliases); + } + return canonicalAlias || altAliases?.[0]; +} + +export function looksLikeDirectMessageRoom(room: Room, myUserId: string): boolean { + const myMembership = room.getMyMembership(); + const me = room.getMember(myUserId); + + if (myMembership == "join" || myMembership === "ban" || (me && me.isKicked())) { + // Used to split rooms via tags + const tagNames = Object.keys(room.tags); + // Used for 1:1 direct chats + // Show 1:1 chats in seperate "Direct Messages" section as long as they haven't + // been moved to a different tag section + const totalMemberCount = room.currentState.getJoinedMemberCount() + + room.currentState.getInvitedMemberCount(); + if (totalMemberCount === 2 && !tagNames.length) { + return true; + } + } + return false; +} + +export function guessAndSetDMRoom(room: Room, isDirect: boolean): Promise { + let newTarget; + if (isDirect) { + const guessedUserId = guessDMRoomTargetId( + room, MatrixClientPeg.get().getUserId(), + ); + newTarget = guessedUserId; + } else { + newTarget = null; + } + + return setDMRoom(room.roomId, newTarget); +} + +/** + * Marks or unmarks the given room as being as a DM room. + * @param {string} roomId The ID of the room to modify + * @param {string} userId The user ID of the desired DM + room target user or null to un-mark + this room as a DM room + * @returns {object} A promise + */ +export async function setDMRoom(roomId: string, userId: string): Promise { + if (MatrixClientPeg.get().isGuest()) return; + + const mDirectEvent = MatrixClientPeg.get().getAccountData('m.direct'); + let dmRoomMap = {}; + + if (mDirectEvent !== undefined) dmRoomMap = mDirectEvent.getContent(); + + // remove it from the lists of any others users + // (it can only be a DM room for one person) + for (const thisUserId of Object.keys(dmRoomMap)) { + const roomList = dmRoomMap[thisUserId]; + + if (thisUserId != userId) { + const indexOfRoom = roomList.indexOf(roomId); + if (indexOfRoom > -1) { + roomList.splice(indexOfRoom, 1); + } + } + } + + // now add it, if it's not already there + if (userId) { + const roomList = dmRoomMap[userId] || []; + if (roomList.indexOf(roomId) == -1) { + roomList.push(roomId); + } + dmRoomMap[userId] = roomList; + } + + await MatrixClientPeg.get().setAccountData('m.direct', dmRoomMap); +} + +/** + * Given a room, estimate which of its members is likely to + * be the target if the room were a DM room and return that user. + * + * @param {Object} room Target room + * @param {string} myUserId User ID of the current user + * @returns {string} User ID of the user that the room is probably a DM with + */ +function guessDMRoomTargetId(room: Room, myUserId: string): string { + let oldestTs; + let oldestUser; + + // Pick the joined user who's been here longest (and isn't us), + for (const user of room.getJoinedMembers()) { + if (user.userId == myUserId) continue; + + if (oldestTs === undefined || (user.events.member && user.events.member.getTs() < oldestTs)) { + oldestUser = user; + oldestTs = user.events.member.getTs(); + } + } + if (oldestUser) return oldestUser.userId; + + // if there are no joined members other than us, use the oldest member + for (const user of room.currentState.getMembers()) { + if (user.userId == myUserId) continue; + + if (oldestTs === undefined || (user.events.member && user.events.member.getTs() < oldestTs)) { + oldestUser = user; + oldestTs = user.events.member.getTs(); + } + } + + if (oldestUser === undefined) return myUserId; + return oldestUser.userId; +} diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js deleted file mode 100644 index 819fe3c998..0000000000 --- a/src/ScalarAuthClient.js +++ /dev/null @@ -1,287 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd -Copyright 2019 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 url from 'url'; -import SettingsStore from "./settings/SettingsStore"; -import { Service, startTermsFlow, TermsNotSignedError } from './Terms'; -import {MatrixClientPeg} from "./MatrixClientPeg"; -import request from "browser-request"; - -import * as Matrix from 'matrix-js-sdk'; -import SdkConfig from "./SdkConfig"; - -// The version of the integration manager API we're intending to work with -const imApiVersion = "1.1"; - -export default class ScalarAuthClient { - constructor(apiUrl, uiUrl) { - this.apiUrl = apiUrl; - this.uiUrl = uiUrl; - this.scalarToken = null; - // `undefined` to allow `startTermsFlow` to fallback to a default - // callback if this is unset. - this.termsInteractionCallback = undefined; - - // We try and store the token on a per-manager basis, but need a fallback - // for the default manager. - const configApiUrl = SdkConfig.get()['integrations_rest_url']; - const configUiUrl = SdkConfig.get()['integrations_ui_url']; - this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl; - } - - _writeTokenToStore() { - window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken); - if (this.isDefaultManager) { - // We remove the old token from storage to migrate upwards. This is safe - // to do because even if the user switches to /app when this is on /develop - // they'll at worst register for a new token. - window.localStorage.removeItem("mx_scalar_token"); // no-op when not present - } - } - - _readTokenFromStore() { - let token = window.localStorage.getItem("mx_scalar_token_at_" + this.apiUrl); - if (!token && this.isDefaultManager) { - token = window.localStorage.getItem("mx_scalar_token"); - } - return token; - } - - _readToken() { - if (this.scalarToken) return this.scalarToken; - return this._readTokenFromStore(); - } - - setTermsInteractionCallback(callback) { - this.termsInteractionCallback = callback; - } - - connect() { - return this.getScalarToken().then((tok) => { - this.scalarToken = tok; - }); - } - - hasCredentials() { - return this.scalarToken != null; // undef or null - } - - // Returns a promise that resolves to a scalar_token string - getScalarToken() { - const token = this._readToken(); - - if (!token) { - return this.registerForToken(); - } else { - return this._checkToken(token).catch((e) => { - if (e instanceof TermsNotSignedError) { - // retrying won't help this - throw e; - } - return this.registerForToken(); - }); - } - } - - _getAccountName(token) { - const url = this.apiUrl + "/account"; - - return new Promise(function(resolve, reject) { - request({ - method: "GET", - uri: url, - qs: {scalar_token: token, v: imApiVersion}, - json: true, - }, (err, response, body) => { - if (err) { - reject(err); - } else if (body && body.errcode === 'M_TERMS_NOT_SIGNED') { - reject(new TermsNotSignedError()); - } else if (response.statusCode / 100 !== 2) { - reject(body); - } else if (!body || !body.user_id) { - reject(new Error("Missing user_id in response")); - } else { - resolve(body.user_id); - } - }); - }); - } - - _checkToken(token) { - return this._getAccountName(token).then(userId => { - const me = MatrixClientPeg.get().getUserId(); - if (userId !== me) { - throw new Error("Scalar token is owned by someone else: " + me); - } - return token; - }).catch((e) => { - if (e instanceof TermsNotSignedError) { - console.log("Integration manager requires new terms to be agreed to"); - // The terms endpoints are new and so live on standard _matrix prefixes, - // but IM rest urls are currently configured with paths, so remove the - // path from the base URL before passing it to the js-sdk - - // We continue to use the full URL for the calls done by - // matrix-react-sdk, but the standard terms API called - // by the js-sdk lives on the standard _matrix path. This means we - // don't support running IMs on a non-root path, but it's the only - // realistic way of transitioning to _matrix paths since configs in - // the wild contain bits of the API path. - - // Once we've fully transitioned to _matrix URLs, we can give people - // a grace period to update their configs, then use the rest url as - // a regular base url. - const parsedImRestUrl = url.parse(this.apiUrl); - parsedImRestUrl.path = ''; - parsedImRestUrl.pathname = ''; - return startTermsFlow([new Service( - Matrix.SERVICE_TYPES.IM, - parsedImRestUrl.format(), - token, - )], this.termsInteractionCallback).then(() => { - return token; - }); - } else { - throw e; - } - }); - } - - registerForToken() { - // Get openid bearer token from the HS as the first part of our dance - return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => { - // Now we can send that to scalar and exchange it for a scalar token - return this.exchangeForScalarToken(tokenObject); - }).then((token) => { - // Validate it (this mostly checks to see if the IM needs us to agree to some terms) - return this._checkToken(token); - }).then((token) => { - this.scalarToken = token; - this._writeTokenToStore(); - return token; - }); - } - - exchangeForScalarToken(openidTokenObject) { - const scalarRestUrl = this.apiUrl; - - return new Promise(function(resolve, reject) { - request({ - method: 'POST', - uri: scalarRestUrl + '/register', - qs: {v: imApiVersion}, - body: openidTokenObject, - json: true, - }, (err, response, body) => { - if (err) { - reject(err); - } else if (response.statusCode / 100 !== 2) { - reject({statusCode: response.statusCode}); - } else if (!body || !body.scalar_token) { - reject(new Error("Missing scalar_token in response")); - } else { - resolve(body.scalar_token); - } - }); - }); - } - - getScalarPageTitle(url) { - let scalarPageLookupUrl = this.apiUrl + '/widgets/title_lookup'; - scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl); - scalarPageLookupUrl += '&curl=' + encodeURIComponent(url); - - return new Promise(function(resolve, reject) { - request({ - method: 'GET', - uri: scalarPageLookupUrl, - json: true, - }, (err, response, body) => { - if (err) { - reject(err); - } else if (response.statusCode / 100 !== 2) { - reject({statusCode: response.statusCode}); - } else if (!body) { - reject(new Error("Missing page title in response")); - } else { - let title = ""; - if (body.page_title_cache_item && body.page_title_cache_item.cached_title) { - title = body.page_title_cache_item.cached_title; - } - resolve(title); - } - }); - }); - } - - /** - * Mark all assets associated with the specified widget as "disabled" in the - * integration manager database. - * This can be useful to temporarily prevent purchased assets from being displayed. - * @param {string} widgetType [description] - * @param {string} widgetId [description] - * @return {Promise} Resolves on completion - */ - disableWidgetAssets(widgetType, widgetId) { - let url = this.apiUrl + '/widgets/set_assets_state'; - url = this.getStarterLink(url); - return new Promise((resolve, reject) => { - request({ - method: 'GET', - uri: url, - json: true, - qs: { - 'widget_type': widgetType, - 'widget_id': widgetId, - 'state': 'disable', - }, - }, (err, response, body) => { - if (err) { - reject(err); - } else if (response.statusCode / 100 !== 2) { - reject({statusCode: response.statusCode}); - } else if (!body) { - reject(new Error("Failed to set widget assets state")); - } else { - resolve(); - } - }); - }); - } - - getScalarInterfaceUrlForRoom(room, screen, id) { - const roomId = room.roomId; - const roomName = room.name; - let url = this.uiUrl; - url += "?scalar_token=" + encodeURIComponent(this.scalarToken); - url += "&room_id=" + encodeURIComponent(roomId); - url += "&room_name=" + encodeURIComponent(roomName); - url += "&theme=" + encodeURIComponent(SettingsStore.getValue("theme")); - if (id) { - url += '&integ_id=' + encodeURIComponent(id); - } - if (screen) { - url += '&screen=' + encodeURIComponent(screen); - } - return url; - } - - getStarterLink(starterLinkUrl) { - return starterLinkUrl + "?scalar_token=" + encodeURIComponent(this.scalarToken); - } -} diff --git a/src/ScalarAuthClient.ts b/src/ScalarAuthClient.ts new file mode 100644 index 0000000000..86dd4b7a0f --- /dev/null +++ b/src/ScalarAuthClient.ts @@ -0,0 +1,292 @@ +/* +Copyright 2016, 2019, 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 url from 'url'; +import SettingsStore from "./settings/SettingsStore"; +import { Service, startTermsFlow, TermsInteractionCallback, TermsNotSignedError } from './Terms'; +import { MatrixClientPeg } from "./MatrixClientPeg"; +import request from "browser-request"; + +import SdkConfig from "./SdkConfig"; +import { WidgetType } from "./widgets/WidgetType"; +import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types"; +import { Room } from "matrix-js-sdk/src/models/room"; + +// The version of the integration manager API we're intending to work with +const imApiVersion = "1.1"; + +// TODO: Generify the name of this class and all components within - it's not just for Scalar. + +export default class ScalarAuthClient { + private scalarToken: string; + private termsInteractionCallback: TermsInteractionCallback; + private isDefaultManager: boolean; + + constructor(private apiUrl: string, private uiUrl: string) { + this.scalarToken = null; + // `undefined` to allow `startTermsFlow` to fallback to a default + // callback if this is unset. + this.termsInteractionCallback = undefined; + + // We try and store the token on a per-manager basis, but need a fallback + // for the default manager. + const configApiUrl = SdkConfig.get()['integrations_rest_url']; + const configUiUrl = SdkConfig.get()['integrations_ui_url']; + this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl; + } + + private writeTokenToStore() { + window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken); + if (this.isDefaultManager) { + // We remove the old token from storage to migrate upwards. This is safe + // to do because even if the user switches to /app when this is on /develop + // they'll at worst register for a new token. + window.localStorage.removeItem("mx_scalar_token"); // no-op when not present + } + } + + private readTokenFromStore(): string { + let token = window.localStorage.getItem("mx_scalar_token_at_" + this.apiUrl); + if (!token && this.isDefaultManager) { + token = window.localStorage.getItem("mx_scalar_token"); + } + return token; + } + + private readToken(): string { + if (this.scalarToken) return this.scalarToken; + return this.readTokenFromStore(); + } + + setTermsInteractionCallback(callback) { + this.termsInteractionCallback = callback; + } + + connect(): Promise { + return this.getScalarToken().then((tok) => { + this.scalarToken = tok; + }); + } + + hasCredentials(): boolean { + return this.scalarToken != null; // undef or null + } + + // Returns a promise that resolves to a scalar_token string + getScalarToken(): Promise { + const token = this.readToken(); + + if (!token) { + return this.registerForToken(); + } else { + return this.checkToken(token).catch((e) => { + if (e instanceof TermsNotSignedError) { + // retrying won't help this + throw e; + } + return this.registerForToken(); + }); + } + } + + private getAccountName(token: string): Promise { + const url = this.apiUrl + "/account"; + + return new Promise(function(resolve, reject) { + request({ + method: "GET", + uri: url, + qs: { scalar_token: token, v: imApiVersion }, + json: true, + }, (err, response, body) => { + if (err) { + reject(err); + } else if (body && body.errcode === 'M_TERMS_NOT_SIGNED') { + reject(new TermsNotSignedError()); + } else if (response.statusCode / 100 !== 2) { + reject(body); + } else if (!body || !body.user_id) { + reject(new Error("Missing user_id in response")); + } else { + resolve(body.user_id); + } + }); + }); + } + + private checkToken(token: string): Promise { + return this.getAccountName(token).then(userId => { + const me = MatrixClientPeg.get().getUserId(); + if (userId !== me) { + throw new Error("Scalar token is owned by someone else: " + me); + } + return token; + }).catch((e) => { + if (e instanceof TermsNotSignedError) { + console.log("Integration manager requires new terms to be agreed to"); + // The terms endpoints are new and so live on standard _matrix prefixes, + // but IM rest urls are currently configured with paths, so remove the + // path from the base URL before passing it to the js-sdk + + // We continue to use the full URL for the calls done by + // matrix-react-sdk, but the standard terms API called + // by the js-sdk lives on the standard _matrix path. This means we + // don't support running IMs on a non-root path, but it's the only + // realistic way of transitioning to _matrix paths since configs in + // the wild contain bits of the API path. + + // Once we've fully transitioned to _matrix URLs, we can give people + // a grace period to update their configs, then use the rest url as + // a regular base url. + const parsedImRestUrl = url.parse(this.apiUrl); + parsedImRestUrl.path = ''; + parsedImRestUrl.pathname = ''; + return startTermsFlow([new Service( + SERVICE_TYPES.IM, + url.format(parsedImRestUrl), + token, + )], this.termsInteractionCallback).then(() => { + return token; + }); + } else { + throw e; + } + }); + } + + registerForToken(): Promise { + // Get openid bearer token from the HS as the first part of our dance + return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => { + // Now we can send that to scalar and exchange it for a scalar token + return this.exchangeForScalarToken(tokenObject); + }).then((token) => { + // Validate it (this mostly checks to see if the IM needs us to agree to some terms) + return this.checkToken(token); + }).then((token) => { + this.scalarToken = token; + this.writeTokenToStore(); + return token; + }); + } + + exchangeForScalarToken(openidTokenObject: any): Promise { + const scalarRestUrl = this.apiUrl; + + return new Promise(function(resolve, reject) { + request({ + method: 'POST', + uri: scalarRestUrl + '/register', + qs: { v: imApiVersion }, + body: openidTokenObject, + json: true, + }, (err, response, body) => { + if (err) { + reject(err); + } else if (response.statusCode / 100 !== 2) { + reject(new Error(`Scalar request failed: ${response.statusCode}`)); + } else if (!body || !body.scalar_token) { + reject(new Error("Missing scalar_token in response")); + } else { + resolve(body.scalar_token); + } + }); + }); + } + + getScalarPageTitle(url: string): Promise { + let scalarPageLookupUrl = this.apiUrl + '/widgets/title_lookup'; + scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl); + scalarPageLookupUrl += '&curl=' + encodeURIComponent(url); + + return new Promise(function(resolve, reject) { + request({ + method: 'GET', + uri: scalarPageLookupUrl, + json: true, + }, (err, response, body) => { + if (err) { + reject(err); + } else if (response.statusCode / 100 !== 2) { + reject(new Error(`Scalar request failed: ${response.statusCode}`)); + } else if (!body) { + reject(new Error("Missing page title in response")); + } else { + let title = ""; + if (body.page_title_cache_item && body.page_title_cache_item.cached_title) { + title = body.page_title_cache_item.cached_title; + } + resolve(title); + } + }); + }); + } + + /** + * Mark all assets associated with the specified widget as "disabled" in the + * integration manager database. + * This can be useful to temporarily prevent purchased assets from being displayed. + * @param {WidgetType} widgetType The Widget Type to disable assets for + * @param {string} widgetId The widget ID to disable assets for + * @return {Promise} Resolves on completion + */ + disableWidgetAssets(widgetType: WidgetType, widgetId: string): Promise { + let url = this.apiUrl + '/widgets/set_assets_state'; + url = this.getStarterLink(url); + return new Promise((resolve, reject) => { + request({ + method: 'GET', // XXX: Actions shouldn't be GET requests + uri: url, + json: true, + qs: { + 'widget_type': widgetType.preferred, + 'widget_id': widgetId, + 'state': 'disable', + }, + }, (err, response, body) => { + if (err) { + reject(err); + } else if (response.statusCode / 100 !== 2) { + reject(new Error(`Scalar request failed: ${response.statusCode}`)); + } else if (!body) { + reject(new Error("Failed to set widget assets state")); + } else { + resolve(); + } + }); + }); + } + + getScalarInterfaceUrlForRoom(room: Room, screen: string, id: string): string { + const roomId = room.roomId; + const roomName = room.name; + let url = this.uiUrl; + url += "?scalar_token=" + encodeURIComponent(this.scalarToken); + url += "&room_id=" + encodeURIComponent(roomId); + url += "&room_name=" + encodeURIComponent(roomName); + url += "&theme=" + encodeURIComponent(SettingsStore.getValue("theme")); + if (id) { + url += '&integ_id=' + encodeURIComponent(id); + } + if (screen) { + url += '&screen=' + encodeURIComponent(screen); + } + return url; + } + + getStarterLink(starterLinkUrl: string): string { + return starterLinkUrl + "?scalar_token=" + encodeURIComponent(this.scalarToken); + } +} diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 2211e513c3..600241bc06 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -16,6 +16,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +// TODO: Generify the name of this and all components within - it's not just for scalar. + /* Listens for incoming postMessage requests from the integrations UI URL. The following API is exposed: { @@ -172,6 +174,7 @@ Request: Response: [ { + // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) type: "im.vector.modular.widgets", state_key: "wid1", content: { @@ -190,6 +193,7 @@ Example: room_id: "!foo:bar", response: [ { + // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) type: "im.vector.modular.widgets", state_key: "wid1", content: { @@ -204,7 +208,6 @@ Example: ] } - membership_state AND bot_options -------------------------------- Get the content of the "m.room.member" or "m.room.bot.options" state event respectively. @@ -232,23 +235,25 @@ Example: } */ -import {MatrixClientPeg} from './MatrixClientPeg'; -import { MatrixEvent } from 'matrix-js-sdk'; -import dis from './dispatcher'; +import { MatrixClientPeg } from './MatrixClientPeg'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import dis from './dispatcher/dispatcher'; import WidgetUtils from './utils/WidgetUtils'; import RoomViewStore from './stores/RoomViewStore'; import { _t } from './languageHandler'; -import {IntegrationManagers} from "./integrations/IntegrationManagers"; +import { IntegrationManagers } from "./integrations/IntegrationManagers"; +import { WidgetType } from "./widgets/WidgetType"; +import { objectClone } from "./utils/objects"; function sendResponse(event, res) { - const data = JSON.parse(JSON.stringify(event.data)); + const data = objectClone(event.data); data.response = res; event.source.postMessage(data, event.origin); } function sendError(event, msg, nestedError) { console.error("Action:" + event.data.action + " failed with message: " + msg); - const data = JSON.parse(JSON.stringify(event.data)); + const data = objectClone(event.data); data.response = { error: { message: msg, @@ -290,7 +295,7 @@ function inviteUser(event, roomId, userId) { function setWidget(event, roomId) { const widgetId = event.data.widget_id; - const widgetType = event.data.type; + let widgetType = event.data.type; const widgetUrl = event.data.url; const widgetName = event.data.name; // optional const widgetData = event.data.data; // optional @@ -322,6 +327,9 @@ function setWidget(event, roomId) { } } + // convert the widget type to a known widget type + widgetType = WidgetType.fromString(widgetType); + if (userWidget) { WidgetUtils.setUserWidget(widgetId, widgetType, widgetUrl, widgetName, widgetData).then(() => { sendResponse(event, { @@ -599,7 +607,7 @@ const onMessage = function(event) { } if (roomId !== RoomViewStore.getRoomId()) { - sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId})); + sendError(event, _t('Room %(roomId)s not visible', { roomId: roomId })); return; } diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index 400d29a20f..7d7caa2d24 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -1,6 +1,6 @@ /* Copyright 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 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. @@ -20,6 +20,8 @@ export interface ConfigOptions { } export const DEFAULTS: ConfigOptions = { + // Brand name of the app + brand: "Element", // URL to a page we show in an iframe to configure integrations integrations_ui_url: "https://scalar.vector.im/", // Base URL to the REST interface of the integrations server @@ -31,6 +33,11 @@ export const DEFAULTS: ConfigOptions = { // Default conference domain preferredDomain: "jitsi.riot.im", }, + desktopBuilds: { + available: true, + logo: require("../res/img/element-desktop-logo.svg"), + url: "https://element.io/get-started", + }, }; export default class SdkConfig { diff --git a/src/Searching.js b/src/Searching.js deleted file mode 100644 index 663328fe41..0000000000 --- a/src/Searching.js +++ /dev/null @@ -1,140 +0,0 @@ -/* -Copyright 2019 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 EventIndexPeg from "./indexing/EventIndexPeg"; -import {MatrixClientPeg} from "./MatrixClientPeg"; - -function serverSideSearch(term, roomId = undefined) { - let filter; - if (roomId !== undefined) { - // XXX: it's unintuitive that the filter for searching doesn't have - // the same shape as the v2 filter API :( - filter = { - rooms: [roomId], - }; - } - - const searchPromise = MatrixClientPeg.get().searchRoomEvents({ - filter, - term, - }); - - return searchPromise; -} - -async function combinedSearch(searchTerm) { - // Create two promises, one for the local search, one for the - // server-side search. - const serverSidePromise = serverSideSearch(searchTerm); - const localPromise = localSearch(searchTerm); - - // Wait for both promises to resolve. - await Promise.all([serverSidePromise, localPromise]); - - // Get both search results. - const localResult = await localPromise; - const serverSideResult = await serverSidePromise; - - // Combine the search results into one result. - const result = {}; - - // Our localResult and serverSideResult are both ordered by - // recency separately, when we combine them the order might not - // be the right one so we need to sort them. - const compare = (a, b) => { - const aEvent = a.context.getEvent().event; - const bEvent = b.context.getEvent().event; - - if (aEvent.origin_server_ts > - bEvent.origin_server_ts) return -1; - if (aEvent.origin_server_ts < - bEvent.origin_server_ts) return 1; - return 0; - }; - - result.count = localResult.count + serverSideResult.count; - result.results = localResult.results.concat( - serverSideResult.results).sort(compare); - result.highlights = localResult.highlights.concat( - serverSideResult.highlights); - - return result; -} - -async function localSearch(searchTerm, roomId = undefined) { - const searchArgs = { - search_term: searchTerm, - before_limit: 1, - after_limit: 1, - order_by_recency: true, - room_id: undefined, - }; - - if (roomId !== undefined) { - searchArgs.room_id = roomId; - } - - const emptyResult = { - results: [], - highlights: [], - }; - - if (searchTerm === "") return emptyResult; - - const eventIndex = EventIndexPeg.get(); - - const localResult = await eventIndex.search(searchArgs); - - const response = { - search_categories: { - room_events: localResult, - }, - }; - - const result = MatrixClientPeg.get()._processRoomEventsSearch( - emptyResult, response); - - return result; -} - -function eventIndexSearch(term, roomId = undefined) { - let searchPromise; - - if (roomId !== undefined) { - if (MatrixClientPeg.get().isRoomEncrypted(roomId)) { - // The search is for a single encrypted room, use our local - // search method. - searchPromise = localSearch(term, roomId); - } else { - // The search is for a single non-encrypted room, use the - // server-side search. - searchPromise = serverSideSearch(term, roomId); - } - } else { - // Search across all rooms, combine a server side search and a - // local search. - searchPromise = combinedSearch(term); - } - - return searchPromise; -} - -export default function eventSearch(term, roomId = undefined) { - const eventIndex = EventIndexPeg.get(); - - if (eventIndex === null) return serverSideSearch(term, roomId); - else return eventIndexSearch(term, roomId); -} diff --git a/src/Searching.ts b/src/Searching.ts new file mode 100644 index 0000000000..37f85efa77 --- /dev/null +++ b/src/Searching.ts @@ -0,0 +1,642 @@ +/* +Copyright 2019 - 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 { + IResultRoomEvents, + ISearchRequestBody, + ISearchResponse, + ISearchResult, + ISearchResults, + SearchOrderBy, +} from "matrix-js-sdk/src/@types/search"; +import { IRoomEventFilter } from "matrix-js-sdk/src/filter"; +import { EventType } from "matrix-js-sdk/src/@types/event"; + +import { ISearchArgs } from "./indexing/BaseEventIndexManager"; +import EventIndexPeg from "./indexing/EventIndexPeg"; +import { MatrixClientPeg } from "./MatrixClientPeg"; +import { SearchResult } from "matrix-js-sdk/src/models/search-result"; + +const SEARCH_LIMIT = 10; + +async function serverSideSearch( + term: string, + roomId: string = undefined, +): Promise<{ response: ISearchResponse, query: ISearchRequestBody }> { + const client = MatrixClientPeg.get(); + + const filter: IRoomEventFilter = { + limit: SEARCH_LIMIT, + }; + + if (roomId !== undefined) filter.rooms = [roomId]; + + const body: ISearchRequestBody = { + search_categories: { + room_events: { + search_term: term, + filter: filter, + order_by: SearchOrderBy.Recent, + event_context: { + before_limit: 1, + after_limit: 1, + include_profile: true, + }, + }, + }, + }; + + const response = await client.search({ body: body }); + + return { response, query: body }; +} + +async function serverSideSearchProcess(term: string, roomId: string = undefined): Promise { + const client = MatrixClientPeg.get(); + const result = await serverSideSearch(term, roomId); + + // The js-sdk method backPaginateRoomEventsSearch() uses _query internally + // so we're reusing the concept here since we want to delegate the + // pagination back to backPaginateRoomEventsSearch() in some cases. + const searchResults: ISearchResults = { + _query: result.query, + results: [], + highlights: [], + }; + + return client.processRoomEventsSearch(searchResults, result.response); +} + +function compareEvents(a: ISearchResult, b: ISearchResult): number { + const aEvent = a.result; + const bEvent = b.result; + + if (aEvent.origin_server_ts > bEvent.origin_server_ts) return -1; + if (aEvent.origin_server_ts < bEvent.origin_server_ts) return 1; + + return 0; +} + +async function combinedSearch(searchTerm: string): Promise { + const client = MatrixClientPeg.get(); + + // Create two promises, one for the local search, one for the + // server-side search. + const serverSidePromise = serverSideSearch(searchTerm); + const localPromise = localSearch(searchTerm); + + // Wait for both promises to resolve. + await Promise.all([serverSidePromise, localPromise]); + + // Get both search results. + const localResult = await localPromise; + const serverSideResult = await serverSidePromise; + + const serverQuery = serverSideResult.query; + const serverResponse = serverSideResult.response; + + const localQuery = localResult.query; + const localResponse = localResult.response; + + // Store our queries for later on so we can support pagination. + // + // We're reusing _query here again to not introduce separate code paths and + // concepts for our different pagination methods. We're storing the + // server-side next batch separately since the query is the json body of + // the request and next_batch needs to be a query parameter. + // + // We can't put it in the final result that _processRoomEventsSearch() + // returns since that one can be either a server-side one, a local one or a + // fake one to fetch the remaining cached events. See the docs for + // combineEvents() for an explanation why we need to cache events. + const emptyResult: ISeshatSearchResults = { + seshatQuery: localQuery, + _query: serverQuery, + serverSideNextBatch: serverResponse.search_categories.room_events.next_batch, + cachedEvents: [], + oldestEventFrom: "server", + results: [], + highlights: [], + }; + + // Combine our results. + const combinedResult = combineResponses(emptyResult, localResponse, serverResponse.search_categories.room_events); + + // Let the client process the combined result. + const response: ISearchResponse = { + search_categories: { + room_events: combinedResult, + }, + }; + + const result = client.processRoomEventsSearch(emptyResult, response); + + // Restore our encryption info so we can properly re-verify the events. + restoreEncryptionInfo(result.results); + + return result; +} + +async function localSearch( + searchTerm: string, + roomId: string = undefined, + processResult = true, +): Promise<{ response: IResultRoomEvents, query: ISearchArgs }> { + const eventIndex = EventIndexPeg.get(); + + const searchArgs: ISearchArgs = { + search_term: searchTerm, + before_limit: 1, + after_limit: 1, + limit: SEARCH_LIMIT, + order_by_recency: true, + room_id: undefined, + }; + + if (roomId !== undefined) { + searchArgs.room_id = roomId; + } + + const localResult = await eventIndex.search(searchArgs); + + searchArgs.next_batch = localResult.next_batch; + + const result = { + response: localResult, + query: searchArgs, + }; + + return result; +} + +export interface ISeshatSearchResults extends ISearchResults { + seshatQuery?: ISearchArgs; + cachedEvents?: ISearchResult[]; + oldestEventFrom?: "local" | "server"; + serverSideNextBatch?: string; +} + +async function localSearchProcess(searchTerm: string, roomId: string = undefined): Promise { + const emptyResult = { + results: [], + highlights: [], + } as ISeshatSearchResults; + + if (searchTerm === "") return emptyResult; + + const result = await localSearch(searchTerm, roomId); + + emptyResult.seshatQuery = result.query; + + const response: ISearchResponse = { + search_categories: { + room_events: result.response, + }, + }; + + const processedResult = MatrixClientPeg.get().processRoomEventsSearch(emptyResult, response); + // Restore our encryption info so we can properly re-verify the events. + restoreEncryptionInfo(processedResult.results); + + return processedResult; +} + +async function localPagination(searchResult: ISeshatSearchResults): Promise { + const eventIndex = EventIndexPeg.get(); + + const searchArgs = searchResult.seshatQuery; + + const localResult = await eventIndex.search(searchArgs); + searchResult.seshatQuery.next_batch = localResult.next_batch; + + // We only need to restore the encryption state for the new results, so + // remember how many of them we got. + const newResultCount = localResult.results.length; + + const response = { + search_categories: { + room_events: localResult, + }, + }; + + const result = MatrixClientPeg.get().processRoomEventsSearch(searchResult, response); + + // Restore our encryption info so we can properly re-verify the events. + const newSlice = result.results.slice(Math.max(result.results.length - newResultCount, 0)); + restoreEncryptionInfo(newSlice); + + searchResult.pendingRequest = null; + + return result; +} + +function compareOldestEvents(firstResults: ISearchResult[], secondResults: ISearchResult[]): number { + try { + const oldestFirstEvent = firstResults[firstResults.length - 1].result; + const oldestSecondEvent = secondResults[secondResults.length - 1].result; + + if (oldestFirstEvent.origin_server_ts <= oldestSecondEvent.origin_server_ts) { + return -1; + } else { + return 1; + } + } catch { + return 0; + } +} + +function combineEventSources( + previousSearchResult: ISeshatSearchResults, + response: IResultRoomEvents, + a: ISearchResult[], + b: ISearchResult[], +): void { + // Merge event sources and sort the events. + const combinedEvents = a.concat(b).sort(compareEvents); + // Put half of the events in the response, and cache the other half. + response.results = combinedEvents.slice(0, SEARCH_LIMIT); + previousSearchResult.cachedEvents = combinedEvents.slice(SEARCH_LIMIT); +} + +/** + * Combine the events from our event sources into a sorted result + * + * This method will first be called from the combinedSearch() method. In this + * case we will fetch SEARCH_LIMIT events from the server and the local index. + * + * The method will put the SEARCH_LIMIT newest events from the server and the + * local index in the results part of the response, the rest will be put in the + * cachedEvents field of the previousSearchResult (in this case an empty search + * result). + * + * Every subsequent call will be made from the combinedPagination() method, in + * this case we will combine the cachedEvents and the next SEARCH_LIMIT events + * from either the server or the local index. + * + * Since we have two event sources and we need to sort the results by date we + * need keep on looking for the oldest event. We are implementing a variation of + * a sliding window. + * + * The event sources are here represented as two sorted lists where the smallest + * number represents the newest event. The two lists need to be merged in a way + * that preserves the sorted property so they can be shown as one search result. + * We first fetch SEARCH_LIMIT events from both sources. + * + * If we set SEARCH_LIMIT to 3: + * + * Server events [01, 02, 04, 06, 07, 08, 11, 13] + * |01, 02, 04| + * Local events [03, 05, 09, 10, 12, 14, 15, 16] + * |03, 05, 09| + * + * We note that the oldest event is from the local index, and we combine the + * results: + * + * Server window [01, 02, 04] + * Local window [03, 05, 09] + * + * Combined events [01, 02, 03, 04, 05, 09] + * + * We split the combined result in the part that we want to present and a part + * that will be cached. + * + * Presented events [01, 02, 03] + * Cached events [04, 05, 09] + * + * We slide the window for the server since the oldest event is from the local + * index. + * + * Server events [01, 02, 04, 06, 07, 08, 11, 13] + * |06, 07, 08| + * Local events [03, 05, 09, 10, 12, 14, 15, 16] + * |XX, XX, XX| + * Cached events [04, 05, 09] + * + * We note that the oldest event is from the server and we combine the new + * server events with the cached ones. + * + * Cached events [04, 05, 09] + * Server events [06, 07, 08] + * + * Combined events [04, 05, 06, 07, 08, 09] + * + * We split again. + * + * Presented events [04, 05, 06] + * Cached events [07, 08, 09] + * + * We slide the local window, the oldest event is on the server. + * + * Server events [01, 02, 04, 06, 07, 08, 11, 13] + * |XX, XX, XX| + * Local events [03, 05, 09, 10, 12, 14, 15, 16] + * |10, 12, 14| + * + * Cached events [07, 08, 09] + * Local events [10, 12, 14] + * Combined events [07, 08, 09, 10, 12, 14] + * + * Presented events [07, 08, 09] + * Cached events [10, 12, 14] + * + * Next up we slide the server window again. + * + * Server events [01, 02, 04, 06, 07, 08, 11, 13] + * |11, 13| + * Local events [03, 05, 09, 10, 12, 14, 15, 16] + * |XX, XX, XX| + * + * Cached events [10, 12, 14] + * Server events [11, 13] + * Combined events [10, 11, 12, 13, 14] + * + * Presented events [10, 11, 12] + * Cached events [13, 14] + * + * We have one source exhausted, we fetch the rest of our events from the other + * source and combine it with our cached events. + * + * + * @param {object} previousSearchResult A search result from a previous search + * call. + * @param {object} localEvents An unprocessed search result from the event + * index. + * @param {object} serverEvents An unprocessed search result from the server. + * + * @return {object} A response object that combines the events from the + * different event sources. + * + */ +function combineEvents( + previousSearchResult: ISeshatSearchResults, + localEvents: IResultRoomEvents = undefined, + serverEvents: IResultRoomEvents = undefined, +): IResultRoomEvents { + const response = {} as IResultRoomEvents; + + const cachedEvents = previousSearchResult.cachedEvents; + let oldestEventFrom = previousSearchResult.oldestEventFrom; + response.highlights = previousSearchResult.highlights; + + if (localEvents && serverEvents && serverEvents.results) { + // This is a first search call, combine the events from the server and + // the local index. Note where our oldest event came from, we shall + // fetch the next batch of events from the other source. + if (compareOldestEvents(localEvents.results, serverEvents.results) < 0) { + oldestEventFrom = "local"; + } + + combineEventSources(previousSearchResult, response, localEvents.results, serverEvents.results); + response.highlights = localEvents.highlights.concat(serverEvents.highlights); + } else if (localEvents) { + // This is a pagination call fetching more events from the local index, + // meaning that our oldest event was on the server. + // Change the source of the oldest event if our local event is older + // than the cached one. + if (compareOldestEvents(localEvents.results, cachedEvents) < 0) { + oldestEventFrom = "local"; + } + combineEventSources(previousSearchResult, response, localEvents.results, cachedEvents); + } else if (serverEvents && serverEvents.results) { + // This is a pagination call fetching more events from the server, + // meaning that our oldest event was in the local index. + // Change the source of the oldest event if our server event is older + // than the cached one. + if (compareOldestEvents(serverEvents.results, cachedEvents) < 0) { + oldestEventFrom = "server"; + } + combineEventSources(previousSearchResult, response, serverEvents.results, cachedEvents); + } else { + // This is a pagination call where we exhausted both of our event + // sources, let's push the remaining cached events. + response.results = cachedEvents; + previousSearchResult.cachedEvents = []; + } + + previousSearchResult.oldestEventFrom = oldestEventFrom; + + return response; +} + +/** + * Combine the local and server search responses + * + * @param {object} previousSearchResult A search result from a previous search + * call. + * @param {object} localEvents An unprocessed search result from the event + * index. + * @param {object} serverEvents An unprocessed search result from the server. + * + * @return {object} A response object that combines the events from the + * different event sources. + */ +function combineResponses( + previousSearchResult: ISeshatSearchResults, + localEvents: IResultRoomEvents = undefined, + serverEvents: IResultRoomEvents = undefined, +): IResultRoomEvents { + // Combine our events first. + const response = combineEvents(previousSearchResult, localEvents, serverEvents); + + // Our first search will contain counts from both sources, subsequent + // pagination requests will fetch responses only from one of the sources, so + // reuse the first count when we're paginating. + if (previousSearchResult.count) { + response.count = previousSearchResult.count; + } else { + response.count = localEvents.count + serverEvents.count; + } + + // Update our next batch tokens for the given search sources. + if (localEvents) { + previousSearchResult.seshatQuery.next_batch = localEvents.next_batch; + } + if (serverEvents) { + previousSearchResult.serverSideNextBatch = serverEvents.next_batch; + } + + // Set the response next batch token to one of the tokens from the sources, + // this makes sure that if we exhaust one of the sources we continue with + // the other one. + if (previousSearchResult.seshatQuery.next_batch) { + response.next_batch = previousSearchResult.seshatQuery.next_batch; + } else if (previousSearchResult.serverSideNextBatch) { + response.next_batch = previousSearchResult.serverSideNextBatch; + } + + // We collected all search results from the server as well as from Seshat, + // we still have some events cached that we'll want to display on the next + // pagination request. + // + // Provide a fake next batch token for that case. + if (!response.next_batch && previousSearchResult.cachedEvents.length > 0) { + response.next_batch = "cached"; + } + + return response; +} + +interface IEncryptedSeshatEvent { + curve25519Key: string; + ed25519Key: string; + algorithm: string; + forwardingCurve25519KeyChain: string[]; +} + +function restoreEncryptionInfo(searchResultSlice: SearchResult[] = []): void { + for (let i = 0; i < searchResultSlice.length; i++) { + const timeline = searchResultSlice[i].context.getTimeline(); + + for (let j = 0; j < timeline.length; j++) { + const mxEv = timeline[j]; + const ev = mxEv.event as IEncryptedSeshatEvent; + + if (ev.curve25519Key) { + mxEv.makeEncrypted( + EventType.RoomMessageEncrypted, + { algorithm: ev.algorithm }, + ev.curve25519Key, + ev.ed25519Key, + ); + // @ts-ignore + mxEv.forwardingCurve25519KeyChain = ev.forwardingCurve25519KeyChain; + + delete ev.curve25519Key; + delete ev.ed25519Key; + delete ev.algorithm; + delete ev.forwardingCurve25519KeyChain; + } + } + } +} + +async function combinedPagination(searchResult: ISeshatSearchResults): Promise { + const eventIndex = EventIndexPeg.get(); + const client = MatrixClientPeg.get(); + + const searchArgs = searchResult.seshatQuery; + const oldestEventFrom = searchResult.oldestEventFrom; + + let localResult: IResultRoomEvents; + let serverSideResult: ISearchResponse; + + // Fetch events from the local index if we have a token for it and if it's + // the local indexes turn or the server has exhausted its results. + if (searchArgs.next_batch && (!searchResult.serverSideNextBatch || oldestEventFrom === "server")) { + localResult = await eventIndex.search(searchArgs); + } + + // Fetch events from the server if we have a token for it and if it's the + // local indexes turn or the local index has exhausted its results. + if (searchResult.serverSideNextBatch && (oldestEventFrom === "local" || !searchArgs.next_batch)) { + const body = { body: searchResult._query, next_batch: searchResult.serverSideNextBatch }; + serverSideResult = await client.search(body); + } + + let serverEvents: IResultRoomEvents; + + if (serverSideResult) { + serverEvents = serverSideResult.search_categories.room_events; + } + + // Combine our events. + const combinedResult = combineResponses(searchResult, localResult, serverEvents); + + const response = { + search_categories: { + room_events: combinedResult, + }, + }; + + const oldResultCount = searchResult.results ? searchResult.results.length : 0; + + // Let the client process the combined result. + const result = client.processRoomEventsSearch(searchResult, response); + + // Restore our encryption info so we can properly re-verify the events. + const newResultCount = result.results.length - oldResultCount; + const newSlice = result.results.slice(Math.max(result.results.length - newResultCount, 0)); + restoreEncryptionInfo(newSlice); + + searchResult.pendingRequest = null; + + return result; +} + +function eventIndexSearch(term: string, roomId: string = undefined): Promise { + let searchPromise: Promise; + + if (roomId !== undefined) { + if (MatrixClientPeg.get().isRoomEncrypted(roomId)) { + // The search is for a single encrypted room, use our local + // search method. + searchPromise = localSearchProcess(term, roomId); + } else { + // The search is for a single non-encrypted room, use the + // server-side search. + searchPromise = serverSideSearchProcess(term, roomId); + } + } else { + // Search across all rooms, combine a server side search and a + // local search. + searchPromise = combinedSearch(term); + } + + return searchPromise; +} + +function eventIndexSearchPagination(searchResult: ISeshatSearchResults): Promise { + const client = MatrixClientPeg.get(); + + const seshatQuery = searchResult.seshatQuery; + const serverQuery = searchResult._query; + + if (!seshatQuery) { + // This is a search in a non-encrypted room. Do the normal server-side + // pagination. + return client.backPaginateRoomEventsSearch(searchResult); + } else if (!serverQuery) { + // This is a search in a encrypted room. Do a local pagination. + const promise = localPagination(searchResult); + searchResult.pendingRequest = promise; + + return promise; + } else { + // We have both queries around, this is a search across all rooms so a + // combined pagination needs to be done. + const promise = combinedPagination(searchResult); + searchResult.pendingRequest = promise; + + return promise; + } +} + +export function searchPagination(searchResult: ISearchResults): Promise { + const eventIndex = EventIndexPeg.get(); + const client = MatrixClientPeg.get(); + + if (searchResult.pendingRequest) return searchResult.pendingRequest; + + if (eventIndex === null) return client.backPaginateRoomEventsSearch(searchResult); + else return eventIndexSearchPagination(searchResult); +} + +export default function eventSearch(term: string, roomId: string = undefined): Promise { + const eventIndex = EventIndexPeg.get(); + + if (eventIndex === null) return serverSideSearchProcess(term, roomId); + else return eventIndexSearch(term, roomId); +} diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts new file mode 100644 index 0000000000..370b21b396 --- /dev/null +++ b/src/SecurityManager.ts @@ -0,0 +1,463 @@ +/* +Copyright 2019, 2020 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 { ICryptoCallbacks } from 'matrix-js-sdk/src/matrix'; +import { ISecretStorageKeyInfo } from 'matrix-js-sdk/src/crypto/api'; +import { MatrixClient } from 'matrix-js-sdk/src/client'; +import Modal from './Modal'; +import * as sdk from './index'; +import { MatrixClientPeg } from './MatrixClientPeg'; +import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; +import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; +import { _t } from './languageHandler'; +import { encodeBase64 } from "matrix-js-sdk/src/crypto/olmlib"; +import { isSecureBackupRequired } from './utils/WellKnownUtils'; +import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog'; +import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog'; +import SettingsStore from "./settings/SettingsStore"; +import SecurityCustomisations from "./customisations/Security"; +import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; + +// This stores the secret storage private keys in memory for the JS SDK. This is +// only meant to act as a cache to avoid prompting the user multiple times +// during the same single operation. Use `accessSecretStorage` below to scope a +// single secret storage operation, as it will clear the cached keys once the +// operation ends. +let secretStorageKeys: Record = {}; +let secretStorageKeyInfo: Record = {}; +let secretStorageBeingAccessed = false; + +let nonInteractive = false; + +let dehydrationCache: { + key?: Uint8Array; + keyInfo?: ISecretStorageKeyInfo; +} = {}; + +function isCachingAllowed(): boolean { + return secretStorageBeingAccessed; +} + +/** + * This can be used by other components to check if secret storage access is in + * progress, so that we can e.g. avoid intermittently showing toasts during + * secret storage setup. + * + * @returns {bool} + */ +export function isSecretStorageBeingAccessed(): boolean { + return secretStorageBeingAccessed; +} + +export class AccessCancelledError extends Error { + constructor() { + super("Secret storage access canceled"); + } +} + +async function confirmToDismiss(): Promise { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const [sure] = await Modal.createDialog(QuestionDialog, { + title: _t("Cancel entering passphrase?"), + description: _t("Are you sure you want to cancel entering passphrase?"), + danger: false, + button: _t("Go Back"), + cancelButton: _t("Cancel"), + }).finished; + return !sure; +} + +function makeInputToKey( + keyInfo: ISecretStorageKeyInfo, +): (keyParams: { passphrase: string, recoveryKey: string }) => Promise { + return async ({ passphrase, recoveryKey }) => { + if (passphrase) { + return deriveKey( + passphrase, + keyInfo.passphrase.salt, + keyInfo.passphrase.iterations, + ); + } else { + return decodeRecoveryKey(recoveryKey); + } + }; +} + +async function getSecretStorageKey( + { keys: keyInfos }: { keys: Record }, + ssssItemName, +): Promise<[string, Uint8Array]> { + const cli = MatrixClientPeg.get(); + let keyId = await cli.getDefaultSecretStorageKeyId(); + let keyInfo; + if (keyId) { + // use the default SSSS key if set + keyInfo = keyInfos[keyId]; + if (!keyInfo) { + // if the default key is not available, pretend the default key + // isn't set + keyId = undefined; + } + } + if (!keyId) { + // if no default SSSS key is set, fall back to a heuristic of using the + // only available key, if only one key is set + const keyInfoEntries = Object.entries(keyInfos); + if (keyInfoEntries.length > 1) { + throw new Error("Multiple storage key requests not implemented"); + } + [keyId, keyInfo] = keyInfoEntries[0]; + } + + // Check the in-memory cache + if (isCachingAllowed() && secretStorageKeys[keyId]) { + return [keyId, secretStorageKeys[keyId]]; + } + + if (dehydrationCache.key) { + if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationCache.key, keyInfo)) { + cacheSecretStorageKey(keyId, keyInfo, dehydrationCache.key); + return [keyId, dehydrationCache.key]; + } + } + + const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.(); + if (keyFromCustomisations) { + console.log("Using key from security customisations (secret storage)"); + cacheSecretStorageKey(keyId, keyInfo, keyFromCustomisations); + return [keyId, keyFromCustomisations]; + } + + if (nonInteractive) { + throw new Error("Could not unlock non-interactively"); + } + + const inputToKey = makeInputToKey(keyInfo); + const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", + AccessSecretStorageDialog, + /* props= */ + { + keyInfo, + checkPrivateKey: async (input) => { + const key = await inputToKey(input); + return await MatrixClientPeg.get().checkSecretStorageKey(key, keyInfo); + }, + }, + /* className= */ null, + /* isPriorityModal= */ false, + /* isStaticModal= */ false, + /* options= */ { + onBeforeClose: async (reason) => { + if (reason === "backgroundClick") { + return confirmToDismiss(); + } + return true; + }, + }, + ); + const [input] = await finished; + if (!input) { + throw new AccessCancelledError(); + } + const key = await inputToKey(input); + + // Save to cache to avoid future prompts in the current session + cacheSecretStorageKey(keyId, keyInfo, key); + + return [keyId, key]; +} + +export async function getDehydrationKey( + keyInfo: ISecretStorageKeyInfo, + checkFunc: (Uint8Array) => void, +): Promise { + const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.(); + if (keyFromCustomisations) { + console.log("Using key from security customisations (dehydration)"); + return keyFromCustomisations; + } + + const inputToKey = makeInputToKey(keyInfo); + const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", + AccessSecretStorageDialog, + /* props= */ + { + keyInfo, + checkPrivateKey: async (input) => { + const key = await inputToKey(input); + try { + checkFunc(key); + return true; + } catch (e) { + return false; + } + }, + }, + /* className= */ null, + /* isPriorityModal= */ false, + /* isStaticModal= */ false, + /* options= */ { + onBeforeClose: async (reason) => { + if (reason === "backgroundClick") { + return confirmToDismiss(); + } + return true; + }, + }, + ); + const [input] = await finished; + if (!input) { + throw new AccessCancelledError(); + } + const key = await inputToKey(input); + + // need to copy the key because rehydration (unpickling) will clobber it + dehydrationCache = { key: new Uint8Array(key), keyInfo }; + + return key; +} + +function cacheSecretStorageKey( + keyId: string, + keyInfo: ISecretStorageKeyInfo, + key: Uint8Array, +): void { + if (isCachingAllowed()) { + secretStorageKeys[keyId] = key; + secretStorageKeyInfo[keyId] = keyInfo; + } +} + +async function onSecretRequested( + userId: string, + deviceId: string, + requestId: string, + name: string, + deviceTrust: DeviceTrustLevel, +): Promise { + console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust); + const client = MatrixClientPeg.get(); + if (userId !== client.getUserId()) { + return; + } + if (!deviceTrust || !deviceTrust.isVerified()) { + console.log(`Ignoring secret request from untrusted device ${deviceId}`); + return; + } + if ( + name === "m.cross_signing.master" || + name === "m.cross_signing.self_signing" || + name === "m.cross_signing.user_signing" + ) { + const callbacks = client.getCrossSigningCacheCallbacks(); + if (!callbacks.getCrossSigningKeyCache) return; + const keyId = name.replace("m.cross_signing.", ""); + const key = await callbacks.getCrossSigningKeyCache(keyId); + if (!key) { + console.log( + `${keyId} requested by ${deviceId}, but not found in cache`, + ); + } + return key && encodeBase64(key); + } else if (name === "m.megolm_backup.v1") { + const key = await client.crypto.getSessionBackupPrivateKey(); + if (!key) { + console.log( + `session backup key requested by ${deviceId}, but not found in cache`, + ); + } + return key && encodeBase64(key); + } + console.warn("onSecretRequested didn't recognise the secret named ", name); +} + +export const crossSigningCallbacks: ICryptoCallbacks = { + getSecretStorageKey, + cacheSecretStorageKey, + onSecretRequested, + getDehydrationKey, +}; + +export async function promptForBackupPassphrase(): Promise { + let key; + + const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { + showSummary: false, keyCallback: k => key = k, + }, null, /* priority = */ false, /* static = */ true); + + const success = await finished; + if (!success) throw new Error("Key backup prompt cancelled"); + + return key; +} + +/** + * This helper should be used whenever you need to access secret storage. It + * ensures that secret storage (and also cross-signing since they each depend on + * each other in a cycle of sorts) have been bootstrapped before running the + * provided function. + * + * Bootstrapping secret storage may take one of these paths: + * 1. Create secret storage from a passphrase and store cross-signing keys + * in secret storage. + * 2. Access existing secret storage by requesting passphrase and accessing + * cross-signing keys as needed. + * 3. All keys are loaded and there's nothing to do. + * + * Additionally, the secret storage keys are cached during the scope of this function + * to ensure the user is prompted only once for their secret storage + * passphrase. The cache is then cleared once the provided function completes. + * + * @param {Function} [func] An operation to perform once secret storage has been + * bootstrapped. Optional. + * @param {bool} [forceReset] Reset secret storage even if it's already set up + */ +export async function accessSecretStorage(func = async () => { }, forceReset = false) { + const cli = MatrixClientPeg.get(); + secretStorageBeingAccessed = true; + try { + if (!await cli.hasSecretStorageKey() || forceReset) { + // This dialog calls bootstrap itself after guiding the user through + // passphrase creation. + const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '', + import("./async-components/views/dialogs/security/CreateSecretStorageDialog"), + { + forceReset, + }, + null, + /* priority = */ false, + /* static = */ true, + /* options = */ { + onBeforeClose: async (reason) => { + // If Secure Backup is required, you cannot leave the modal. + if (reason === "backgroundClick") { + return !isSecureBackupRequired(); + } + return true; + }, + }, + ); + const [confirmed] = await finished; + if (!confirmed) { + throw new Error("Secret storage creation canceled"); + } + } else { + // FIXME: Using an import will result in test failures + const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); + await cli.bootstrapCrossSigning({ + authUploadDeviceSigningKeys: async (makeRequest) => { + const { finished } = Modal.createTrackedDialog( + 'Cross-signing keys dialog', '', InteractiveAuthDialog, + { + title: _t("Setting up keys"), + matrixClient: cli, + makeRequest, + }, + ); + const [confirmed] = await finished; + if (!confirmed) { + throw new Error("Cross-signing key upload auth canceled"); + } + }, + }); + await cli.bootstrapSecretStorage({ + getKeyBackupPassphrase: promptForBackupPassphrase, + }); + + const keyId = Object.keys(secretStorageKeys)[0]; + if (keyId && SettingsStore.getValue("feature_dehydration")) { + let dehydrationKeyInfo = {}; + if (secretStorageKeyInfo[keyId] && secretStorageKeyInfo[keyId].passphrase) { + dehydrationKeyInfo = { passphrase: secretStorageKeyInfo[keyId].passphrase }; + } + console.log("Setting dehydration key"); + await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device"); + } else if (!keyId) { + console.warn("Not setting dehydration key: no SSSS key found"); + } else { + console.log("Not setting dehydration key: feature disabled"); + } + } + + // `return await` needed here to ensure `finally` block runs after the + // inner operation completes. + return await func(); + } catch (e) { + SecurityCustomisations.catchAccessSecretStorageError?.(e); + console.error(e); + // Re-throw so that higher level logic can abort as needed + throw e; + } finally { + // Clear secret storage key cache now that work is complete + secretStorageBeingAccessed = false; + if (!isCachingAllowed()) { + secretStorageKeys = {}; + secretStorageKeyInfo = {}; + } + } +} + +// FIXME: this function name is a bit of a mouthful +export async function tryToUnlockSecretStorageWithDehydrationKey( + client: MatrixClient, +): Promise { + const key = dehydrationCache.key; + let restoringBackup = false; + if (key && await client.isSecretStorageReady()) { + console.log("Trying to set up cross-signing using dehydration key"); + secretStorageBeingAccessed = true; + nonInteractive = true; + try { + await client.checkOwnCrossSigningTrust(); + + // we also need to set a new dehydrated device to replace the + // device we rehydrated + let dehydrationKeyInfo = {}; + if (dehydrationCache.keyInfo && dehydrationCache.keyInfo.passphrase) { + dehydrationKeyInfo = { passphrase: dehydrationCache.keyInfo.passphrase }; + } + await client.setDehydrationKey(key, dehydrationKeyInfo, "Backup device"); + + // and restore from backup + const backupInfo = await client.getKeyBackupVersion(); + if (backupInfo) { + restoringBackup = true; + // don't await, because this can take a long time + client.restoreKeyBackupWithSecretStorage(backupInfo) + .finally(() => { + secretStorageBeingAccessed = false; + nonInteractive = false; + if (!isCachingAllowed()) { + secretStorageKeys = {}; + secretStorageKeyInfo = {}; + } + }); + } + } finally { + dehydrationCache = {}; + // the secret storage cache is needed for restoring from backup, so + // don't clear it yet if we're restoring from backup + if (!restoringBackup) { + secretStorageBeingAccessed = false; + nonInteractive = false; + if (!isCachingAllowed()) { + secretStorageKeys = {}; + secretStorageKeyInfo = {}; + } + } + } + } +} diff --git a/src/SendHistoryManager.js b/src/SendHistoryManager.js deleted file mode 100644 index 794a58ad6f..0000000000 --- a/src/SendHistoryManager.js +++ /dev/null @@ -1,60 +0,0 @@ -//@flow -/* -Copyright 2017 Aviral Dasgupta - -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 _clamp from 'lodash/clamp'; - -export default class SendHistoryManager { - history: Array = []; - prefix: string; - lastIndex: number = 0; // used for indexing the storage - currentIndex: number = 0; // used for indexing the loaded validated history Array - - constructor(roomId: string, prefix: string) { - this.prefix = prefix + roomId; - - // TODO: Performance issues? - let index = 0; - let itemJSON; - - while (itemJSON = sessionStorage.getItem(`${this.prefix}[${index}]`)) { - try { - const serializedParts = JSON.parse(itemJSON); - this.history.push(serializedParts); - } catch (e) { - console.warn("Throwing away unserialisable history", e); - break; - } - ++index; - } - this.lastIndex = this.history.length - 1; - // reset currentIndex to account for any unserialisable history - this.currentIndex = this.lastIndex + 1; - } - - save(editorModel: Object) { - const serializedParts = editorModel.serializeParts(); - this.history.push(serializedParts); - this.currentIndex = this.history.length; - this.lastIndex += 1; - sessionStorage.setItem(`${this.prefix}[${this.lastIndex}]`, JSON.stringify(serializedParts)); - } - - getItem(offset: number): ?HistoryItem { - this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1); - return this.history[this.currentIndex]; - } -} diff --git a/src/SendHistoryManager.ts b/src/SendHistoryManager.ts new file mode 100644 index 0000000000..eeba643d81 --- /dev/null +++ b/src/SendHistoryManager.ts @@ -0,0 +1,74 @@ +/* +Copyright 2017 Aviral Dasgupta + +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 { clamp } from "lodash"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + +import { SerializedPart } from "./editor/parts"; +import EditorModel from "./editor/model"; + +interface IHistoryItem { + parts: SerializedPart[]; + replyEventId?: string; +} + +export default class SendHistoryManager { + history: Array = []; + prefix: string; + lastIndex = 0; // used for indexing the storage + currentIndex = 0; // used for indexing the loaded validated history Array + + constructor(roomId: string, prefix: string) { + this.prefix = prefix + roomId; + + // TODO: Performance issues? + let index = 0; + let itemJSON; + + while (itemJSON = sessionStorage.getItem(`${this.prefix}[${index}]`)) { + try { + this.history.push(JSON.parse(itemJSON)); + } catch (e) { + console.warn("Throwing away unserialisable history", e); + break; + } + ++index; + } + this.lastIndex = this.history.length - 1; + // reset currentIndex to account for any unserialisable history + this.currentIndex = this.lastIndex + 1; + } + + static createItem(model: EditorModel, replyEvent?: MatrixEvent): IHistoryItem { + return { + parts: model.serializeParts(), + replyEventId: replyEvent ? replyEvent.getId() : undefined, + }; + } + + save(editorModel: EditorModel, replyEvent?: MatrixEvent) { + const item = SendHistoryManager.createItem(editorModel, replyEvent); + this.history.push(item); + this.currentIndex = this.history.length; + this.lastIndex += 1; + sessionStorage.setItem(`${this.prefix}[${this.lastIndex}]`, JSON.stringify(item)); + } + + getItem(offset: number): IHistoryItem { + this.currentIndex = clamp(this.currentIndex + offset, 0, this.history.length - 1); + return this.history[this.currentIndex]; + } +} diff --git a/src/Skinner.js b/src/Skinner.js index 87c5a7be7f..ef340e4052 100644 --- a/src/Skinner.js +++ b/src/Skinner.js @@ -23,7 +23,7 @@ class Skinner { if (!name) throw new Error(`Invalid component name: ${name}`); if (this.components === null) { throw new Error( - "Attempted to get a component before a skin has been loaded."+ + `Attempted to get a component (${name}) before a skin has been loaded.`+ " This is probably because either:"+ " a) Your app has not called sdk.loadSkin(), or"+ " b) A component has called getComponent at the root level", @@ -50,8 +50,8 @@ class Skinner { return null; } - // components have to be functions. - const validType = typeof comp === 'function'; + // components have to be functions or forwardRef objects with a render function. + const validType = typeof comp === 'function' || comp.render; if (!validType) { throw new Error(`Not a valid component: ${name} (type = ${typeof(comp)}).`); } diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index aac42b6740..7753ff6f75 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -17,24 +17,44 @@ See the License for the specific language governing permissions and limitations under the License. */ - import * as React from 'react'; +import { User } from "matrix-js-sdk/src/models/user"; -import {MatrixClientPeg} from './MatrixClientPeg'; -import dis from './dispatcher'; -import * as sdk from './index'; -import {_t, _td} from './languageHandler'; +import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers'; +import { MatrixClientPeg } from './MatrixClientPeg'; +import dis from './dispatcher/dispatcher'; +import { _t, _td } from './languageHandler'; import Modal from './Modal'; import MultiInviter from './utils/MultiInviter'; import { linkifyAndSanitizeHtml } from './HtmlUtils'; import QuestionDialog from "./components/views/dialogs/QuestionDialog"; import WidgetUtils from "./utils/WidgetUtils"; -import {textToHtmlRainbow} from "./utils/colour"; +import { textToHtmlRainbow } from "./utils/colour"; import { getAddressType } from './UserAddress'; import { abbreviateUrl } from './utils/UrlUtils'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils'; -import {isPermalinkHost, parsePermalink} from "./utils/permalinks/Permalinks"; -import {inviteUsersToRoom} from "./RoomInvite"; +import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks"; +import { inviteUsersToRoom } from "./RoomInvite"; +import { WidgetType } from "./widgets/WidgetType"; +import { Jitsi } from "./widgets/Jitsi"; +import { parseFragment as parseHtml, Element as ChildElement } from "parse5"; +import BugReportDialog from "./components/views/dialogs/BugReportDialog"; +import { ensureDMExists } from "./createRoom"; +import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload"; +import { Action } from "./dispatcher/actions"; +import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from "./utils/membership"; +import SdkConfig from "./SdkConfig"; +import SettingsStore from "./settings/SettingsStore"; +import { UIFeature } from "./settings/UIFeature"; +import { CHAT_EFFECTS } from "./effects"; +import CallHandler from "./CallHandler"; +import { guessAndSetDMRoom } from "./Rooms"; +import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog'; +import ErrorDialog from './components/views/dialogs/ErrorDialog'; +import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog'; +import RoomUpgradeWarningDialog from "./components/views/dialogs/RoomUpgradeWarningDialog"; +import InfoDialog from "./components/views/dialogs/InfoDialog"; +import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog"; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -48,7 +68,6 @@ const singleMxcUpload = async (): Promise => { fileSelector.onchange = (ev: HTMLInputEvent) => { const file = ev.target.files[0]; - const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog"); Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, { file, onFinished: (shouldContinue) => { @@ -66,6 +85,7 @@ export const CommandCategories = { "actions": _td("Actions"), "admin": _td("Admin"), "advanced": _td("Advanced"), + "effects": _td("Effects"), "other": _td("Other"), }; @@ -79,9 +99,10 @@ interface ICommandOpts { runFn?: RunFn; category: string; hideCompletionAfterSpace?: boolean; + isEnabled?(): boolean; } -class Command { +export class Command { command: string; aliases: string[]; args: undefined | string; @@ -89,6 +110,7 @@ class Command { runFn: undefined | RunFn; category: string; hideCompletionAfterSpace: boolean; + _isEnabled?: () => boolean; constructor(opts: ICommandOpts) { this.command = opts.command; @@ -98,6 +120,7 @@ class Command { this.runFn = opts.runFn; this.category = opts.category || CommandCategories.other; this.hideCompletionAfterSpace = opts.hideCompletionAfterSpace || false; + this._isEnabled = opts.isEnabled; } getCommand() { @@ -108,23 +131,31 @@ class Command { return this.getCommand() + " " + this.args; } - run(roomId: string, args: string, cmd: string) { + run(roomId: string, args: string) { // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me` - if (!this.runFn) return; - return this.runFn.bind(this)(roomId, args, cmd); + if (!this.runFn) return reject(_t("Command error")); + return this.runFn.bind(this)(roomId, args); } getUsage() { return _t('Usage') + ': ' + this.getCommandWithArgs(); } + + isEnabled() { + return this._isEnabled ? this._isEnabled() : true; + } } function reject(error) { - return {error}; + return { error }; } function success(promise?: Promise) { - return {promise}; + return { promise }; +} + +function successSync(value: any) { + return success(Promise.resolve(value)); } /* Disable the "unexpected this" error for these commands - all of the run @@ -132,6 +163,18 @@ function success(promise?: Promise) { */ export const Commands = [ + new Command({ + command: 'spoiler', + args: '', + description: _td('Sends the given message as a spoiler'), + runFn: function(roomId, message) { + return successSync(ContentHelpers.makeHtmlMessage( + message, + `${message}`, + )); + }, + category: CommandCategories.messages, + }), new Command({ command: 'shrug', args: '', @@ -141,7 +184,46 @@ export const Commands = [ if (args) { message = message + ' ' + args; } - return success(MatrixClientPeg.get().sendTextMessage(roomId, message)); + return successSync(ContentHelpers.makeTextMessage(message)); + }, + category: CommandCategories.messages, + }), + new Command({ + command: 'tableflip', + args: '', + description: _td('Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message'), + runFn: function(roomId, args) { + let message = '(╯°□°)╯︵ ┻━┻'; + if (args) { + message = message + ' ' + args; + } + return successSync(ContentHelpers.makeTextMessage(message)); + }, + category: CommandCategories.messages, + }), + new Command({ + command: 'unflip', + args: '', + description: _td('Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message'), + runFn: function(roomId, args) { + let message = '┬──┬ ノ( ゜-゜ノ)'; + if (args) { + message = message + ' ' + args; + } + return successSync(ContentHelpers.makeTextMessage(message)); + }, + category: CommandCategories.messages, + }), + new Command({ + command: 'lenny', + args: '', + description: _td('Prepends ( ͡° ͜ʖ ͡°) to a plain-text message'), + runFn: function(roomId, args) { + let message = '( ͡° ͜ʖ ͡°)'; + if (args) { + message = message + ' ' + args; + } + return successSync(ContentHelpers.makeTextMessage(message)); }, category: CommandCategories.messages, }), @@ -150,7 +232,7 @@ export const Commands = [ args: '', description: _td('Sends a message as plain text, without interpreting it as markdown'), runFn: function(roomId, messages) { - return success(MatrixClientPeg.get().sendTextMessage(roomId, messages)); + return successSync(ContentHelpers.makeTextMessage(messages)); }, category: CommandCategories.messages, }), @@ -159,7 +241,7 @@ export const Commands = [ args: '', description: _td('Sends a message as html, without interpreting it as markdown'), runFn: function(roomId, messages) { - return success(MatrixClientPeg.get().sendHtmlMessage(roomId, messages, messages)); + return successSync(ContentHelpers.makeHtmlMessage(messages, messages)); }, category: CommandCategories.messages, }), @@ -168,7 +250,6 @@ export const Commands = [ args: '', description: _td('Searches DuckDuckGo for results'), runFn: function() { - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); // TODO Don't explain this away, actually show a search UI here. Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, { title: _t('/ddg is not a command'), @@ -191,10 +272,8 @@ export const Commands = [ return reject(_t("You do not have the required permissions to use this command.")); } - const RoomUpgradeWarningDialog = sdk.getComponent("dialogs.RoomUpgradeWarningDialog"); - - const {finished} = Modal.createTrackedDialog('Slash Commands', 'upgrade room confirmation', - RoomUpgradeWarningDialog, {roomId: roomId, targetVersion: args}, /*className=*/null, + const { finished } = Modal.createTrackedDialog('Slash Commands', 'upgrade room confirmation', + RoomUpgradeWarningDialog, { roomId: roomId, targetVersion: args }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); return success(finished.then(async ([resp]) => { @@ -210,7 +289,7 @@ export const Commands = [ if (resp.invite) { checkForUpgradeFn = async (newRoom) => { // The upgradePromise should be done by the time we await it here. - const {replacement_room: newRoomId} = await upgradePromise; + const { replacement_room: newRoomId } = await upgradePromise; if (newRoom.roomId !== newRoomId) return; const toInvite = [ @@ -236,7 +315,6 @@ export const Commands = [ if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn); - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, { title: _t('Error upgrading room'), description: _t( @@ -292,7 +370,7 @@ export const Commands = [ return success(promise.then((url) => { if (!url) return; - return MatrixClientPeg.get().sendStateEvent(roomId, 'm.room.avatar', {url}, ''); + return MatrixClientPeg.get().sendStateEvent(roomId, 'm.room.avatar', { url }, ''); })); }, category: CommandCategories.actions, @@ -350,16 +428,16 @@ export const Commands = [ return success(cli.setRoomTopic(roomId, args)); } const room = cli.getRoom(roomId); - if (!room) return reject('Bad room ID: ' + roomId); + if (!room) return reject(_t("Failed to set topic")); const topicEvents = room.currentState.getStateEvents('m.room.topic', ''); const topic = topicEvents && topicEvents.getContent().topic; const topicHtml = topic ? linkifyAndSanitizeHtml(topic) : _t('This room has no topic.'); - const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); Modal.createTrackedDialog('Slash Commands', 'Topic', InfoDialog, { title: room.name, description:
, + hasCloseButton: true, }); return success(); }, @@ -379,26 +457,27 @@ export const Commands = [ }), new Command({ command: 'invite', - args: '', + args: ' []', description: _td('Invites user with given id to current room'), runFn: function(roomId, args) { if (args) { - const matches = args.match(/^(\S+)$/); - if (matches) { + const [address, reason] = args.split(/\s+(.+)/); + if (address) { // We use a MultiInviter to re-use the invite logic, even though // we're only inviting one user. - const address = matches[1]; // If we need an identity server but don't have one, things // get a bit more complex here, but we try to show something // meaningful. - let finished = Promise.resolve(); + let prom = Promise.resolve(); if ( getAddressType(address) === 'email' && !MatrixClientPeg.get().getIdentityServerUrl() ) { const defaultIdentityServerUrl = getDefaultIdentityServerUrl(); if (defaultIdentityServerUrl) { - ({ finished } = Modal.createTrackedDialog('Slash Commands', 'Identity server', + const { finished } = Modal.createTrackedDialog<[boolean]>( + 'Slash Commands', + 'Identity server', QuestionDialog, { title: _t("Use an identity server"), description:

{_t( @@ -411,9 +490,9 @@ export const Commands = [ )}

, button: _t("Continue"), }, - )); + ); - finished = finished.then(([useDefault]: any) => { + prom = finished.then(([useDefault]) => { if (useDefault) { useDefaultIdentityServer(); return; @@ -425,8 +504,8 @@ export const Commands = [ } } const inviter = new MultiInviter(roomId); - return success(finished.then(() => { - return inviter.invite([address]); + return success(prom.then(() => { + return inviter.invite([address], reason); }).then(() => { if (inviter.getCompletionState(address) !== "invited") { throw new Error(inviter.getErrorText(address)); @@ -441,8 +520,8 @@ export const Commands = [ new Command({ command: 'join', aliases: ['j', 'goto'], - args: '', - description: _td('Joins room with given alias'), + args: '', + description: _td('Joins room with given address'), runFn: function(_, args) { if (args) { // Note: we support 2 versions of this command. The first is @@ -467,7 +546,7 @@ export const Commands = [ const parsedUrl = new URL(params[0]); const hostname = parsedUrl.host || parsedUrl.hostname; // takes first non-falsey value - // if we're using a Riot permalink handler, this will catch it before we get much further. + // if we're using a Element permalink handler, this will catch it before we get much further. // see below where we make assumptions about parsing the URL. if (isPermalinkHost(hostname)) { isPermalink = true; @@ -483,11 +562,11 @@ export const Commands = [ action: 'view_room', room_alias: roomAlias, auto_join: true, + _type: "slash_command", // instrumentation }); return success(); } else if (params[0][0] === '!') { - const roomId = params[0]; - const viaServers = params.splice(0); + const [roomId, ...viaServers] = params; dis.dispatch({ action: 'view_room', @@ -498,6 +577,7 @@ export const Commands = [ }, via_servers: viaServers, // for the rejoin button auto_join: true, + _type: "slash_command", // instrumentation }); return success(); } else if (isPermalink) { @@ -522,6 +602,7 @@ export const Commands = [ const dispatch = { action: 'view_room', auto_join: true, + _type: "slash_command", // instrumentation }; if (entity[0] === '!') dispatch["room_id"] = entity; @@ -553,7 +634,7 @@ export const Commands = [ }), new Command({ command: 'part', - args: '[]', + args: '[]', description: _td('Leave room'), runFn: function(roomId, args) { const cli = MatrixClientPeg.get(); @@ -585,16 +666,12 @@ export const Commands = [ } if (targetRoomId) break; } - if (!targetRoomId) return reject(_t('Unrecognised room alias:') + ' ' + roomAlias); + if (!targetRoomId) return reject(_t('Unrecognised room address:') + ' ' + roomAlias); } } if (!targetRoomId) targetRoomId = roomId; - return success( - cli.leaveRoomChain(targetRoomId).then(function() { - dis.dispatch({action: 'view_next_room'}); - }), - ); + return success(leaveRoomBehaviour(targetRoomId)); }, category: CommandCategories.actions, }), @@ -652,18 +729,17 @@ export const Commands = [ if (args) { const cli = MatrixClientPeg.get(); - const matches = args.match(/^(\S+)$/); + const matches = args.match(/^(@[^:]+:\S+)$/); if (matches) { const userId = matches[1]; const ignoredUsers = cli.getIgnoredUsers(); ignoredUsers.push(userId); // de-duped internally in the js-sdk return success( cli.setIgnoredUsers(ignoredUsers).then(() => { - const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); Modal.createTrackedDialog('Slash Commands', 'User ignored', InfoDialog, { title: _t('Ignored user'), description:
-

{ _t('You are now ignoring %(userId)s', {userId}) }

+

{ _t('You are now ignoring %(userId)s', { userId }) }

, }); }), @@ -682,7 +758,7 @@ export const Commands = [ if (args) { const cli = MatrixClientPeg.get(); - const matches = args.match(/^(\S+)$/); + const matches = args.match(/(^@[^:]+:\S+$)/); if (matches) { const userId = matches[1]; const ignoredUsers = cli.getIgnoredUsers(); @@ -690,11 +766,10 @@ export const Commands = [ if (index !== -1) ignoredUsers.splice(index, 1); return success( cli.setIgnoredUsers(ignoredUsers).then(() => { - const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); Modal.createTrackedDialog('Slash Commands', 'User unignored', InfoDialog, { title: _t('Unignored user'), description:
-

{ _t('You are no longer ignoring %(userId)s', {userId}) }

+

{ _t('You are no longer ignoring %(userId)s', { userId }) }

, }); }), @@ -721,8 +796,11 @@ export const Commands = [ if (!isNaN(powerLevel)) { const cli = MatrixClientPeg.get(); const room = cli.getRoom(roomId); - if (!room) return reject('Bad room ID: ' + roomId); - + if (!room) return reject(_t("Command failed")); + const member = room.getMember(userId); + if (!member || getEffectiveMembership(member.membership) === EffectiveMembership.Leave) { + return reject(_t("Could not find user in room")); + } const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', ''); return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent)); } @@ -742,9 +820,10 @@ export const Commands = [ if (matches) { const cli = MatrixClientPeg.get(); const room = cli.getRoom(roomId); - if (!room) return reject('Bad room ID: ' + roomId); + if (!room) return reject(_t("Command failed")); const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', ''); + if (!powerLevelEvent.getContent().users[args]) return reject(_t("Could not find user in room")); return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent)); } } @@ -756,26 +835,58 @@ export const Commands = [ command: 'devtools', description: _td('Opens the Developer Tools dialog'), runFn: function(roomId) { - const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog'); - Modal.createDialog(DevtoolsDialog, {roomId}); + Modal.createDialog(DevtoolsDialog, { roomId }); return success(); }, category: CommandCategories.advanced, }), new Command({ command: 'addwidget', - args: '', + args: '', description: _td('Adds a custom widget by URL to the room'), - runFn: function(roomId, args) { - if (!args || (!args.startsWith("https://") && !args.startsWith("http://"))) { + isEnabled: () => SettingsStore.getValue(UIFeature.Widgets), + runFn: function(roomId, widgetUrl) { + if (!widgetUrl) { + return reject(_t("Please supply a widget URL or embed code")); + } + + // Try and parse out a widget URL from iframes + if (widgetUrl.toLowerCase().startsWith("; + return