diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 9973cfb120..1faffbbdf7 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -6,25 +6,20 @@ 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 diff --git a/.eslintrc.js b/.eslintrc.js index 6a0576c58a..fc82e75ce2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,121 +1,34 @@ -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 = { + extends: ["matrix-org", "matrix-org/react-legacy"], parser: "babel-eslint", - extends: [matrixJsSdkPath + "/.eslintrc.js"], - plugins: [ - "react", - "react-hooks", - "flowtype", - "babel" - ], + + 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", - - // 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) - // 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, - } - ], - "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", + // Things we do that break the ideal style + "no-constant-condition": "off", + "prefer-promise-reject-errors": "off", + "no-async-promise-executor": "off", + "quotes": "off", + "indent": "off", }, - settings: { - flowtype: { - onlyFilesWithFlowAnnotation: true + + overrides: [{ + "files": ["src/**/*.{ts, tsx}"], + "extends": ["matrix-org/ts"], + "rules": { + // 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", + + "quotes": "off", + "no-extra-boolean-cast": "off", }, - }, + }], }; diff --git a/.stylelintrc.js b/.stylelintrc.js index 1690f2186f..313102ea83 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -17,7 +17,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..29cbb040f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,1712 @@ +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/README.md b/README.md index 69aafeb724..e468d272d0 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.riot.im/widgets/element-web/-/multi-auto.svg)](https://translate.riot.im/engage/element-web/?utm_source=widget) Developer Guide =============== @@ -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`, @@ -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/code_style.md b/code_style.md index 3ad0d38873..fe04d2cc3d 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 ------------- @@ -151,6 +151,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 ---------- diff --git a/docs/ciderEditor.md b/docs/ciderEditor.md index 00033b5b8c..f522dc2fc4 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, ...). 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/room-list-store.md b/docs/room-list-store.md new file mode 100644 index 0000000000..fa849e2505 --- /dev/null +++ b/docs/room-list-store.md @@ -0,0 +1,151 @@ +# 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, which are then passed along to the algorithm +implementations. The implementations then get to decide how to actually filter the rooms, however in +practice the base `Algorithm` class deals with the filtering in a more optimized/generic way. + +The results of 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. + +## 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..4172c72c15 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,38 +96,29 @@ 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)`. ## Setting controllers @@ -162,7 +130,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 +190,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 +198,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 +208,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 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/package.json b/package.json index 67907c0d98..ab71f68b08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.3.1", + "version": "3.2.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -45,118 +45,135 @@ "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": "yarn lint:types && 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: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" }, "dependencies": { - "@babel/runtime": "^7.8.3", - "blueimp-canvas-to-blob": "^3.5.0", + "@babel/runtime": "^7.10.5", + "await-lock": "^2.0.1", + "blueimp-canvas-to-blob": "^3.27.0", "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": "^4.0.2", - "emojibase-regex": "^3.0.0", + "classnames": "^2.2.6", + "commonmark": "^0.29.1", + "counterpart": "^0.18.6", + "create-react-class": "^15.6.3", + "diff-dom": "^4.1.6", + "diff-match-patch": "^1.0.5", + "emojibase-data": "^5.0.1", + "emojibase-regex": "^4.0.1", "escape-html": "^1.0.3", - "file-saver": "^1.3.3", - "filesize": "3.5.6", + "file-saver": "^1.3.8", + "filesize": "3.6.1", "flux": "2.1.1", - "focus-visible": "^5.0.2", - "fuse.js": "^2.2.0", - "gfm.css": "^1.1.1", + "focus-visible": "^5.1.0", + "fuse.js": "^2.7.4", + "gfm.css": "^1.1.2", "glob-to-regexp": "^0.4.1", - "highlight.js": "^9.15.8", - "html-entities": "^1.2.1", + "highlight.js": "^10.1.2", + "html-entities": "^1.3.1", "is-ip": "^2.0.0", - "linkifyjs": "^2.1.6", - "lodash": "^4.17.14", + "linkifyjs": "^2.1.9", + "lodash": "^4.17.19", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", - "minimist": "^1.2.0", - "pako": "^1.0.5", + "minimist": "^1.2.5", + "pako": "^1.0.11", "parse5": "^5.1.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", - "qs": "^6.6.0", - "react": "^16.9.0", + "qs": "^6.9.4", + "re-resizable": "^6.5.4", + "react": "^16.13.1", "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", + "react-dom": "^16.13.1", + "react-focus-lock": "^2.4.1", + "react-transition-group": "^4.4.1", + "resize-observer-polyfill": "^1.5.1", + "sanitize-html": "^1.27.1", + "tar-js": "^0.3.0", + "text-encoding-utf-8": "^1.0.2", "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", + "@babel/cli": "^7.10.5", + "@babel/core": "^7.10.5", + "@babel/parser": "^7.11.0", + "@babel/plugin-proposal-class-properties": "^7.10.4", + "@babel/plugin-proposal-decorators": "^7.10.5", + "@babel/plugin-proposal-export-default-from": "^7.10.4", + "@babel/plugin-proposal-numeric-separator": "^7.10.4", + "@babel/plugin-proposal-object-rest-spread": "^7.10.4", + "@babel/plugin-transform-flow-comments": "^7.10.4", + "@babel/plugin-transform-runtime": "^7.10.5", + "@babel/preset-env": "^7.10.4", + "@babel/preset-flow": "^7.10.4", + "@babel/preset-react": "^7.10.4", + "@babel/preset-typescript": "^7.10.4", + "@babel/register": "^7.10.5", + "@babel/traverse": "^7.11.0", + "@peculiar/webcrypto": "^1.1.2", "@types/classnames": "^2.2.10", + "@types/counterpart": "^0.18.1", + "@types/flux": "^3.1.9", + "@types/linkifyjs": "^2.1.3", + "@types/lodash": "^4.14.158", "@types/modernizr": "^3.5.3", - "@types/react": "16.9", - "babel-eslint": "^10.0.3", + "@types/node": "^12.12.51", + "@types/pako": "^1.0.1", + "@types/qrcode": "^1.3.4", + "@types/react": "^16.9", + "@types/react-dom": "^16.9.8", + "@types/react-transition-group": "^4.4.0", + "@types/sanitize-html": "^1.23.3", + "@types/zxcvbn": "^4.4.0", + "@typescript-eslint/eslint-plugin": "^3.7.0", + "@typescript-eslint/parser": "^3.7.0", + "babel-eslint": "^10.1.0", "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", + "chokidar": "^3.4.1", + "concurrently": "^4.1.2", + "enzyme": "^3.11.0", + "enzyme-adapter-react-16": "^1.15.2", + "eslint": "7.5.0", + "eslint-config-matrix-org": "^0.1.2", + "eslint-plugin-babel": "^5.3.1", + "eslint-plugin-flowtype": "^2.50.3", + "eslint-plugin-react": "^7.20.3", + "eslint-plugin-react-hooks": "^2.5.1", "file-loader": "^3.0.1", - "flow-parser": "^0.57.3", - "glob": "^5.0.14", + "glob": "^5.0.15", "jest": "^24.9.0", + "jest-canvas-mock": "^2.2.0", "lolex": "^5.1.2", "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", + "react-test-renderer": "^16.13.1", + "rimraf": "^2.7.1", + "source-map-loader": "^0.2.4", "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" + "stylelint-config-standard": "^18.3.0", + "stylelint-scss": "^3.18.0", + "typescript": "^3.9.7", + "walk": "^2.3.14", + "webpack": "^4.43.0", + "webpack-cli": "^3.3.12" }, "jest": { "testMatch": [ "/test/**/*-test.js" ], + "setupFiles": [ + "jest-canvas-mock" + ], "setupFilesAfterEnv": [ "/test/setupTests.js" ], diff --git a/res/css/_common.scss b/res/css/_common.scss index 03442ca510..a22d77f3d3 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -19,7 +19,7 @@ limitations under the License. @import "./_font-sizes.scss"; :root { - font-size: 15px; + font-size: 10px; } html { @@ -38,7 +38,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; } @@ -160,9 +160,8 @@ 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; } @@ -175,7 +174,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); } } @@ -227,7 +226,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: @@ -264,7 +263,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { padding: 25px 30px 30px 30px; max-height: 80%; box-shadow: 2px 15px 30px 0 $dialog-shadow-color; - border-radius: 4px; + border-radius: 8px; overflow-y: auto; } @@ -319,7 +318,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; @@ -335,6 +334,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; @@ -425,12 +427,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; @@ -578,3 +584,36 @@ 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 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 0ba2b609e8..aedb5c1334 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"; @@ -15,19 +16,20 @@ @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/_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"; @@ -41,11 +43,15 @@ @import "./views/auth/_CountryDropdown.scss"; @import "./views/auth/_InteractiveAuthEntryComponents.scss"; @import "./views/auth/_LanguageSelector.scss"; +@import "./views/auth/_PassphraseField.scss"; @import "./views/auth/_ServerConfig.scss"; @import "./views/auth/_ServerTypeSelector.scss"; @import "./views/auth/_Welcome.scss"; @import "./views/avatars/_BaseAvatar.scss"; +@import "./views/avatars/_DecoratedRoomAvatar.scss"; @import "./views/avatars/_MemberStatusMessageAvatar.scss"; +@import "./views/avatars/_PulsedAvatar.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"; @@ -54,15 +60,14 @@ @import "./views/context_menus/_WidgetContextMenu.scss"; @import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_Analytics.scss"; +@import "./views/dialogs/_BugReportDialog.scss"; @import "./views/dialogs/_ChangelogDialog.scss"; @import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss"; @import "./views/dialogs/_ConfirmUserActionDialog.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/_GroupAddressPicker.scss"; @import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_InviteDialog.scss"; @@ -73,6 +78,7 @@ @import "./views/dialogs/_RoomSettingsDialogBridges.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; @import "./views/dialogs/_RoomUpgradeWarningDialog.scss"; +@import "./views/dialogs/_ServerOfflineDialog.scss"; @import "./views/dialogs/_SetEmailDialog.scss"; @import "./views/dialogs/_SetMxIdDialog.scss"; @import "./views/dialogs/_SetPasswordDialog.scss"; @@ -81,7 +87,6 @@ @import "./views/dialogs/_SlashCommandHelpDialog.scss"; @import "./views/dialogs/_TabbedIntegrationManagerDialog.scss"; @import "./views/dialogs/_TermsDialog.scss"; -@import "./views/dialogs/_UnknownDeviceDialog.scss"; @import "./views/dialogs/_UploadConfirmDialog.scss"; @import "./views/dialogs/_UserSettingsDialog.scss"; @import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss"; @@ -104,16 +109,19 @@ @import "./views/elements/_IconButton.scss"; @import "./views/elements/_ImageView.scss"; @import "./views/elements/_InlineSpinner.scss"; -@import "./views/elements/_InteractiveTooltip.scss"; @import "./views/elements/_ManageIntegsButton.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/_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,7 +129,6 @@ @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"; @@ -139,7 +146,7 @@ @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"; @@ -159,31 +166,31 @@ @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/_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/_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/_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/_WhoIsTypingTile.scss"; @import "./views/settings/_AvatarSetting.scss"; @import "./views/settings/_CrossSigningPanel.scss"; @@ -197,10 +204,12 @@ @import "./views/settings/_ProfileSettings.scss"; @import "./views/settings/_SetIdServer.scss"; @import "./views/settings/_SetIntegrationManager.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/_MjolnirUserSettingsTab.scss"; @@ -209,7 +218,8 @@ @import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss"; @import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss"; @import "./views/terms/_InlineTermsAgreement.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"; diff --git a/res/css/_font-sizes.scss b/res/css/_font-sizes.scss index 5cd19ce620..caa3a452b0 100644 --- a/res/css/_font-sizes.scss +++ b/res/css/_font-sizes.scss @@ -14,58 +14,60 @@ See the License for the specific language governing permissions and limitations under the License. */ -$font-1px: 0.067rem; -$font-2px: 0.133rem; -$font-3px: 0.200rem; -$font-4px: 0.267rem; -$font-5px: 0.333rem; -$font-6px: 0.400rem; -$font-7px: 0.467rem; -$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-88px: 5.887rem; -$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..658033339a 100644 --- a/res/css/structures/_ContextualMenu.scss +++ b/res/css/structures/_ContextualMenu.scss @@ -31,7 +31,7 @@ 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; diff --git a/res/css/structures/_CustomRoomTagPanel.scss b/res/css/structures/_CustomRoomTagPanel.scss index 1fb18ec41e..3feb2565be 100644 --- a/res/css/structures/_CustomRoomTagPanel.scss +++ b/res/css/structures/_CustomRoomTagPanel.scss @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +// TODO: Update design for custom tags to match new designs + .mx_LeftPanel_tagPanelContainer { display: flex; flex-direction: column; @@ -50,7 +52,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..21b30d804a 100644 --- a/res/css/structures/_FilePanel.scss +++ b/res/css/structures/_FilePanel.scss @@ -41,13 +41,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 +66,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 +86,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 +96,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 +115,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/_GroupView.scss b/res/css/structures/_GroupView.scss index ed0cf121a4..2350d9f28a 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 { diff --git a/res/css/structures/_HeaderButtons.scss b/res/css/structures/_HeaderButtons.scss index eef7653b24..9ef40e9d6a 100644 --- a/res/css/structures/_HeaderButtons.scss +++ b/res/css/structures/_HeaderButtons.scss @@ -22,7 +22,7 @@ limitations under the License. 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..04527bff48 100644 --- a/res/css/structures/_HomePage.scss +++ b/res/css/structures/_HomePage.scss @@ -72,7 +72,7 @@ limitations under the License. &:hover { color: $accent-color; - background: rgba(#03b381, 0.06); + background: rgba($accent-color, 0.06); &::before { background-color: $accent-color; diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 7d57425f6f..5112d07c46 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,182 @@ 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; -} +$tagPanelWidth: 56px; // only applies in this file, used for calculations .mx_LeftPanel { - flex: 1; - overflow-x: hidden; - display: flex; - flex-direction: column; - min-height: 0; -} + background-color: $roomlist-bg-color; + min-width: 260px; + max-width: 50%; -.mx_LeftPanel .mx_AppTile_mini { - height: 132px; -} - -.mx_LeftPanel .mx_RoomList_scrollbar { - order: 1; - - 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; - } - - .mx_LeftPanel_container.collapsed .mx_BottomLeftMenu { - flex: 0 0 160px; - } - - .mx_LeftPanel .mx_BottomLeftMenu_options { - margin-top: 12px; - } -} - -.mx_LeftPanel_exploreAndFilterRow { + // Create a row-based flexbox for the TagPanel and the room list display: flex; - .mx_SearchBox { - flex: 1 1 0; - min-width: 0; - margin: 4px 9px 1px 9px; - } -} + .mx_LeftPanel_tagPanelContainer { + flex-grow: 0; + flex-shrink: 0; + flex-basis: $tagPanelWidth; + height: 100%; -.mx_LeftPanel_explore { - flex: 0 0 50%; - overflow: hidden; - transition: flex-basis 0.2s; - box-sizing: border-box; + // Create another flexbox so the TagPanel fills the container + display: flex; - &.mx_LeftPanel_explore_hidden { - flex-basis: 0; + // TagPanel handles its own CSS } - .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; + &:not(.mx_LeftPanel_hasTagPanel) { + .mx_LeftPanel_roomListContainer { + width: 100%; + } + } - &:hover { - background-color: $primary-bg-color; + // 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 { + width: calc(100% - $tagPanelWidth); + background-color: $roomlist-bg-color; + + // Create another flexbox (this time a column) for the room list components + display: flex; + flex-direction: column; + + .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 + + // 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; + + &.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_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_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; + + // We have to forcefully set the width to override the resizer's style attribute. + &.mx_LeftPanel_hasTagPanel { + width: calc(68px + $tagPanelWidth) !important; + } + &:not(.mx_LeftPanel_hasTagPanel) { + width: 68px !important; + } + + .mx_LeftPanel_roomListContainer { + width: 68px; + + .mx_LeftPanel_filterContainer { + // Organize the flexbox into a centered column layout + flex-direction: column; + justify-content: center; + + .mx_LeftPanel_exploreButton { + margin-left: 0; + margin-top: 8px; + background-color: transparent; + } + } } } } diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss index 25e1153fce..dc62cb8218 100644 --- a/res/css/structures/_MainSplit.scss +++ b/res/css/structures/_MainSplit.scss @@ -21,8 +21,22 @@ limitations under the License. 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; + + &: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..f4e46a8e94 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,36 @@ 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_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_LeftPanel2:hover + .mx_ResizeHandle_horizontal, +.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/_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..715a94fe2c 100644 --- a/res/css/structures/_NotificationPanel.scss +++ b/res/css/structures/_NotificationPanel.scss @@ -63,6 +63,10 @@ limitations under the License. padding-left: 32px; padding-top: 8px; position: relative; + + a { + display: flex; + } } .mx_NotificationPanel .mx_EventTile_roomName a, @@ -95,3 +99,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..c7c0d6fac4 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -19,9 +19,16 @@ 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%; + + .mx_RoomView_MessageList { + padding: 14px 18px; // top and bottom is 4px smaller to balance with the padding set above + } } .mx_RightPanel_header { @@ -43,56 +50,67 @@ 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'); + mask-image: url('$(res)/img/element-icons/room/members.svg'); + mask-position: center; } .mx_RightPanel_filesButton::before { - mask-image: url('$(res)/img/feather-customised/files.svg'); + mask-image: url('$(res)/img/element-icons/room/files.svg'); + mask-position: center; } .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_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; +.mx_RightPanel_headerButton_highlight { + background: rgba($accent-color, 0.25); + // make the icon the accent color too + &::before { + background-color: $accent-color !important; + } +} + +.mx_RightPanel_headerButton:not(.mx_RightPanel_headerButton_highlight) { + &:hover { + background: rgba($accent-color, 0.1); + + &::before { + background-color: $accent-color; + } + } } .mx_RightPanel_headerButton_badge { @@ -126,3 +144,28 @@ limitations under the License. order: 2; margin: auto; } + +.mx_RightPanel_empty { + margin-right: -42px; + + 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; + } +} diff --git a/res/css/structures/_RoomSearch.scss b/res/css/structures/_RoomSearch.scss new file mode 100644 index 0000000000..c33a3c0ff9 --- /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: 2px; + + // 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/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss deleted file mode 100644 index 2c53258b08..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: $font-16px; - 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..3b60c4e62b 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -261,7 +261,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/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 4a78c8df92..b2d05ad7e6 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -33,7 +33,7 @@ limitations under the License. .mx_TagPanel .mx_TagPanel_clearButton_container { /* Constant height within flex mx_TagPanel */ height: 70px; - width: 60px; + width: 56px; flex: none; @@ -51,7 +51,7 @@ limitations under the License. .mx_TagPanel .mx_TagPanel_divider { height: 0px; - width: 42px; + width: 34px; border-bottom: 1px solid $panel-divider-color; display: none; } @@ -66,15 +66,13 @@ limitations under the License. flex-direction: column; align-items: center; - height: 100%; + padding-top: 6px; } .mx_TagPanel .mx_TagPanel_tagTileContainer > div { - height: $font-40px; - padding: 10px 0 9px 0; + margin: 6px 0; } .mx_TagPanel .mx_TagTile { - margin: 9px 0; // opacity: 0.5; position: relative; } @@ -86,8 +84,8 @@ limitations under the License. .mx_TagPanel .mx_TagTile_plus { margin-bottom: 12px; - height: 40px; - width: 40px; + height: 32px; + width: 32px; border-radius: 20px; background-color: $roomheader-addroom-bg-color; position: relative; @@ -110,13 +108,12 @@ limitations under the License. .mx_TagPanel .mx_TagTile.mx_TagTile_selected::before { content: ''; - height: calc(100% + 16px); + height: 100%; background-color: $accent-color; - width: 5px; + width: 4px; position: absolute; - left: -15px; + left: -12px; border-radius: 0 3px 3px 0; - top: -8px; // (16px / 2) } .mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus { @@ -159,7 +156,7 @@ limitations under the License. font-weight: 600; font-size: $font-14px; padding: 0 5px; - background-color: $roomtile-name-color; + background-color: $muted-fg-color; } .mx_TagTile_badgeHighlight { diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index 6ec4a0d152..544dcbc180 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -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,59 @@ 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: 90%; + } + + &::after { + mask-image: url("$(res)/img/e2e/warning.svg"); + background-color: $notice-primary-color; + } } - h2, .mx_Toast_body { + .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,7 +127,13 @@ limitations under the License. } .mx_Toast_buttons { + float: right; display: flex; + + .mx_FormButton { + min-width: 96px; + box-sizing: border-box; + } } .mx_Toast_description { @@ -96,6 +142,15 @@ limitations under the License. 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_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/_UserMenu.scss b/res/css/structures/_UserMenu.scss new file mode 100644 index 0000000000..78795c85a2 --- /dev/null +++ b/res/css/structures/_UserMenu.scss @@ -0,0 +1,196 @@ +/* +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 ... button sort of aligned with the explore button below + padding-right: 6px; + + .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: $primary-fg-color; + mask-image: url('$(res)/img/element-icons/context-menu.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 + + .mx_UserMenu_userAvatar { + border-radius: 32px; // should match avatar size + object-fit: cover; + } + } + + .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_minimized { + .mx_UserMenu_userHeader { + .mx_UserMenu_row { + justify-content: center; + } + + .mx_UserMenu_userAvatarContainer { + margin-right: 0; + } + } + } +} + +.mx_UserMenu_contextMenu { + width: 247px; + + &.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_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_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'); + } +} diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index 4b2d6b1bf1..0ba0d10e06 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -128,6 +128,11 @@ limitations under the License. margin-top: 16px; font-size: $font-15px; line-height: $font-24px; + + .mx_InlineSpinner img { + vertical-align: sub; + margin-right: 5px; + } } .mx_AuthBody_paddedFooter_subtitle { @@ -147,26 +152,11 @@ limitations under the License. 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..b1372affee 100644 --- a/res/css/views/auth/_AuthHeader.scss +++ b/res/css/views/auth/_AuthHeader.scss @@ -21,3 +21,9 @@ limitations under the License. padding: 25px 40px; 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..917dcabf67 100644 --- a/res/css/views/auth/_AuthHeaderLogo.scss +++ b/res/css/views/auth/_AuthHeaderLogo.scss @@ -23,3 +23,9 @@ limitations under the License. .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/_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/avatars/_BaseAvatar.scss b/res/css/views/avatars/_BaseAvatar.scss index e59598278f..1a1e14e7ac 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; diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.scss b/res/css/views/avatars/_DecoratedRoomAvatar.scss new file mode 100644 index 0000000000..e0afd9de66 --- /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. +*/ + +// XXX: We shouldn't be using TemporaryTile anywhere - delete it. +.mx_DecoratedRoomAvatar, .mx_TemporaryTile { + position: relative; + + &.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/_PulsedAvatar.scss b/res/css/views/avatars/_PulsedAvatar.scss new file mode 100644 index 0000000000..ce9e3382ab --- /dev/null +++ b/res/css/views/avatars/_PulsedAvatar.scss @@ -0,0 +1,30 @@ +/* +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_PulsedAvatar { + @keyframes shadow-pulse { + 0% { + box-shadow: 0 0 0 0px rgba($accent-color, 0.2); + } + 100% { + box-shadow: 0 0 0 6px rgba($accent-color, 0); + } + } + + img { + animation: shadow-pulse 1s infinite; + } +} diff --git a/res/css/views/context_menus/_IconizedContextMenu.scss b/res/css/views/context_menus/_IconizedContextMenu.scss new file mode 100644 index 0000000000..7913058995 --- /dev/null +++ b/res/css/views/context_menus/_IconizedContextMenu.scss @@ -0,0 +1,148 @@ +/* +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; + } + + img, .mx_IconizedContextMenu_icon { // icons + width: 16px; + min-width: 16px; + max-width: 16px; + } + + span.mx_IconizedContextMenu_label { // labels + padding-left: 14px; + width: 100%; + flex: 1; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + } + + .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/_TagTileContextMenu.scss b/res/css/views/context_menus/_TagTileContextMenu.scss index e4ccc030a2..8929c8906e 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,22 @@ 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_hideCommunity::before { + mask-image: url('$(res)/img/element-icons/hide.svg'); } .mx_TagTileContextMenu_separator { 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/_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/_RoomSettingsDialog.scss b/res/css/views/dialogs/_RoomSettingsDialog.scss index 3751c15643..d4199a1e66 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 { 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/_ShareDialog.scss b/res/css/views/dialogs/_ShareDialog.scss index 9a2f67dea3..c343b872fd 100644 --- a/res/css/views/dialogs/_ShareDialog.scss +++ b/res/css/views/dialogs/_ShareDialog.scss @@ -51,10 +51,12 @@ limitations under the License. display: inherit; } .mx_ShareDialog_matrixto_copy > div { - background-image: url($copy-button-url); + 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,9 +66,6 @@ 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; 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/_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/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index b9babd05f5..9be98e25b2 100644 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -35,17 +35,6 @@ limitations under the License. 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; diff --git a/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss index db11e91bdb..63d0ca555d 100644 --- a/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss @@ -15,20 +15,79 @@ See the License for the specific language governing permissions and limitations under the License. */ +.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_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_primaryContainer { - /* FIXME: plinth colour in new theme(s). background-color: $accent-color; */ - padding: 20px; -} - -.mx_AccessSecretStorageDialog_passPhraseInput, -.mx_AccessSecretStorageDialog_recoveryKeyInput { +.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/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss index a9ebd54b31..d30803b1f0 100644 --- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss @@ -48,6 +48,29 @@ limitations under the License. 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; @@ -59,6 +82,36 @@ limitations under the License. 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; @@ -68,49 +121,47 @@ limitations under the License. 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; + width: 380px; + margin-left: auto; + margin-right: auto; } .mx_CreateSecretStorageDialog_recoveryKey { - width: 262px; + font-weight: bold; + text-align: center; padding: 20px; color: $info-plinth-fg-color; background-color: $info-plinth-bg-color; - margin-right: 12px; + border-radius: 6px; + word-spacing: 1em; + margin-bottom: 20px; } .mx_CreateSecretStorageDialog_recoveryKeyButtons { - flex: 1; display: flex; + justify-content: space-between; align-items: center; } .mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton { - margin-right: 10px; -} - -.mx_CreateSecretStorageDialog_recoveryKeyButtons button { - flex: 1; + 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/directory/_NetworkDropdown.scss b/res/css/views/directory/_NetworkDropdown.scss index bd5c67c7ed..ae0927386a 100644 --- a/res/css/views/directory/_NetworkDropdown.scss +++ b/res/css/views/directory/_NetworkDropdown.scss @@ -145,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/_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/_Dropdown.scss b/res/css/views/elements/_Dropdown.scss index dd8511cc42..2a2508c17c 100644 --- a/res/css/views/elements/_Dropdown.scss +++ b/res/css/views/elements/_Dropdown.scss @@ -77,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/_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/_ImageView.scss b/res/css/views/elements/_ImageView.scss index 983ef074f2..0a4ed2a194 100644 --- a/res/css/views/elements/_ImageView.scss +++ b/res/css/views/elements/_ImageView.scss @@ -37,7 +37,7 @@ limitations under the License. order: 2; /* min-width hack needed for FF */ min-width: 0px; - max-height: 90%; + height: 90%; flex: 15 15 0; display: flex; align-items: center; diff --git a/res/css/views/elements/_InlineSpinner.scss b/res/css/views/elements/_InlineSpinner.scss index 612b6209c6..6b91e45923 100644 --- a/res/css/views/elements/_InlineSpinner.scss +++ b/res/css/views/elements/_InlineSpinner.scss @@ -18,7 +18,7 @@ limitations under the License. display: inline; } -.mx_InlineSpinner img { +.mx_InlineSpinner_spin img { margin: 0px 6px; vertical-align: -3px; } 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/_ProgressBar.scss b/res/css/views/elements/_ProgressBar.scss index a3fee232d0..e49d85af04 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 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: 4px; + width: 60px; + border-radius: 10px; + overflow: hidden; + appearance: none; + border: 0; -.mx_ProgressBar_fill { - height: 100%; - background-color: $progressbar-color; + @mixin ProgressBarBorderRadius "10px"; + @mixin ProgressBarColour $accent-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/src/utils/rem.js b/res/css/views/elements/_QRCode.scss similarity index 85% rename from src/utils/rem.js rename to res/css/views/elements/_QRCode.scss index 1f18c9de05..96d9114b54 100644 --- a/src/utils/rem.js +++ b/res/css/views/elements/_QRCode.scss @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -// converts a pixel value to rem. -export default function(pixelVal) { - return pixelVal / 15 + "rem"; +.mx_QRCode { + img { + border-radius: 8px; + } } 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/_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/_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 7c65fbc90d..62669889ee 100644 --- a/res/css/views/elements/_ToggleSwitch.scss +++ b/res/css/views/elements/_ToggleSwitch.scss @@ -24,11 +24,6 @@ limitations under the License. background-color: $togglesw-off-color; opacity: 0.5; - - display: flex; - flex-direction: row; - flex: 0 0 auto; - align-items: center; } .mx_ToggleSwitch_enabled { @@ -38,16 +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; + position: relative; width: $font-20px; height: $font-20px; border-radius: $font-20px; background-color: $togglesw-ball-color; -} - -.mx_ToggleSwitch_on { - flex-direction: row-reverse; + 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/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 547b16e9ad..1c773c2f06 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -25,6 +25,7 @@ limitations under the License. height: 100%; left: 0; top: 0; + border-radius: 4px; } .mx_MImageBody_thumbnail_container { diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 9f3971ecf0..1254b496b5 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -24,7 +24,7 @@ limitations under the License. line-height: $font-24px; border-radius: 4px; background: $message-action-bar-bg-color; - top: -18px; + top: -26px; right: 8px; user-select: none; // Ensure the action bar appears above over things, like the read marker. @@ -41,7 +41,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; @@ -91,17 +91,17 @@ limitations under the License. } .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'); } 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/_ReactionsRowButton.scss b/res/css/views/messages/_ReactionsRowButton.scss index fe5b081042..7158ffc027 100644 --- a/res/css/views/messages/_ReactionsRowButton.scss +++ b/res/css/views/messages/_ReactionsRowButton.scss @@ -16,7 +16,6 @@ limitations under the License. .mx_ReactionsRowButton { display: inline-flex; - height: 20px; line-height: $font-21px; margin-right: 6px; padding: 0 6px; @@ -35,11 +34,6 @@ limitations under the License. border-color: $reaction-row-button-selected-border-color; } - // ignore mouse events for all children, treat it as one entire hoverable entity - * { - pointer-events: none; - } - .mx_ReactionsRowButton_content { max-width: 100px; overflow: hidden; diff --git a/res/css/views/messages/_RedactedBody.scss b/res/css/views/messages/_RedactedBody.scss new file mode 100644 index 0000000000..e4ab0c0835 --- /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: 2px; + left: 0; + } +} diff --git a/res/css/views/messages/_common_CryptoEvent.scss b/res/css/views/messages/_common_CryptoEvent.scss index 637d25d7a1..09c78ae5b4 100644 --- a/res/css/views/messages/_common_CryptoEvent.scss +++ b/res/css/views/messages/_common_CryptoEvent.scss @@ -15,28 +15,45 @@ limitations under the License. */ .mx_cryptoEvent { - display: grid; grid-template-columns: 24px minmax(0, 1fr) min-content; + &.mx_cryptoEvent_icon::before, &.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%; + top: 0; + bottom: 0; + left: 0; + right: 0; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/e2e/normal.svg'); + background-color: $composer-e2e-icon-color; margin-top: 4px; } + // 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: 90%; + } + &.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 { diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 26b81e94f3..6f86d1ad18 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -53,7 +53,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 { @@ -121,7 +121,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; } diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index d6fb055bc0..aae255f81a 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -49,7 +49,7 @@ $MiniAppTileHeight: 114px; order: 2; cursor: pointer; padding: 0; - margin: 5px auto 5px 0px; + margin: 5px auto 5px 0; color: $accent-color; font-size: $font-12px; } @@ -87,7 +87,7 @@ $MiniAppTileHeight: 114px; margin: 0; padding: 0; border: 5px solid $widget-menu-bar-bg-color; - border-radius: 4px; + border-radius: 8px; display: flex; flex-direction: column; } @@ -117,6 +117,7 @@ $MiniAppTileHeight: 114px; .mx_AppTile_persistedWrapper div { width: 100%; height: 100%; + min-width: 300px; } .mx_AppTileMenuBar { diff --git a/res/css/views/rooms/_Autocomplete.scss b/res/css/views/rooms/_Autocomplete.scss index a4aebdb708..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 { diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index 584ea17433..a3473dedec 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: 90%; +} + +// 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: 65%; + } } .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 8db71f297c..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; } } diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 99377ee5be..eb0e1dd7b0 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -15,6 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +$left-gutter: 64px; + .mx_EventTile { max-width: 100%; clear: both; @@ -37,7 +39,6 @@ limitations under the License. } .mx_EventTile_avatar { - position: absolute; top: 14px; left: 8px; cursor: pointer; @@ -46,7 +47,7 @@ limitations under the License. .mx_EventTile.mx_EventTile_info .mx_EventTile_avatar { top: $font-8px; - left: 65px; + left: $left-gutter; } .mx_EventTile_continuation { @@ -68,15 +69,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 { @@ -104,9 +103,7 @@ limitations under the License. visibility: hidden; white-space: nowrap; left: 0px; - width: 46px; /* 8 + 30 (avatar) + 8 */ text-align: center; - position: absolute; user-select: none; } @@ -116,17 +113,14 @@ limitations under the License. .mx_EventTile_line, .mx_EventTile_reply { position: relative; - padding-left: 65px; /* left gutter */ - padding-top: 3px; - padding-bottom: 3px; + padding-left: $left-gutter; border-radius: 4px; - line-height: $font-22px; } .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; } @@ -151,10 +145,6 @@ 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; @@ -171,10 +161,15 @@ limitations under the License. } // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) +// The first set is to handle the 'group layout' (default) and the second for the IRC layout .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 { +.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp, +.mx_IRCLayout .mx_EventTile_last > a > .mx_MessageTimestamp, +.mx_IRCLayout .mx_EventTile:hover > a > .mx_MessageTimestamp, +.mx_IRCLayout .mx_EventTile.mx_EventTile_actionBarFocused > a > .mx_MessageTimestamp, +.mx_IRCLayout .mx_EventTile.focus-visible:focus-within > a > .mx_MessageTimestamp { visibility: visible; } @@ -189,7 +184,7 @@ 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; + border-left: $accent-color 4px solid; padding-left: 60px; background-color: $event-selected-color; } @@ -244,34 +239,6 @@ limitations under the License. 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_contextual { opacity: 0.4; } @@ -363,29 +330,67 @@ 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: 90%; + } } .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_unauthenticated { + &::after { + mask-image: url('$(res)/img/e2e/normal.svg'); + background-color: $composer-e2e-icon-color; + } opacity: 1; } @@ -427,10 +432,6 @@ 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 { @@ -438,15 +439,15 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { } .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line { - border-left: $e2e-verified-color 5px solid; + border-left: $e2e-verified-color 4px solid; } .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line { - border-left: $e2e-unverified-color 5px solid; + border-left: $e2e-unverified-color 4px solid; } .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { - border-left: $e2e-unknown-color 5px solid; + border-left: $e2e-unknown-color 4px solid; } .mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, @@ -539,7 +540,8 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { right: 6px; width: 19px; height: 19px; - background-image: url($copy-button-url); + mask-image: url($copy-button-url); + background-color: $message-action-bar-fg-color; } .mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_copyButton, @@ -567,7 +569,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 { @@ -588,84 +589,6 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { /* end of overrides */ -.mx_MatrixChat_useCompactLayout { - .mx_EventTile { - padding-top: 4px; - } - - .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 .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 - } - } -} - .mx_EventTile_tileError { color: red; text-align: center; @@ -685,3 +608,14 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { 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..2b447be44a --- /dev/null +++ b/res/css/views/rooms/_GroupLayout.scss @@ -0,0 +1,130 @@ +/* +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-17px; + padding-left: $left-gutter; + } + + > .mx_EventTile_line { + padding-left: $left-gutter; + } + + > .mx_EventTile_avatar { + position: absolute; + } + + .mx_MessageTimestamp { + position: absolute; + width: 46px; /* 8 + 30 (avatar) + 8 */ + } + + .mx_EventTile_line, .mx_EventTile_reply { + padding-top: 3px; + padding-bottom: 3px; + line-height: $font-22px; + } + } + + .mx_EventTile_info .mx_EventTile_line { + padding-left: calc($left-gutter + 18px); + } +} + +/* 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 { + 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_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..958d718b11 --- /dev/null +++ b/res/css/views/rooms/_IRCLayout.scss @@ -0,0 +1,225 @@ +/* +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; + } + + 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_SenderProfile { + order: 2; + flex-shrink: 0; + width: var(--name-width); + text-overflow: ellipsis; + text-align: left; + display: flex; + align-items: center; + overflow: visible; + justify-content: flex-end; + } + + .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, + .mx_ReplyThread_wrapper_empty { + 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_hover { + background-color: $primary-bg-color; + overflow: hidden; + + > span { + display: flex; + + > .mx_SenderProfile_name, + > .mx_SenderProfile_aux { + overflow: hidden; + text-overflow: ellipsis; + min-width: var(--name-width); + } + } + } + + .mx_SenderProfile:hover { + justify-content: flex-start; + } + + .mx_SenderProfile_hover:hover { + overflow: visible; + width: max(auto, 100%); + z-index: 10; + } + + .mx_ReplyThread { + margin: 0; + .mx_SenderProfile { + width: unset; + max-width: var(--name-width); + } + } + + .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..6cb3b6bce9 100644 --- a/res/css/views/rooms/_JumpToBottomButton.scss +++ b/res/css/views/rooms/_JumpToBottomButton.scss @@ -41,6 +41,11 @@ 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; } @@ -51,7 +56,7 @@ 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; } @@ -62,8 +67,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/_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/_MemberList.scss b/res/css/views/rooms/_MemberList.scss index 99dc2338d4..90667d41b4 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; @@ -59,32 +63,27 @@ limitations under the License. .mx_GroupMemberList_query, .mx_GroupRoomList_query { flex: 1 1 0; + + // 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 +92,17 @@ limitations under the License. } .mx_MemberList_invite span { - background-image: url('$(res)/img/feather-customised/user-add.svg'); + background-image: url('$(res)/img/element-icons/room/invite.svg'); background-repeat: no-repeat; background-position: center left; - padding-left: 25px; + background-size: 20px; + padding: 8px 0 8px 25px; +} + +.mx_MemberList_inviteCommunity span { + background-image: url('$(res)/img/icon-invite-people.svg'); +} + +.mx_MemberList_addRoomToCommunity span { + background-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..a403a8dc4c 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,72 @@ 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::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'); + mask-image: url('$(res)/img/element-icons/call/hangup.svg'); } .mx_MessageComposer_voicecall::before { - mask-image: url('$(res)/img/feather-customised/phone.svg'); + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); } .mx_MessageComposer_videocall::before { - mask-image: url('$(res)/img/feather-customised/video.svg'); + mask-image: url('$(res)/img/element-icons/call/video-call.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_formatting { @@ -279,7 +308,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..d97c49630a 100644 --- a/res/css/views/rooms/_MessageComposerFormatBar.scss +++ b/res/css/views/rooms/_MessageComposerFormatBar.scss @@ -75,23 +75,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/_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/_ReplyPreview.scss b/res/css/views/rooms/_ReplyPreview.scss index 4dc4cb2c40..10f8e21e43 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 { diff --git a/res/css/views/rooms/_RoomBreadcrumbs.scss b/res/css/views/rooms/_RoomBreadcrumbs.scss index 3858d836e6..6512797401 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 { + margin-left: -40px; // 32px for the avatar, 8px for the margin + } + &.mx_RoomBreadcrumbs-enter-active { + margin-left: 0; + + // Timing function is as-requested by design. + // NOTE: The transition time MUST match the value passed to CSSTransition! + transition: margin-left 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 80f6c40f39..a880a7bee2 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,7 +180,7 @@ limitations under the License. .mx_RoomHeader_avatar { flex: 0; - margin: 0 7px; + margin: 0 6px 0 7px; position: relative; } @@ -201,41 +208,53 @@ 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; } + + &:hover { + background: rgba($accent-color, 0.1); + + &::before { + background-color: $accent-color; + } + } } .mx_RoomHeader_settingsButton::before { - mask-image: url('$(res)/img/feather-customised/settings.svg'); + mask-image: url('$(res)/img/element-icons/settings.svg'); } .mx_RoomHeader_forgetButton::before { - mask-image: url('$(res)/img/leave.svg'); + mask-image: url('$(res)/img/element-icons/leave.svg'); width: 26px; } .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'); + mask-image: url('$(res)/img/element-icons/room/share.svg'); } .mx_RoomHeader_manageIntegsButton::before { - mask-image: url('$(res)/img/feather-customised/grid.svg'); + mask-image: url('$(res)/img/element-icons/room/integrations.svg'); } .mx_RoomHeader_showPanel { @@ -251,7 +270,7 @@ limitations under the License. } .mx_RoomHeader_pinnedButton::before { - mask-image: url('$(res)/img/icons-pin.svg'); + mask-image: url('$(res)/img/element-icons/room/pin.svg'); } .mx_RoomHeader_pinsIndicator { @@ -267,3 +286,12 @@ limitations under the License. .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..78e7307bc0 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,45 @@ 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.svg'); +} +.mx_RoomList_iconExplore::before { + mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); } -/* hide resize handles next to collapsed / empty sublists */ -.mx_RoomList .mx_RoomSubList:not(.mx_RoomSubList_nonEmpty) + .mx_ResizeHandle { - display: none; -} - -.mx_RoomList_expandButton { - margin-left: 8px; - cursor: pointer; - padding-left: 12px; - padding-right: 12px; -} - -.mx_RoomList_emptySubListTip_container { - padding-left: 18px; - padding-right: 18px; - padding-top: 8px; - padding-bottom: 7px; -} - -.mx_RoomList_emptySubListTip { +.mx_RoomList_explorePrompt { + margin: 4px 12px 4px; + padding-top: 12px; + border-top: 1px solid $tertiary-fg-color; 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; -} -.mx_RoomList_emptySubListTip .mx_RoleButton { - vertical-align: -2px; -} + div:first-child { + font-weight: $font-semi-bold; + margin-bottom: 8px; + } -.mx_RoomList_headerButtons { - position: absolute; - right: 60px; + .mx_AccessibleButton { + color: $secondary-fg-color; + position: relative; + padding: 0 0 0 24px; + font-size: inherit; + + &::before { + content: ''; + width: 16px; + height: 16px; + position: absolute; + top: 0; + left: 0; + background: $secondary-fg-color; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/roomlist/explore.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/_RoomSublist.scss b/res/css/views/rooms/_RoomSublist.scss new file mode 100644 index 0000000000..543940fb78 --- /dev/null +++ b/res/css/views/rooms/_RoomSublist.scss @@ -0,0 +1,389 @@ +/* +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_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; + 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 + width: calc(100% - 22px); + } + + &.mx_RoomSublist_headerContainer_stickyBottom { + bottom: 0; + } + + // 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: 32px; + + &::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; + } + } + + // 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; + + mask-image: linear-gradient(0deg, transparent, black 4px); + } + + .mx_RoomSublist_resizerHandles_showNButton { + flex: 0 0 32px; + } + + .mx_RoomSublist_resizerHandles { + flex: 0 0 4px; + } + + // 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 + width: unset !important; + height: 4px !important; // Update RESIZE_HANDLE_HEIGHT if this changes + + // This is positioned directly below the 'show more' button. + position: absolute; + bottom: 0 !important; // override from library + + // 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_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; +} diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 5bc7d5624d..8eca3f1efa 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,213 +14,182 @@ 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: $font-34px; - margin: 0; - padding: 0 8px 0 10px; - position: relative; - - .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_UserOnlineDot { - display: block; - margin-right: 5px; - } -} - -.mx_RoomTile:focus { - filter: none !important; - background-color: $roomtile-focused-bg-color; -} - -.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; -} + // The tile is also a flexbox row itself + display: flex; -.mx_RoomTile_dm { - display: block; - position: absolute; - bottom: 0; - right: -5px; - z-index: 2; -} + &.mx_RoomTile_selected, + &:hover, + &:focus-within, + &.mx_RoomTile_hasMenuOpen { + background-color: $roomtile-selected-bg-color; + border-radius: 8px; + } -// 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_DecoratedRoomAvatar, .mx_RoomTile_avatarContainer { + margin-right: 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_RoomTile_nameContainer { + flex-grow: 1; + min-width: 0; // allow flex to shrink it + margin-right: 8px; // spacing to buttons/badges -.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; -} - -.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 - } - .mx_UserOnlineDot { - 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_UserOnlineDot { - display: none; + + .mx_RoomTile_menuButton::before { + mask-image: url('$(res)/img/element-icons/context-menu.svg'); + } + + &: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_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_unreadNotify .mx_RoomTile_badge, -.mx_RoomTile_badge.mx_RoomTile_badgeUnread { - background-color: $roomtile-name-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_RoomTile_highlight .mx_RoomTile_badge, -.mx_RoomTile_badge.mx_RoomTile_badgeRed { - color: $accent-fg-color; - background-color: $warning-color; -} +.mx_RoomTile_contextMenu { + .mx_RoomTile_iconStar::before { + mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg'); + } -.mx_RoomTile_unread, .mx_RoomTile_highlight { - .mx_RoomTile_name { - font-weight: 600; - color: $roomtile-selected-color; + .mx_RoomTile_iconArrowDown::before { + mask-image: url('$(res)/img/element-icons/roomlist/low-priority.svg'); + } + + .mx_RoomTile_iconSettings::before { + mask-image: url('$(res)/img/element-icons/settings.svg'); + } + + .mx_RoomTile_iconSignOut::before { + mask-image: url('$(res)/img/element-icons/leave.svg'); } } - -.mx_RoomTile_selected { - border-radius: 4px; - background-color: $roomtile-selected-bg-color; -} - -.mx_DNDRoomTile { - transform: none; - transition: transform 0.2s; -} - -.mx_DNDRoomTile_dragging { - transform: scale(1.05, 1.05); -} - -.mx_RoomTile_arrow { - position: absolute; - right: 0px; -} - -.mx_RoomTile.mx_RoomTile_transparent { - background-color: transparent; -} - -.mx_RoomTile.mx_RoomTile_transparent:focus { - background-color: $roomtile-transparent-focused-color; -} - -.mx_GroupInviteTile .mx_RoomTile_name { - flex: 1; -} 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..4bd45631cc 100644 --- a/res/css/views/rooms/_Stickers.scss +++ b/res/css/views/rooms/_Stickers.scss @@ -31,7 +31,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/_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 9fa10907b4..eddcf9f55a 100644 --- a/res/css/views/settings/_AvatarSetting.scss +++ b/res/css/views/settings/_AvatarSetting.scss @@ -27,28 +27,6 @@ limitations under the License. .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 { diff --git a/res/css/views/rooms/_UserOnlineDot.scss b/res/css/views/settings/_UpdateCheckButton.scss similarity index 73% rename from res/css/views/rooms/_UserOnlineDot.scss rename to res/css/views/settings/_UpdateCheckButton.scss index f9da8648ed..f35a023ac1 100644 --- a/res/css/views/rooms/_UserOnlineDot.scss +++ b/res/css/views/settings/_UpdateCheckButton.scss @@ -1,5 +1,5 @@ /* -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. @@ -14,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_UserOnlineDot { - border-radius: 50%; - background-color: $accent-color; - height: 6px; - width: 6px; - display: inline-block; +.mx_UpdateCheckButton_summary { + margin-left: 16px; + + .mx_AccessibleButton_kind_link { + padding: 0; + } } 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..6c9b89cf5a 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,12 @@ limitations under the License. margin-top: 0; } +.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/_SecurityUserSettingsTab.scss b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss index 8700f8747d..d6466a03f9 100644 --- a/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss @@ -63,4 +63,25 @@ limitations under the License. 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/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..8d1b68dd99 --- /dev/null +++ b/res/css/views/voip/_CallContainer.scss @@ -0,0 +1,89 @@ +/* +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; + border-radius: 8px; + overflow: hidden; + z-index: 100; + box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); + + cursor: pointer; + + .mx_CallPreview { + .mx_VideoView { + width: 350px; + } + + .mx_VideoView_localVideoFeed { + border-radius: 8px; + overflow: hidden; + } + } + + .mx_IncomingCallBox { + min-width: 250px; + background-color: $primary-bg-color; + padding: 8px; + + .mx_IncomingCallBox_CallerInfo { + display: flex; + direction: row; + + img { + 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; + } + } + } +} diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 4650f30c1d..f6f3d40308 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. @@ -18,8 +19,76 @@ limitations under the License. background-color: $accent-color; color: $accent-fg-color; cursor: pointer; - text-align: center; padding: 6px; font-weight: bold; - font-size: $font-13px; + + border-radius: 8px; + min-width: 200px; + + display: flex; + align-items: center; + + img { + margin: 4px; + margin-right: 10px; + } + + > div { + display: flex; + flex-direction: column; + // Hacky vertical align + padding-top: 3px; + } + + > div > p, + > div > h1 { + padding: 0; + margin: 0; + font-size: $font-13px; + line-height: $font-15px; + } + + > div > p { + font-weight: bold; + } + + > * { + flex-grow: 0; + flex-shrink: 0; + } +} + +.mx_CallView_hangup { + position: absolute; + + right: 8px; + bottom: 10px; + + height: 35px; + width: 35px; + + border-radius: 35px; + + background-color: $notice-primary-color; + + z-index: 101; + + cursor: pointer; + + &::before { + content: ''; + position: absolute; + + height: 20px; + width: 20px; + + top: 6.5px; + left: 7.5px; + + mask: url('$(res)/img/hangup.svg'); + mask-size: contain; + background-size: contain; + + background-color: $primary-fg-color; + } } 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/fonts/Inter/Inter-Bold.woff b/res/fonts/Inter/Inter-Bold.woff new file mode 100644 index 0000000000..61e1c25e64 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..6c401bb09b 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..2de403edd1 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..80efd4848d 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..e7da6663fe 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..8559dfde38 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..8c36a6345e 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..3b31d3350a 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..fb79e91ff4 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..d32c111f9c 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..7d587c40bf 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..d5ffd2a1f1 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..99df06cbee 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..df746af999 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..91e192b9f1 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..ff8774ccb4 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/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/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.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/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..23ca78e44d 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..ac4827baed 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..d42922892a 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-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/video-muted.svg b/res/img/element-icons/call/video-muted.svg new file mode 100644 index 0000000000..d2aea71d11 --- /dev/null +++ b/res/img/element-icons/call/video-muted.svg @@ -0,0 +1,5 @@ + + + + + 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/call/voice-muted.svg b/res/img/element-icons/call/voice-muted.svg new file mode 100644 index 0000000000..32abafb04a --- /dev/null +++ b/res/img/element-icons/call/voice-muted.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/call/voice-unmuted.svg b/res/img/element-icons/call/voice-unmuted.svg new file mode 100644 index 0000000000..e664080217 --- /dev/null +++ b/res/img/element-icons/call/voice-unmuted.svg @@ -0,0 +1,3 @@ + + + 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/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/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/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/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..9613d9edd9 --- /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/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/in-call.svg b/res/img/element-icons/room/in-call.svg new file mode 100644 index 0000000000..0e574faa84 --- /dev/null +++ b/res/img/element-icons/room/in-call.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/img/element-icons/room/integrations.svg b/res/img/element-icons/room/integrations.svg new file mode 100644 index 0000000000..3a39506411 --- /dev/null +++ b/res/img/element-icons/room/integrations.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/room/invite.svg b/res/img/element-icons/room/invite.svg new file mode 100644 index 0000000000..f713e57d73 --- /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..697f656b8a --- /dev/null +++ b/res/img/element-icons/room/message-bar/emoji.svg @@ -0,0 +1,5 @@ + + + + + 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.svg b/res/img/element-icons/room/pin.svg new file mode 100644 index 0000000000..16941b329b --- /dev/null +++ b/res/img/element-icons/room/pin.svg @@ -0,0 +1,7 @@ + + + + + + + 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/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/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/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.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/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/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/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/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/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/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/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_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/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/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/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.svg b/res/img/spinner.svg new file mode 100644 index 0000000000..08965e982e --- /dev/null +++ b/res/img/spinner.svg @@ -0,0 +1,141 @@ + + + start + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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.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/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 6224c0820f..d48abf6a4c 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -1,14 +1,14 @@ // 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; $search-bg-color: #181b21; $search-placeholder-color: #61708b; $room-highlight-color: #343a46; @@ -18,6 +18,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 +39,20 @@ $info-plinth-fg-color: #888; $preview-bar-bg-color: $header-panel-bg-color; -$tagpanel-bg-color: $base-color; +$tagpanel-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; @@ -91,27 +96,38 @@ $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; $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; +// ******************** -$roomtile-transparent-focused-color: rgba(0, 0, 0, 0.1); +$theme-button-bg-color: #e3e8f0; -$panel-divider-color: $header-panel-border-color; +$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; + +$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; @@ -180,6 +196,15 @@ $breadcrumb-placeholder-bg-color: #272c35; $user-tile-hover-bg-color: $header-panel-bg-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; +$tagpanel-background-blur-amount: 30px; + +$composer-shadow-color: rgba(0, 0, 0, 0.28); + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/dark/css/dark.scss b/res/themes/dark/css/dark.scss index d81db4595f..6d9dc7352c 100644 --- a/res/themes/dark/css/dark.scss +++ b/res/themes/dark/css/dark.scss @@ -2,5 +2,10 @@ @import "../../light/css/_paths.scss"; @import "../../light/css/_fonts.scss"; @import "../../light/css/_light.scss"; +// important this goes before _mods, +// as $tagpanel-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"; 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..4ab5f99942 --- /dev/null +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -0,0 +1,275 @@ +// 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; + +// 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; + +$tagpanel-bg-color: $base-color; +$inverted-bg-color: $tagpanel-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; + +// 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; + +$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-overlay-placeholder-fg-color: #454545; +$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; +$tagpanel-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; + +// ******************** + +$theme-button-bg-color: #e3e8f0; + +$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; + +$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; + +// event tile lifecycle +$event-sending-color: $text-secondary-color; + +// 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; + +$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: $tagpanel-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; + +// 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: 4px; + 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 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; + } + + tr:nth-child(2n) { + background-color: #080808; + } + } +} + +// diff highlight colors +// intentionally swapped to avoid inversion +.hljs-addition { + background: #fdd; +} + +.hljs-deletion { + background: #dfd; +} 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..2a4d432d26 --- /dev/null +++ b/res/themes/legacy-dark/css/legacy-dark.scss @@ -0,0 +1,6 @@ +@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"; 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..6e66964fdf --- /dev/null +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -0,0 +1,375 @@ +// 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 + +// 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; + +$tagpanel-bg-color: #27303a; +$inverted-bg-color: $tagpanel-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; + +$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; + +$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-bg-color: #000; +$settings-profile-overlay-placeholder-bg-color: transparent; +$settings-profile-overlay-fg-color: #fff; +$settings-profile-overlay-placeholder-fg-color: #2e2f32; +$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; +$tagpanel-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; + +// ******************** + +$theme-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; + +$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; + +// ******************** + +// 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-encrypting-color: #abddbc; +$event-sending-color: #ddd; +$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"; + + +// 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-color: #000; + +$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: $tagpanel-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; + +// 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: 4px; + 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..e39a1765f3 --- /dev/null +++ b/res/themes/legacy-light/css/legacy-light.scss @@ -0,0 +1,5 @@ +@import "../../../../res/css/_font-sizes.scss"; +@import "_paths.scss"; +@import "_fonts.scss"; +@import "_legacy-light.scss"; +@import "../../../../res/css/_components.scss"; diff --git a/res/themes/light-custom/css/_custom.scss b/res/themes/light-custom/css/_custom.scss index 6206496150..b830e86e02 100644 --- a/res/themes/light-custom/css/_custom.scss +++ b/res/themes/light-custom/css/_custom.scss @@ -14,6 +14,8 @@ 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); @@ -23,7 +25,6 @@ $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); @@ -38,10 +39,10 @@ $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); @@ -51,6 +52,7 @@ $interactive-tooltip-bg-color: var(--sidebar-color); $tagpanel-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 $header-panel-bg-color: 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,7 +123,8 @@ $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 $username-variant1-color: var(--username-colors_1, $username-variant1-color); $username-variant2-color: var(--username-colors_2, $username-variant2-color); $username-variant3-color: var(--username-colors_3, $username-variant3-color); @@ -133,6 +133,10 @@ $username-variant5-color: var(--username-colors_5, $username-variant5-color); $username-variant6-color: var(--username-colors_6, $username-variant6-color); $username-variant7-color: var(--username-colors_7, $username-variant7-color); $username-variant8-color: var(--username-colors_8, $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..ba64830f15 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.13") format("woff2"), + url("$(res)/fonts/Inter/Inter-Regular.woff?v=3.13") 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.13") format("woff2"), + url("$(res)/fonts/Inter/Inter-Italic.woff?v=3.13") 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.13") format("woff2"), + url("$(res)/fonts/Inter/Inter-Medium.woff?v=3.13") 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.13") format("woff2"), + url("$(res)/fonts/Inter/Inter-MediumItalic.woff?v=3.13") 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.13") format("woff2"), + url("$(res)/fonts/Inter/Inter-SemiBold.woff?v=3.13") 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.13") format("woff2"), + url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff?v=3.13") 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.13") format("woff2"), + url("$(res)/fonts/Inter/Inter-Bold.woff?v=3.13") 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.13") format("woff2"), + url("$(res)/fonts/Inter/Inter-BoldItalic.woff?v=3.13") 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..ceb8d5677c 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -8,21 +8,22 @@ /* 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; $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 +35,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 +52,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,7 +62,7 @@ $preview-bar-bg-color: #f7f7f7; $secondary-accent-color: #f2f5f8; $tertiary-accent-color: #d3efe1; -$tagpanel-bg-color: #27303a; +$tagpanel-bg-color: rgba(232, 232, 232, 0.77); // used by RoomDirectory permissions $plinth-bg-color: $secondary-accent-color; @@ -77,10 +74,10 @@ $droptarget-bg-color: rgba(255,255,255,0.5); $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; @@ -157,45 +154,55 @@ $rte-group-pill-color: #aaa; $topleftmenu-color: #212121; $roomheader-color: #45474a; -$roomheader-addroom-bg-color: #91A1C0; -$roomheader-addroom-fg-color: $accent-fg-color; +$roomheader-bg-color: $primary-bg-color; +$roomheader-addroom-bg-color: rgba(92, 100, 112, 0.2); +$roomheader-addroom-fg-color: #5c6470; $tagpanel-button-color: #91A1C0; -$roomheader-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; // ******************** -$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; + +$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; + +$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; // ******************** @@ -223,7 +230,7 @@ $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"; // e2e $e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color @@ -262,14 +269,18 @@ $togglesw-off-color: #c1c9d6; $togglesw-on-color: $accent-color; $togglesw-ball-color: #fff; +// Slider +$slider-selection-color: $accent-color; +$slider-background-color: #c1c9d6; + $progressbar-color: #000; $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 +303,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,6 +314,15 @@ $breadcrumb-placeholder-bg-color: #e8eef5; $user-tile-hover-bg-color: $header-panel-bg-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; +$tagpanel-background-blur-amount: 20px; + +$composer-shadow-color: rgba(0, 0, 0, 0.04); + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_mods.scss b/res/themes/light/css/_mods.scss new file mode 100644 index 0000000000..9a59acba8e --- /dev/null +++ b/res/themes/light/css/_mods.scss @@ -0,0 +1,32 @@ +// 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); + background-repeat: no-repeat; + background-size: cover; + background-position: left top; + } + + .mx_TagPanel { + backdrop-filter: blur($tagpanel-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..f31ce5c139 100644 --- a/res/themes/light/css/light.scss +++ b/res/themes/light/css/light.scss @@ -2,4 +2,5 @@ @import "_paths.scss"; @import "_fonts.scss"; @import "_light.scss"; +@import "_mods.scss"; @import "../../../../res/css/_components.scss"; diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh index 1233677db4..7a62c03b12 100755 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/end-to-end-tests.sh @@ -13,7 +13,7 @@ handle_error() { trap 'handle_error' ERR -echo "--- Building Riot" +echo "--- Building Element" scripts/ci/layered-riot-web.sh cd ../riot-web riot_web_dir=`pwd` diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js index a1823cdf50..91733469f7 100755 --- a/scripts/gen-i18n.js +++ b/scripts/gen-i18n.js @@ -18,7 +18,7 @@ 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 + * parsing each file with the appropriate 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. * @@ -29,8 +29,8 @@ const path = require('path'); const walk = require('walk'); -const flowParser = require('flow-parser'); -const estreeWalker = require('estree-walker'); +const parser = require("@babel/parser"); +const traverse = require("@babel/traverse"); const TRANSLATIONS_FUNCS = ['_t', '_td']; @@ -44,17 +44,9 @@ const OUTPUT_FILE = 'src/i18n/strings/en_EN.json'; // 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) { + if (prop.key.type === 'Identifier' && prop.key.name === key) { return prop.value; } } @@ -62,11 +54,11 @@ function getObjectValue(obj, key) { } function getTKey(arg) { - if (arg.type == 'Literal') { + if (arg.type === 'Literal' || arg.type === "StringLiteral") { return arg.value; - } else if (arg.type == 'BinaryExpression' && arg.operator == '+') { + } else if (arg.type === 'BinaryExpression' && arg.operator === '+') { return getTKey(arg.left) + getTKey(arg.right); - } else if (arg.type == 'TemplateLiteral') { + } else if (arg.type === 'TemplateLiteral') { return arg.quasis.map((q) => { return q.value.raw; }).join(''); @@ -110,81 +102,112 @@ function getFormatStrings(str) { } function getTranslationsJs(file) { - const tree = flowParser.parse(fs.readFileSync(file, { encoding: 'utf8' }), FLOW_PARSER_OPTS); + const contents = fs.readFileSync(file, { encoding: 'utf8' }); 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; + try { + const plugins = [ + // https://babeljs.io/docs/en/babel-parser#plugins + "classProperties", + "objectRestSpread", + "throwExpressions", + "exportDefaultFrom", + "decorators-legacy", + ]; - // 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}'`); - } - } + if (file.endsWith(".js") || file.endsWith(".jsx")) { + // all JS is assumed to be flow or react + plugins.push("flow", "jsx"); + } else if (file.endsWith(".ts")) { + // TS can't use JSX unless it's a TSX file (otherwise angle casts fail) + plugins.push("typescript"); + } else if (file.endsWith(".tsx")) { + // When the file is a TSX file though, enable JSX parsing + plugins.push("typescript", "jsx"); + } - // 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}`); + const babelParsed = parser.parse(contents, { + allowImportExportEverywhere: true, + errorRecovery: true, + sourceFilename: file, + tokens: true, + plugins, + }); + traverse.default(babelParsed, { + enter: (p) => { + const node = p.node; + if (p.isCallExpression() && node.callee && 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); + } catch (e) { + console.log(); + console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`); + console.error(e); + process.exit(1); } } - } else { - trs.add(tKey); + + 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); + } } - } - } - }); + }, + }); + } catch (e) { + console.error(e); + process.exit(1); + } return trs; } @@ -194,7 +217,7 @@ function getTranslationsOther(file) { const trs = new Set(); - // Taken from riot-web src/components/structures/HomePage.js + // Taken from element-web src/components/structures/HomePage.js const translationsRegex = /_t\(['"]([\s\S]*?)['"]\)/mg; let matches; while (matches = translationsRegex.exec(contents)) { diff --git a/src/@types/common.ts b/src/@types/common.ts new file mode 100644 index 0000000000..b887bd4090 --- /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/global.d.ts b/src/@types/global.d.ts index e6e339d067..86ee995a13 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -15,18 +15,38 @@ limitations under the License. */ import * as ModernizrStatic from "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"; declare global { interface Window { Modernizr: ModernizrStatic; + mxMatrixClientPeg: IMatrixClientPeg; Olm: { init: () => Promise; }; - } - // workaround for https://github.com/microsoft/TypeScript/issues/30933 - interface ObjectConstructor { - fromEntries?(xs: [string|number|symbol, any][]): object + mxContentMessages: ContentMessages; + mxToastStore: ToastStore; + mxDeviceListener: DeviceListener; + mxRoomListStore: RoomListStoreClass; + mxRoomListLayoutStore: RoomListLayoutStore; + mxActiveRoomObserver: ActiveRoomObserver; + mxPlatformPeg: PlatformPeg; + mxIntegrationManagers: typeof IntegrationManagers; + singletonModalManager: ModalManager; + mxSettingsStore: SettingsStore; + mxNotifier: typeof Notifier; } interface Document { @@ -34,6 +54,10 @@ declare global { hasStorageAccess?: () => Promise; } + interface Navigator { + userLanguage?: string; + } + interface StorageEstimate { usageDetails?: {[key: string]: number}; } @@ -50,4 +74,8 @@ declare global { interface PromiseConstructor { allSettled(promises: Promise[]): Promise | ISettledRejected>>; } + + interface HTMLAudioElement { + type?: string; + } } 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/ActiveRoomObserver.js b/src/ActiveRoomObserver.ts similarity index 52% rename from src/ActiveRoomObserver.js rename to src/ActiveRoomObserver.ts index d6fbb460b5..1126dc9496 100644 --- a/src/ActiveRoomObserver.js +++ b/src/ActiveRoomObserver.ts @@ -16,6 +16,8 @@ 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 @@ -25,53 +27,57 @@ import RoomViewStore from './stores/RoomViewStore'; * TODO: If we introduce an observer for something else, factor out * the adding / removing of listeners & emitting into a common class. */ -class ActiveRoomObserver { +export class ActiveRoomObserver { + private listeners: {[key: string]: Listener[]} = {}; + private _activeRoomId = RoomViewStore.getRoomId(); + private readonly roomStoreToken: string; + 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)); + // TODO: We could self-destruct when the last listener goes away, or at least stop listening. + this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); } - addListener(roomId, listener) { - if (!this._listeners[roomId]) this._listeners[roomId] = []; - this._listeners[roomId].push(listener); + public get activeRoomId(): string { + return this._activeRoomId; } - removeListener(roomId, listener) { - if (this._listeners[roomId]) { - const i = this._listeners[roomId].indexOf(listener); + 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); + this.listeners[roomId].splice(i, 1); } } else { console.warn("Unregistering unrecognised listener (roomId=" + roomId + ")"); } } - _emit(roomId) { - if (!this._listeners[roomId]) return; + private emit(roomId, isActive: boolean) { + if (!this.listeners[roomId]) return; - for (const l of this._listeners[roomId]) { - l.call(); + for (const l of this.listeners[roomId]) { + l.call(null, isActive); } } - _onRoomViewStoreUpdate() { + private onRoomViewStoreUpdate = () => { // emit for the old room ID - if (this._activeRoomId) this._emit(this._activeRoomId); + 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); - } + if (this._activeRoomId) this.emit(this._activeRoomId, true); + }; } -if (global.mx_ActiveRoomObserver === undefined) { - global.mx_ActiveRoomObserver = new ActiveRoomObserver(); +if (window.mxActiveRoomObserver === undefined) { + window.mxActiveRoomObserver = new ActiveRoomObserver(); } -export default global.mx_ActiveRoomObserver; +export default window.mxActiveRoomObserver; diff --git a/src/Analytics.js b/src/Analytics.js index e55612c4f1..9966d0845e 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -66,7 +66,10 @@ const customVariables = { }, 'App Version': { id: 2, - expl: _td('The version of Riot'), + expl: _td('The version of %(brand)s'), + getTextVariables: () => ({ + brand: SdkConfig.get().brand, + }), example: '15.0.0', }, 'User Type': { @@ -96,7 +99,10 @@ const customVariables = { }, 'Touch Input': { id: 8, - expl: _td("Whether you're using Riot on a device where touch is the primary input mechanism"), + 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': { @@ -106,7 +112,10 @@ const customVariables = { }, 'Installed PWA': { id: 10, - expl: _td("Whether you're using Riot as an installed Progressive Web App"), + expl: _td("Whether you're using %(brand)s as an installed Progressive Web App"), + getTextVariables: () => ({ + brand: SdkConfig.get().brand, + }), example: 'false', }, }; @@ -195,8 +204,11 @@ class Analytics { this._setVisitVariable('Chosen Language', getCurrentLanguage()); - if (window.location.hostname === 'riot.im') { + 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"; @@ -356,12 +368,17 @@ class Analytics { Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, { title: _t('Analytics'), description:
-
- { _t('The information being sent to us to help make Riot better includes:') } -
+
{_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) => diff --git a/src/AsyncWrapper.js b/src/AsyncWrapper.js index 05054cf63a..94de5df214 100644 --- a/src/AsyncWrapper.js +++ b/src/AsyncWrapper.js @@ -41,7 +41,7 @@ export default createReactClass({ componentDidMount: function() { this._unmounted = false; // XXX: temporary logging to try to diagnose - // https://github.com/vector-im/riot-web/issues/3148 + // 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) { diff --git a/src/Avatar.js b/src/Avatar.js index 8393ce02b2..d76ea6f2c4 100644 --- a/src/Avatar.js +++ b/src/Avatar.js @@ -19,6 +19,7 @@ import {MatrixClientPeg} from './MatrixClientPeg'; import DMRoomMap from './utils/DMRoomMap'; import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; +// Not to be used for BaseAvatar urls as that has similar default avatar fallback already export function avatarUrlForMember(member, width, height, resizeMethod) { let url; if (member && member.getAvatarUrl) { @@ -81,7 +82,7 @@ function urlForColor(color) { const colorToDataURLCache = new Map(); export function defaultAvatarUrlForString(s) { - const defaultColors = ['#03b381', '#368bd6', '#ac3ba8']; + const defaultColors = ['#0DBD8B', '#368bd6', '#ac3ba8']; let total = 0; for (let i = 0; i < s.length; ++i) { total += s.charCodeAt(i); diff --git a/src/BasePlatform.js b/src/BasePlatform.js deleted file mode 100644 index 7214031586..0000000000 --- a/src/BasePlatform.js +++ /dev/null @@ -1,195 +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 - } - - onKeyDown(ev: KeyboardEvent): boolean { - return false; // no shortcuts implemented - } -} diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts new file mode 100644 index 0000000000..4d06c5df73 --- /dev/null +++ b/src/BasePlatform.ts @@ -0,0 +1,292 @@ +/* +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 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"; + +export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url"; +export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url"; + +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 async 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 { + 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(); + } + + /** + * 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"); + } + + 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; + } + + setLanguage(preferredLangs: string[]) {} + + 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. + */ + startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: 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()); + } + const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin); + window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // 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 { + 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} userId 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 { + return null; + } + + /** + * 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 { + } +} diff --git a/src/CallHandler.js b/src/CallHandler.js index 2bfe10850a..18f6aeb98a 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -59,14 +59,14 @@ 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 dis from './dispatcher/dispatcher'; import WidgetUtils from './utils/WidgetUtils'; import WidgetEchoStore from './stores/WidgetEchoStore'; -import SettingsStore, { SettingLevel } from './settings/SettingsStore'; +import SettingsStore from './settings/SettingsStore'; import {generateHumanReadableId} from "./utils/NamingUtils"; import {Jitsi} from "./widgets/Jitsi"; import {WidgetType} from "./widgets/WidgetType"; +import {SettingLevel} from "./settings/SettingLevel"; global.mxCalls = { //room_id: MatrixCall @@ -90,7 +90,7 @@ function play(audioId) { // 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 + // https://github.com/vector-im/element-web/issues/7657 console.log("Unable to play audio clip", e); } }; @@ -119,62 +119,22 @@ function pause(audioId) { } } -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, - }); + 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"); @@ -514,15 +474,15 @@ const callHandler = { /** * The conference handler is a module that deals with implementation-specific - * multi-party calling implementations. Riot passes in its own which creates + * multi-party calling implementations. Element 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 + * 1. So Element 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 + * is much harder to remove: probably either we make Element 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 diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js index a0364f798a..8d56467c57 100644 --- a/src/CallMediaHandler.js +++ b/src/CallMediaHandler.js @@ -15,7 +15,8 @@ */ import * as Matrix from 'matrix-js-sdk'; -import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; +import SettingsStore from "./settings/SettingsStore"; +import {SettingLevel} from "./settings/SettingLevel"; export default { hasAnyLabeledDevices: async function() { diff --git a/src/ContentMessages.js b/src/ContentMessages.tsx similarity index 73% rename from src/ContentMessages.js rename to src/ContentMessages.tsx index 34379c029b..6f55a75d0c 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.tsx @@ -1,6 +1,7 @@ /* 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. @@ -15,20 +16,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - +import React from "react"; import extend from './extend'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import {MatrixClientPeg} from './MatrixClientPeg'; +import {MatrixClient} from "matrix-js-sdk/src/client"; 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"; // Polyfill for Canvas.toBlob API using Canvas.toDataURL import "blueimp-canvas-to-blob"; +import { Action } from "./dispatcher/actions"; const MAX_WIDTH = 800; const MAX_HEIGHT = 600; @@ -39,6 +42,50 @@ const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01]; export class UploadCanceledError extends Error {} +type ThumbnailableElement = HTMLImageElement | HTMLVideoElement; + +interface IUpload { + fileName: string; + roomId: string; + total: number; + loaded: number; + promise: Promise; + canceled?: boolean; +} + +interface IMediaConfig { + "m.upload.size"?: number; +} + +interface IContent { + body: string; + msgtype: string; + info: { + size: number; + mimetype?: string; + }; + file?: string; + url?: string; +} + +interface IThumbnail { + info: { + thumbnail_info: { + w: number; + h: number; + mimetype: string; + size: number; + }; + w: number; + h: number; + }; + thumbnail: Blob; +} + +interface IAbortablePromise extends Promise { + abort(): void; +} + /** * Create a thumbnail for a image DOM element. * The image will be smaller than MAX_WIDTH and MAX_HEIGHT. @@ -51,13 +98,13 @@ export class UploadCanceledError extends Error {} * 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 {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. */ -function createThumbnail(element, inputWidth, inputHeight, mimeType) { +function createThumbnail(element: ThumbnailableElement, inputWidth: number, inputHeight: number, mimeType: string): Promise { return new Promise((resolve) => { let targetWidth = inputWidth; let targetHeight = inputHeight; @@ -98,7 +145,7 @@ function createThumbnail(element, inputWidth, inputHeight, mimeType) { * @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) { +async function loadImageElement(imageFile: File) { // Load the file into an html element const img = document.createElement("img"); const objectUrl = URL.createObjectURL(imageFile); @@ -128,8 +175,7 @@ async function loadImageElement(imageFile) { 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 chunk.data.every((val, i) => val === PHYS_HIDPI[i]); } } return false; @@ -152,7 +198,7 @@ async function loadImageElement(imageFile) { */ function infoForImageFile(matrixClient, roomId, imageFile) { let thumbnailType = "image/png"; - if (imageFile.type == "image/jpeg") { + if (imageFile.type === "image/jpeg") { thumbnailType = "image/jpeg"; } @@ -175,15 +221,15 @@ function infoForImageFile(matrixClient, roomId, imageFile) { * @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) { +function loadVideoElement(videoFile): Promise { 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; + reader.onload = function(ev) { + video.src = ev.target.result as string; // Once ready, returns its size // Wait until we have enough data to thumbnail the first frame. @@ -231,11 +277,11 @@ function infoForVideoFile(matrixClient, roomId, videoFile) { * @return {Promise} A promise that resolves with an ArrayBuffer when the file * is read. */ -function readFileAsArrayBuffer(file) { +function readFileAsArrayBuffer(file: File | Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function(e) { - resolve(e.target.result); + resolve(e.target.result as ArrayBuffer); }; reader.onerror = function(e) { reject(e); @@ -257,11 +303,11 @@ function readFileAsArrayBuffer(file) { * 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) { +function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blob, progressHandler?: any) { + 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 canceled = false; let uploadPromise; let encryptInfo; const prom = readFileAsArrayBuffer(file).then(function(data) { @@ -278,9 +324,9 @@ function uploadFile(matrixClient, roomId, file, progressHandler) { 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. @@ -290,7 +336,7 @@ function uploadFile(matrixClient, roomId, file, progressHandler) { } return {"file": encryptInfo}; }); - prom.abort = () => { + (prom as IAbortablePromise).abort = () => { canceled = true; if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise); }; @@ -300,55 +346,23 @@ function uploadFile(matrixClient, roomId, file, progressHandler) { 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": url}; }); - // XXX: copy over the abort method to the new promise - promise1.abort = basePromise.abort; + promise1.abort = () => { + canceled = true; + MatrixClientPeg.get().cancelUpload(basePromise); + }; return promise1; } } export default class ContentMessages { - constructor() { - this.inprogress = []; - this.nextId = 0; - this._mediaConfig = null; - } + private inprogress: IUpload[] = []; + private mediaConfig: IMediaConfig = 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) { + sendStickerContentToRoom(url: string, roomId: string, info: string, text: string, matrixClient: 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; @@ -356,14 +370,14 @@ export default class ContentMessages { } getUploadLimit() { - if (this._mediaConfig !== null && this._mediaConfig["m.upload.size"] !== undefined) { - return this._mediaConfig["m.upload.size"]; + if (this.mediaConfig !== null && this.mediaConfig["m.upload.size"] !== undefined) { + return this.mediaConfig["m.upload.size"]; } else { return null; } } - async sendContentListToRoom(files, roomId, matrixClient) { + async sendContentListToRoom(files: File[], roomId: string, matrixClient: MatrixClient) { if (matrixClient.isGuest()) { dis.dispatch({action: 'require_registration'}); return; @@ -372,32 +386,32 @@ export default class ContentMessages { 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); - }, - }); + 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; } - await this._ensureMediaConfigFetched(); + 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(); + modal.close(); + } const tooBigFiles = []; const okFiles = []; for (let i = 0; i < files.length; ++i) { - if (this._isFileSizeAcceptable(files[i])) { + if (this.isFileSizeAcceptable(files[i])) { okFiles.push(files[i]); } else { tooBigFiles.push(files[i]); @@ -406,17 +420,12 @@ export default class ContentMessages { 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 {finished} = Modal.createTrackedDialog<[boolean]>('Upload Failure', '', UploadFailureDialog, { + badFiles: tooBigFiles, + totalFiles: files.length, + contentMessages: this, }); - const shouldContinue = await uploadFailureDialogPromise; + const [shouldContinue] = await finished; if (!shouldContinue) return; } @@ -428,31 +437,47 @@ export default class ContentMessages { 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); - }, - }); + 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); + promBefore = this.sendContentToRoom(file, roomId, matrixClient, promBefore); } } - _sendContentToRoom(file, roomId, matrixClient, promBefore) { - const content = { + getCurrentUploads() { + return this.inprogress.filter(u => !u.canceled); + } + + cancelUpload(promise: Promise) { + 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; + MatrixClientPeg.get().cancelUpload(upload.promise); + dis.dispatch({action: 'upload_canceled', upload}); + } + } + + private sendContentToRoom(file: File, roomId: string, matrixClient: MatrixClient, promBefore: Promise) { + 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 @@ -461,25 +486,25 @@ export default class ContentMessages { } const prom = new Promise((resolve) => { - if (file.type.indexOf('image/') == 0) { + if (file.type.indexOf('image/') === 0) { content.msgtype = 'm.image'; - infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{ + infoForImageFile(matrixClient, roomId, file).then((imageInfo) => { extend(content.info, imageInfo); resolve(); - }, (error)=>{ - console.error(error); + }, (e) => { + console.error(e); content.msgtype = 'm.file'; resolve(); }); - } else if (file.type.indexOf('audio/') == 0) { + } else if (file.type.indexOf('audio/') === 0) { content.msgtype = 'm.audio'; resolve(); - } else if (file.type.indexOf('video/') == 0) { + } else if (file.type.indexOf('video/') === 0) { content.msgtype = 'm.video'; - infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{ + infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => { extend(content.info, videoInfo); resolve(); - }, (error)=>{ + }, (e) => { content.msgtype = 'm.file'; resolve(); }); @@ -489,19 +514,23 @@ export default class ContentMessages { } }); - const upload = { + // create temporary abort handler for before the actual upload gets passed off to js-sdk + (prom as IAbortablePromise).abort = () => { + upload.canceled = true; + }; + + const upload: IUpload = { fileName: file.name || 'Attachment', roomId: roomId, - total: 0, + total: file.size, loaded: 0, + promise: prom, }; this.inprogress.push(upload); dis.dispatch({action: 'upload_started'}); // Focus the composer view - dis.dispatch({action: 'focus_composer'}); - - let error; + dis.fire(Action.FocusComposer); function onProgress(ev) { upload.total = ev.total; @@ -509,7 +538,9 @@ export default class ContentMessages { dis.dispatch({action: 'upload_progress', upload: 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. @@ -520,16 +551,17 @@ export default class ContentMessages { content.file = result.file; content.url = result.url; }); - }).then((url) => { + }).then(() => { // Await previous message being sent into the room return promBefore; }).then(function() { + if (upload.canceled) throw new UploadCanceledError(); 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) { + if (err.http_status === 413) { desc = _t( "The file '%(fileName)s' exceeds this homeserver's size limit for uploads", {fileName: upload.fileName}, @@ -542,11 +574,9 @@ export default class ContentMessages { }); } }).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); + if (this.inprogress[i].promise === upload.promise) { + this.inprogress.splice(i, 1); break; } } @@ -555,7 +585,7 @@ export default class ContentMessages { // 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; + this.mediaConfig = null; } dis.dispatch({action: 'upload_failed', upload, error}); } else { @@ -565,24 +595,35 @@ export default class ContentMessages { }); } - getCurrentUploads() { - return this.inprogress.filter(u => !u.canceled); + 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; } - 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}); + private 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; + }); + } + + static sharedInstance() { + if (window.mxContentMessages === undefined) { + window.mxContentMessages = new ContentMessages(); } + return window.mxContentMessages; } } diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index c37d0f8bf5..676c41d7d7 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -20,7 +20,6 @@ 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 @@ -32,10 +31,7 @@ let secretStorageKeys = {}; let secretStorageBeingAccessed = false; function isCachingAllowed() { - return ( - secretStorageBeingAccessed || - SettingsStore.getValue("keepSecretStoragePassphraseForSession") - ); + return secretStorageBeingAccessed; } export class AccessCancelledError extends Error { @@ -44,25 +40,16 @@ export class AccessCancelledError extends Error { } } -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 operation."); - } - +async function confirmToDismiss() { 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"), + description: _t("Are you sure you want to cancel entering passphrase?"), + danger: false, + button: _t("Go Back"), + cancelButton: _t("Cancel"), }).finished; - return sure; + return !sure; } async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { @@ -106,7 +93,7 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { /* options= */ { onBeforeClose: async (reason) => { if (reason === "backgroundClick") { - return confirmToDismiss(ssssItemName); + return confirmToDismiss(); } return true; }, @@ -142,27 +129,21 @@ const onSecretRequested = async function({ console.log(`CrossSigningManager: Ignoring request from untrusted device ${deviceId}`); return; } - if (name.startsWith("m.cross_signing")) { + 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; - /* 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); + 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) { diff --git a/src/DeviceListener.js b/src/DeviceListener.js deleted file mode 100644 index d948911d7c..0000000000 --- a/src/DeviceListener.js +++ /dev/null @@ -1,306 +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'; - -const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; -const THIS_DEVICE_TOAST_KEY = 'setupencryption'; -const OTHER_DEVICES_TOAST_KEY = 'reviewsessions'; - -function toastKey(deviceId) { - return "unverified_session_" + deviceId; -} - -export default class DeviceListener { - static sharedInstance() { - if (!global.mx_DeviceListener) global.mx_DeviceListener = new DeviceListener(); - return global.mx_DeviceListener; - } - - constructor() { - // 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; - - // 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. - this._ourDeviceIdsAtStart = null; - - // The set of device IDs we're currently displaying toasts for - this._displayingToastsForDeviceIds = new Set(); - } - - 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); - 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); - } - this._dismissed.clear(); - this._dismissedThisDeviceToast = false; - this._keyBackupInfo = null; - this._keyBackupFetchedAt = null; - } - - /** - * Dismiss notifications about our own unverified devices - * - * @param {String[]} deviceIds List of device IDs to dismiss notifications for - */ - async dismissUnverifiedSessions(deviceIds) { - 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, initialFetch) => { - // 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) => { - 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(); - } - } - - _onSync = (state, prevState) => { - if (state === 'PREPARED' && prevState === null) 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.getValue("feature_cross_signing") || - !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(); - - if (this._dismissedThisDeviceToast) { - ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); - } else { - if (!crossSigningReady) { - // make sure our keys are finished downlaoding - await cli.downloadKeys([cli.getUserId()]); - // 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"), - }); - } - } - } 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); - } - } - - // 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) { - ToastStore.sharedInstance().addOrReplaceToast({ - key: OTHER_DEVICES_TOAST_KEY, - title: _t("Review where you’re logged in"), - icon: "verification_warning", - priority: ToastStore.PRIORITY_LOW, - props: { - deviceIds: oldUnverifiedDeviceIds, - }, - component: sdk.getComponent("toasts.BulkUnverifiedSessionsToast"), - }); - } else { - ToastStore.sharedInstance().dismissToast(OTHER_DEVICES_TOAST_KEY); - } - - // Show toasts for new unverified devices if they aren't already there - for (const deviceId of newUnverifiedDeviceIds) { - ToastStore.sharedInstance().addOrReplaceToast({ - key: toastKey(deviceId), - title: _t("New login. Was this you?"), - icon: "verification_warning", - props: { deviceId }, - component: sdk.getComponent("toasts.UnverifiedSessionToast"), - }); - } - - // ...and hide any we don't need any more - for (const deviceId of this._displayingToastsForDeviceIds) { - if (!newUnverifiedDeviceIds.has(deviceId)) { - ToastStore.sharedInstance().dismissToast(toastKey(deviceId)); - } - } - - this._displayingToastsForDeviceIds = newUnverifiedDeviceIds; - } -} diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts new file mode 100644 index 0000000000..a37521118f --- /dev/null +++ b/src/DeviceListener.ts @@ -0,0 +1,266 @@ +/* +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 { + 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 {privateShouldBeEncrypted} from "./createRoom"; + +const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; + +export default class DeviceListener { + // 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); + 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); + } + 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.') + ) { + this._recheck(); + } + }; + + _onSync = (state, prevState) => { + if (state === 'PREPARED' && prevState === null) 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() { + // In a default configuration, show the toasts. If the well-known config causes e2ee default to be false + // then do not show the toasts until user is in at least one encrypted room. + if (privateShouldBeEncrypted()) return true; + 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(); + + if (this.dismissedThisDeviceToast || crossSigningReady) { + 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.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) + 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 index c9793d40f7..d5d7c08d50 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -17,7 +17,7 @@ limitations under the License. */ import URL from 'url'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import WidgetMessagingEndpoint from './WidgetMessagingEndpoint'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; import {MatrixClientPeg} from "./MatrixClientPeg"; @@ -25,6 +25,7 @@ import RoomViewStore from "./stores/RoomViewStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import SettingsStore from "./settings/SettingsStore"; import {Capability} from "./widgets/WidgetApi"; +import {objectClone} from "./utils/objects"; const WIDGET_API_VERSION = '0.0.2'; // Current API version const SUPPORTED_WIDGET_API_VERSIONS = [ @@ -196,7 +197,7 @@ export default class FromWidgetPostMessageApi { const integId = (data && data.integId) ? data.integId : null; // TODO: Open the right integration manager for the widget - if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) { + if (SettingsStore.getValue("feature_many_integration_managers")) { IntegrationManagers.sharedInstance().openAll( MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), `type_${integType}`, @@ -247,7 +248,7 @@ export default class FromWidgetPostMessageApi { * @param {Object} res Response data */ 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); } @@ -260,7 +261,7 @@ export default class FromWidgetPostMessageApi { */ 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, diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index 9131a89e5d..e7ae3217bb 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -22,6 +22,7 @@ import { _t } from './languageHandler'; 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 +62,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'], diff --git a/src/HtmlUtils.js b/src/HtmlUtils.tsx similarity index 82% rename from src/HtmlUtils.js rename to src/HtmlUtils.tsx index 34e9e55d25..5d33645bb7 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.tsx @@ -17,10 +17,6 @@ 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'; @@ -28,12 +24,13 @@ import linkifyMatrix from './linkify-matrix'; import _linkifyElement from 'linkifyjs/element'; import _linkifyString from 'linkifyjs/string'; import classNames from 'classnames'; -import {MatrixClientPeg} from './MatrixClientPeg'; +import EMOJIBASE_REGEX from 'emojibase-regex'; import url from 'url'; -import EMOJIBASE_REGEX from 'emojibase-regex'; +import {MatrixClientPeg} from './MatrixClientPeg'; import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji"; +import ReplyThread from "./components/views/elements/ReplyThread"; linkifyMatrix(linkify); @@ -64,7 +61,7 @@ const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet']; * need emojification. * unicodeToImage uses this function. */ -function mightContainEmoji(str) { +function mightContainEmoji(str: string) { return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str); } @@ -74,7 +71,7 @@ function mightContainEmoji(str) { * @param {String} char The emoji character * @return {String} The shortcode (such as :thumbup:) */ -export function unicodeToShortcode(char) { +export function unicodeToShortcode(char: string) { const data = getEmojiFromUnicode(char); return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : ''); } @@ -85,7 +82,7 @@ export function unicodeToShortcode(char) { * @param {String} shortcode The shortcode (such as :thumbup:) * @return {String} The emoji character; null if none exists */ -export function shortcodeToUnicode(shortcode) { +export function shortcodeToUnicode(shortcode: string) { shortcode = shortcode.slice(1, shortcode.length - 1); const data = SHORTCODE_TO_EMOJI.get(shortcode); return data ? data.unicode : null; @@ -100,7 +97,7 @@ export function processHtmlForSending(html: string): string { } let contentHTML = ""; - for (let i=0; i < contentDiv.children.length; i++) { + for (let i = 0; i < contentDiv.children.length; i++) { const element = contentDiv.children[i]; if (element.tagName.toLowerCase() === 'p') { contentHTML += element.innerHTML; @@ -122,12 +119,19 @@ export function processHtmlForSending(html: string): string { * Given an untrusted HTML string, return a React node with an sanitized version * of that HTML. */ -export function sanitizedHtmlNode(insaneHtml) { +export function sanitizedHtmlNode(insaneHtml: string) { const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams); return
; } +export function sanitizedHtmlNodeInnerText(insaneHtml: string) { + const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams); + const contentDiv = document.createElement("div"); + contentDiv.innerHTML = saneHtml; + return contentDiv.innerText; +} + /** * Tests if a URL from an untrusted source may be safely put into the DOM * The biggest threat here is javascript: URIs. @@ -136,7 +140,7 @@ export function sanitizedHtmlNode(insaneHtml) { * other places we need to sanitise URLs. * @return true if permitted, otherwise false */ -export function isUrlPermitted(inputUrl) { +export function isUrlPermitted(inputUrl: string) { try { const parsed = url.parse(inputUrl); if (!parsed.protocol) return false; @@ -147,9 +151,9 @@ export function isUrlPermitted(inputUrl) { } } -const transformTags = { // custom to matrix +const transformTags: sanitizeHtml.IOptions["transformTags"] = { // custom to matrix // add blank targets to all hyperlinks except vector URLs - 'a': function(tagName, attribs) { + 'a': function(tagName: string, attribs: sanitizeHtml.Attributes) { if (attribs.href) { attribs.target = '_blank'; // by default @@ -162,7 +166,7 @@ const transformTags = { // custom to matrix attribs.rel = 'noreferrer noopener'; // https://mathiasbynens.github.io/rel-noopener/ return { tagName, attribs }; }, - 'img': function(tagName, attribs) { + 'img': function(tagName: string, attribs: sanitizeHtml.Attributes) { // 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. @@ -176,17 +180,17 @@ const transformTags = { // custom to matrix ); return { tagName, attribs }; }, - 'code': function(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-'); + return cl.startsWith('language-') && !cl.startsWith('language-_'); }); attribs.class = classes.join(' '); } return { tagName, attribs }; }, - '*': function(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; @@ -220,7 +224,7 @@ const transformTags = { // custom to matrix }, }; -const sanitizeHtmlParams = { +const sanitizeHtmlParams: sanitizeHtml.IOptions = { allowedTags: [ 'font', // custom to matrix for IRC-style font coloring 'del', // for markdown @@ -247,16 +251,16 @@ const sanitizeHtmlParams = { }; // this is the same as the above except with less rewriting -const composerSanitizeHtmlParams = Object.assign({}, sanitizeHtmlParams); -composerSanitizeHtmlParams.transformTags = { - 'code': transformTags['code'], - '*': transformTags['*'], +const composerSanitizeHtmlParams: sanitizeHtml.IOptions = { + ...sanitizeHtmlParams, + transformTags: { + 'code': transformTags['code'], + '*': transformTags['*'], + }, }; -class BaseHighlighter { - constructor(highlightClass, highlightLink) { - this.highlightClass = highlightClass; - this.highlightLink = highlightLink; +abstract class BaseHighlighter { + constructor(public highlightClass: string, public highlightLink: string) { } /** @@ -270,47 +274,49 @@ class BaseHighlighter { * returns a list of results (strings for HtmlHighligher, react nodes for * TextHighlighter). */ - applyHighlights(safeSnippet, safeHighlights) { + public applyHighlights(safeSnippet: string, safeHighlights: string[]): T[] { let lastOffset = 0; let offset; - let nodes = []; + let nodes: T[] = []; 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)); + 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)); + 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)); + const subSnippet = safeSnippet.substring(lastOffset, undefined); + nodes = nodes.concat(this.applySubHighlights(subSnippet, safeHighlights)); } return nodes; } - _applySubHighlights(safeSnippet, safeHighlights) { + 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)]; + return [this.processSnippet(safeSnippet, false)]; } } + + protected abstract processSnippet(snippet: string, highlight: boolean): T; } -class HtmlHighlighter extends BaseHighlighter { +class HtmlHighlighter extends BaseHighlighter { /* highlight the given snippet if required * * snippet: content of the span; must have been sanitised @@ -318,28 +324,23 @@ class HtmlHighlighter extends BaseHighlighter { * * returns an HTML string */ - _processSnippet(snippet, highlight) { + protected processSnippet(snippet: string, highlight: boolean): string { if (!highlight) { // nothing required here return snippet; } - let span = "" - + snippet + ""; + let span = `${snippet}`; if (this.highlightLink) { - span = "" - +span+""; + span = `${span}`; } return span; } } -class TextHighlighter extends BaseHighlighter { - constructor(highlightClass, highlightLink) { - super(highlightClass, highlightLink); - this._key = 0; - } +class TextHighlighter extends BaseHighlighter { + private key = 0; /* create a node to hold the given content * @@ -348,13 +349,12 @@ class TextHighlighter extends BaseHighlighter { * * returns a React node */ - _processSnippet(snippet, highlight) { - const key = this._key++; + protected processSnippet(snippet: string, highlight: boolean): React.ReactNode { + const key = this.key++; - let node = - - { snippet } - ; + let node = + { snippet } + ; if (highlight && this.highlightLink) { node = { node }; @@ -364,6 +364,20 @@ class TextHighlighter extends BaseHighlighter { } } +interface IContent { + format?: string; + formatted_body?: string; + body: string; +} + +interface IOpts { + highlightLink?: string; + disableBigEmoji?: boolean; + stripReplyFallback?: boolean; + returnString?: boolean; + forComposerQuote?: boolean; + ref?: React.Ref; +} /* turn a matrix event body into html * @@ -378,7 +392,7 @@ class TextHighlighter extends BaseHighlighter { * 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={}) { +export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts = {}) { const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body; let bodyHasEmoji = false; @@ -387,9 +401,9 @@ export function bodyToHtml(content, highlights, opts={}) { sanitizeParams = composerSanitizeHtmlParams; } - let strippedBody; - let safeBody; - let isDisplayedWithHtml; + 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 @@ -407,7 +421,7 @@ export function bodyToHtml(content, highlights, opts={}) { } let formattedBody = typeof content.formatted_body === 'string' ? content.formatted_body : null; - const plainBody = typeof content.body === 'string' ? content.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; @@ -471,7 +485,7 @@ export function bodyToHtml(content, highlights, opts={}) { * @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options * @returns {string} Linkified string */ -export function linkifyString(str, options = linkifyMatrix.options) { +export function linkifyString(str: string, options = linkifyMatrix.options) { return _linkifyString(str, options); } @@ -482,7 +496,7 @@ export function linkifyString(str, options = linkifyMatrix.options) { * @param {object} [options] Options for linkifyElement. Default: linkifyMatrix.options * @returns {object} */ -export function linkifyElement(element, options = linkifyMatrix.options) { +export function linkifyElement(element: HTMLElement, options = linkifyMatrix.options) { return _linkifyElement(element, options); } @@ -493,7 +507,7 @@ export function linkifyElement(element, options = linkifyMatrix.options) { * @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options * @returns {string} */ -export function linkifyAndSanitizeHtml(dirtyHtml, options = linkifyMatrix.options) { +export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatrix.options) { return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams); } @@ -504,7 +518,7 @@ export function linkifyAndSanitizeHtml(dirtyHtml, options = linkifyMatrix.option * @param {Node} node * @returns {bool} */ -export function checkBlockNode(node) { +export function checkBlockNode(node: Node) { switch (node.nodeName) { case "H1": case "H2": diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 4a830d6506..fbdb6812ee 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -177,7 +177,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/KeyRequestHandler.js b/src/KeyRequestHandler.js deleted file mode 100644 index ceaff0c54d..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.getValue("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.getValue("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/Lifecycle.js b/src/Lifecycle.js index 1baa6c8e0c..d2de31eb80 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -26,7 +26,7 @@ import Analytics from './Analytics'; import Notifier from './Notifier'; import UserActivity from './UserActivity'; import Presence from './Presence'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import DMRoomMap from './utils/DMRoomMap'; import Modal from './Modal'; import * as sdk from './index'; @@ -41,6 +41,10 @@ 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} from "./BasePlatform"; + +const HOMESERVER_URL_KEY = "mx_hs_url"; +const ID_SERVER_URL_KEY = "mx_is_url"; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -163,14 +167,16 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) { return Promise.resolve(false); } - if (!queryParams.homeserver) { + 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"); return Promise.resolve(false); } return sendLoginRequest( - queryParams.homeserver, - queryParams.identityServer, + homeserver, + identityServer, "m.login.token", { token: queryParams.loginToken, initial_device_display_name: defaultDeviceDisplayName, @@ -256,8 +262,8 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { * @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 hsUrl = localStorage.getItem(HOMESERVER_URL_KEY); + const isUrl = localStorage.getItem(ID_SERVER_URL_KEY); const accessToken = localStorage.getItem("mx_access_token"); const userId = localStorage.getItem("mx_user_id"); const deviceId = localStorage.getItem("mx_device_id"); @@ -298,6 +304,13 @@ async function _restoreFromLocalStorage(opts) { return false; } + const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId); + if (pickleKey) { + console.log("Got pickle key"); + } else { + console.log("No pickle key available"); + } + console.log(`Restoring session for ${userId}`); await _doSetLoggedIn({ userId: userId, @@ -306,6 +319,7 @@ async function _restoreFromLocalStorage(opts) { homeserverUrl: hsUrl, identityServerUrl: isUrl, guest: isGuest, + pickleKey: pickleKey, }, false); return true; } else { @@ -348,9 +362,19 @@ async function _handleLoadSessionFailure(e) { * * @returns {Promise} promise which resolves to the new MatrixClient once it has been started */ -export function setLoggedIn(credentials) { +export async function setLoggedIn(credentials) { stopMatrixClient(); - return _doSetLoggedIn(credentials, true); + 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); } /** @@ -479,14 +503,22 @@ function _showStorageEvictedDialog() { class AbortLoginAndRebuildStorage extends Error { } function _persistCredentialsToLocalStorage(credentials) { - localStorage.setItem("mx_hs_url", credentials.homeserverUrl); + localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl); if (credentials.identityServerUrl) { - localStorage.setItem("mx_is_url", credentials.identityServerUrl); + localStorage.setItem(ID_SERVER_URL_KEY, 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 (credentials.pickleKey) { + localStorage.setItem("mx_has_pickle_key", true); + } else { + 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". // @@ -516,7 +548,9 @@ export function logout() { } _isLoggingOut = true; - MatrixClientPeg.get().logout().then(onLoggedOut, + 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 @@ -575,10 +609,12 @@ async function startMatrixClient(startSyncing=true) { // 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(); - TypingStore.sharedInstance().reset(); // just in case - ToastStore.sharedInstance().reset(); DMRoomMap.makeShared().start(); IntegrationManagers.sharedInstance().startWatching(); ActiveWidgetStore.start(); @@ -608,7 +644,7 @@ async function startMatrixClient(startSyncing=true) { } // Now that we have a MatrixClientPeg, update the Jitsi info - await Jitsi.getInstance().update(); + 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. diff --git a/src/Login.js b/src/Login.js index 1590e5ac28..04805b4af9 100644 --- a/src/Login.js +++ b/src/Login.js @@ -95,6 +95,8 @@ export default class Login { identifier = { type: 'm.id.phone', country: phoneCountry, + phone: phoneNumber, + // XXX: Synapse historically wanted `number` and not `phone` number: phoneNumber, }; } else if (isEmail) { diff --git a/src/Markdown.js b/src/Markdown.js index fb1f8bf0ea..e57507b4de 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -99,7 +99,7 @@ export default class Markdown { // 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) + // (https://github.com/vector-im/element-web/issues/3154) softbreak: '
', }); @@ -166,7 +166,7 @@ export default class Markdown { * 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). + * (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(!). @@ -175,14 +175,6 @@ export default class Markdown { 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 diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.ts similarity index 73% rename from src/MatrixClientPeg.js rename to src/MatrixClientPeg.ts index 21f05b9759..be16f5fe10 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.ts @@ -17,8 +17,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClient, MemoryStore} from 'matrix-js-sdk'; - +import {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'; @@ -34,37 +34,26 @@ 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, +export interface IMatrixClientCreds { + homeserverUrl: string; + identityServerUrl: string; + userId: string; + deviceId: string; + accessToken: string; + guest: boolean; + pickleKey?: string; } -/** - * 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; +// TODO: Move this to the js-sdk +export interface IOpts { + initialSyncLimit?: number; + pendingEventOrdering?: "detached" | "chronological"; + lazyLoadMembers?: boolean; + clientWellKnownPollPeriod?: number; +} - // 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; - } +export interface IMatrixClientPeg { + opts: IOpts; /** * Sets the script href passed to the IndexedDB web worker @@ -73,19 +62,23 @@ class _MatrixClientPeg { * * @param {string} script href to the script to be passed to the web worker */ - setIndexedDbWorkerScript(script) { - createMatrixClient.indexedDbWorkerScript = script; - } + setIndexedDbWorkerScript(script: string): void; - get(): MatrixClient { - return this.matrixClient; - } + /** + * 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; - unset() { - this.matrixClient = null; + get(): MatrixClient; + unset(): void; + assign(): Promise; + start(): Promise; - MatrixActionCreators.stop(); - } + getCredentials(): IMatrixClientCreds; /** * If we've registered a user ID we set this to the ID of the @@ -95,9 +88,7 @@ class _MatrixClientPeg { * * @param {string} uid The user ID of the user we've just registered */ - setJustRegisteredUserId(uid) { - this._justRegisteredUserId = uid; - } + setJustRegisteredUserId(uid: string): void; /** * Returns true if the current user has just been registered by this @@ -105,23 +96,73 @@ class _MatrixClientPeg { * * @returns {bool} True if user has just been registered */ - currentUserIsJustRegistered() { + currentUserIsJustRegistered(): 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: IOpts = { + 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 setIndexedDbWorkerScript(script: string): void { + createMatrixClient.indexedDbWorkerScript = script; + } + + public get(): MatrixClient { + return this.matrixClient; + } + + public unset(): void { + this.matrixClient = null; + + MatrixActionCreators.stop(); + } + + public setJustRegisteredUserId(uid: string): void { + this.justRegisteredUserId = uid; + } + + public currentUserIsJustRegistered(): boolean { return ( this.matrixClient && - this.matrixClient.credentials.userId === this._justRegisteredUserId + 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); + public replaceUsingCreds(creds: IMatrixClientCreds): void { + this.currentClientCreds = creds; + this.createClient(creds); } - async assign() { + public async assign(): Promise { for (const dbType of ['indexeddb', 'memory']) { try { const promise = this.matrixClient.store.startup(); @@ -132,7 +173,7 @@ class _MatrixClientPeg { if (dbType === 'indexeddb') { console.error('Error starting matrixclient store - falling back to memory store', err); this.matrixClient.store = new MemoryStore({ - localStorage: global.localStorage, + localStorage: localStorage, }); } else { console.error('Failed to start memory store!', err); @@ -158,9 +199,7 @@ class _MatrixClientPeg { // 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, - }); + 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. @@ -171,6 +210,7 @@ class _MatrixClientPeg { // the react sdk doesn't work without this, so don't allow opts.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); @@ -179,7 +219,7 @@ class _MatrixClientPeg { return opts; } - async start() { + public async start(): Promise { const opts = await this.assign(); console.log(`MatrixClientPeg: really starting MatrixClient`); @@ -187,7 +227,7 @@ class _MatrixClientPeg { console.log(`MatrixClientPeg: MatrixClient started`); } - getCredentials(): MatrixClientCreds { + public getCredentials(): IMatrixClientCreds { return { homeserverUrl: this.matrixClient.baseUrl, identityServerUrl: this.matrixClient.idBaseUrl, @@ -198,12 +238,7 @@ class _MatrixClientPeg { }; } - /* - * 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() { + 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!"); @@ -211,15 +246,17 @@ class _MatrixClientPeg { return matches[1]; } - _createClient(creds: MatrixClientCreds) { + private createClient(creds: IMatrixClientCreds): void { + // TODO: Make these opts typesafe with the js-sdk const opts = { baseUrl: creds.homeserverUrl, idBaseUrl: creds.identityServerUrl, accessToken: creds.accessToken, userId: creds.userId, deviceId: creds.deviceId, + pickleKey: creds.pickleKey, timelineSupport: true, - forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false), + forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer'), fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), verificationMethods: [ verificationMethods.SAS, @@ -228,9 +265,9 @@ class _MatrixClientPeg { ], unstableClientRelationAggregation: true, identityServer: new IdentityAuthClient(), + cryptoCallbacks: {}, }; - 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. @@ -253,8 +290,8 @@ class _MatrixClientPeg { } } -if (!global.mxMatrixClientPeg) { - global.mxMatrixClientPeg = new _MatrixClientPeg(); +if (!window.mxMatrixClientPeg) { + window.mxMatrixClientPeg = new _MatrixClientPeg(); } -export const MatrixClientPeg = global.mxMatrixClientPeg; +export const MatrixClientPeg = window.mxMatrixClientPeg; 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..82ed33b794 --- /dev/null +++ b/src/Modal.tsx @@ -0,0 +1,383 @@ +/* +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 Analytics from './Analytics'; +import dis from './dispatcher/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"; + +interface IModal { + elem: React.ReactNode; + className?: string; + beforeClosePromise?: Promise; + closeReason?: string; + onBeforeClose?(reason?: string): Promise; + onFinished(...args: T): void; + close(...args: T): void; +} + +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 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); + } + + 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) { + const classes = classNames("mx_Dialog_wrapper", modal.className, { + mx_Dialog_wrapperWithStaticUnder: this.staticModal, + }); + + const dialog = ( +
+
+ {modal.elem} +
+
+
+ ); + + 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/Notifier.js b/src/Notifier.ts similarity index 87% rename from src/Notifier.js rename to src/Notifier.ts index ec92840998..473de6c161 100644 --- a/src/Notifier.js +++ b/src/Notifier.ts @@ -2,6 +2,7 @@ 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. @@ -16,16 +17,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClientPeg} from './MatrixClientPeg'; +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'; +import dis from './dispatcher/dispatcher'; import * as sdk from './index'; import { _t } from './languageHandler'; import Modal from './Modal'; -import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; +import SettingsStore from "./settings/SettingsStore"; +import { hideToast as hideNotificationsToast } from "./toasts/DesktopNotificationsToast"; +import {SettingLevel} from "./settings/SettingLevel"; /* * Dispatches: @@ -49,7 +56,7 @@ const typehandlers = { }, }; -const Notifier = { +export const Notifier = { notifsByRoom: {}, // A list of event IDs that we've received but need to wait until @@ -57,14 +64,14 @@ const Notifier = { // or not pendingEncryptedEventIds: [], - notificationMessageForEvent: function(ev) { + notificationMessageForEvent: function(ev: MatrixEvent) { if (typehandlers.hasOwnProperty(ev.getContent().msgtype)) { return typehandlers[ev.getContent().msgtype](ev); } return TextForEvent.textForEvent(ev); }, - _displayPopupNotification: function(ev, room) { + _displayPopupNotification: function(ev: MatrixEvent, room: Room) { const plaf = PlatformPeg.get(); if (!plaf) { return; @@ -119,7 +126,7 @@ const Notifier = { } }, - getSoundForRoom: async function(roomId) { + 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); @@ -147,12 +154,13 @@ const Notifier = { }; }, - _playAudioNotification: async function(ev, room) { - const sound = await this.getSoundForRoom(room.roomId); + _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"); + const selector = + document.querySelector(sound ? `audio[src='${sound.url}']` : "#messageAudio"); let audioElement = selector; if (!selector) { if (!sound) { @@ -201,7 +209,7 @@ const Notifier = { return plaf && plaf.supportsNotifications(); }, - setEnabled: function(enable, callback) { + setEnabled: function(enable: boolean, callback?: () => void) { const plaf = PlatformPeg.get(); if (!plaf) return; @@ -223,10 +231,11 @@ const Notifier = { 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('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'); + ? _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 }); const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); Modal.createTrackedDialog('Unable to enable Notifications', result, ErrorDialog, { title: _t('Unable to enable Notifications'), @@ -270,24 +279,20 @@ const Notifier = { }, isAudioEnabled: function() { - return this.isEnabled() && SettingsStore.getValue("audioNotificationsEnabled"); + // We don't route Audio via the HTML Notifications API so it is possible regardless of other things + return SettingsStore.getValue("audioNotificationsEnabled"); }, - setToolbarHidden: function(hidden, persistent = true) { + setToolbarHidden: function(hidden: boolean, 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(), - }); + hideNotificationsToast(); // update the info to localStorage for persistent settings if (persistent && global.localStorage) { - global.localStorage.setItem("notifications_hidden", hidden); + global.localStorage.setItem("notifications_hidden", String(hidden)); } }, @@ -310,7 +315,7 @@ const Notifier = { return this.toolbarHidden; }, - onSyncStateChange: function(state) { + onSyncStateChange: function(state: string) { if (state === "SYNCING") { this.isSyncing = true; } else if (state === "STOPPED" || state === "ERROR") { @@ -318,7 +323,7 @@ const Notifier = { } }, - onEvent: function(ev) { + onEvent: function(ev: MatrixEvent) { if (!this.isSyncing) return; // don't alert for any messages initially if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return; @@ -336,7 +341,7 @@ const Notifier = { this._evaluateEvent(ev); }, - onEventDecrypted: function(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; @@ -348,7 +353,7 @@ const Notifier = { this._evaluateEvent(ev); }, - onRoomReceipt: function(ev, room) { + 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 @@ -381,8 +386,8 @@ const Notifier = { }, }; -if (!global.mxNotifier) { - global.mxNotifier = Notifier; +if (!window.mxNotifier) { + window.mxNotifier = Notifier; } -export default global.mxNotifier; +export default window.mxNotifier; diff --git a/src/PasswordReset.js b/src/PasswordReset.js index 320599f6d9..9472ddc633 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.js @@ -84,8 +84,14 @@ export default class PasswordReset { 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/PlatformPeg.js b/src/PlatformPeg.ts similarity index 80% rename from src/PlatformPeg.js rename to src/PlatformPeg.ts index 34131fde7d..1d2b813ebc 100644 --- a/src/PlatformPeg.js +++ b/src/PlatformPeg.ts @@ -1,5 +1,6 @@ /* 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. @@ -14,6 +15,8 @@ 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) @@ -21,10 +24,8 @@ limitations under the License. * This allows the app layer to set a Platform without necessarily * having to have a MatrixChat object */ -class PlatformPeg { - constructor() { - this.platform = null; - } +export class PlatformPeg { + platform: BasePlatform = null; /** * Returns the current Platform object for the application. @@ -39,12 +40,12 @@ class PlatformPeg { * application. * This should be an instance of a class extending BasePlatform. */ - set(plaf) { + set(plaf: BasePlatform) { this.platform = plaf; } } -if (!global.mxPlatformPeg) { - global.mxPlatformPeg = new PlatformPeg(); +if (!window.mxPlatformPeg) { + window.mxPlatformPeg = new PlatformPeg(); } -export default global.mxPlatformPeg; +export default window.mxPlatformPeg; diff --git a/src/Presence.js b/src/Presence.js index 2fc13a090b..42bca35f96 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -17,7 +17,7 @@ limitations under the License. */ import {MatrixClientPeg} from "./MatrixClientPeg"; -import dis from "./dispatcher"; +import dis from "./dispatcher/dispatcher"; import Timer from './utils/Timer'; // Time in ms after that a user is considered as unavailable/away diff --git a/src/Registration.js b/src/Registration.js index ca162bac03..9c0264c067 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -20,7 +20,7 @@ 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'; @@ -52,7 +52,7 @@ export async function startAnyRegistrationFlow(options) { // caution though. // XXX: ILAG is disabled for now, - // see https://github.com/vector-im/riot-web/issues/8222 + // see https://github.com/vector-im/element-web/issues/8222 // const flows = await _getRegistrationFlows(); // const hasIlagFlow = flows.some((flow) => { diff --git a/src/Resend.js b/src/Resend.js index 6d6c18cf27..5638313306 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -16,7 +16,7 @@ limitations under the License. */ import {MatrixClientPeg} from './MatrixClientPeg'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import { EventStatus } from 'matrix-js-sdk'; export default class Resend { @@ -45,7 +45,7 @@ export default class Resend { }); }, function(err) { // XXX: temporary logging to try to diagnose - // https://github.com/vector-im/riot-web/issues/3148 + // https://github.com/vector-im/element-web/issues/3148 console.log('Resend got send failure: ' + err.name + '(' + err + ')'); dis.dispatch({ diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js index c67acaf314..a86c521ac4 100644 --- a/src/RoomNotifs.js +++ b/src/RoomNotifs.js @@ -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); 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/ScalarMessaging.js b/src/ScalarMessaging.js index 9731e42825..896e27d92c 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -174,7 +174,7 @@ Request: Response: [ { - // TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111) + // 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: { @@ -193,7 +193,7 @@ Example: room_id: "!foo:bar", response: [ { - // TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111) + // 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: { @@ -238,22 +238,23 @@ Example: import {MatrixClientPeg} from './MatrixClientPeg'; import { MatrixEvent } from 'matrix-js-sdk'; -import dis from './dispatcher'; +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 {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, diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index 400d29a20f..b914aaaf6d 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 diff --git a/src/Searching.js b/src/Searching.js index 663328fe41..b1507e6a49 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -17,25 +17,71 @@ 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 SEARCH_LIMIT = 10; - const searchPromise = MatrixClientPeg.get().searchRoomEvents({ - filter, - term, - }); +async function serverSideSearch(term, roomId = undefined) { + const client = MatrixClientPeg.get(); - return searchPromise; + const filter = { + limit: SEARCH_LIMIT, + }; + + if (roomId !== undefined) filter.rooms = [roomId]; + + const body = { + search_categories: { + room_events: { + search_term: term, + filter: filter, + order_by: "recent", + event_context: { + before_limit: 1, + after_limit: 1, + include_profile: true, + }, + }, + }, + }; + + const response = await client.search({body: body}); + + const result = { + response: response, + query: body, + }; + + return result; +} + +async function serverSideSearchProcess(term, roomId = undefined) { + 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 wan't to delegate the + // pagination back to backPaginateRoomEventsSearch() in some cases. + const searchResult = { + _query: result.query, + results: [], + highlights: [], + }; + + return client._processRoomEventsSearch(searchResult, result.response); +} + +function compareEvents(a, b) { + 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) { + const client = MatrixClientPeg.get(); + // Create two promises, one for the local search, one for the // server-side search. const serverSidePromise = serverSideSearch(searchTerm); @@ -48,37 +94,59 @@ async function combinedSearch(searchTerm) { const localResult = await localPromise; const serverSideResult = await serverSidePromise; - // Combine the search results into one result. - const result = {}; + const serverQuery = serverSideResult.query; + const serverResponse = serverSideResult.response; - // 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; + const localQuery = localResult.query; + const localResponse = localResult.response; - if (aEvent.origin_server_ts > - bEvent.origin_server_ts) return -1; - if (aEvent.origin_server_ts < - bEvent.origin_server_ts) return 1; - return 0; + // 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 = { + seshatQuery: localQuery, + _query: serverQuery, + serverSideNextBatch: serverResponse.next_batch, + cachedEvents: [], + oldestEventFrom: "server", + results: [], + highlights: [], }; - result.count = localResult.count + serverSideResult.count; - result.results = localResult.results.concat( - serverSideResult.results).sort(compare); - result.highlights = localResult.highlights.concat( - serverSideResult.highlights); + // Combine our results. + const combinedResult = combineResponses(emptyResult, localResponse, serverResponse.search_categories.room_events); + + // Let the client process the combined result. + const response = { + 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, roomId = undefined) { +async function localSearch(searchTerm, roomId = undefined, processResult = true) { + const eventIndex = EventIndexPeg.get(); + const searchArgs = { search_term: searchTerm, before_limit: 1, after_limit: 1, + limit: SEARCH_LIMIT, order_by_recency: true, room_id: undefined, }; @@ -87,6 +155,19 @@ async function localSearch(searchTerm, 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; +} + +async function localSearchProcess(searchTerm, roomId = undefined) { const emptyResult = { results: [], highlights: [], @@ -94,9 +175,34 @@ async function localSearch(searchTerm, roomId = undefined) { if (searchTerm === "") return emptyResult; + const result = await localSearch(searchTerm, roomId); + + emptyResult.seshatQuery = result.query; + + const response = { + 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) { 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: { @@ -104,8 +210,324 @@ async function localSearch(searchTerm, roomId = undefined) { }, }; - const result = MatrixClientPeg.get()._processRoomEventsSearch( - emptyResult, response); + 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, secondResults) { + try { + const oldestFirstEvent = firstResults.results[firstResults.results.length - 1].result; + const oldestSecondEvent = secondResults.results[secondResults.results.length - 1].result; + + if (oldestFirstEvent.origin_server_ts <= oldestSecondEvent.origin_server_ts) { + return -1; + } else { + return 1; + } + } catch { + return 0; + } +} + +function combineEventSources(previousSearchResult, response, a, b) { + // 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, localEvents = undefined, serverEvents = undefined) { + const response = {}; + + const cachedEvents = previousSearchResult.cachedEvents; + let oldestEventFrom = previousSearchResult.oldestEventFrom; + response.highlights = previousSearchResult.highlights; + + if (localEvents && serverEvents) { + // 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, serverEvents) < 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, cachedEvents) < 0) { + oldestEventFrom = "local"; + } + combineEventSources(previousSearchResult, response, localEvents.results, cachedEvents); + } else if (serverEvents) { + // 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, 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, localEvents = undefined, serverEvents = undefined) { + // 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; +} + +function restoreEncryptionInfo(searchResultSlice) { + for (let i = 0; i < searchResultSlice.length; i++) { + const timeline = searchResultSlice[i].context.getTimeline(); + + for (let j = 0; j < timeline.length; j++) { + const ev = timeline[j]; + + if (ev.event.curve25519Key) { + ev.makeEncrypted( + "m.room.encrypted", + { algorithm: ev.event.algorithm }, + ev.event.curve25519Key, + ev.event.ed25519Key, + ); + ev._forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain; + + delete ev.event.curve25519Key; + delete ev.event.ed25519Key; + delete ev.event.algorithm; + delete ev.event.forwardingCurve25519KeyChain; + } + } + } +} + +async function combinedPagination(searchResult) { + const eventIndex = EventIndexPeg.get(); + const client = MatrixClientPeg.get(); + + const searchArgs = searchResult.seshatQuery; + const oldestEventFrom = searchResult.oldestEventFrom; + + let localResult; + let serverSideResult; + + // Fetch events from the local index if we have a token for itand 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; + + 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.length; + + // 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; } @@ -117,11 +539,11 @@ function eventIndexSearch(term, roomId = undefined) { if (MatrixClientPeg.get().isRoomEncrypted(roomId)) { // The search is for a single encrypted room, use our local // search method. - searchPromise = localSearch(term, roomId); + searchPromise = localSearchProcess(term, roomId); } else { // The search is for a single non-encrypted room, use the // server-side search. - searchPromise = serverSideSearch(term, roomId); + searchPromise = serverSideSearchProcess(term, roomId); } } else { // Search across all rooms, combine a server side search and a @@ -132,9 +554,45 @@ function eventIndexSearch(term, roomId = undefined) { return searchPromise; } +function eventIndexSearchPagination(searchResult) { + 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) { + 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, roomId = undefined) { const eventIndex = EventIndexPeg.get(); - if (eventIndex === null) return serverSideSearch(term, roomId); + if (eventIndex === null) return serverSideSearchProcess(term, roomId); else return eventIndexSearch(term, roomId); } diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index bd7e60e2f4..50a49ccf1c 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -21,7 +21,7 @@ limitations under the License. import * as React from 'react'; import {MatrixClientPeg} from './MatrixClientPeg'; -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import * as sdk from './index'; import {_t, _td} from './languageHandler'; import Modal from './Modal'; @@ -40,6 +40,10 @@ import { Jitsi } from "./widgets/Jitsi"; import { parseFragment as parseHtml } from "parse5"; import sendBugReport from "./rageshake/submit-rageshake"; import SdkConfig from "./SdkConfig"; +import { ensureDMExists } from "./createRoom"; +import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload"; +import { Action } from "./dispatcher/actions"; +import { EffectiveMembership, getEffectiveMembership } from "./utils/membership"; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -115,7 +119,7 @@ export class Command { run(roomId: string, args: string, cmd: string) { // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me` - if (!this.runFn) return; + if (!this.runFn) return reject(_t("Command error")); return this.runFn.bind(this)(roomId, args, cmd); } @@ -365,6 +369,7 @@ export const Commands = [ Modal.createTrackedDialog('Slash Commands', 'Topic', InfoDialog, { title: room.name, description:
, + hasCloseButton: true, }); return success(); }, @@ -396,14 +401,16 @@ export const Commands = [ // 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( @@ -416,9 +423,9 @@ export const Commands = [ )}

, button: _t("Continue"), }, - )); + ); - finished = finished.then(([useDefault]: any) => { + prom = finished.then(([useDefault]) => { if (useDefault) { useDefaultIdentityServer(); return; @@ -430,7 +437,7 @@ export const Commands = [ } } const inviter = new MultiInviter(roomId); - return success(finished.then(() => { + return success(prom.then(() => { return inviter.invite([address]); }).then(() => { if (inviter.getCompletionState(address) !== "invited") { @@ -446,8 +453,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 @@ -472,7 +479,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; @@ -491,8 +498,7 @@ export const Commands = [ }); 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', @@ -558,7 +564,7 @@ export const Commands = [ }), new Command({ command: 'part', - args: '[]', + args: '[]', description: _td('Leave room'), runFn: function(roomId, args) { const cli = MatrixClientPeg.get(); @@ -590,7 +596,7 @@ export const Commands = [ } if (targetRoomId) break; } - if (!targetRoomId) return reject(_t('Unrecognised room alias:') + ' ' + roomAlias); + if (!targetRoomId) return reject(_t('Unrecognised room address:') + ' ' + roomAlias); } } @@ -657,7 +663,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(); @@ -687,7 +693,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(); @@ -727,9 +733,11 @@ export const Commands = [ const cli = MatrixClientPeg.get(); const room = cli.getRoom(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', ''); - if (!powerLevelEvent.getContent().users[args]) return reject(_t("Could not find user in room")); return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent)); } } @@ -941,8 +949,10 @@ export const Commands = [ } const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId); - dis.dispatch({ - action: 'view_user', + dis.dispatch({ + action: Action.ViewUser, + // XXX: We should be using a real member object and not assuming what the + // receiver wants. member: member || {userId}, }); return success(); @@ -970,6 +980,52 @@ export const Commands = [ }, category: CommandCategories.advanced, }), + new Command({ + command: "query", + description: _td("Opens chat with the given user"), + args: "", + runFn: function(roomId, userId) { + if (!userId || !userId.startsWith("@") || !userId.includes(":")) { + return reject(this.getUsage()); + } + + return success((async () => { + dis.dispatch({ + action: 'view_room', + room_id: await ensureDMExists(MatrixClientPeg.get(), userId), + }); + })()); + }, + category: CommandCategories.actions, + }), + new Command({ + command: "msg", + description: _td("Sends a message to the given user"), + args: " ", + runFn: function(_, args) { + if (args) { + // matches the first whitespace delimited group and then the rest of the string + const matches = args.match(/^(\S+?)(?: +(.*))?$/s); + if (matches) { + const [userId, msg] = matches.slice(1); + if (msg && userId && userId.startsWith("@") && userId.includes(":")) { + return success((async () => { + const cli = MatrixClientPeg.get(); + const roomId = await ensureDMExists(cli, userId); + dis.dispatch({ + action: 'view_room', + room_id: roomId, + }); + cli.sendTextMessage(roomId, msg); + })()); + } + } + } + + return reject(this.getUsage()); + }, + category: CommandCategories.actions, + }), // Command definitions for autocompletion ONLY: // /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes @@ -995,7 +1051,7 @@ export function parseCommandString(input) { // trim any trailing whitespace, as it can confuse the parser for // IRC-style commands input = input.replace(/\s+$/, ''); - if (input[0] !== '/') return null; // not a command + if (input[0] !== '/') return {}; // not a command const bits = input.match(/^(\S+?)(?: +((.|\n)*))?$/); let cmd; diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 3607d7a676..c55380bd9b 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -345,7 +345,7 @@ function textForCallHangupEvent(event) { } else if (eventContent.reason === "invite_timeout") { reason = _t('(no answer)'); } else if (eventContent.reason === "user hangup") { - // workaround for https://github.com/vector-im/riot-web/issues/5178 + // workaround for https://github.com/vector-im/element-web/issues/5178 // it seems Android randomly sets a reason of "user hangup" which is // interpreted as an error code :( // https://github.com/vector-im/riot-android/issues/2623 @@ -603,7 +603,7 @@ const stateHandlers = { 'm.room.guest_access': textForGuestAccessEvent, 'm.room.related_groups': textForRelatedGroupsEvent, - // TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111) + // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) 'im.vector.modular.widgets': textForWidgetEvent, }; diff --git a/src/Tinter.js b/src/Tinter.js index 24a4d25a00..ca5a460e16 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -327,7 +327,7 @@ class Tinter { // Vector Green as its primary color? // --richvdh - // Yes, tinting assumes that you are using the Riot skin for now. + // Yes, tinting assumes that you are using the Element skin for now. // The right solution will be to move the CSS over to react-sdk. // And yes, the default assets for the base skin might as well use // Vector Green as any other colour. diff --git a/src/Unread.js b/src/Unread.js index ca713b05e4..cf131cac00 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -52,10 +52,10 @@ export function doesRoomHaveUnreadMessages(room) { // as we don't send RRs for our own messages, make sure we special case that // if *we* sent the last message into the room, we consider it not unread! - // Should fix: https://github.com/vector-im/riot-web/issues/3263 - // https://github.com/vector-im/riot-web/issues/2427 + // Should fix: https://github.com/vector-im/element-web/issues/3263 + // https://github.com/vector-im/element-web/issues/2427 // ...and possibly some of the others at - // https://github.com/vector-im/riot-web/issues/3363 + // https://github.com/vector-im/element-web/issues/3363 if (room.timeline.length && room.timeline[room.timeline.length - 1].sender && room.timeline[room.timeline.length - 1].sender.userId === myUserId) { diff --git a/src/UserActivity.js b/src/UserActivity.js index 0d1b4d0cc0..0174aebaf5 100644 --- a/src/UserActivity.js +++ b/src/UserActivity.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import Timer from './utils/Timer'; // important these are larger than the timeouts of timers diff --git a/src/VectorConferenceHandler.js b/src/VectorConferenceHandler.js index 180dad876b..c10bc659ae 100644 --- a/src/VectorConferenceHandler.js +++ b/src/VectorConferenceHandler.js @@ -19,13 +19,13 @@ import {createNewMatrixCall as jsCreateNewMatrixCall, Room} from "matrix-js-sdk" import CallHandler from './CallHandler'; import {MatrixClientPeg} from "./MatrixClientPeg"; -// FIXME: this is Riot (Vector) specific code, but will be removed shortly when -// we switch over to jitsi entirely for video conferencing. +// FIXME: this is Element specific code, but will be removed shortly when we +// switch over to Jitsi entirely for video conferencing. -// FIXME: This currently forces Vector to try to hit the matrix.org AS for conferencing. -// This is bad because it prevents people running their own ASes from being used. -// This isn't permanent and will be customisable in the future: see the proposal -// at docs/conferencing.md for more info. +// FIXME: This currently forces Element to try to hit the matrix.org AS for +// conferencing. This is bad because it prevents people running their own ASes +// from being used. This isn't permanent and will be customisable in the future: +// see the proposal at docs/conferencing.md for more info. const USER_PREFIX = "fs_"; const DOMAIN = "matrix.org"; diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index 5f877bd48a..6aed08c39d 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -41,9 +41,18 @@ if (!global.mxToWidgetMessaging) { const OUTBOUND_API_NAME = 'toWidget'; export default class WidgetMessaging { - constructor(widgetId, widgetUrl, isUserWidget, target) { + /** + * @param {string} widgetId The widget's ID + * @param {string} wurl The raw URL of the widget as in the event (the 'wURL') + * @param {string} renderedUrl The url used in the widget's iframe (either similar to the wURL + * or a different URL of the clients choosing if it is using its own impl). + * @param {bool} isUserWidget If true, the widget is a user widget, otherwise it's a room widget + * @param {object} target Where widget messages should be sent (eg. the iframe object) + */ + constructor(widgetId, wurl, renderedUrl, isUserWidget, target) { this.widgetId = widgetId; - this.widgetUrl = widgetUrl; + this.wurl = wurl; + this.renderedUrl = renderedUrl; this.isUserWidget = isUserWidget; this.target = target; this.fromWidget = global.mxFromWidgetMessaging; @@ -67,7 +76,7 @@ export default class WidgetMessaging { console.error(err._error); } // Potential XSS attack if 'msg' is not appropriately sanitized, - // as it is untrusted input by our parent window (which we assume is Riot). + // as it is untrusted input by our parent window (which we assume is Element). // We can't aggressively sanitize [A-z0-9] since it might be a translation. throw new Error(msg); } @@ -87,6 +96,17 @@ export default class WidgetMessaging { }); } + /** + * Tells the widget that it should terminate now. + * @returns {Promise<*>} Resolves when widget has acknowledged the message. + */ + terminate() { + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: KnownWidgetActions.Terminate, + }); + } + /** * Request a screenshot from a widget * @return {Promise} To be resolved with screenshot data when it has been generated @@ -128,19 +148,19 @@ export default class WidgetMessaging { } start() { - this.fromWidget.addEndpoint(this.widgetId, this.widgetUrl); + this.fromWidget.addEndpoint(this.widgetId, this.renderedUrl); this.fromWidget.addListener("get_openid", this._onOpenIdRequest); } stop() { - this.fromWidget.removeEndpoint(this.widgetId, this.widgetUrl); + this.fromWidget.removeEndpoint(this.widgetId, this.renderedUrl); this.fromWidget.removeListener("get_openid", this._onOpenIdRequest); } async _onOpenIdRequest(ev, rawEv) { if (ev.widgetId !== this.widgetId) return; // not interesting - const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, this.widgetUrl, this.isUserWidget); + const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, this.wurl, this.isUserWidget); const settings = SettingsStore.getValue("widgetOpenIDPermissions"); if (settings.deny && settings.deny.includes(widgetSecurityKey)) { @@ -161,7 +181,7 @@ export default class WidgetMessaging { // Actually ask for permission to send the user's data Modal.createTrackedDialog("OpenID widget permissions", '', WidgetOpenIDPermissionsDialog, { - widgetUrl: this.widgetUrl, + widgetUrl: this.wurl, widgetId: this.widgetId, isUserWidget: this.isUserWidget, diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx index bcbf3d6810..f527ab4a14 100644 --- a/src/accessibility/KeyboardShortcuts.tsx +++ b/src/accessibility/KeyboardShortcuts.tsx @@ -34,6 +34,7 @@ export enum Categories { CALLS = "Calls", COMPOSER = "Composer", ROOM_LIST = "Room List", + ROOM = "Room", AUTOCOMPLETE = "Autocomplete", } @@ -142,6 +143,34 @@ const shortcuts: Record = { }, ], + [Categories.ROOM]: [ + { + keybinds: [{ + key: Key.PAGE_UP, + }, { + key: Key.PAGE_DOWN, + }], + description: _td("Scroll up/down in the timeline"), + }, { + keybinds: [{ + key: Key.ESCAPE, + }], + description: _td("Dismiss read marker and jump to bottom"), + }, { + keybinds: [{ + modifiers: [Modifiers.SHIFT], + key: Key.PAGE_UP, + }], + description: _td("Jump to oldest unread message"), + }, { + keybinds: [{ + modifiers: [CMD_OR_CTRL, Modifiers.SHIFT], + key: Key.U, + }], + description: _td("Upload a file"), + } + ], + [Categories.ROOM_LIST]: [ { keybinds: [{ @@ -181,13 +210,6 @@ const shortcuts: Record = { [Categories.NAVIGATION]: [ { - keybinds: [{ - key: Key.PAGE_UP, - }, { - key: Key.PAGE_DOWN, - }], - description: _td("Scroll up/down in the timeline"), - }, { keybinds: [{ modifiers: [Modifiers.ALT, Modifiers.SHIFT], key: Key.ARROW_UP, @@ -257,10 +279,11 @@ const shortcuts: Record = { const categoryOrder = [ Categories.COMPOSER, - Categories.CALLS, - Categories.ROOM_LIST, Categories.AUTOCOMPLETE, + Categories.ROOM, + Categories.ROOM_LIST, Categories.NAVIGATION, + Categories.CALLS, ]; interface IModal { diff --git a/src/accessibility/RovingTabIndex.js b/src/accessibility/RovingTabIndex.tsx similarity index 79% rename from src/accessibility/RovingTabIndex.js rename to src/accessibility/RovingTabIndex.tsx index b481f08fe2..5a650d4b6e 100644 --- a/src/accessibility/RovingTabIndex.js +++ b/src/accessibility/RovingTabIndex.tsx @@ -22,9 +22,12 @@ import React, { useMemo, useRef, useReducer, + Reducer, + Dispatch, } from "react"; -import PropTypes from "prop-types"; + import {Key} from "../Keyboard"; +import {FocusHandler, Ref} from "./roving/types"; /** * Module to simplify implementing the Roving TabIndex accessibility technique @@ -41,7 +44,17 @@ import {Key} from "../Keyboard"; const DOCUMENT_POSITION_PRECEDING = 2; -const RovingTabIndexContext = createContext({ +export interface IState { + activeRef: Ref; + refs: Ref[]; +} + +interface IContext { + state: IState; + dispatch: Dispatch; +} + +const RovingTabIndexContext = createContext({ state: { activeRef: null, refs: [], // list of refs in DOM order @@ -50,16 +63,22 @@ const RovingTabIndexContext = createContext({ }); RovingTabIndexContext.displayName = "RovingTabIndexContext"; -// TODO use a TypeScript type here -const types = { - REGISTER: "REGISTER", - UNREGISTER: "UNREGISTER", - SET_FOCUS: "SET_FOCUS", -}; +enum Type { + Register = "REGISTER", + Unregister = "UNREGISTER", + SetFocus = "SET_FOCUS", +} -const reducer = (state, action) => { +interface IAction { + type: Type; + payload: { + ref: Ref; + }; +} + +const reducer = (state: IState, action: IAction) => { switch (action.type) { - case types.REGISTER: { + case Type.Register: { if (state.refs.length === 0) { // Our list of refs was empty, set activeRef to this first item return { @@ -92,7 +111,7 @@ const reducer = (state, action) => { ], }; } - case types.UNREGISTER: { + case Type.Unregister: { // filter out the ref which we are removing const refs = state.refs.filter(r => r !== action.payload.ref); @@ -117,7 +136,7 @@ const reducer = (state, action) => { refs, }; } - case types.SET_FOCUS: { + case Type.SetFocus: { // update active ref return { ...state, @@ -129,13 +148,21 @@ const reducer = (state, action) => { } }; -export const RovingTabIndexProvider = ({children, handleHomeEnd, onKeyDown}) => { - const [state, dispatch] = useReducer(reducer, { +interface IProps { + handleHomeEnd?: boolean; + children(renderProps: { + onKeyDownHandler(ev: React.KeyboardEvent); + }); + onKeyDown?(ev: React.KeyboardEvent, state: IState); +} + +export const RovingTabIndexProvider: React.FC = ({children, handleHomeEnd, onKeyDown}) => { + const [state, dispatch] = useReducer>(reducer, { activeRef: null, refs: [], }); - const context = useMemo(() => ({state, dispatch}), [state]); + const context = useMemo(() => ({state, dispatch}), [state]); const onKeyDownHandler = useCallback((ev) => { let handled = false; @@ -163,7 +190,7 @@ export const RovingTabIndexProvider = ({children, handleHomeEnd, onKeyDown}) => ev.preventDefault(); ev.stopPropagation(); } else if (onKeyDown) { - return onKeyDown(ev); + return onKeyDown(ev, state); } }, [context.state, onKeyDown, handleHomeEnd]); @@ -171,19 +198,15 @@ export const RovingTabIndexProvider = ({children, handleHomeEnd, onKeyDown}) => { children({onKeyDownHandler}) } ; }; -RovingTabIndexProvider.propTypes = { - handleHomeEnd: PropTypes.bool, - onKeyDown: PropTypes.func, -}; // Hook to register a roving tab index // inputRef parameter specifies the ref to use // onFocus should be called when the index gained focus in any manner // isActive should be used to set tabIndex in a manner such as `tabIndex={isActive ? 0 : -1}` // ref should be passed to a DOM node which will be used for DOM compareDocumentPosition -export const useRovingTabIndex = (inputRef) => { +export const useRovingTabIndex = (inputRef: Ref): [FocusHandler, boolean, Ref] => { const context = useContext(RovingTabIndexContext); - let ref = useRef(null); + let ref = useRef(null); if (inputRef) { // if we are given a ref, use it instead of ours @@ -193,13 +216,13 @@ export const useRovingTabIndex = (inputRef) => { // setup (after refs) useLayoutEffect(() => { context.dispatch({ - type: types.REGISTER, + type: Type.Register, payload: {ref}, }); // teardown return () => { context.dispatch({ - type: types.UNREGISTER, + type: Type.Unregister, payload: {ref}, }); }; @@ -207,7 +230,7 @@ export const useRovingTabIndex = (inputRef) => { const onFocus = useCallback(() => { context.dispatch({ - type: types.SET_FOCUS, + type: Type.SetFocus, payload: {ref}, }); }, [ref, context]); @@ -216,9 +239,7 @@ export const useRovingTabIndex = (inputRef) => { return [onFocus, isActive, ref]; }; -// Wrapper to allow use of useRovingTabIndex outside of React Functional Components. -export const RovingTabIndexWrapper = ({children, inputRef}) => { - const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); - return children({onFocus, isActive, ref}); -}; - +// re-export the semantic helper components for simplicity +export {RovingTabIndexWrapper} from "./roving/RovingTabIndexWrapper"; +export {RovingAccessibleButton} from "./roving/RovingAccessibleButton"; +export {RovingAccessibleTooltipButton} from "./roving/RovingAccessibleTooltipButton"; diff --git a/src/accessibility/Toolbar.tsx b/src/accessibility/Toolbar.tsx new file mode 100644 index 0000000000..0e968461a8 --- /dev/null +++ b/src/accessibility/Toolbar.tsx @@ -0,0 +1,69 @@ +/* +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 {IState, RovingTabIndexProvider} from "./RovingTabIndex"; +import {Key} from "../Keyboard"; + +interface IProps extends Omit, "onKeyDown"> { +} + +// This component implements the Toolbar design pattern from the WAI-ARIA Authoring Practices guidelines. +// https://www.w3.org/TR/wai-aria-practices-1.1/#toolbar +// All buttons passed in children must use RovingTabIndex to set `onFocus`, `isActive`, `ref` +const Toolbar: React.FC = ({children, ...props}) => { + const onKeyDown = (ev: React.KeyboardEvent, state: IState) => { + const target = ev.target as HTMLElement; + let handled = true; + + switch (ev.key) { + case Key.ARROW_UP: + case Key.ARROW_DOWN: + if (target.hasAttribute('aria-haspopup')) { + target.click(); + } + break; + + case Key.ARROW_LEFT: + case Key.ARROW_RIGHT: + if (state.refs.length > 0) { + const i = state.refs.findIndex(r => r === state.activeRef); + const delta = ev.key === Key.ARROW_RIGHT ? 1 : -1; + state.refs.slice((i + delta) % state.refs.length)[0].current.focus(); + } + break; + + // HOME and END are handled by RovingTabIndexProvider + + default: + handled = false; + } + + if (handled) { + ev.preventDefault(); + ev.stopPropagation(); + } + }; + + return + {({onKeyDownHandler}) =>
+ { children } +
} +
; +}; + +export default Toolbar; diff --git a/src/accessibility/context_menu/ContextMenuButton.tsx b/src/accessibility/context_menu/ContextMenuButton.tsx new file mode 100644 index 0000000000..e211a4c933 --- /dev/null +++ b/src/accessibility/context_menu/ContextMenuButton.tsx @@ -0,0 +1,51 @@ +/* +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 React from "react"; + +import AccessibleButton from "../../components/views/elements/AccessibleButton"; + +interface IProps extends React.ComponentProps { + label?: string; + // whether or not the context menu is currently open + isExpanded: boolean; +} + +// Semantic component for representing the AccessibleButton which launches a +export const ContextMenuButton: React.FC = ({ + label, + isExpanded, + children, + onClick, + onContextMenu, + ...props +}) => { + return ( + + { children } + + ); +}; diff --git a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx new file mode 100644 index 0000000000..abc5412100 --- /dev/null +++ b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx @@ -0,0 +1,47 @@ +/* +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 React from "react"; + +import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; + +interface IProps extends React.ComponentProps { + // whether or not the context menu is currently open + isExpanded: boolean; +} + +// Semantic component for representing the AccessibleButton which launches a +export const ContextMenuTooltipButton: React.FC = ({ + isExpanded, + children, + onClick, + onContextMenu, + ...props +}) => { + return ( + + { children } + + ); +}; diff --git a/src/accessibility/context_menu/MenuGroup.tsx b/src/accessibility/context_menu/MenuGroup.tsx new file mode 100644 index 0000000000..9334e17a18 --- /dev/null +++ b/src/accessibility/context_menu/MenuGroup.tsx @@ -0,0 +1,30 @@ +/* +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 React from "react"; + +interface IProps extends React.HTMLAttributes { + label: string; +} + +// Semantic component for representing a role=group for grouping menu radios/checkboxes +export const MenuGroup: React.FC = ({children, label, ...props}) => { + return
+ { children } +
; +}; diff --git a/src/components/views/rooms/RoomDropTarget.js b/src/accessibility/context_menu/MenuItem.tsx similarity index 53% rename from src/components/views/rooms/RoomDropTarget.js rename to src/accessibility/context_menu/MenuItem.tsx index 61b7ca6d59..64233e51ad 100644 --- a/src/components/views/rooms/RoomDropTarget.js +++ b/src/accessibility/context_menu/MenuItem.tsx @@ -1,5 +1,6 @@ /* 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"); @@ -15,21 +16,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import createReactClass from 'create-react-class'; +import React from "react"; -export default createReactClass({ - displayName: 'RoomDropTarget', +import AccessibleButton from "../../components/views/elements/AccessibleButton"; + +interface IProps extends React.ComponentProps { + label?: string; +} + +// Semantic component for representing a role=menuitem +export const MenuItem: React.FC = ({children, label, ...props}) => { + return ( + + { children } + + ); +}; - render: function() { - return ( -
-
-
- { this.props.label } -
-
-
- ); - }, -}); diff --git a/src/accessibility/context_menu/MenuItemCheckbox.tsx b/src/accessibility/context_menu/MenuItemCheckbox.tsx new file mode 100644 index 0000000000..5eb8cc4819 --- /dev/null +++ b/src/accessibility/context_menu/MenuItemCheckbox.tsx @@ -0,0 +1,43 @@ +/* +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 React from "react"; + +import AccessibleButton from "../../components/views/elements/AccessibleButton"; + +interface IProps extends React.ComponentProps { + label?: string; + active: boolean; +} + +// Semantic component for representing a role=menuitemcheckbox +export const MenuItemCheckbox: React.FC = ({children, label, active, disabled, ...props}) => { + return ( + + { children } + + ); +}; diff --git a/src/accessibility/context_menu/MenuItemRadio.tsx b/src/accessibility/context_menu/MenuItemRadio.tsx new file mode 100644 index 0000000000..472f13ff14 --- /dev/null +++ b/src/accessibility/context_menu/MenuItemRadio.tsx @@ -0,0 +1,43 @@ +/* +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 React from "react"; + +import AccessibleButton from "../../components/views/elements/AccessibleButton"; + +interface IProps extends React.ComponentProps { + label?: string; + active: boolean; +} + +// Semantic component for representing a role=menuitemradio +export const MenuItemRadio: React.FC = ({children, label, active, disabled, ...props}) => { + return ( + + { children } + + ); +}; diff --git a/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx new file mode 100644 index 0000000000..d373f892c9 --- /dev/null +++ b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx @@ -0,0 +1,64 @@ +/* +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 React from "react"; + +import {Key} from "../../Keyboard"; +import StyledCheckbox from "../../components/views/elements/StyledCheckbox"; + +interface IProps extends React.ComponentProps { + label?: string; + onChange(); // we handle keyup/down ourselves so lose the ChangeEvent + onClose(): void; // gets called after onChange on Key.ENTER +} + +// Semantic component for representing a styled role=menuitemcheckbox +export const StyledMenuItemCheckbox: React.FC = ({children, label, onChange, onClose, ...props}) => { + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === Key.ENTER || e.key === Key.SPACE) { + e.stopPropagation(); + e.preventDefault(); + onChange(); + // Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12 + if (e.key === Key.ENTER) { + onClose(); + } + } + }; + const onKeyUp = (e: React.KeyboardEvent) => { + // prevent the input default handler as we handle it on keydown to match + // https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html + if (e.key === Key.SPACE || e.key === Key.ENTER) { + e.stopPropagation(); + e.preventDefault(); + } + }; + return ( + + { children } + + ); +}; diff --git a/src/accessibility/context_menu/StyledMenuItemRadio.tsx b/src/accessibility/context_menu/StyledMenuItemRadio.tsx new file mode 100644 index 0000000000..5e5aa90a38 --- /dev/null +++ b/src/accessibility/context_menu/StyledMenuItemRadio.tsx @@ -0,0 +1,64 @@ +/* +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 React from "react"; + +import {Key} from "../../Keyboard"; +import StyledRadioButton from "../../components/views/elements/StyledRadioButton"; + +interface IProps extends React.ComponentProps { + label?: string; + onChange(); // we handle keyup/down ourselves so lose the ChangeEvent + onClose(): void; // gets called after onChange on Key.ENTER +} + +// Semantic component for representing a styled role=menuitemradio +export const StyledMenuItemRadio: React.FC = ({children, label, onChange, onClose, ...props}) => { + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === Key.ENTER || e.key === Key.SPACE) { + e.stopPropagation(); + e.preventDefault(); + onChange(); + // Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12 + if (e.key === Key.ENTER) { + onClose(); + } + } + }; + const onKeyUp = (e: React.KeyboardEvent) => { + // prevent the input default handler as we handle it on keydown to match + // https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html + if (e.key === Key.SPACE || e.key === Key.ENTER) { + e.stopPropagation(); + e.preventDefault(); + } + }; + return ( + + { children } + + ); +}; diff --git a/src/accessibility/roving/RovingAccessibleButton.tsx b/src/accessibility/roving/RovingAccessibleButton.tsx new file mode 100644 index 0000000000..3473ef1bc9 --- /dev/null +++ b/src/accessibility/roving/RovingAccessibleButton.tsx @@ -0,0 +1,32 @@ +/* +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 AccessibleButton from "../../components/views/elements/AccessibleButton"; +import {useRovingTabIndex} from "../RovingTabIndex"; +import {Ref} from "./types"; + +interface IProps extends Omit, "onFocus" | "inputRef" | "tabIndex"> { + inputRef?: Ref; +} + +// Wrapper to allow use of useRovingTabIndex for simple AccessibleButtons outside of React Functional Components. +export const RovingAccessibleButton: React.FC = ({inputRef, ...props}) => { + const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); + return ; +}; + diff --git a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx new file mode 100644 index 0000000000..cc824fef22 --- /dev/null +++ b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx @@ -0,0 +1,32 @@ +/* +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 AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; +import {useRovingTabIndex} from "../RovingTabIndex"; +import {Ref} from "./types"; + +interface IProps extends Omit, "onFocus" | "inputRef" | "tabIndex"> { + inputRef?: Ref; +} + +// Wrapper to allow use of useRovingTabIndex for simple AccessibleTooltipButtons outside of React Functional Components. +export const RovingAccessibleTooltipButton: React.FC = ({inputRef, ...props}) => { + const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); + return ; +}; + diff --git a/src/accessibility/roving/RovingTabIndexWrapper.tsx b/src/accessibility/roving/RovingTabIndexWrapper.tsx new file mode 100644 index 0000000000..c826b74497 --- /dev/null +++ b/src/accessibility/roving/RovingTabIndexWrapper.tsx @@ -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. +*/ + +import React from "react"; + +import AccessibleButton from "../../components/views/elements/AccessibleButton"; +import {useRovingTabIndex} from "../RovingTabIndex"; +import {FocusHandler, Ref} from "./types"; + +interface IProps { + inputRef?: Ref; + children(renderProps: { + onFocus: FocusHandler; + isActive: boolean; + ref: Ref; + }); +} + +// Wrapper to allow use of useRovingTabIndex outside of React Functional Components. +export const RovingTabIndexWrapper: React.FC = ({children, inputRef}) => { + const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); + return children({onFocus, isActive, ref}); +}; diff --git a/src/accessibility/roving/types.ts b/src/accessibility/roving/types.ts new file mode 100644 index 0000000000..f0a43e5fb8 --- /dev/null +++ b/src/accessibility/roving/types.ts @@ -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. +*/ + +import {RefObject} from "react"; + +export type Ref = RefObject; + +export type FocusHandler = () => void; diff --git a/src/actions/GroupActions.js b/src/actions/GroupActions.js deleted file mode 100644 index 006c2da5b8..0000000000 --- a/src/actions/GroupActions.js +++ /dev/null @@ -1,34 +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 { asyncAction } from './actionCreators'; - -const GroupActions = {}; - -/** - * Creates an action thunk that will do an asynchronous request to fetch - * the groups to which a user is joined. - * - * @param {MatrixClient} matrixClient the matrix client to query. - * @returns {function} an action thunk that will dispatch actions - * indicating the status of the request. - * @see asyncAction - */ -GroupActions.fetchJoinedGroups = function(matrixClient) { - return asyncAction('GroupActions.fetchJoinedGroups', () => matrixClient.getJoinedGroups()); -}; - -export default GroupActions; diff --git a/src/actions/GroupActions.ts b/src/actions/GroupActions.ts new file mode 100644 index 0000000000..81470d1221 --- /dev/null +++ b/src/actions/GroupActions.ts @@ -0,0 +1,34 @@ +/* +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 { asyncAction } from './actionCreators'; +import { AsyncActionPayload } from "../dispatcher/payloads"; +import { MatrixClient } from "matrix-js-sdk/src/client"; + +export default class GroupActions { + /** + * Creates an action thunk that will do an asynchronous request to fetch + * the groups to which a user is joined. + * + * @param {MatrixClient} matrixClient the matrix client to query. + * @returns {AsyncActionPayload} An async action payload. + * @see asyncAction + */ + public static fetchJoinedGroups(matrixClient: MatrixClient): AsyncActionPayload { + return asyncAction('GroupActions.fetchJoinedGroups', () => matrixClient.getJoinedGroups(), null); + } +} diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js index c89ec44435..93a4fcf07c 100644 --- a/src/actions/MatrixActionCreators.js +++ b/src/actions/MatrixActionCreators.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import dis from '../dispatcher'; +import dis from '../dispatcher/dispatcher'; // TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events // become dispatches in the same place. diff --git a/src/actions/RoomListActions.js b/src/actions/RoomListActions.js deleted file mode 100644 index 10a3848dda..0000000000 --- a/src/actions/RoomListActions.js +++ /dev/null @@ -1,145 +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 { asyncAction } from './actionCreators'; -import RoomListStore, {TAG_DM} from '../stores/RoomListStore'; -import Modal from '../Modal'; -import * as Rooms from '../Rooms'; -import { _t } from '../languageHandler'; -import * as sdk from '../index'; - -const RoomListActions = {}; - -/** - * Creates an action thunk that will do an asynchronous request to - * tag room. - * - * @param {MatrixClient} matrixClient the matrix client to set the - * account data on. - * @param {Room} room the room to tag. - * @param {string} oldTag the tag to remove (unless oldTag ==== newTag) - * @param {string} newTag the tag with which to tag the room. - * @param {?number} oldIndex the previous position of the room in the - * list of rooms. - * @param {?number} newIndex the new position of the room in the list - * of rooms. - * @returns {function} an action thunk. - * @see asyncAction - */ -RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, newIndex) { - let metaData = null; - - // Is the tag ordered manually? - if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { - const lists = RoomListStore.getRoomLists(); - const newList = [...lists[newTag]]; - - newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order); - - // If the room was moved "down" (increasing index) in the same list we - // need to use the orders of the tiles with indices shifted by +1 - const offset = ( - newTag === oldTag && oldIndex < newIndex - ) ? 1 : 0; - - const indexBefore = offset + newIndex - 1; - const indexAfter = offset + newIndex; - - const prevOrder = indexBefore <= 0 ? - 0 : newList[indexBefore].tags[newTag].order; - const nextOrder = indexAfter >= newList.length ? - 1 : newList[indexAfter].tags[newTag].order; - - metaData = { - order: (prevOrder + nextOrder) / 2.0, - }; - } - - return asyncAction('RoomListActions.tagRoom', () => { - const promises = []; - const roomId = room.roomId; - - // Evil hack to get DMs behaving - if ((oldTag === undefined && newTag === TAG_DM) || - (oldTag === TAG_DM && newTag === undefined) - ) { - return Rooms.guessAndSetDMRoom( - room, newTag === TAG_DM, - ).catch((err) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to set direct chat tag " + err); - Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, { - title: _t('Failed to set direct chat tag'), - description: ((err && err.message) ? err.message : _t('Operation failed')), - }); - }); - } - - const hasChangedSubLists = oldTag !== newTag; - - // More evilness: We will still be dealing with moving to favourites/low prio, - // but we avoid ever doing a request with TAG_DM. - // - // if we moved lists, remove the old tag - if (oldTag && oldTag !== TAG_DM && - hasChangedSubLists - ) { - const promiseToDelete = matrixClient.deleteRoomTag( - roomId, oldTag, - ).catch(function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to remove tag " + oldTag + " from room: " + err); - Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { - title: _t('Failed to remove tag %(tagName)s from room', {tagName: oldTag}), - description: ((err && err.message) ? err.message : _t('Operation failed')), - }); - }); - - promises.push(promiseToDelete); - } - - // if we moved lists or the ordering changed, add the new tag - if (newTag && newTag !== TAG_DM && - (hasChangedSubLists || metaData) - ) { - // metaData is the body of the PUT to set the tag, so it must - // at least be an empty object. - metaData = metaData || {}; - - const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to add tag " + newTag + " to room: " + err); - Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { - title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}), - description: ((err && err.message) ? err.message : _t('Operation failed')), - }); - - throw err; - }); - - promises.push(promiseToAdd); - } - - return Promise.all(promises); - }, () => { - // For an optimistic update - return { - room, oldTag, newTag, metaData, - }; - }); -}; - -export default RoomListActions; diff --git a/src/actions/RoomListActions.ts b/src/actions/RoomListActions.ts new file mode 100644 index 0000000000..88946ee26f --- /dev/null +++ b/src/actions/RoomListActions.ts @@ -0,0 +1,153 @@ +/* +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 { asyncAction } from './actionCreators'; +import Modal from '../Modal'; +import * as Rooms from '../Rooms'; +import { _t } from '../languageHandler'; +import * as sdk from '../index'; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { AsyncActionPayload } from "../dispatcher/payloads"; +import RoomListStore from "../stores/room-list/RoomListStore"; +import { SortAlgorithm } from "../stores/room-list/algorithms/models"; +import { DefaultTagID } from "../stores/room-list/models"; + +export default class RoomListActions { + /** + * Creates an action thunk that will do an asynchronous request to + * tag room. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @param {Room} room the room to tag. + * @param {string} oldTag the tag to remove (unless oldTag ==== newTag) + * @param {string} newTag the tag with which to tag the room. + * @param {?number} oldIndex the previous position of the room in the + * list of rooms. + * @param {?number} newIndex the new position of the room in the list + * of rooms. + * @returns {AsyncActionPayload} an async action payload + * @see asyncAction + */ + public static tagRoom( + matrixClient: MatrixClient, room: Room, + oldTag: string, newTag: string, + oldIndex: number | null, newIndex: number | null, + ): AsyncActionPayload { + let metaData = null; + + // Is the tag ordered manually? + const store = RoomListStore.instance; + if (newTag && store.getTagSorting(newTag) === SortAlgorithm.Manual) { + const newList = [...store.orderedLists[newTag]]; + + newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order); + + // If the room was moved "down" (increasing index) in the same list we + // need to use the orders of the tiles with indices shifted by +1 + const offset = ( + newTag === oldTag && oldIndex < newIndex + ) ? 1 : 0; + + const indexBefore = offset + newIndex - 1; + const indexAfter = offset + newIndex; + + const prevOrder = indexBefore <= 0 ? + 0 : newList[indexBefore].tags[newTag].order; + const nextOrder = indexAfter >= newList.length ? + 1 : newList[indexAfter].tags[newTag].order; + + metaData = { + order: (prevOrder + nextOrder) / 2.0, + }; + } + + return asyncAction('RoomListActions.tagRoom', () => { + const promises = []; + const roomId = room.roomId; + + // Evil hack to get DMs behaving + if ((oldTag === undefined && newTag === DefaultTagID.DM) || + (oldTag === DefaultTagID.DM && newTag === undefined) + ) { + return Rooms.guessAndSetDMRoom( + room, newTag === DefaultTagID.DM, + ).catch((err) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to set direct chat tag " + err); + Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, { + title: _t('Failed to set direct chat tag'), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + }); + } + + const hasChangedSubLists = oldTag !== newTag; + + // More evilness: We will still be dealing with moving to favourites/low prio, + // but we avoid ever doing a request with TAG_DM. + // + // if we moved lists, remove the old tag + if (oldTag && oldTag !== DefaultTagID.DM && + hasChangedSubLists + ) { + const promiseToDelete = matrixClient.deleteRoomTag( + roomId, oldTag, + ).catch(function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to remove tag " + oldTag + " from room: " + err); + Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { + title: _t('Failed to remove tag %(tagName)s from room', {tagName: oldTag}), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + }); + + promises.push(promiseToDelete); + } + + // if we moved lists or the ordering changed, add the new tag + if (newTag && newTag !== DefaultTagID.DM && + (hasChangedSubLists || metaData) + ) { + // metaData is the body of the PUT to set the tag, so it must + // at least be an empty object. + metaData = metaData || {}; + + const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to add tag " + newTag + " to room: " + err); + Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { + title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + + throw err; + }); + + promises.push(promiseToAdd); + } + + return Promise.all(promises); + }, () => { + // For an optimistic update + return { + room, oldTag, newTag, metaData, + }; + }); + } +} diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js deleted file mode 100644 index a257ff16d8..0000000000 --- a/src/actions/TagOrderActions.js +++ /dev/null @@ -1,109 +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 Analytics from '../Analytics'; -import { asyncAction } from './actionCreators'; -import TagOrderStore from '../stores/TagOrderStore'; - -const TagOrderActions = {}; - -/** - * Creates an action thunk that will do an asynchronous request to - * move a tag in TagOrderStore to destinationIx. - * - * @param {MatrixClient} matrixClient the matrix client to set the - * account data on. - * @param {string} tag the tag to move. - * @param {number} destinationIx the new position of the tag. - * @returns {function} an action thunk that will dispatch actions - * indicating the status of the request. - * @see asyncAction - */ -TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) { - // Only commit tags if the state is ready, i.e. not null - let tags = TagOrderStore.getOrderedTags(); - let removedTags = TagOrderStore.getRemovedTagsAccountData() || []; - if (!tags) { - return; - } - - tags = tags.filter((t) => t !== tag); - tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)]; - - removedTags = removedTags.filter((t) => t !== tag); - - const storeId = TagOrderStore.getStoreId(); - - return asyncAction('TagOrderActions.moveTag', () => { - Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); - return matrixClient.setAccountData( - 'im.vector.web.tag_ordering', - {tags, removedTags, _storeId: storeId}, - ); - }, () => { - // For an optimistic update - return {tags, removedTags}; - }); -}; - -/** - * Creates an action thunk that will do an asynchronous request to - * label a tag as removed in im.vector.web.tag_ordering account data. - * - * The reason this is implemented with new state `removedTags` is that - * we incrementally and initially populate `tags` with groups that - * have been joined. If we remove a group from `tags`, it will just - * get added (as it looks like a group we've recently joined). - * - * NB: If we ever support adding of tags (which is planned), we should - * take special care to remove the tag from `removedTags` when we add - * it. - * - * @param {MatrixClient} matrixClient the matrix client to set the - * account data on. - * @param {string} tag the tag to remove. - * @returns {function} an action thunk that will dispatch actions - * indicating the status of the request. - * @see asyncAction - */ -TagOrderActions.removeTag = function(matrixClient, tag) { - // Don't change tags, just removedTags - const tags = TagOrderStore.getOrderedTags(); - const removedTags = TagOrderStore.getRemovedTagsAccountData() || []; - - if (removedTags.includes(tag)) { - // Return a thunk that doesn't do anything, we don't even need - // an asynchronous action here, the tag is already removed. - return () => {}; - } - - removedTags.push(tag); - - const storeId = TagOrderStore.getStoreId(); - - return asyncAction('TagOrderActions.removeTag', () => { - Analytics.trackEvent('TagOrderActions', 'removeTag'); - return matrixClient.setAccountData( - 'im.vector.web.tag_ordering', - {tags, removedTags, _storeId: storeId}, - ); - }, () => { - // For an optimistic update - return {removedTags}; - }); -}; - -export default TagOrderActions; diff --git a/src/actions/TagOrderActions.ts b/src/actions/TagOrderActions.ts new file mode 100644 index 0000000000..c203172874 --- /dev/null +++ b/src/actions/TagOrderActions.ts @@ -0,0 +1,110 @@ +/* +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 Analytics from '../Analytics'; +import { asyncAction } from './actionCreators'; +import TagOrderStore from '../stores/TagOrderStore'; +import { AsyncActionPayload } from "../dispatcher/payloads"; +import { MatrixClient } from "matrix-js-sdk/src/client"; + +export default class TagOrderActions { + /** + * Creates an action thunk that will do an asynchronous request to + * move a tag in TagOrderStore to destinationIx. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @param {string} tag the tag to move. + * @param {number} destinationIx the new position of the tag. + * @returns {AsyncActionPayload} an async action payload that will + * dispatch actions indicating the status of the request. + * @see asyncAction + */ + public static moveTag(matrixClient: MatrixClient, tag: string, destinationIx: number): AsyncActionPayload { + // Only commit tags if the state is ready, i.e. not null + let tags = TagOrderStore.getOrderedTags(); + let removedTags = TagOrderStore.getRemovedTagsAccountData() || []; + if (!tags) { + return; + } + + tags = tags.filter((t) => t !== tag); + tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)]; + + removedTags = removedTags.filter((t) => t !== tag); + + const storeId = TagOrderStore.getStoreId(); + + return asyncAction('TagOrderActions.moveTag', () => { + Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); + return matrixClient.setAccountData( + 'im.vector.web.tag_ordering', + {tags, removedTags, _storeId: storeId}, + ); + }, () => { + // For an optimistic update + return {tags, removedTags}; + }); + } + + /** + * Creates an action thunk that will do an asynchronous request to + * label a tag as removed in im.vector.web.tag_ordering account data. + * + * The reason this is implemented with new state `removedTags` is that + * we incrementally and initially populate `tags` with groups that + * have been joined. If we remove a group from `tags`, it will just + * get added (as it looks like a group we've recently joined). + * + * NB: If we ever support adding of tags (which is planned), we should + * take special care to remove the tag from `removedTags` when we add + * it. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @param {string} tag the tag to remove. + * @returns {function} an async action payload that will dispatch + * actions indicating the status of the request. + * @see asyncAction + */ + public static removeTag(matrixClient: MatrixClient, tag: string): AsyncActionPayload { + // Don't change tags, just removedTags + const tags = TagOrderStore.getOrderedTags(); + const removedTags = TagOrderStore.getRemovedTagsAccountData() || []; + + if (removedTags.includes(tag)) { + // Return a thunk that doesn't do anything, we don't even need + // an asynchronous action here, the tag is already removed. + return new AsyncActionPayload(() => {}); + } + + removedTags.push(tag); + + const storeId = TagOrderStore.getStoreId(); + + return asyncAction('TagOrderActions.removeTag', () => { + Analytics.trackEvent('TagOrderActions', 'removeTag'); + return matrixClient.setAccountData( + 'im.vector.web.tag_ordering', + {tags, removedTags, _storeId: storeId}, + ); + }, () => { + // For an optimistic update + return {removedTags}; + }); + } +} diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.ts similarity index 76% rename from src/actions/actionCreators.js rename to src/actions/actionCreators.ts index 967ce609e7..c789e3cd07 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.ts @@ -1,5 +1,6 @@ /* 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. @@ -14,6 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { AsyncActionPayload } from "../dispatcher/payloads"; + /** * Create an action thunk that will dispatch actions indicating the current * status of the Promise returned by fn. @@ -25,9 +28,9 @@ limitations under the License. * @param {function?} pendingFn a function that returns an object to assign * to the `request` key of the ${id}.pending * payload. - * @returns {function} an action thunk - a function that uses its single - * argument as a dispatch function to dispatch the - * following actions: + * @returns {AsyncActionPayload} an async action payload. Includes a function + * that uses its single argument as a dispatch function + * to dispatch the following actions: * `${id}.pending` and either * `${id}.success` or * `${id}.failure`. @@ -41,12 +44,11 @@ limitations under the License. * result is the result of the promise returned by * `fn`. */ -export function asyncAction(id, fn, pendingFn) { - return (dispatch) => { +export function asyncAction(id: string, fn: () => Promise, pendingFn: () => any | null): AsyncActionPayload { + const helper = (dispatch) => { dispatch({ action: id + '.pending', - request: - typeof pendingFn === 'function' ? pendingFn() : undefined, + request: typeof pendingFn === 'function' ? pendingFn() : undefined, }); fn().then((result) => { dispatch({action: id + '.success', result}); @@ -54,4 +56,5 @@ export function asyncAction(id, fn, pendingFn) { dispatch({action: id + '.failure', err}); }); }; + return new AsyncActionPayload(helper); } diff --git a/src/async-components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js deleted file mode 100644 index 9eb4439816..0000000000 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ /dev/null @@ -1,206 +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 createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import {Key} from "../../../Keyboard"; -import * as sdk from "../../../index"; - -// XXX: This component is not cross-signing aware. -// https://github.com/vector-im/riot-web/issues/11752 tracks either updating this -// component or taking it out to pasture. -export default createReactClass({ - displayName: 'EncryptedEventDialog', - - propTypes: { - event: PropTypes.object.isRequired, - onFinished: PropTypes.func.isRequired, - }, - - getInitialState: function() { - return { device: null }; - }, - - componentDidMount: function() { - this._unmounted = false; - const client = MatrixClientPeg.get(); - - // first try to load the device from our store. - // - this.refreshDevice().then((dev) => { - if (dev) { - return dev; - } - - // tell the client to try to refresh the device list for this user - return client.downloadKeys([this.props.event.getSender()], true).then(() => { - return this.refreshDevice(); - }); - }).then((dev) => { - if (this._unmounted) { - return; - } - - this.setState({ device: dev }); - client.on("deviceVerificationChanged", this.onDeviceVerificationChanged); - }, (err)=>{ - console.log("Error downloading devices", err); - }); - }, - - componentWillUnmount: function() { - this._unmounted = true; - const client = MatrixClientPeg.get(); - if (client) { - client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); - } - }, - - refreshDevice: function() { - // Promise.resolve to handle transition from static result to promise; can be removed - // in future - return Promise.resolve(MatrixClientPeg.get().getEventSenderDeviceInfo(this.props.event)); - }, - - onDeviceVerificationChanged: function(userId, device) { - if (userId === this.props.event.getSender()) { - this.refreshDevice().then((dev) => { - this.setState({ device: dev }); - }); - } - }, - - onKeyDown: function(e) { - if (e.key === Key.ESCAPE) { - e.stopPropagation(); - e.preventDefault(); - this.props.onFinished(false); - } - }, - - _renderDeviceInfo: function() { - const device = this.state.device; - if (!device) { - return ({ _t('unknown device') }); - } - - let verificationStatus = ({ _t('NOT verified') }); - if (device.isBlocked()) { - verificationStatus = ({ _t('Blacklisted') }); - } else if (device.isVerified()) { - verificationStatus = _t('verified'); - } - - return ( -
{ _t(customVariables[row[0]].expl) }{_t( + customVariables[row[0]].expl, + customVariables[row[0]].getTextVariables ? + customVariables[row[0]].getTextVariables() : + null, + )}{ row[1] }
- - - - - - - - - - - - - - - - - - -
{ _t('Name') }{ device.getDisplayName() }
{ _t('Device ID') }{ device.deviceId }
{ _t('Verification') }{ verificationStatus }
{ _t('Ed25519 fingerprint') }{ device.getFingerprint() }
- ); - }, - - _renderEventInfo: function() { - const event = this.props.event; - - return ( - - - - - - - - - - - - - - - - - - - { - event.getContent().msgtype === 'm.bad.encrypted' ? ( - - - - - ) : null - } - - - - - -
{ _t('User ID') }{ event.getSender() }
{ _t('Curve25519 identity key') }{ event.getSenderKey() || { _t('none') } }
{ _t('Claimed Ed25519 fingerprint key') }{ event.getKeysClaimed().ed25519 || { _t('none') } }
{ _t('Algorithm') }{ event.getWireContent().algorithm || { _t('unencrypted') } }
{ _t('Decryption error') }{ event.getContent().body }
{ _t('Session ID') }{ event.getWireContent().session_id || { _t('none') } }
- ); - }, - - render: function() { - const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons'); - - let buttons = null; - if (this.state.device) { - buttons = ( - - ); - } - - return ( -
-
- { _t('End-to-end encryption information') } -
-
-

{ _t('Event information') }

- { this._renderEventInfo() } - -

{ _t('Sender session information') }

- { this._renderDeviceInfo() } -
-
- - { buttons } -
-
- ); - }, -}); diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index 7ec9da39de..a92578a547 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -84,7 +84,7 @@ export default createReactClass({ const blob = new Blob([f], { type: 'text/plain;charset=us-ascii', }); - FileSaver.saveAs(blob, 'riot-keys.txt'); + FileSaver.saveAs(blob, 'element-keys.txt'); this.props.onFinished(true); }).catch((e) => { console.error("Error exporting e2e keys:", e); diff --git a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js index 120b086ef6..de50feaedb 100644 --- a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js @@ -17,11 +17,13 @@ limitations under the License. import React from 'react'; import * as sdk from '../../../../index'; import PropTypes from 'prop-types'; -import dis from "../../../../dispatcher"; +import dis from "../../../../dispatcher/dispatcher"; import { _t } from '../../../../languageHandler'; -import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore"; +import SettingsStore from "../../../../settings/SettingsStore"; import EventIndexPeg from "../../../../indexing/EventIndexPeg"; +import {Action} from "../../../../dispatcher/actions"; +import {SettingLevel} from "../../../../settings/SettingLevel"; /* * Allows the user to disable the Event Index. @@ -47,7 +49,7 @@ export default class DisableEventIndexDialog extends React.Component { await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false); await EventIndexPeg.deleteEventIndex(); this.props.onFinished(); - dis.dispatch({ action: 'view_user_settings' }); + dis.fire(Action.ViewUserSettings); } render() { diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js index bb2cf7f0b8..be3368b87b 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js @@ -18,11 +18,13 @@ import React from 'react'; import * as sdk from '../../../../index'; import PropTypes from 'prop-types'; import { _t } from '../../../../languageHandler'; -import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore"; +import SdkConfig from '../../../../SdkConfig'; +import SettingsStore from "../../../../settings/SettingsStore"; import Modal from '../../../../Modal'; import {formatBytes, formatCountLong} from "../../../../utils/FormattingUtils"; import EventIndexPeg from "../../../../indexing/EventIndexPeg"; +import {SettingLevel} from "../../../../settings/SettingLevel"; /* * Allows the user to introspect the event index state and disable it. @@ -134,8 +136,10 @@ export default class ManageEventIndexDialog extends React.Component { }; render() { - let crawlerState; + const brand = SdkConfig.get().brand; + const Field = sdk.getComponent('views.elements.Field'); + let crawlerState; if (this.state.currentRoom === null) { crawlerState = _t("Not currently indexing messages for any room."); } else { @@ -144,17 +148,15 @@ export default class ManageEventIndexDialog extends React.Component { ); } - const Field = sdk.getComponent('views.elements.Field'); - const doneRooms = Math.max(0, (this.state.roomCount - this.state.crawlingRoomsCount)); const eventIndexingSettings = (
- { - _t( "Riot is securely caching encrypted messages locally for them " + - "to appear in search results:", - ) - } + {_t( + "%(brand)s is securely caching encrypted messages locally for them " + + "to appear in search results:", + { brand }, + )}
{crawlerState}
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}
diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index e4e39400f6..c3aef9109a 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -15,17 +15,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {createRef} from 'react'; import FileSaver from 'file-saver'; import * as sdk from '../../../../index'; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import PropTypes from 'prop-types'; -import { scorePassword } from '../../../../utils/PasswordScorer'; -import { _t } from '../../../../languageHandler'; +import {_t, _td} from '../../../../languageHandler'; import { accessSecretStorage } from '../../../../CrossSigningManager'; -import SettingsStore from '../../../../settings/SettingsStore'; import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; import {copyNode} from "../../../../utils/strings"; +import PassphraseField from "../../../../components/views/auth/PassphraseField"; const PHASE_PASSPHRASE = 0; const PHASE_PASSPHRASE_CONFIRM = 1; @@ -36,7 +35,6 @@ const PHASE_DONE = 5; const PHASE_OPTOUT_CONFIRM = 6; const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. -const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms. /* * Walks the user through the process of creating an e2e key backup @@ -52,25 +50,23 @@ export default class CreateKeyBackupDialog extends React.PureComponent { this._recoveryKeyNode = null; this._keyBackupInfo = null; - this._setZxcvbnResultTimeout = null; this.state = { secureSecretStorage: null, phase: PHASE_PASSPHRASE, passPhrase: '', + passPhraseValid: false, passPhraseConfirm: '', copied: false, downloaded: false, - zxcvbnResult: null, }; + + this._passphraseField = createRef(); } async componentDidMount() { const cli = MatrixClientPeg.get(); - const secureSecretStorage = ( - SettingsStore.getValue("feature_cross_signing") && - await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing") - ); + const secureSecretStorage = await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"); this.setState({ secureSecretStorage }); // If we're using secret storage, skip ahead to the backing up step, as @@ -81,12 +77,6 @@ export default class CreateKeyBackupDialog extends React.PureComponent { } } - componentWillUnmount() { - if (this._setZxcvbnResultTimeout !== null) { - clearTimeout(this._setZxcvbnResultTimeout); - } - } - _collectRecoveryKeyNode = (n) => { this._recoveryKeyNode = n; } @@ -180,22 +170,16 @@ export default class CreateKeyBackupDialog extends React.PureComponent { _onPassPhraseNextClick = async (e) => { e.preventDefault(); + if (!this._passphraseField.current) return; // unmounting - // If we're waiting for the timeout before updating the result at this point, - // skip ahead and do it now, otherwise we'll deny the attempt to proceed - // even if the user entered a valid passphrase - if (this._setZxcvbnResultTimeout !== null) { - clearTimeout(this._setZxcvbnResultTimeout); - this._setZxcvbnResultTimeout = null; - await new Promise((resolve) => { - this.setState({ - zxcvbnResult: scorePassword(this.state.passPhrase), - }, resolve); - }); - } - if (this._passPhraseIsValid()) { - this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); + await this._passphraseField.current.validate({ allowEmpty: false }); + if (!this._passphraseField.current.state.valid) { + this._passphraseField.current.focus(); + this._passphraseField.current.validate({ allowEmpty: false, focused: true }); + return; } + + this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); }; _onPassPhraseConfirmNextClick = async (e) => { @@ -214,9 +198,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent { _onSetAgainClick = () => { this.setState({ passPhrase: '', + passPhraseValid: false, passPhraseConfirm: '', phase: PHASE_PASSPHRASE, - zxcvbnResult: null, }); } @@ -226,23 +210,16 @@ export default class CreateKeyBackupDialog extends React.PureComponent { }); } + _onPassPhraseValidate = (result) => { + this.setState({ + passPhraseValid: result.valid, + }); + }; + _onPassPhraseChange = (e) => { this.setState({ passPhrase: e.target.value, }); - - if (this._setZxcvbnResultTimeout !== null) { - clearTimeout(this._setZxcvbnResultTimeout); - } - this._setZxcvbnResultTimeout = setTimeout(() => { - this._setZxcvbnResultTimeout = null; - this.setState({ - // precompute this and keep it in state: zxcvbn is fast but - // we use it in a couple of different places so no point recomputing - // it unnecessarily. - zxcvbnResult: scorePassword(this.state.passPhrase), - }); - }, PASSPHRASE_FEEDBACK_DELAY); } _onPassPhraseConfirmChange = (e) => { @@ -251,35 +228,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent { }); } - _passPhraseIsValid() { - return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE; - } - _renderPhasePassPhrase() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - let strengthMeter; - let helpText; - if (this.state.zxcvbnResult) { - if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) { - helpText = _t("Great! This recovery passphrase looks strong enough."); - } else { - const suggestions = []; - for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) { - suggestions.push(
{this.state.zxcvbnResult.feedback.suggestions[i]}
); - } - const suggestionBlock =
{suggestions.length > 0 ? suggestions : _t("Keep going...")}
; - - helpText =
- {this.state.zxcvbnResult.feedback.warning} - {suggestionBlock} -
; - } - strengthMeter =
- -
; - } - return

{_t( "Warning: You should only set up key backup from a trusted computer.", {}, @@ -293,17 +244,19 @@ export default class CreateKeyBackupDialog extends React.PureComponent {

- -
- {strengthMeter} - {helpText} -
@@ -311,7 +264,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { primaryButton={_t('Next')} onPrimaryButtonClick={this._onPassPhraseNextClick} hasCancel={false} - disabled={!this._passPhraseIsValid()} + disabled={!this.state.passPhraseValid} />
@@ -327,17 +280,20 @@ export default class CreateKeyBackupDialog extends React.PureComponent { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); let matchText; + let changeText; if (this.state.passPhraseConfirm === this.state.passPhrase) { matchText = _t("That matches!"); + changeText = _t("Use a different passphrase?"); } else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) { // only tell them they're wrong if they've actually gone wrong. - // Security concious readers will note that if you left riot-web unattended + // Security concious readers will note that if you left element-web unattended // on this screen, this would make it easy for a malicious person to guess // your passphrase one letter at a time, but they could get this faster by // just opening the browser's developer tools and reading it. // Note that not having typed anything at all will not hit this clause and // fall through so empty box === no hint. matchText = _t("That doesn't match."); + changeText = _t("Go back to set it again."); } let passPhraseMatch = null; @@ -346,7 +302,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
{matchText}
- {_t("Go back to set it again.")} + {changeText}
; diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js index 9e2264a960..74552a5c08 100644 --- a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -19,9 +19,10 @@ import React from "react"; import PropTypes from "prop-types"; import * as sdk from "../../../../index"; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; -import dis from "../../../../dispatcher"; +import dis from "../../../../dispatcher/dispatcher"; import { _t } from "../../../../languageHandler"; import Modal from "../../../../Modal"; +import {Action} from "../../../../dispatcher/actions"; export default class NewRecoveryMethodDialog extends React.PureComponent { static propTypes = { @@ -36,7 +37,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { onGoToSettingsClick = () => { this.props.onFinished(); - dis.dispatch({ action: 'view_user_settings' }); + dis.fire(Action.ViewUserSettings); } onSetupClick = async () => { diff --git a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js index c5222dafd5..cda353e717 100644 --- a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js +++ b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js @@ -18,9 +18,10 @@ limitations under the License. import React from "react"; import PropTypes from "prop-types"; import * as sdk from "../../../../index"; -import dis from "../../../../dispatcher"; +import dis from "../../../../dispatcher/dispatcher"; import { _t } from "../../../../languageHandler"; import Modal from "../../../../Modal"; +import {Action} from "../../../../dispatcher/actions"; export default class RecoveryMethodRemovedDialog extends React.PureComponent { static propTypes = { @@ -29,7 +30,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent { onGoToSettingsClick = () => { this.props.onFinished(); - dis.dispatch({ action: 'view_user_settings' }); + dis.fire(Action.ViewUserSettings); } onSetupClick = () => { diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 2c05f231e7..53b3033330 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -15,30 +15,37 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import * as sdk from '../../../../index'; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; -import { scorePassword } from '../../../../utils/PasswordScorer'; import FileSaver from 'file-saver'; -import { _t } from '../../../../languageHandler'; +import {_t, _td} from '../../../../languageHandler'; import Modal from '../../../../Modal'; import { promptForBackupPassphrase } from '../../../../CrossSigningManager'; import {copyNode} from "../../../../utils/strings"; +import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents"; +import PassphraseField from "../../../../components/views/auth/PassphraseField"; +import StyledRadioButton from '../../../../components/views/elements/StyledRadioButton'; +import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; +import DialogButtons from "../../../../components/views/elements/DialogButtons"; +import InlineSpinner from "../../../../components/views/elements/InlineSpinner"; const PHASE_LOADING = 0; const PHASE_LOADERROR = 1; -const PHASE_MIGRATE = 2; -const PHASE_PASSPHRASE = 3; -const PHASE_PASSPHRASE_CONFIRM = 4; -const PHASE_SHOWKEY = 5; -const PHASE_KEEPITSAFE = 6; -const PHASE_STORING = 7; -const PHASE_DONE = 8; -const PHASE_CONFIRM_SKIP = 9; +const PHASE_CHOOSE_KEY_PASSPHRASE = 2; +const PHASE_MIGRATE = 3; +const PHASE_PASSPHRASE = 4; +const PHASE_PASSPHRASE_CONFIRM = 5; +const PHASE_SHOWKEY = 6; +const PHASE_STORING = 8; +const PHASE_CONFIRM_SKIP = 10; const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. -const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms. + +// these end up as strings from being values in the radio buttons, so just use strings +const CREATE_STORAGE_OPTION_KEY = 'key'; +const CREATE_STORAGE_OPTION_PASSPHRASE = 'passphrase'; /* * Walks the user through the process of creating a passphrase to guard Secure @@ -61,16 +68,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this._recoveryKey = null; this._recoveryKeyNode = null; - this._setZxcvbnResultTimeout = null; this._backupKey = null; this.state = { phase: PHASE_LOADING, passPhrase: '', + passPhraseValid: false, passPhraseConfirm: '', copied: false, downloaded: false, - zxcvbnResult: null, + setPassphrase: false, backupInfo: null, backupSigStatus: null, // does the server offer a UI auth flow with just m.login.password @@ -78,10 +85,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent { canUploadKeysWithPasswordOnly: null, accountPassword: props.accountPassword || "", accountPasswordCorrect: null, - // status of the key backup toggle switch - useKeyBackup: true, + + passPhraseKeySelected: CREATE_STORAGE_OPTION_KEY, }; + this._passphraseField = createRef(); + this._fetchBackupInfo(); if (this.state.accountPassword) { // If we have an account password in memory, let's simplify and @@ -98,9 +107,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { componentWillUnmount() { MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange); - if (this._setZxcvbnResultTimeout !== null) { - clearTimeout(this._setZxcvbnResultTimeout); - } } async _fetchBackupInfo() { @@ -112,7 +118,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { ); const { force } = this.props; - const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_PASSPHRASE; + const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_CHOOSE_KEY_PASSPHRASE; this.setState({ phase, @@ -154,14 +160,33 @@ export default class CreateSecretStorageDialog extends React.PureComponent { if (this.state.phase === PHASE_MIGRATE) this._fetchBackupInfo(); } + _onKeyPassphraseChange = e => { + this.setState({ + passPhraseKeySelected: e.target.value, + }); + } + _collectRecoveryKeyNode = (n) => { this._recoveryKeyNode = n; } - _onUseKeyBackupChange = (enabled) => { - this.setState({ - useKeyBackup: enabled, - }); + _onChooseKeyPassphraseFormSubmit = async () => { + if (this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY) { + this._recoveryKey = + await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); + this.setState({ + copied: false, + downloaded: false, + setPassphrase: false, + phase: PHASE_SHOWKEY, + }); + } else { + this.setState({ + copied: false, + downloaded: false, + phase: PHASE_PASSPHRASE, + }); + } } _onMigrateFormSubmit = (e) => { @@ -178,7 +203,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { if (successful) { this.setState({ copied: true, - phase: PHASE_KEEPITSAFE, }); } } @@ -191,7 +215,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.setState({ downloaded: true, - phase: PHASE_KEEPITSAFE, }); } @@ -203,18 +226,39 @@ export default class CreateSecretStorageDialog extends React.PureComponent { type: 'm.id.user', user: MatrixClientPeg.get().getUserId(), }, - // https://github.com/matrix-org/synapse/issues/5665 + // TODO: Remove `user` once servers support proper UIA + // See https://github.com/matrix-org/synapse/issues/5665 user: MatrixClientPeg.get().getUserId(), password: this.state.accountPassword, }); } else { const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); + + const dialogAesthetics = { + [SSOAuthEntry.PHASE_PREAUTH]: { + title: _t("Use Single Sign On to continue"), + body: _t("To continue, use Single Sign On to prove your identity."), + continueText: _t("Single Sign On"), + continueKind: "primary", + }, + [SSOAuthEntry.PHASE_POSTAUTH]: { + title: _t("Confirm encryption setup"), + body: _t("Click the button below to confirm setting up encryption."), + continueText: _t("Confirm"), + continueKind: "primary", + }, + }; + const { finished } = Modal.createTrackedDialog( 'Cross-signing keys dialog', '', InteractiveAuthDialog, { title: _t("Setting up keys"), matrixClient: MatrixClientPeg.get(), makeRequest, + aestheticsForStagePhases: { + [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, + [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, + }, }, ); const [confirmed] = await finished; @@ -236,6 +280,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { try { if (force) { + console.log("Forcing secret storage reset"); // log something so we can debug this later await cli.bootstrapSecretStorage({ authUploadDeviceSigningKeys: this._doBootstrapUIAuth, createSecretStorageKey: async () => this._recoveryKey, @@ -247,7 +292,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { authUploadDeviceSigningKeys: this._doBootstrapUIAuth, createSecretStorageKey: async () => this._recoveryKey, keyBackupInfo: this.state.backupInfo, - setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup, + setupNewKeyBackup: !this.state.backupInfo, getKeyBackupPassphrase: () => { // We may already have the backup key if we earlier went // through the restore backup path, so pass it along @@ -259,9 +304,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }, }); } - this.setState({ - phase: PHASE_DONE, - }); + this.props.onFinished(true); } catch (e) { if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) { this.setState({ @@ -315,42 +358,30 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this._fetchBackupInfo(); } - _onSkipSetupClick = () => { + _onShowKeyContinueClick = () => { + this._bootstrapSecretStorage(); + } + + _onCancelClick = () => { this.setState({phase: PHASE_CONFIRM_SKIP}); } - _onSetUpClick = () => { - this.setState({phase: PHASE_PASSPHRASE}); - } - - _onSkipPassPhraseClick = async () => { - this._recoveryKey = - await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); - this.setState({ - copied: false, - downloaded: false, - phase: PHASE_SHOWKEY, - }); + _onGoBackClick = () => { + this.setState({phase: PHASE_CHOOSE_KEY_PASSPHRASE}); } _onPassPhraseNextClick = async (e) => { e.preventDefault(); + if (!this._passphraseField.current) return; // unmounting - // If we're waiting for the timeout before updating the result at this point, - // skip ahead and do it now, otherwise we'll deny the attempt to proceed - // even if the user entered a valid passphrase - if (this._setZxcvbnResultTimeout !== null) { - clearTimeout(this._setZxcvbnResultTimeout); - this._setZxcvbnResultTimeout = null; - await new Promise((resolve) => { - this.setState({ - zxcvbnResult: scorePassword(this.state.passPhrase), - }, resolve); - }); - } - if (this._passPhraseIsValid()) { - this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); + await this._passphraseField.current.validate({ allowEmpty: false }); + if (!this._passphraseField.current.state.valid) { + this._passphraseField.current.focus(); + this._passphraseField.current.validate({ allowEmpty: false, focused: true }); + return; } + + this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); }; _onPassPhraseConfirmNextClick = async (e) => { @@ -363,6 +394,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.setState({ copied: false, downloaded: false, + setPassphrase: true, phase: PHASE_SHOWKEY, }); } @@ -370,35 +402,22 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _onSetAgainClick = () => { this.setState({ passPhrase: '', + passPhraseValid: false, passPhraseConfirm: '', phase: PHASE_PASSPHRASE, - zxcvbnResult: null, }); } - _onKeepItSafeBackClick = () => { + _onPassPhraseValidate = (result) => { this.setState({ - phase: PHASE_SHOWKEY, + passPhraseValid: result.valid, }); - } + }; _onPassPhraseChange = (e) => { this.setState({ passPhrase: e.target.value, }); - - if (this._setZxcvbnResultTimeout !== null) { - clearTimeout(this._setZxcvbnResultTimeout); - } - this._setZxcvbnResultTimeout = setTimeout(() => { - this._setZxcvbnResultTimeout = null; - this.setState({ - // precompute this and keep it in state: zxcvbn is fast but - // we use it in a couple of different places so no point recomputing - // it unnecessarily. - zxcvbnResult: scorePassword(this.state.passPhrase), - }); - }, PASSPHRASE_FEEDBACK_DELAY); } _onPassPhraseConfirmChange = (e) => { @@ -407,23 +426,61 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); } - _passPhraseIsValid() { - return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE; - } - _onAccountPasswordChange = (e) => { this.setState({ accountPassword: e.target.value, }); } + _renderPhaseChooseKeyPassphrase() { + return +

{_t( + "Safeguard against losing access to encrypted messages & data by " + + "backing up encryption keys on your server.", + )}

+
+ +
+ + {_t("Generate a Security Key")} +
+
{_t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.")}
+
+ +
+ + {_t("Enter a Security Phrase")} +
+
{_t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.")}
+
+
+ + ; + } + _renderPhaseMigrate() { // TODO: This is a temporary screen so people who have the labs flag turned on and // click the button are aware they're making a change to their account. // Once we're confident enough in this (and it's supported enough) we can do // it automatically. - // https://github.com/vector-im/riot-web/issues/11696 - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + // https://github.com/vector-im/element-web/issues/11696 const Field = sdk.getComponent('views.elements.Field'); let authPrompt; @@ -436,7 +493,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { label={_t("Password")} value={this.state.accountPassword} onChange={this._onAccountPasswordChange} - flagInvalid={this.state.accountPasswordCorrect === false} + forceValidity={this.state.accountPasswordCorrect === false ? false : null} autoFocus={true} />
; @@ -464,7 +521,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { hasCancel={false} primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword} > - @@ -472,102 +529,60 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _renderPhasePassPhrase() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const Field = sdk.getComponent('views.elements.Field'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch'); - - let strengthMeter; - let helpText; - if (this.state.zxcvbnResult) { - if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) { - helpText = _t("Great! This recovery passphrase looks strong enough."); - } else { - // We take the warning from zxcvbn or failing that, the first - // suggestion. In practice The first is generally the most relevant - // and it's probably better to present the user with one thing to - // improve about their password than a whole collection - it can - // spit out a warning and multiple suggestions which starts getting - // very information-dense. - const suggestion = ( - this.state.zxcvbnResult.feedback.warning || - this.state.zxcvbnResult.feedback.suggestions[0] - ); - const suggestionBlock =
{suggestion || _t("Keep going...")}
; - - helpText =
- {suggestionBlock} -
; - } - strengthMeter =
- -
; - } - return

{_t( - "Set a recovery passphrase to secure encrypted information and recover it if you log out. " + - "This should be different to your account password:", + "Enter a security phrase only you know, as it’s used to safeguard your data. " + + "To be secure, you shouldn’t re-use your account password.", )}

- -
- {strengthMeter} - {helpText} -
- - + >{_t("Cancel")} - -
- {_t("Advanced")} - - {_t("Set up with a recovery key")} - -
; } _renderPhasePassPhraseConfirm() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const Field = sdk.getComponent('views.elements.Field'); let matchText; + let changeText; if (this.state.passPhraseConfirm === this.state.passPhrase) { matchText = _t("That matches!"); + changeText = _t("Use a different passphrase?"); } else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) { // only tell them they're wrong if they've actually gone wrong. - // Security concious readers will note that if you left riot-web unattended + // Security concious readers will note that if you left element-web unattended // on this screen, this would make it easy for a malicious person to guess // your passphrase one letter at a time, but they could get this faster by // just opening the browser's developer tools and reading it. // Note that not having typed anything at all will not hit this clause and // fall through so empty box === no hint. matchText = _t("That doesn't match."); + changeText = _t("Go back to set it again."); } let passPhraseMatch = null; @@ -576,12 +591,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
{matchText}
- {_t("Go back to set it again.")} + {changeText}
; } - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

{_t( "Enter your recovery passphrase a second time to confirm it.", @@ -607,7 +621,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { disabled={this.state.passPhrase !== this.state.passPhraseConfirm} > @@ -615,66 +629,48 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _renderPhaseShowKey() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + let continueButton; + if (this.state.phase === PHASE_SHOWKEY) { + continueButton = ; + } else { + continueButton =

+ +
; + } return

{_t( - "Your recovery key is a safety net - you can use it to restore " + - "access to your encrypted messages if you forget your recovery passphrase.", - )}

-

{_t( - "Keep a copy of it somewhere secure, like a password manager or even a safe.", + "Store your Security Key somewhere safe, like a password manager or a safe, " + + "as it’s used to safeguard your encrypted data.", )}

-
- {_t("Your recovery key")} -
{this._recoveryKey.encodedPrivateKey}
+ + {_t("Download")} + + {_t("or")} - {_t("Copy")} - - - {_t("Download")} + {this.state.copied ? _t("Copied!") : _t("Copy")}
-
; - } - - _renderPhaseKeepItSafe() { - let introText; - if (this.state.copied) { - introText = _t( - "Your recovery key has been copied to your clipboard, paste it to:", - {}, {b: s => {s}}, - ); - } else if (this.state.downloaded) { - introText = _t( - "Your recovery key is in your Downloads folder.", - {}, {b: s => {s}}, - ); - } - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return
- {introText} -
    -
  • {_t("Print it and store it somewhere safe", {}, {b: s => {s}})}
  • -
  • {_t("Save it on a USB key or backup drive", {}, {b: s => {s}})}
  • -
  • {_t("Copy it to your personal cloud storage", {}, {b: s => {s}})}
  • -
- - - + {continueButton}
; } @@ -686,7 +682,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _renderPhaseLoadError() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

{_t("Unable to query secret storage status")}

@@ -699,53 +694,39 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
; } - _renderPhaseDone() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + _renderPhaseSkipConfirm() { return

{_t( - "You can now verify your other devices, " + - "and other users to keep your chats safe.", + "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", + )}

+

{_t( + "You can also set up Secure Backup & manage your keys in Settings.", )}

- -
; - } - - _renderPhaseSkipConfirm() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return
- {_t( - "Without completing security on this session, it won’t have " + - "access to encrypted messages.", - )} - +
; } _titleForPhase(phase) { switch (phase) { + case PHASE_CHOOSE_KEY_PASSPHRASE: + return _t('Set up Secure backup'); case PHASE_MIGRATE: return _t('Upgrade your encryption'); case PHASE_PASSPHRASE: - return _t('Set up encryption'); + return _t('Set a Security Phrase'); case PHASE_PASSPHRASE_CONFIRM: - return _t('Confirm recovery passphrase'); + return _t('Confirm Security Phrase'); case PHASE_CONFIRM_SKIP: return _t('Are you sure?'); case PHASE_SHOWKEY: - case PHASE_KEEPITSAFE: - return _t('Make a copy of your recovery key'); + return _t('Save your Security Key'); case PHASE_STORING: return _t('Setting up keys'); - case PHASE_DONE: - return _t("You're done!"); default: return ''; } @@ -756,7 +737,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { let content; if (this.state.error) { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); content =

{_t("Unable to set up secret storage")}

@@ -775,6 +755,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { case PHASE_LOADERROR: content = this._renderPhaseLoadError(); break; + case PHASE_CHOOSE_KEY_PASSPHRASE: + content = this._renderPhaseChooseKeyPassphrase(); + break; case PHASE_MIGRATE: content = this._renderPhaseMigrate(); break; @@ -787,31 +770,40 @@ export default class CreateSecretStorageDialog extends React.PureComponent { case PHASE_SHOWKEY: content = this._renderPhaseShowKey(); break; - case PHASE_KEEPITSAFE: - content = this._renderPhaseKeepItSafe(); - break; case PHASE_STORING: content = this._renderBusyPhase(); break; - case PHASE_DONE: - content = this._renderPhaseDone(); - break; case PHASE_CONFIRM_SKIP: content = this._renderPhaseSkipConfirm(); break; } } - let headerImage; - if (this._titleForPhase(this.state.phase)) { - headerImage = require("../../../../../res/img/e2e/normal.svg"); + let titleClass = null; + switch (this.state.phase) { + case PHASE_PASSPHRASE: + case PHASE_PASSPHRASE_CONFIRM: + titleClass = [ + 'mx_CreateSecretStorageDialog_titleWithIcon', + 'mx_CreateSecretStorageDialog_securePhraseTitle', + ]; + break; + case PHASE_SHOWKEY: + titleClass = [ + 'mx_CreateSecretStorageDialog_titleWithIcon', + 'mx_CreateSecretStorageDialog_secureBackupTitle', + ]; + break; + case PHASE_CHOOSE_KEY_PASSPHRASE: + titleClass = 'mx_CreateSecretStorageDialog_centeredTitle'; + break; } return ( diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index 8384eb9d4f..2615736e09 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -35,15 +35,15 @@ export interface ISelectionRange { export interface ICompletion { type: "at-room" | "command" | "community" | "room" | "user"; - completion: string, + completion: string; completionId?: string; - component?: ReactElement, - range: ISelectionRange, - command?: string, + component?: ReactElement; + range: ISelectionRange; + command?: string; suffix?: string; // If provided, apply a LINK entity to the completion with the // data = { url: href }. - href?: string, + href?: string; } const PROVIDERS = [ diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx index 3edb1ff81d..f34fee890e 100644 --- a/src/autocomplete/CommunityProvider.tsx +++ b/src/autocomplete/CommunityProvider.tsx @@ -53,7 +53,7 @@ export default class CommunityProvider extends AutocompleteProvider { const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar'); // Disable autocompletions when composing commands because of various issues - // (see https://github.com/vector-im/riot-web/issues/4762) + // (see https://github.com/vector-im/element-web/issues/4762) if (/^(\/join|\/leave)/.test(query)) { return []; } @@ -90,11 +90,12 @@ export default class CommunityProvider extends AutocompleteProvider { type: "community", href: makeGroupPermalink(groupId), component: ( - - } title={name} description={groupId} /> + ), range, })) diff --git a/src/autocomplete/Components.tsx b/src/autocomplete/Components.tsx index 19a7a969d6..6ac2f4db14 100644 --- a/src/autocomplete/Components.tsx +++ b/src/autocomplete/Components.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {forwardRef} from 'react'; import classNames from 'classnames'; /* These were earlier stateless functional components but had to be converted @@ -30,50 +30,37 @@ interface ITextualCompletionProps { className?: string; } -export class TextualCompletion extends React.PureComponent { - render() { - const { - title, - subtitle, - description, - className, - ...restProps - } = this.props; - return ( -
- { title } - { subtitle } - { description } -
- ); - } +export const TextualCompletion = forwardRef((props, ref) => { + const {title, subtitle, description, className, ...restProps} = props; + return ( +
+ { title } + { subtitle } + { description } +
+ ); +}); + +interface IPillCompletionProps extends ITextualCompletionProps { + children?: React.ReactNode; } -interface IPillCompletionProps { - title?: string; - subtitle?: string; - description?: string; - initialComponent?: React.ReactNode, - className?: string; -} - -export class PillCompletion extends React.PureComponent { - render() { - const { - title, - subtitle, - description, - initialComponent, - className, - ...restProps - } = this.props; - return ( -
- { initialComponent } - { title } - { subtitle } - { description } -
- ); - } -} +export const PillCompletion = forwardRef((props, ref) => { + const {title, subtitle, description, className, children, ...restProps} = props; + return ( +
+ { children } + { title } + { subtitle } + { description } +
+ ); +}); diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index b67e26117b..147d68f5ff 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -34,7 +34,8 @@ import EMOTICON_REGEX from 'emojibase-regex/emoticon'; const LIMIT = 20; // Match for ascii-style ";-)" emoticons or ":wink:" shortcodes provided by emojibase -const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|:[+-\\w]*:?)$', 'g'); +// anchored to only match from the start of parts otherwise it'll show emoji suggestions whilst typing matrix IDs +const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w]*:?)$', 'g'); interface IEmojiShort { emoji: IEmoji; @@ -69,7 +70,7 @@ export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); - this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, { + this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, { keys: ['emoji.emoticon', 'shortname'], funcs: [ (o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases @@ -121,9 +122,9 @@ export default class EmojiProvider extends AutocompleteProvider { return { completion: unicode, component: ( - { unicode } - } /> + + { unicode } + ), range, }; diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx index b217612b0e..ef1823c0ca 100644 --- a/src/autocomplete/NotifProvider.tsx +++ b/src/autocomplete/NotifProvider.tsx @@ -48,7 +48,9 @@ export default class NotifProvider extends AutocompleteProvider { type: "at-room", suffix: ' ', component: ( - } title="@room" description={_t("Notify the whole room")} /> + + + ), range, }]; diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index 95bcfb25ef..9c91414556 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -17,26 +17,24 @@ limitations under the License. */ import _at from 'lodash/at'; -import _flatMap from 'lodash/flatMap'; -import _sortBy from 'lodash/sortBy'; import _uniq from 'lodash/uniq'; - -function stripDiacritics(str: string): string { - return str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); -} +import {removeHiddenChars} from "matrix-js-sdk/src/utils"; interface IOptions { keys: Array; funcs?: Array<(T) => string>; shouldMatchWordsOnly?: boolean; shouldMatchPrefix?: boolean; + // whether to apply unhomoglyph and strip diacritics to fuzz up the search. Defaults to true + fuzzy?: boolean; } /** * Simple search matcher that matches any results with the query string anywhere * in the search string. Returns matches in the order the query string appears - * in the search key, earliest first, then in the order the items appeared in - * the source array. + * in the search key, earliest first, then in the order the search key appears + * in the provided array of keys, then in the order the items appeared in the + * source array. * * @param {Object[]} objects Initial list of objects. Equivalent to calling * setObjects() after construction @@ -45,16 +43,12 @@ interface IOptions { * @param {function[]} options.funcs List of functions that when called with the * object as an arg will return a string to use as an index */ -export default class QueryMatcher { +export default class QueryMatcher { private _options: IOptions; - private _keys: IOptions["keys"]; - private _funcs: Required["funcs"]>; - private _items: Map; + private _items: Map; constructor(objects: T[], options: IOptions = { keys: [] }) { this._options = options; - this._keys = options.keys; - this._funcs = options.funcs || []; this.setObjects(objects); @@ -75,57 +69,82 @@ export default class QueryMatcher { this._items = new Map(); for (const object of objects) { - const keyValues = _at(object, this._keys); + // Need to use unsafe coerce here because the objects can have any + // type for their values. We assume that those values who's keys have + // been specified will be string. Also, we cannot infer all the + // types of the keys of the objects at compile. + const keyValues = _at(object, this._options.keys); - for (const f of this._funcs) { - keyValues.push(f(object)); + if (this._options.funcs) { + for (const f of this._options.funcs) { + keyValues.push(f(object)); + } } - for (const keyValue of keyValues) { + for (const [index, keyValue] of Object.entries(keyValues)) { if (!keyValue) continue; // skip falsy keyValues - const key = stripDiacritics(keyValue).toLowerCase(); + const key = this.processQuery(keyValue); if (!this._items.has(key)) { this._items.set(key, []); } - this._items.get(key).push(object); + this._items.get(key).push({ + keyWeight: Number(index), + object, + }); } } } match(query: string): T[] { - query = stripDiacritics(query).toLowerCase(); + query = this.processQuery(query); if (this._options.shouldMatchWordsOnly) { query = query.replace(/[^\w]/g, ''); } if (query.length === 0) { return []; } - const results = []; + const matches = []; // Iterate through the map & check each key. // ES6 Map iteration order is defined to be insertion order, so results // here will come out in the order they were put in. - for (const key of this._items.keys()) { + for (const [key, candidates] of this._items.entries()) { let resultKey = key; if (this._options.shouldMatchWordsOnly) { resultKey = resultKey.replace(/[^\w]/g, ''); } const index = resultKey.indexOf(query); if (index !== -1 && (!this._options.shouldMatchPrefix || index === 0)) { - results.push({key, index}); + matches.push( + ...candidates.map((candidate) => ({index, ...candidate})), + ); } } - // Sort them by where the query appeared in the search key - // lodash sortBy is a stable sort, so results where the query - // appeared in the same place will retain their order with - // respect to each other. - const sortedResults = _sortBy(results, (candidate) => { - return candidate.index; + // Sort matches by where the query appeared in the search key, then by + // where the matched key appeared in the provided array of keys. + matches.sort((a, b) => { + if (a.index < b.index) { + return -1; + } else if (a.index === b.index) { + if (a.keyWeight < b.keyWeight) { + return -1; + } else if (a.keyWeight === b.keyWeight) { + return 0; + } + } + + return 1; }); - // Now map the keys to the result objects. Each result object is a list, so - // flatMap will flatten those lists out into a single list. Also remove any - // duplicates. - return _uniq(_flatMap(sortedResults, (candidate) => this._items.get(candidate.key))); + // Now map the keys to the result objects. Also remove any duplicates. + return _uniq(matches.map((match) => match.object)); + } + + private processQuery(query: string): string { + if (this._options.fuzzy !== false) { + // lower case both the input and the output for consistency + return removeHiddenChars(query.toLowerCase()).toLowerCase(); + } + return query.toLowerCase(); } } diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index 01e770407c..b18b2d132c 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -25,9 +25,9 @@ import {MatrixClientPeg} from '../MatrixClientPeg'; import QueryMatcher from './QueryMatcher'; import {PillCompletion} from './Components'; import * as sdk from '../index'; -import _sortBy from 'lodash/sortBy'; import {makeRoomPermalink} from "../utils/permalinks/Permalinks"; import {ICompletion, ISelectionRange} from "./Autocompleter"; +import { uniqBy, sortBy } from 'lodash'; const ROOM_REGEX = /\B#\S*/g; @@ -91,10 +91,11 @@ export default class RoomProvider extends AutocompleteProvider { this.matcher.setObjects(matcherObjects); const matchedString = command[0]; completions = this.matcher.match(matchedString); - completions = _sortBy(completions, [ + completions = sortBy(completions, [ (c) => score(matchedString, c.displayedAlias), (c) => c.displayedAlias.length, ]); + completions = uniqBy(completions, (match) => match.room); completions = completions.map((room) => { return { completion: room.displayedAlias, @@ -103,7 +104,9 @@ export default class RoomProvider extends AutocompleteProvider { suffix: ' ', href: makeRoomPermalink(room.displayedAlias), component: ( - } title={room.room.name} description={room.displayedAlias} /> + + + ), range, }; @@ -115,7 +118,7 @@ export default class RoomProvider extends AutocompleteProvider { } getName() { - return '💬 ' + _t('Rooms'); + return _t('Rooms'); } renderCompletions(completions: React.ReactNode[]): React.ReactNode { diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 1680eb5d54..c957b5e597 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -125,10 +125,9 @@ export default class UserProvider extends AutocompleteProvider { suffix: (selection.beginning && range.start === 0) ? ': ' : ' ', href: makeUserPermalink(user.userId), component: ( - } - title={displayName} - description={user.userId} /> + + + ), range, }; @@ -138,7 +137,7 @@ export default class UserProvider extends AutocompleteProvider { } getName(): string { - return '👥 ' + _t('Users'); + return _t('Users'); } _makeUsers() { diff --git a/src/components/structures/AutoHideScrollbar.js b/src/components/structures/AutoHideScrollbar.js index 04323bb548..14f7c9ca83 100644 --- a/src/components/structures/AutoHideScrollbar.js +++ b/src/components/structures/AutoHideScrollbar.js @@ -38,12 +38,13 @@ export default class AutoHideScrollbar extends React.Component { render() { return (
+ ref={this._collectContainerRef} + style={this.props.style} + className={["mx_AutoHideScrollbar", this.props.className].join(" ")} + onScroll={this.props.onScroll} + onWheel={this.props.onWheel} + tabIndex={this.props.tabIndex} + > { this.props.children }
); } diff --git a/src/components/structures/CompatibilityPage.js b/src/components/structures/CompatibilityPage.js index 9a3fdb5f39..1fa6068675 100644 --- a/src/components/structures/CompatibilityPage.js +++ b/src/components/structures/CompatibilityPage.js @@ -1,7 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -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,7 @@ import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { _t } from '../../languageHandler'; +import SdkConfig from '../../SdkConfig'; export default createReactClass({ displayName: 'CompatibilityPage', @@ -38,14 +39,25 @@ export default createReactClass({ }, render: function() { + const brand = SdkConfig.get().brand; + return (
-

{ _t("Sorry, your browser is not able to run Riot.", {}, { 'b': (sub) => {sub} }) }

+

{_t( + "Sorry, your browser is not able to run %(brand)s.", + { + brand, + }, + { + 'b': (sub) => {sub}, + }) + }

{ _t( - "Riot uses many advanced browser features, some of which are not available " + + "%(brand)s uses many advanced browser features, some of which are not available " + "or experimental in your current browser.", + { brand }, ) }

diff --git a/src/components/structures/ContextMenu.js b/src/components/structures/ContextMenu.tsx similarity index 64% rename from src/components/structures/ContextMenu.js rename to src/components/structures/ContextMenu.tsx index 98b0867ccc..587ae2cb6b 100644 --- a/src/components/structures/ContextMenu.js +++ b/src/components/structures/ContextMenu.tsx @@ -16,13 +16,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useRef, useState} from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; +import React, {CSSProperties, useRef, useState} from "react"; +import ReactDOM from "react-dom"; +import classNames from "classnames"; + import {Key} from "../../Keyboard"; -import * as sdk from "../../index"; -import AccessibleButton from "../views/elements/AccessibleButton"; +import {Writeable} from "../../@types/common"; // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and @@ -30,8 +29,8 @@ import AccessibleButton from "../views/elements/AccessibleButton"; const ContextualMenuContainerId = "mx_ContextualMenu_Container"; -function getOrCreateContainer() { - let container = document.getElementById(ContextualMenuContainerId); +function getOrCreateContainer(): HTMLDivElement { + let container = document.getElementById(ContextualMenuContainerId) as HTMLDivElement; if (!container) { container = document.createElement("div"); @@ -43,50 +42,70 @@ function getOrCreateContainer() { } const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]); + +interface IPosition { + top?: number; + bottom?: number; + left?: number; + right?: number; +} + +export enum ChevronFace { + Top = "top", + Bottom = "bottom", + Left = "left", + Right = "right", + None = "none", +} + +export interface IProps extends IPosition { + menuWidth?: number; + menuHeight?: number; + + chevronOffset?: number; + chevronFace?: ChevronFace; + + menuPaddingTop?: number; + menuPaddingBottom?: number; + menuPaddingLeft?: number; + menuPaddingRight?: number; + + zIndex?: number; + + // If true, insert an invisible screen-sized element behind the menu that when clicked will close it. + hasBackground?: boolean; + // whether this context menu should be focus managed. If false it must handle itself + managed?: boolean; + + // Function to be called on menu close + onFinished(); + // on resize callback + windowResize?(); +} + +interface IState { + contextMenuElem: HTMLDivElement; +} + // Generic ContextMenu Portal wrapper // all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1} // this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines. -export class ContextMenu extends React.Component { - static propTypes = { - top: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - right: PropTypes.number, - menuWidth: PropTypes.number, - menuHeight: PropTypes.number, - chevronOffset: PropTypes.number, - chevronFace: PropTypes.string, // top, bottom, left, right or none - // Function to be called on menu close - onFinished: PropTypes.func.isRequired, - menuPaddingTop: PropTypes.number, - menuPaddingRight: PropTypes.number, - menuPaddingBottom: PropTypes.number, - menuPaddingLeft: PropTypes.number, - zIndex: PropTypes.number, - - // If true, insert an invisible screen-sized element behind the - // menu that when clicked will close it. - hasBackground: PropTypes.bool, - - // on resize callback - windowResize: PropTypes.func, - - managed: PropTypes.bool, // whether this context menu should be focus managed. If false it must handle itself - }; +export class ContextMenu extends React.PureComponent { + private initialFocus: HTMLElement; static defaultProps = { hasBackground: true, managed: true, }; - constructor() { - super(); + constructor(props, context) { + super(props, context); this.state = { contextMenuElem: null, }; // persist what had focus when we got initialized so we can return it after - this.initialFocus = document.activeElement; + this.initialFocus = document.activeElement as HTMLElement; } componentWillUnmount() { @@ -94,7 +113,7 @@ export class ContextMenu extends React.Component { this.initialFocus.focus(); } - collectContextMenuRect = (element) => { + private collectContextMenuRect = (element) => { // We don't need to clean up when unmounting, so ignore if (!element) return; @@ -111,11 +130,12 @@ export class ContextMenu extends React.Component { }); }; - onContextMenu = (e) => { + private onContextMenu = (e) => { if (this.props.onFinished) { this.props.onFinished(); e.preventDefault(); + e.stopPropagation(); const x = e.clientX; const y = e.clientY; @@ -133,7 +153,20 @@ export class ContextMenu extends React.Component { } }; - _onMoveFocus = (element, up) => { + private onContextMenuPreventBubbling = (e) => { + // stop propagation so that any context menu handlers don't leak out of this context menu + // but do not inhibit the default browser menu + e.stopPropagation(); + }; + + // Prevent clicks on the background from going through to the component which opened the menu. + private onFinished = (ev: React.MouseEvent) => { + ev.stopPropagation(); + ev.preventDefault(); + if (this.props.onFinished) this.props.onFinished(); + }; + + private onMoveFocus = (element: Element, up: boolean) => { let descending = false; // are we currently descending or ascending through the DOM tree? do { @@ -167,25 +200,25 @@ export class ContextMenu extends React.Component { } while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role"))); if (element) { - element.focus(); + (element as HTMLElement).focus(); } }; - _onMoveFocusHomeEnd = (element, up) => { + private onMoveFocusHomeEnd = (element: Element, up: boolean) => { let results = element.querySelectorAll('[role^="menuitem"]'); if (!results) { results = element.querySelectorAll('[tab-index]'); } if (results && results.length) { if (up) { - results[0].focus(); + (results[0] as HTMLElement).focus(); } else { - results[results.length - 1].focus(); + (results[results.length - 1] as HTMLElement).focus(); } } }; - _onKeyDown = (ev) => { + private onKeyDown = (ev: React.KeyboardEvent) => { if (!this.props.managed) { if (ev.key === Key.ESCAPE) { this.props.onFinished(); @@ -200,19 +233,22 @@ export class ContextMenu extends React.Component { switch (ev.key) { case Key.TAB: case Key.ESCAPE: + // close on left and right arrows too for when it is a context menu on a + case Key.ARROW_LEFT: + case Key.ARROW_RIGHT: this.props.onFinished(); break; case Key.ARROW_UP: - this._onMoveFocus(ev.target, true); + this.onMoveFocus(ev.target as Element, true); break; case Key.ARROW_DOWN: - this._onMoveFocus(ev.target, false); + this.onMoveFocus(ev.target as Element, false); break; case Key.HOME: - this._onMoveFocusHomeEnd(this.state.contextMenuElem, true); + this.onMoveFocusHomeEnd(this.state.contextMenuElem, true); break; case Key.END: - this._onMoveFocusHomeEnd(this.state.contextMenuElem, false); + this.onMoveFocusHomeEnd(this.state.contextMenuElem, false); break; default: handled = false; @@ -225,9 +261,8 @@ export class ContextMenu extends React.Component { } }; - renderMenu(hasBackground=this.props.hasBackground) { - const position = {}; - let chevronFace = null; + protected renderMenu(hasBackground = this.props.hasBackground) { + const position: Partial> = {}; const props = this.props; if (props.top) { @@ -236,23 +271,24 @@ export class ContextMenu extends React.Component { position.bottom = props.bottom; } + let chevronFace: ChevronFace; if (props.left) { position.left = props.left; - chevronFace = 'left'; + chevronFace = ChevronFace.Left; } else { position.right = props.right; - chevronFace = 'right'; + chevronFace = ChevronFace.Right; } const contextMenuRect = this.state.contextMenuElem ? this.state.contextMenuElem.getBoundingClientRect() : null; - const chevronOffset = {}; + const chevronOffset: CSSProperties = {}; if (props.chevronFace) { chevronFace = props.chevronFace; } - const hasChevron = chevronFace && chevronFace !== "none"; + const hasChevron = chevronFace && chevronFace !== ChevronFace.None; - if (chevronFace === 'top' || chevronFace === 'bottom') { + if (chevronFace === ChevronFace.Top || chevronFace === ChevronFace.Bottom) { chevronOffset.left = props.chevronOffset; } else if (position.top !== undefined) { const target = position.top; @@ -282,13 +318,13 @@ export class ContextMenu extends React.Component { 'mx_ContextualMenu_right': !hasChevron && position.right, 'mx_ContextualMenu_top': !hasChevron && position.top, 'mx_ContextualMenu_bottom': !hasChevron && position.bottom, - 'mx_ContextualMenu_withChevron_left': chevronFace === 'left', - 'mx_ContextualMenu_withChevron_right': chevronFace === 'right', - 'mx_ContextualMenu_withChevron_top': chevronFace === 'top', - 'mx_ContextualMenu_withChevron_bottom': chevronFace === 'bottom', + 'mx_ContextualMenu_withChevron_left': chevronFace === ChevronFace.Left, + 'mx_ContextualMenu_withChevron_right': chevronFace === ChevronFace.Right, + 'mx_ContextualMenu_withChevron_top': chevronFace === ChevronFace.Top, + 'mx_ContextualMenu_withChevron_bottom': chevronFace === ChevronFace.Bottom, }); - const menuStyle = {}; + const menuStyle: CSSProperties = {}; if (props.menuWidth) { menuStyle.width = props.menuWidth; } @@ -319,13 +355,28 @@ export class ContextMenu extends React.Component { let background; if (hasBackground) { background = ( -

+
); } return ( -
-
+
+
{ chevron } { props.children }
@@ -334,91 +385,13 @@ export class ContextMenu extends React.Component { ); } - render() { + render(): React.ReactChild { return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer()); } } -// Semantic component for representing the AccessibleButton which launches a -export const ContextMenuButton = ({ label, isExpanded, children, ...props }) => { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - return ( - - { children } - - ); -}; -ContextMenuButton.propTypes = { - ...AccessibleButton.propTypes, - label: PropTypes.string, - isExpanded: PropTypes.bool.isRequired, // whether or not the context menu is currently open -}; - -// Semantic component for representing a role=menuitem -export const MenuItem = ({children, label, ...props}) => { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - return ( - - { children } - - ); -}; -MenuItem.propTypes = { - ...AccessibleButton.propTypes, - label: PropTypes.string, // optional - className: PropTypes.string, // optional - onClick: PropTypes.func.isRequired, -}; - -// Semantic component for representing a role=group for grouping menu radios/checkboxes -export const MenuGroup = ({children, label, ...props}) => { - return
- { children } -
; -}; -MenuGroup.propTypes = { - label: PropTypes.string.isRequired, - className: PropTypes.string, // optional -}; - -// Semantic component for representing a role=menuitemcheckbox -export const MenuItemCheckbox = ({children, label, active=false, disabled=false, ...props}) => { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - return ( - - { children } - - ); -}; -MenuItemCheckbox.propTypes = { - ...AccessibleButton.propTypes, - label: PropTypes.string, // optional - active: PropTypes.bool.isRequired, - disabled: PropTypes.bool, // optional - className: PropTypes.string, // optional - onClick: PropTypes.func.isRequired, -}; - -// Semantic component for representing a role=menuitemradio -export const MenuItemRadio = ({children, label, active=false, disabled=false, ...props}) => { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - return ( - - { children } - - ); -}; -MenuItemRadio.propTypes = { - ...AccessibleButton.propTypes, - label: PropTypes.string, // optional - active: PropTypes.bool.isRequired, - disabled: PropTypes.bool, // optional - className: PropTypes.string, // optional - onClick: PropTypes.func.isRequired, -}; - // Placement method for to position context menu to right of elementRect with chevronOffset -export const toRightOf = (elementRect, chevronOffset=12) => { +export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => { const left = elementRect.right + window.pageXOffset + 3; let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset; top -= chevronOffset + 8; // where 8 is half the height of the chevron @@ -426,8 +399,8 @@ export const toRightOf = (elementRect, chevronOffset=12) => { }; // Placement method for to position context menu right-aligned and flowing to the left of elementRect -export const aboveLeftOf = (elementRect, chevronFace="none") => { - const menuOptions = { chevronFace }; +export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None) => { + const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace }; const buttonRight = elementRect.right + window.pageXOffset; const buttonBottom = elementRect.bottom + window.pageYOffset; @@ -485,3 +458,13 @@ export function createMenu(ElementClass, props) { return {close: onFinished}; } + +// re-export the semantic helper components for simplicity +export {ContextMenuButton} from "../../accessibility/context_menu/ContextMenuButton"; +export {ContextMenuTooltipButton} from "../../accessibility/context_menu/ContextMenuTooltipButton"; +export {MenuGroup} from "../../accessibility/context_menu/MenuGroup"; +export {MenuItem} from "../../accessibility/context_menu/MenuItem"; +export {MenuItemCheckbox} from "../../accessibility/context_menu/MenuItemCheckbox"; +export {MenuItemRadio} from "../../accessibility/context_menu/MenuItemRadio"; +export {StyledMenuItemCheckbox} from "../../accessibility/context_menu/StyledMenuItemCheckbox"; +export {StyledMenuItemRadio} from "../../accessibility/context_menu/StyledMenuItemRadio"; diff --git a/src/components/structures/CustomRoomTagPanel.js b/src/components/structures/CustomRoomTagPanel.js index 6e392ea505..a79bdafeb5 100644 --- a/src/components/structures/CustomRoomTagPanel.js +++ b/src/components/structures/CustomRoomTagPanel.js @@ -18,7 +18,7 @@ import React from 'react'; import CustomRoomTagStore from '../../stores/CustomRoomTagStore'; import AutoHideScrollbar from './AutoHideScrollbar'; import * as sdk from '../../index'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import classNames from 'classnames'; import * as FormattingUtils from '../../utils/FormattingUtils'; @@ -72,17 +72,17 @@ class CustomRoomTagTile extends React.Component { const tag = this.props.tag; const avatarHeight = 40; const className = classNames({ - CustomRoomTagPanel_tileSelected: tag.selected, + "CustomRoomTagPanel_tileSelected": tag.selected, }); const name = tag.name; - const badge = tag.badge; + const badgeNotifState = tag.badgeNotifState; let badgeElement; - if (badge) { + if (badgeNotifState) { const badgeClasses = classNames({ "mx_TagTile_badge": true, - "mx_TagTile_badgeHighlight": badge.highlight, + "mx_TagTile_badgeHighlight": badgeNotifState.hasMentions, }); - badgeElement = (
{FormattingUtils.formatCount(badge.count)}
); + badgeElement = (
{FormattingUtils.formatCount(badgeNotifState.count)}
); } return ( diff --git a/src/components/structures/EmbeddedPage.js b/src/components/structures/EmbeddedPage.js index 0aababf030..49ba3d1227 100644 --- a/src/components/structures/EmbeddedPage.js +++ b/src/components/structures/EmbeddedPage.js @@ -23,7 +23,7 @@ import PropTypes from 'prop-types'; import request from 'browser-request'; import { _t } from '../../languageHandler'; import sanitizeHtml from 'sanitize-html'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import classnames from 'classnames'; import MatrixClientContext from "../../contexts/MatrixClientContext"; diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index f8c03be864..d873dd4094 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -210,6 +210,11 @@ const FilePanel = createReactClass({ const TimelinePanel = sdk.getComponent("structures.TimelinePanel"); const Loader = sdk.getComponent("elements.Spinner"); + const emptyState = (
+

{_t('No files visible in this room')}

+

{_t('Attach files from chat or just drag and drop them anywhere in a room.')}

+
); + if (this.state.timelineSet) { // console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " + // "(" + this.state.timelineSet._timelines.join(", ") + ")" + " with key " + this.props.roomId); @@ -223,7 +228,7 @@ const FilePanel = createReactClass({ onPaginationRequest={this.onPaginationRequest} tileShape="file_grid" resizeNotifier={this.props.resizeNotifier} - empty={_t('There are no visible files in this room')} + empty={emptyState} />
); diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 3b32e5c907..2e2fa25169 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -21,7 +21,7 @@ import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import * as sdk from '../../index'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import { getHostingLink } from '../../utils/HostingLink'; import { sanitizedHtmlNode } from '../../HtmlUtils'; import { _t, _td } from '../../languageHandler'; @@ -92,7 +92,7 @@ const CategoryRoomList = createReactClass({ Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, { title: _t('Add rooms to the community summary'), description: _t("Which rooms would you like to add to this summary?"), - placeholder: _t("Room name or alias"), + placeholder: _t("Room name or address"), button: _t("Add to summary"), pickerType: 'room', validAddressTypes: ['mx-room-id'], @@ -1003,9 +1003,10 @@ export default createReactClass({ this.state.inviterProfile.avatarUrl, 36, 36, ) : null; - let inviterName = group.inviter.userId; + const inviter = group.inviter || {}; + let inviterName = inviter.userId; if (this.state.inviterProfile) { - inviterName = this.state.inviterProfile.displayName || group.inviter.userId; + inviterName = this.state.inviterProfile.displayName || inviter.userId; } return
@@ -1016,7 +1017,7 @@ export default createReactClass({ height={36} /> { _t("%(inviter)s has invited you to join this community", { - inviter: inviterName, + inviter: inviterName || _t("Someone"), }) }
diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index ddf9cd6d00..a42032c9fe 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -21,10 +21,11 @@ import { getHomePageUrl } from "../../utils/pages"; import { _t } from "../../languageHandler"; import SdkConfig from "../../SdkConfig"; import * as sdk from "../../index"; -import dis from "../../dispatcher"; +import dis from "../../dispatcher/dispatcher"; +import { Action } from "../../dispatcher/actions"; const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'}); -const onClickExplore = () => dis.dispatch({action: 'view_room_directory'}); +const onClickExplore = () => dis.fire(Action.ViewRoomDirectory); const onClickNewRoom = () => dis.dispatch({action: 'view_create_room'}); const HomePage = () => { @@ -37,7 +38,7 @@ const HomePage = () => { } const brandingConfig = config.branding; - let logoUrl = "themes/riot/img/logos/riot-logo.svg"; + let logoUrl = "themes/element/img/logos/element-logo.svg"; if (brandingConfig && brandingConfig.authHeaderLogoUrl) { logoUrl = brandingConfig.authHeaderLogoUrl; } @@ -45,8 +46,8 @@ const HomePage = () => { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); return
- Riot -

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

+ {config.brand +

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

{ _t("Liberate your communication") }

diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index 05ad4f7561..cd5510de9d 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -158,7 +158,7 @@ export default class IndicatorScrollbar extends React.Component { } // don't mess with the horizontal scroll for trackpad users - // See https://github.com/vector-im/riot-web/issues/10005 + // See https://github.com/vector-im/element-web/issues/10005 if (this._likelyTrackpadUser) { return; } @@ -192,7 +192,7 @@ export default class IndicatorScrollbar extends React.Component { ref={this._collectScrollerComponent} wrappedRef={this._collectScroller} onWheel={this.onMouseWheel} - {... this.props} + {...this.props} > { leftOverflowIndicator } { this.props.children } diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 351e3bbad0..fa7860ccef 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -203,7 +203,7 @@ export default createReactClass({ // the UI layer, so we ignore this signal and show a spinner until // there's a new screen to show the user. This is implemented by setting // `busy: false` in `_authStateUpdated`. - // See also https://github.com/vector-im/riot-web/issues/12546 + // See also https://github.com/vector-im/element-web/issues/12546 }, _setFocus: function() { diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js deleted file mode 100644 index a9cd12199b..0000000000 --- a/src/components/structures/LeftPanel.js +++ /dev/null @@ -1,302 +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 React from 'react'; -import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { Key } from '../../Keyboard'; -import * as sdk from '../../index'; -import dis from '../../dispatcher'; -import * as VectorConferenceHandler from '../../VectorConferenceHandler'; -import SettingsStore from '../../settings/SettingsStore'; -import {_t} from "../../languageHandler"; -import Analytics from "../../Analytics"; - - -const LeftPanel = createReactClass({ - displayName: 'LeftPanel', - - // NB. If you add props, don't forget to update - // shouldComponentUpdate! - propTypes: { - collapsed: PropTypes.bool.isRequired, - }, - - getInitialState: function() { - return { - searchFilter: '', - breadcrumbs: false, - }; - }, - - // TODO: [REACT-WARNING] Move this to constructor - UNSAFE_componentWillMount: function() { - this.focusedElement = null; - - this._breadcrumbsWatcherRef = SettingsStore.watchSetting( - "breadcrumbs", null, this._onBreadcrumbsChanged); - this._tagPanelWatcherRef = SettingsStore.watchSetting( - "TagPanel.enableTagPanel", null, () => this.forceUpdate()); - - const useBreadcrumbs = !!SettingsStore.getValue("breadcrumbs"); - Analytics.setBreadcrumbs(useBreadcrumbs); - this.setState({breadcrumbs: useBreadcrumbs}); - }, - - componentWillUnmount: function() { - SettingsStore.unwatchSetting(this._breadcrumbsWatcherRef); - SettingsStore.unwatchSetting(this._tagPanelWatcherRef); - }, - - shouldComponentUpdate: function(nextProps, nextState) { - // MatrixChat will update whenever the user switches - // rooms, but propagating this change all the way down - // the react tree is quite slow, so we cut this off - // here. The RoomTiles listen for the room change - // events themselves to know when to update. - // We just need to update if any of these things change. - if ( - this.props.collapsed !== nextProps.collapsed || - this.props.disabled !== nextProps.disabled - ) { - return true; - } - - if (this.state.searchFilter !== nextState.searchFilter) { - return true; - } - if (this.state.searchExpanded !== nextState.searchExpanded) { - return true; - } - - return false; - }, - - componentDidUpdate(prevProps, prevState) { - if (prevState.breadcrumbs !== this.state.breadcrumbs) { - Analytics.setBreadcrumbs(this.state.breadcrumbs); - } - }, - - _onBreadcrumbsChanged: function(settingName, roomId, level, valueAtLevel, value) { - // Features are only possible at a single level, so we can get away with using valueAtLevel. - // The SettingsStore runs on the same tick as the update, so `value` will be wrong. - this.setState({breadcrumbs: valueAtLevel}); - - // For some reason the setState doesn't trigger a render of the component, so force one. - // Probably has to do with the change happening outside of a change detector cycle. - this.forceUpdate(); - }, - - _onFocus: function(ev) { - this.focusedElement = ev.target; - }, - - _onBlur: function(ev) { - this.focusedElement = null; - }, - - _onFilterKeyDown: function(ev) { - if (!this.focusedElement) return; - - switch (ev.key) { - // On enter of rooms filter select and activate first room if such one exists - case Key.ENTER: { - const firstRoom = ev.target.closest(".mx_LeftPanel").querySelector(".mx_RoomTile"); - if (firstRoom) { - firstRoom.click(); - } - break; - } - } - }, - - _onKeyDown: function(ev) { - if (!this.focusedElement) return; - - switch (ev.key) { - case Key.ARROW_UP: - this._onMoveFocus(ev, true, true); - break; - case Key.ARROW_DOWN: - this._onMoveFocus(ev, false, true); - break; - } - }, - - _onMoveFocus: function(ev, up, trap) { - let element = this.focusedElement; - - // unclear why this isn't needed - // var descending = (up == this.focusDirection) ? this.focusDescending : !this.focusDescending; - // this.focusDirection = up; - - let descending = false; // are we currently descending or ascending through the DOM tree? - let classes; - - do { - const child = up ? element.lastElementChild : element.firstElementChild; - const sibling = up ? element.previousElementSibling : element.nextElementSibling; - - if (descending) { - if (child) { - element = child; - } else if (sibling) { - element = sibling; - } else { - descending = false; - element = element.parentElement; - } - } else { - if (sibling) { - element = sibling; - descending = true; - } else { - element = element.parentElement; - } - } - - if (element) { - classes = element.classList; - } - } while (element && !( - classes.contains("mx_RoomTile") || - classes.contains("mx_RoomSubList_label") || - classes.contains("mx_LeftPanel_filterRooms"))); - - if (element) { - ev.stopPropagation(); - ev.preventDefault(); - element.focus(); - this.focusedElement = element; - } else if (trap) { - // if navigation is via up/down arrow-keys, trap in the widget so it doesn't send to composer - ev.stopPropagation(); - ev.preventDefault(); - } - }, - - onSearch: function(term) { - this.setState({ searchFilter: term }); - }, - - onSearchCleared: function(source) { - if (source === "keyboard") { - dis.dispatch({action: 'focus_composer'}); - } - this.setState({searchExpanded: false}); - }, - - collectRoomList: function(ref) { - this._roomList = ref; - }, - - _onSearchFocus: function() { - this.setState({searchExpanded: true}); - }, - - _onSearchBlur: function(event) { - if (event.target.value.length === 0) { - this.setState({searchExpanded: false}); - } - }, - - render: function() { - const RoomList = sdk.getComponent('rooms.RoomList'); - const RoomBreadcrumbs = sdk.getComponent('rooms.RoomBreadcrumbs'); - const TagPanel = sdk.getComponent('structures.TagPanel'); - const CustomRoomTagPanel = sdk.getComponent('structures.CustomRoomTagPanel'); - const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton'); - const SearchBox = sdk.getComponent('structures.SearchBox'); - const CallPreview = sdk.getComponent('voip.CallPreview'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - - const tagPanelEnabled = SettingsStore.getValue("TagPanel.enableTagPanel"); - let tagPanelContainer; - - const isCustomTagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags"); - - if (tagPanelEnabled) { - tagPanelContainer = (
- - { isCustomTagsEnabled ? : undefined } -
); - } - - const containerClasses = classNames( - "mx_LeftPanel_container", "mx_fadable", - { - "collapsed": this.props.collapsed, - "mx_LeftPanel_container_hasTagPanel": tagPanelEnabled, - "mx_fadable_faded": this.props.disabled, - }, - ); - - let exploreButton; - if (!this.props.collapsed) { - exploreButton = ( -
- dis.dispatch({action: 'view_room_directory'})}>{_t("Explore")} -
- ); - } - - const searchBox = (); - - let breadcrumbs; - if (this.state.breadcrumbs) { - breadcrumbs = (); - } - - return ( -
- { tagPanelContainer } - -
- ); - }, -}); - -export default LeftPanel; diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx new file mode 100644 index 0000000000..899dfe222d --- /dev/null +++ b/src/components/structures/LeftPanel.tsx @@ -0,0 +1,430 @@ +/* +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 React from "react"; +import { createRef } from "react"; +import TagPanel from "./TagPanel"; +import CustomRoomTagPanel from "./CustomRoomTagPanel"; +import classNames from "classnames"; +import dis from "../../dispatcher/dispatcher"; +import { _t } from "../../languageHandler"; +import RoomList from "../views/rooms/RoomList"; +import { HEADER_HEIGHT } from "../views/rooms/RoomSublist"; +import { Action } from "../../dispatcher/actions"; +import UserMenu from "./UserMenu"; +import RoomSearch from "./RoomSearch"; +import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs"; +import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; +import { UPDATE_EVENT } from "../../stores/AsyncStore"; +import ResizeNotifier from "../../utils/ResizeNotifier"; +import SettingsStore from "../../settings/SettingsStore"; +import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore"; +import {Key} from "../../Keyboard"; +import IndicatorScrollbar from "../structures/IndicatorScrollbar"; +import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; +import { OwnProfileStore } from "../../stores/OwnProfileStore"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; +import RoomListNumResults from "../views/rooms/RoomListNumResults"; + +interface IProps { + isMinimized: boolean; + resizeNotifier: ResizeNotifier; +} + +interface IState { + showBreadcrumbs: boolean; + showTagPanel: boolean; +} + +// List of CSS classes which should be included in keyboard navigation within the room list +const cssClasses = [ + "mx_RoomSearch_input", + "mx_RoomSearch_icon", // minimized + "mx_RoomSublist_headerText", + "mx_RoomTile", + "mx_RoomSublist_showNButton", +]; + +export default class LeftPanel extends React.Component { + private listContainerRef: React.RefObject = createRef(); + private tagPanelWatcherRef: string; + private bgImageWatcherRef: string; + private focusedElement = null; + private isDoingStickyHeaders = false; + + constructor(props: IProps) { + super(props); + + this.state = { + showBreadcrumbs: BreadcrumbsStore.instance.visible, + showTagPanel: SettingsStore.getValue('TagPanel.enableTagPanel'), + }; + + BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); + RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); + OwnProfileStore.instance.on(UPDATE_EVENT, this.onBackgroundImageUpdate); + this.bgImageWatcherRef = SettingsStore.watchSetting( + "RoomList.backgroundImage", null, this.onBackgroundImageUpdate); + this.tagPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => { + this.setState({showTagPanel: SettingsStore.getValue("TagPanel.enableTagPanel")}); + }); + + // We watch the middle panel because we don't actually get resized, the middle panel does. + // We listen to the noisy channel to avoid choppy reaction times. + this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize); + } + + public componentWillUnmount() { + SettingsStore.unwatchSetting(this.tagPanelWatcherRef); + SettingsStore.unwatchSetting(this.bgImageWatcherRef); + BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); + RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); + OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate); + this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); + } + + private onExplore = () => { + dis.fire(Action.ViewRoomDirectory); + }; + + private onBreadcrumbsUpdate = () => { + const newVal = BreadcrumbsStore.instance.visible; + if (newVal !== this.state.showBreadcrumbs) { + this.setState({showBreadcrumbs: newVal}); + + // Update the sticky headers too as the breadcrumbs will be popping in or out. + if (!this.listContainerRef.current) return; // ignore: no headers to sticky + this.handleStickyHeaders(this.listContainerRef.current); + } + }; + + private onBackgroundImageUpdate = () => { + // Note: we do this in the LeftPanel as it uses this variable most prominently. + const avatarSize = 32; // arbitrary + let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize); + const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage"); + if (settingBgMxc) { + avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(settingBgMxc, avatarSize, avatarSize); + } + const avatarUrlProp = `url(${avatarUrl})`; + if (document.body.style.getPropertyValue("--avatar-url") !== avatarUrlProp) { + document.body.style.setProperty("--avatar-url", avatarUrlProp); + } + }; + + private handleStickyHeaders(list: HTMLDivElement) { + if (this.isDoingStickyHeaders) return; + this.isDoingStickyHeaders = true; + window.requestAnimationFrame(() => { + this.doStickyHeaders(list); + this.isDoingStickyHeaders = false; + }); + } + + private doStickyHeaders(list: HTMLDivElement) { + const topEdge = list.scrollTop; + const bottomEdge = list.offsetHeight + list.scrollTop; + const sublists = list.querySelectorAll(".mx_RoomSublist"); + + const headerRightMargin = 16; // calculated from margins and widths to align with non-sticky tiles + const headerStickyWidth = list.clientWidth - headerRightMargin; + + // We track which styles we want on a target before making the changes to avoid + // excessive layout updates. + const targetStyles = new Map(); + + let lastTopHeader; + let firstBottomHeader; + for (const sublist of sublists) { + const header = sublist.querySelector(".mx_RoomSublist_stickable"); + header.style.removeProperty("display"); // always clear display:none first + + // When an element is <=40% off screen, make it take over + const offScreenFactor = 0.4; + const isOffTop = (sublist.offsetTop + (offScreenFactor * HEADER_HEIGHT)) <= topEdge; + const isOffBottom = (sublist.offsetTop + (offScreenFactor * HEADER_HEIGHT)) >= bottomEdge; + + if (isOffTop || sublist === sublists[0]) { + targetStyles.set(header, { stickyTop: true }); + if (lastTopHeader) { + lastTopHeader.style.display = "none"; + targetStyles.set(lastTopHeader, { makeInvisible: true }); + } + lastTopHeader = header; + } else if (isOffBottom && !firstBottomHeader) { + targetStyles.set(header, { stickyBottom: true }); + firstBottomHeader = header; + } else { + targetStyles.set(header, {}); // nothing == clear + } + } + + // Run over the style changes and make them reality. We check to see if we're about to + // cause a no-op update, as adding/removing properties that are/aren't there cause + // layout updates. + for (const header of targetStyles.keys()) { + const style = targetStyles.get(header); + + if (style.makeInvisible) { + // we will have already removed the 'display: none', so add it back. + header.style.display = "none"; + continue; // nothing else to do, even if sticky somehow + } + + if (style.stickyTop) { + if (!header.classList.contains("mx_RoomSublist_headerContainer_stickyTop")) { + header.classList.add("mx_RoomSublist_headerContainer_stickyTop"); + } + + const newTop = `${list.parentElement.offsetTop}px`; + if (header.style.top !== newTop) { + header.style.top = newTop; + } + } else { + if (header.classList.contains("mx_RoomSublist_headerContainer_stickyTop")) { + header.classList.remove("mx_RoomSublist_headerContainer_stickyTop"); + } + if (header.style.top) { + header.style.removeProperty('top'); + } + } + + if (style.stickyBottom) { + if (!header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) { + header.classList.add("mx_RoomSublist_headerContainer_stickyBottom"); + } + } else { + if (header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) { + header.classList.remove("mx_RoomSublist_headerContainer_stickyBottom"); + } + } + + if (style.stickyTop || style.stickyBottom) { + if (!header.classList.contains("mx_RoomSublist_headerContainer_sticky")) { + header.classList.add("mx_RoomSublist_headerContainer_sticky"); + } + + const newWidth = `${headerStickyWidth}px`; + if (header.style.width !== newWidth) { + header.style.width = newWidth; + } + } else if (!style.stickyTop && !style.stickyBottom) { + if (header.classList.contains("mx_RoomSublist_headerContainer_sticky")) { + header.classList.remove("mx_RoomSublist_headerContainer_sticky"); + } + if (header.style.width) { + header.style.removeProperty('width'); + } + } + } + + // add appropriate sticky classes to wrapper so it has + // the necessary top/bottom padding to put the sticky header in + const listWrapper = list.parentElement; // .mx_LeftPanel_roomListWrapper + if (lastTopHeader) { + listWrapper.classList.add("mx_LeftPanel_roomListWrapper_stickyTop"); + } else { + listWrapper.classList.remove("mx_LeftPanel_roomListWrapper_stickyTop"); + } + if (firstBottomHeader) { + listWrapper.classList.add("mx_LeftPanel_roomListWrapper_stickyBottom"); + } else { + listWrapper.classList.remove("mx_LeftPanel_roomListWrapper_stickyBottom"); + } + } + + private onScroll = (ev: React.MouseEvent) => { + const list = ev.target as HTMLDivElement; + this.handleStickyHeaders(list); + }; + + private onResize = () => { + if (!this.listContainerRef.current) return; // ignore: no headers to sticky + this.handleStickyHeaders(this.listContainerRef.current); + }; + + private onFocus = (ev: React.FocusEvent) => { + this.focusedElement = ev.target; + }; + + private onBlur = () => { + this.focusedElement = null; + }; + + private onKeyDown = (ev: React.KeyboardEvent) => { + if (!this.focusedElement) return; + + switch (ev.key) { + case Key.ARROW_UP: + case Key.ARROW_DOWN: + ev.stopPropagation(); + ev.preventDefault(); + this.onMoveFocus(ev.key === Key.ARROW_UP); + break; + } + }; + + private onEnter = () => { + const firstRoom = this.listContainerRef.current.querySelector(".mx_RoomTile"); + if (firstRoom) { + firstRoom.click(); + return true; // to get the field to clear + } + }; + + private onMoveFocus = (up: boolean) => { + let element = this.focusedElement; + + let descending = false; // are we currently descending or ascending through the DOM tree? + let classes: DOMTokenList; + + do { + const child = up ? element.lastElementChild : element.firstElementChild; + const sibling = up ? element.previousElementSibling : element.nextElementSibling; + + if (descending) { + if (child) { + element = child; + } else if (sibling) { + element = sibling; + } else { + descending = false; + element = element.parentElement; + } + } else { + if (sibling) { + element = sibling; + descending = true; + } else { + element = element.parentElement; + } + } + + if (element) { + classes = element.classList; + } + } while (element && !cssClasses.some(c => classes.contains(c))); + + if (element) { + element.focus(); + this.focusedElement = element; + } + }; + + private renderHeader(): React.ReactNode { + return ( +
+ +
+ ); + } + + private renderBreadcrumbs(): React.ReactNode { + if (this.state.showBreadcrumbs && !this.props.isMinimized) { + return ( + + + + ); + } + } + + private renderSearchExplore(): React.ReactNode { + return ( +
+ + +
+ ); + } + + public render(): React.ReactNode { + const tagPanel = !this.state.showTagPanel ? null : ( +
+ + {SettingsStore.getValue("feature_custom_tags") ? : null} +
+ ); + + const roomList = ; + + const containerClasses = classNames({ + "mx_LeftPanel": true, + "mx_LeftPanel_hasTagPanel": !!tagPanel, + "mx_LeftPanel_minimized": this.props.isMinimized, + }); + + const roomListClasses = classNames( + "mx_LeftPanel_actualRoomListContainer", + "mx_AutoHideScrollbar", + ); + + return ( +
+ {tagPanel} + +
+ ); + } +} diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 9de2aac8e9..d7f2c73a0b 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -19,7 +19,6 @@ limitations under the License. import * as React from 'react'; import * as PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk/src/client'; -import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { DragDropContext } from 'react-beautiful-dnd'; import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent} from '../../Keyboard'; @@ -27,11 +26,10 @@ import PageTypes from '../../PageTypes'; import CallMediaHandler from '../../CallMediaHandler'; import { fixupColorFonts } from '../../utils/FontManager'; import * as sdk from '../../index'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import sessionStore from '../../stores/SessionStore'; -import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg'; +import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg'; import SettingsStore from "../../settings/SettingsStore"; -import RoomListStore from "../../stores/RoomListStore"; import TagOrderActions from '../../actions/TagOrderActions'; import RoomListActions from '../../actions/RoomListActions'; @@ -42,6 +40,23 @@ import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts"; import HomePage from "./HomePage"; import ResizeNotifier from "../../utils/ResizeNotifier"; import PlatformPeg from "../../PlatformPeg"; +import { DefaultTagID } from "../../stores/room-list/models"; +import { + showToast as showSetPasswordToast, + hideToast as hideSetPasswordToast +} from "../../toasts/SetPasswordToast"; +import { + showToast as showServerLimitToast, + hideToast as hideServerLimitToast +} from "../../toasts/ServerLimitToast"; +import { Action } from "../../dispatcher/actions"; +import LeftPanel from "./LeftPanel"; +import CallContainer from '../views/voip/CallContainer'; +import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload"; +import RoomListStore from "../../stores/room-list/RoomListStore"; +import NonUrgentToastContainer from "./NonUrgentToastContainer"; +import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload"; + // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. // NB. this is just for server notices rather than pinned messages in general. @@ -56,7 +71,7 @@ function canElementReceiveInput(el) { interface IProps { matrixClient: MatrixClient; - onRegistered: (credentials: MatrixClientCreds) => Promise; + onRegistered: (credentials: IMatrixClientCreds) => Promise; viaServers?: string[]; hideToSRUsers: boolean; resizeNotifier: ResizeNotifier; @@ -64,10 +79,6 @@ interface IProps { initialEventPixelOffset: number; leftDisabled: boolean; rightDisabled: boolean; - showCookieBar: boolean; - hasNewVersion: boolean; - userHasGeneratedPassword: boolean; - showNotifierToolbar: boolean; page_type: string; autoJoin: boolean; thirdPartyInvite?: object; @@ -75,7 +86,6 @@ interface IProps { currentRoomId: string; ConferenceHandler?: object; collapseLhs: boolean; - checkingForUpdate: boolean; config: { piwik: { policyUrl: string; @@ -85,19 +95,26 @@ interface IProps { currentUserId?: string; currentGroupId?: string; currentGroupIsNew?: boolean; - version?: string; - newVersion?: string; - newVersionReleaseNotes?: string; } + +interface IUsageLimit { + limit_type: "monthly_active_user" | string; + admin_contact?: string; +} + interface IState { mouseDown?: { x: number; y: number; }; - syncErrorData: any; + syncErrorData?: { + error: { + data: IUsageLimit; + errcode: string; + }; + }; + usageLimitEventContent?: IUsageLimit; useCompactLayout: boolean; - serverNoticeEvents: MatrixEvent[]; - userHasGeneratedPassword: boolean; } /** @@ -109,7 +126,7 @@ interface IState { * * Components mounted below us can access the matrix client via the react context. */ -class LoggedInView extends React.PureComponent { +class LoggedInView extends React.Component { static displayName = 'LoggedInView'; static propTypes = { @@ -132,6 +149,7 @@ class LoggedInView extends React.PureComponent { protected readonly _resizeContainer: React.RefObject; protected readonly _sessionStore: sessionStore; protected readonly _sessionStoreToken: { remove: () => void }; + protected readonly _compactLayoutWatcherRef: string; protected resizer: Resizer; constructor(props, context) { @@ -140,11 +158,8 @@ class LoggedInView extends React.PureComponent { this.state = { mouseDown: undefined, syncErrorData: undefined, - userHasGeneratedPassword: false, // use compact timeline view useCompactLayout: SettingsStore.getValue('useCompactLayout'), - // any currently active server notice events - serverNoticeEvents: [], }; // stash the MatrixClient in case we log out before we are unmounted @@ -166,6 +181,10 @@ class LoggedInView extends React.PureComponent { this._matrixClient.on("sync", this.onSync); this._matrixClient.on("RoomState.events", this.onRoomStateEvents); + this._compactLayoutWatcherRef = SettingsStore.watchSetting( + "useCompactLayout", null, this.onCompactLayoutChanged, + ); + fixupColorFonts(); this._roomView = React.createRef(); @@ -178,23 +197,12 @@ class LoggedInView extends React.PureComponent { this._loadResizerPreferences(); } - componentDidUpdate(prevProps, prevState) { - // attempt to guess when a banner was opened or closed - if ( - (prevProps.showCookieBar !== this.props.showCookieBar) || - (prevProps.hasNewVersion !== this.props.hasNewVersion) || - (prevState.userHasGeneratedPassword !== this.state.userHasGeneratedPassword) || - (prevProps.showNotifierToolbar !== this.props.showNotifierToolbar) - ) { - this.props.resizeNotifier.notifyBannersChanged(); - } - } - componentWillUnmount() { document.removeEventListener('keydown', this._onNativeKeyDown, false); this._matrixClient.removeListener("accountData", this.onAccountData); this._matrixClient.removeListener("sync", this.onSync); this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents); + SettingsStore.unwatchSetting(this._compactLayoutWatcherRef); if (this._sessionStoreToken) { this._sessionStoreToken.remove(); } @@ -219,9 +227,11 @@ class LoggedInView extends React.PureComponent { }; _setStateFromSessionStore = () => { - this.setState({ - userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()), - }); + if (this._sessionStore.getCachedPassword()) { + showSetPasswordToast(); + } else { + hideSetPasswordToast(); + } }; _createResizer() { @@ -262,16 +272,17 @@ class LoggedInView extends React.PureComponent { } onAccountData = (event) => { - if (event.getType() === "im.vector.web.settings") { - this.setState({ - useCompactLayout: event.getContent().useCompactLayout, - }); - } if (event.getType() === "m.ignored_user_list") { dis.dispatch({action: "ignore_state_changed"}); } }; + onCompactLayoutChanged = (setting, roomId, level, valueAtLevel, newValue) => { + this.setState({ + useCompactLayout: valueAtLevel, + }); + }; + onSync = (syncState, oldSyncState, data) => { const oldErrCode = ( this.state.syncErrorData && @@ -293,22 +304,37 @@ class LoggedInView extends React.PureComponent { if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') { this._updateServerNoticeEvents(); + } else { + this._calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent); } }; onRoomStateEvents = (ev, state) => { - const roomLists = RoomListStore.getRoomLists(); - if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) { + const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice]; + if (serverNoticeList && serverNoticeList.some(r => r.roomId === ev.getRoomId())) { this._updateServerNoticeEvents(); } }; - _updateServerNoticeEvents = async () => { - const roomLists = RoomListStore.getRoomLists(); - if (!roomLists['m.server_notice']) return []; + _calculateServerLimitToast(syncErrorData: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) { + const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED"; + if (error) { + usageLimitEventContent = syncErrorData.error.data; + } - const pinnedEvents = []; - for (const room of roomLists['m.server_notice']) { + if (usageLimitEventContent) { + showServerLimitToast(usageLimitEventContent.limit_type, usageLimitEventContent.admin_contact, error); + } else { + hideServerLimitToast(); + } + } + + _updateServerNoticeEvents = async () => { + const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice]; + if (!serverNoticeList) return []; + + const events = []; + for (const room of serverNoticeList) { const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", ""); if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue; @@ -317,12 +343,19 @@ class LoggedInView extends React.PureComponent { for (const eventId of pinnedEventIds) { const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0); const event = timeline.getEvents().find(ev => ev.getId() === eventId); - if (event) pinnedEvents.push(event); + if (event) events.push(event); } } - this.setState({ - serverNoticeEvents: pinnedEvents, + + const usageLimitEvent = events.find((e) => { + return ( + e && e.getType() === 'm.room.message' && + e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached' + ); }); + const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent(); + this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent); + this.setState({ usageLimitEventContent }); }; _onPaste = (ev) => { @@ -337,7 +370,7 @@ class LoggedInView extends React.PureComponent { // refocusing during a paste event will make the // paste end up in the newly focused element, // so dispatch synchronously before paste happens - dis.dispatch({action: 'focus_composer'}, true); + dis.fire(Action.FocusComposer, true); } }; @@ -379,20 +412,6 @@ class LoggedInView extends React.PureComponent { }; _onKeyDown = (ev) => { - /* - // Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers - // Will need to find a better meta key if anyone actually cares about using this. - if (ev.altKey && ev.ctrlKey && ev.keyCode > 48 && ev.keyCode < 58) { - dis.dispatch({ - action: 'view_indexed_room', - roomIndex: ev.keyCode - 49, - }); - ev.stopPropagation(); - ev.preventDefault(); - return; - } - */ - let handled = false; const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev); const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey; @@ -429,9 +448,7 @@ class LoggedInView extends React.PureComponent { // composer, so CTRL+` it is if (ctrlCmdOnly) { - dis.dispatch({ - action: 'toggle_top_left_menu', - }); + dis.fire(Action.ToggleUserMenu); handled = true; } break; @@ -446,8 +463,8 @@ class LoggedInView extends React.PureComponent { case Key.ARROW_UP: case Key.ARROW_DOWN: if (ev.altKey && !ev.ctrlKey && !ev.metaKey) { - dis.dispatch({ - action: 'view_room_delta', + dis.dispatch({ + action: Action.ViewRoomDelta, delta: ev.key === Key.ARROW_UP ? -1 : 1, unread: ev.shiftKey, }); @@ -457,8 +474,8 @@ class LoggedInView extends React.PureComponent { case Key.PERIOD: if (ctrlCmdOnly && (this.props.page_type === "room_view" || this.props.page_type === "group_view")) { - dis.dispatch({ - action: 'toggle_right_panel', + dis.dispatch({ + action: Action.ToggleRightPanel, type: this.props.page_type === "room_view" ? "room" : "group", }); handled = true; @@ -487,7 +504,7 @@ class LoggedInView extends React.PureComponent { if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) { // synchronous dispatch so we focus before key generates input - dis.dispatch({action: 'focus_composer'}, true); + dis.fire(Action.FocusComposer, true); ev.stopPropagation(); // we should *not* preventDefault() here as // that would prevent typing in the now-focussed composer @@ -579,7 +596,7 @@ class LoggedInView extends React.PureComponent { const maxRadius = 5; // People shouldn't be straying too far, hopefully // Note: we track how far the user moved their mouse to help - // combat against https://github.com/vector-im/riot-web/issues/7158 + // combat against https://github.com/vector-im/element-web/issues/7158 if (distance < maxRadius) { // This is probably a real click, and not a drag @@ -592,18 +609,11 @@ class LoggedInView extends React.PureComponent { }; render() { - const LeftPanel = sdk.getComponent('structures.LeftPanel'); const RoomView = sdk.getComponent('structures.RoomView'); const UserView = sdk.getComponent('structures.UserView'); const GroupView = sdk.getComponent('structures.GroupView'); const MyGroups = sdk.getComponent('structures.MyGroups'); const ToastContainer = sdk.getComponent('structures.ToastContainer'); - const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); - const CookieBar = sdk.getComponent('globals.CookieBar'); - const NewVersionBar = sdk.getComponent('globals.NewVersionBar'); - const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar'); - const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar'); - const ServerLimitBar = sdk.getComponent('globals.ServerLimitBar'); let pageElement; @@ -647,50 +657,18 @@ class LoggedInView extends React.PureComponent { break; } - const usageLimitEvent = this.state.serverNoticeEvents.find((e) => { - return ( - e && e.getType() === 'm.room.message' && - e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached' - ); - }); - - let topBar; - if (this.state.syncErrorData && this.state.syncErrorData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { - topBar = ; - } else if (usageLimitEvent) { - topBar = ; - } else if (this.props.showCookieBar && - this.props.config.piwik && - navigator.doNotTrack !== "1" - ) { - const policyUrl = this.props.config.piwik.policyUrl || null; - topBar = ; - } else if (this.props.hasNewVersion) { - topBar = ; - } else if (this.props.checkingForUpdate) { - topBar = ; - } else if (this.state.userHasGeneratedPassword) { - topBar = ; - } else if (this.props.showNotifierToolbar) { - topBar = ; - } - let bodyClasses = 'mx_MatrixChat'; - if (topBar) { - bodyClasses += ' mx_MatrixChat_toolbarShowing'; - } if (this.state.useCompactLayout) { bodyClasses += ' mx_MatrixChat_useCompactLayout'; } + const leftPanel = ( + + ); + return (
{ onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp} > - { topBar }
- + { leftPanel } { pageElement }
+ +
); } diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.js index 7c66f21a04..800ed76bb9 100644 --- a/src/components/structures/MainSplit.js +++ b/src/components/structures/MainSplit.js @@ -16,77 +16,24 @@ limitations under the License. */ import React from 'react'; -import ResizeHandle from '../views/elements/ResizeHandle'; -import {Resizer, FixedDistributor} from '../../resizer'; +import { Resizable } from 're-resizable'; export default class MainSplit extends React.Component { - constructor(props) { - super(props); - this._setResizeContainerRef = this._setResizeContainerRef.bind(this); - this._onResized = this._onResized.bind(this); + _onResized = (event, direction, refToElement, delta) => { + window.localStorage.setItem("mx_rhs_size", this._loadSidePanelSize().width + delta.width); } - _onResized(size) { - window.localStorage.setItem("mx_rhs_size", size); - if (this.props.resizeNotifier) { - this.props.resizeNotifier.notifyRightHandleResized(); - } - } + _loadSidePanelSize() { + let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10); - _createResizer() { - const classNames = { - handle: "mx_ResizeHandle", - vertical: "mx_ResizeHandle_vertical", - reverse: "mx_ResizeHandle_reverse", - }; - const resizer = new Resizer( - this.resizeContainer, - FixedDistributor, - {onResized: this._onResized}, - ); - resizer.setClassNames(classNames); - let rhsSize = window.localStorage.getItem("mx_rhs_size"); - if (rhsSize !== null) { - rhsSize = parseInt(rhsSize, 10); - } else { + if (isNaN(rhsSize)) { rhsSize = 350; } - resizer.forHandleAt(0).resize(rhsSize); - resizer.attach(); - this.resizer = resizer; - } - - _setResizeContainerRef(div) { - this.resizeContainer = div; - } - - componentDidMount() { - if (this.props.panel) { - this._createResizer(); - } - } - - componentWillUnmount() { - if (this.resizer) { - this.resizer.detach(); - this.resizer = null; - } - } - - componentDidUpdate(prevProps) { - const wasPanelSet = this.props.panel && !prevProps.panel; - const wasPanelCleared = !this.props.panel && prevProps.panel; - - if (this.resizeContainer && wasPanelSet) { - // The resizer can only be created when **both** expanded and the panel is - // set. Once both are true, the container ref will mount, which is required - // for the resizer to work. - this._createResizer(); - } else if (this.resizer && wasPanelCleared) { - this.resizer.detach(); - this.resizer = null; - } + return { + height: "100%", + width: rhsSize, + }; } render() { @@ -97,13 +44,29 @@ export default class MainSplit extends React.Component { let children; if (hasResizer) { - children = - + children = { panelView } - ; + ; } - return
+ return
{ bodyView } { children }
; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 05354fa5f2..ce96847d28 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -17,12 +17,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef} from 'react'; -import {InvalidStoreError} from "matrix-js-sdk/src/errors"; -import {RoomMember} from "matrix-js-sdk/src/models/room-member"; -import {MatrixEvent} from "matrix-js-sdk/src/models/event"; -import { isCryptoAvailable } from 'matrix-js-sdk/src/crypto'; - +import React, { createRef } from 'react'; +// @ts-ignore - XXX: no idea why this import fails +import * as Matrix from "matrix-js-sdk"; +import { InvalidStoreError } from "matrix-js-sdk/src/errors"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; // focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss import 'focus-visible'; // what-input helps improve keyboard accessibility @@ -30,17 +30,17 @@ import 'what-input'; import Analytics from "../../Analytics"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; -import {MatrixClientPeg} from "../../MatrixClientPeg"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; import PlatformPeg from "../../PlatformPeg"; import SdkConfig from "../../SdkConfig"; import * as RoomListSorter from "../../RoomListSorter"; -import dis from "../../dispatcher"; +import dis from "../../dispatcher/dispatcher"; import Notifier from '../../Notifier'; import Modal from "../../Modal"; import Tinter from "../../Tinter"; import * as sdk from '../../index'; -import { showStartChatInviteDialog, showRoomInviteDialog } from '../../RoomInvite'; +import { showRoomInviteDialog, showStartChatInviteDialog } from '../../RoomInvite'; import * as Rooms from '../../Rooms'; import linkifyMatrix from "../../linkify-matrix"; import * as Lifecycle from '../../Lifecycle'; @@ -50,23 +50,32 @@ import PageTypes from '../../PageTypes'; import { getHomePageUrl } from '../../utils/pages'; import createRoom from "../../createRoom"; -import KeyRequestHandler from '../../KeyRequestHandler'; -import { _t, getCurrentLanguage } from '../../languageHandler'; -import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; +import {_t, _td, getCurrentLanguage} from '../../languageHandler'; +import SettingsStore from "../../settings/SettingsStore"; import ThemeController from "../../settings/controllers/ThemeController"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; import ResizeNotifier from "../../utils/ResizeNotifier"; -import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; -import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; +import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; import DMRoomMap from '../../utils/DMRoomMap'; -import { countRoomsWithNotif } from '../../RoomNotifs'; -import { ThemeWatcher } from "../../theme"; +import ThemeWatcher from "../../settings/watchers/ThemeWatcher"; +import { FontWatcher } from '../../settings/watchers/FontWatcher'; import { storeRoomAliasInCache } from '../../RoomAliasCache'; -import {defer, IDeferred} from "../../utils/promise"; +import { defer, IDeferred } from "../../utils/promise"; import ToastStore from "../../stores/ToastStore"; import * as StorageManager from "../../utils/StorageManager"; import type LoggedInViewType from "./LoggedInView"; +import { ViewUserPayload } from "../../dispatcher/payloads/ViewUserPayload"; +import { Action } from "../../dispatcher/actions"; +import { + showToast as showAnalyticsToast, + hideToast as hideAnalyticsToast +} from "../../toasts/AnalyticsToast"; +import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast"; +import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; +import ErrorDialog from "../views/dialogs/ErrorDialog"; +import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore"; +import { SettingLevel } from "../../settings/SettingLevel"; /** constants for MatrixChat.state.view */ export enum Views { @@ -107,7 +116,7 @@ export enum Views { // re-dispatched. NOTE: some actions are non-trivial and would require // re-factoring to be included in this list in future. const ONBOARDING_FLOW_STARTERS = [ - 'view_user_settings', + Action.ViewUserSettings, 'view_create_chat', 'view_create_room', 'view_create_group', @@ -145,9 +154,9 @@ interface IProps { // TODO type things better // Represents the screen to display as a result of parsing the initial window.location initialScreenAfterLogin?: IScreen; // displayname, if any, to set on the device when logging in/registering. - defaultDeviceDisplayName?: string, + defaultDeviceDisplayName?: string; // A function that makes a registration URL - makeRegistrationUrl: (object) => string, + makeRegistrationUrl: (object) => string; } interface IState { @@ -168,12 +177,6 @@ interface IState { leftDisabled: boolean; middleDisabled: boolean; // the right panel's disabled state is tracked in its store. - version?: string; - newVersion?: string; - hasNewVersion: boolean; - newVersionReleaseNotes?: string; - checkingForUpdate?: string; // updateCheckStatusEnum - showCookieBar: boolean; // Parameters used in the registration dance with the IS register_client_secret?: string; register_session_id?: string; @@ -183,7 +186,6 @@ interface IState { hideToSRUsers: boolean; syncError?: Error; resizeNotifier: ResizeNotifier; - showNotifierToolbar: boolean; serverConfig?: ValidatedServerConfig; ready: boolean; thirdPartyInvite?: object; @@ -216,6 +218,7 @@ export default class MatrixChat extends React.PureComponent { private readonly loggedInView: React.RefObject; private readonly dispatcherRef: any; private readonly themeWatcher: ThemeWatcher; + private readonly fontWatcher: FontWatcher; constructor(props, context) { super(props, context); @@ -226,17 +229,10 @@ export default class MatrixChat extends React.PureComponent { leftDisabled: false, middleDisabled: false, - hasNewVersion: false, - newVersionReleaseNotes: null, - checkingForUpdate: null, - - showCookieBar: false, - hideToSRUsers: false, syncError: null, // If the current syncing status is ERROR, the error object, otherwise null. resizeNotifier: new ResizeNotifier(), - showNotifierToolbar: false, ready: false, }; @@ -283,8 +279,11 @@ export default class MatrixChat extends React.PureComponent { this.accountPasswordTimer = null; this.dispatcherRef = dis.register(this.onAction); + this.themeWatcher = new ThemeWatcher(); + this.fontWatcher = new FontWatcher(); this.themeWatcher.start(); + this.fontWatcher.start(); this.focusComposer = false; @@ -334,12 +333,6 @@ export default class MatrixChat extends React.PureComponent { }); } - if (SettingsStore.getValue("showCookieBar")) { - this.setState({ - showCookieBar: true, - }); - } - if (SettingsStore.getValue("analyticsOptIn")) { Analytics.enable(); } @@ -358,7 +351,7 @@ export default class MatrixChat extends React.PureComponent { Analytics.trackPageChange(durationMs); } if (this.focusComposer) { - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); this.focusComposer = false; } } @@ -367,6 +360,7 @@ export default class MatrixChat extends React.PureComponent { Lifecycle.stopMatrixClient(); dis.unregister(this.dispatcherRef); this.themeWatcher.stop(); + this.fontWatcher.stop(); window.removeEventListener('resize', this.handleResize); this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize); @@ -421,7 +415,7 @@ export default class MatrixChat extends React.PureComponent { return; } this.pageChanging = true; - performance.mark('riot_MatrixChat_page_change_start'); + performance.mark('element_MatrixChat_page_change_start'); } stopPageChangeTimer() { @@ -433,15 +427,15 @@ export default class MatrixChat extends React.PureComponent { return; } this.pageChanging = false; - performance.mark('riot_MatrixChat_page_change_stop'); + performance.mark('element_MatrixChat_page_change_stop'); performance.measure( - 'riot_MatrixChat_page_change_delta', - 'riot_MatrixChat_page_change_start', - 'riot_MatrixChat_page_change_stop', + 'element_MatrixChat_page_change_delta', + 'element_MatrixChat_page_change_start', + 'element_MatrixChat_page_change_stop', ); - performance.clearMarks('riot_MatrixChat_page_change_start'); - performance.clearMarks('riot_MatrixChat_page_change_stop'); - const measurement = performance.getEntriesByName('riot_MatrixChat_page_change_delta').pop(); + performance.clearMarks('element_MatrixChat_page_change_start'); + performance.clearMarks('element_MatrixChat_page_change_stop'); + const measurement = performance.getEntriesByName('element_MatrixChat_page_change_delta').pop(); // In practice, sometimes the entries list is empty, so we get no measurement if (!measurement) return null; @@ -468,7 +462,6 @@ export default class MatrixChat extends React.PureComponent { onAction = (payload) => { // console.log(`MatrixClientPeg.onAction: ${payload.action}`); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); // Start the onboarding process for certain actions @@ -562,6 +555,9 @@ export default class MatrixChat extends React.PureComponent { case 'leave_room': this.leaveRoom(payload.room_id); break; + case 'forget_room': + this.forgetRoom(payload.room_id); + break; case 'reject_invite': Modal.createTrackedDialog('Reject invitation', '', QuestionDialog, { title: _t('Reject invitation'), @@ -604,33 +600,30 @@ export default class MatrixChat extends React.PureComponent { } break; } - case 'view_prev_room': - this.viewNextRoom(-1); - break; case 'view_next_room': this.viewNextRoom(1); break; - case 'view_indexed_room': - this.viewIndexedRoom(payload.roomIndex); - break; - case 'view_user_settings': { + case Action.ViewUserSettings: { + const tabPayload = payload as OpenToTabPayload; const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog"); - Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, - /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); + Modal.createTrackedDialog('User settings', '', UserSettingsDialog, + {initialTabId: tabPayload.initialTabId}, + /*className=*/null, /*isPriority=*/false, /*isStatic=*/true + ); // View the welcome or home page if we need something to look at this.viewSomethingBehindModal(); break; } case 'view_create_room': - this.createRoom(); + this.createRoom(payload.public); break; case 'view_create_group': { const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog"); Modal.createTrackedDialog('Create Community', '', CreateGroupDialog); break; } - case 'view_room_directory': { + case Action.ViewRoomDirectory: { const RoomDirectory = sdk.getComponent("structures.RoomDirectory"); Modal.createTrackedDialog('Room directory', '', RoomDirectory, {}, 'mx_RoomDirectory_dialogWrapper', false, true); @@ -680,18 +673,19 @@ export default class MatrixChat extends React.PureComponent { dis.dispatch({action: 'view_my_groups'}); } break; - case 'notifier_enabled': - this.setState({showNotifierToolbar: Notifier.shouldShowToolbar()}); - break; case 'hide_left_panel': this.setState({ collapseLhs: true, + }, () => { + this.state.resizeNotifier.notifyLeftHandleResized(); }); break; case 'focus_room_filter': // for CtrlOrCmd+K to work by expanding the left panel first case 'show_left_panel': this.setState({ collapseLhs: false, + }, () => { + this.state.resizeNotifier.notifyLeftHandleResized(); }); break; case 'panel_disable': { @@ -730,15 +724,6 @@ export default class MatrixChat extends React.PureComponent { case 'client_started': this.onClientStarted(); break; - case 'new_version': - this.onVersion( - payload.currentVersion, payload.newVersion, - payload.releaseNotes, - ); - break; - case 'check_updates': - this.setState({ checkingForUpdate: payload.value }); - break; case 'send_event': this.onSendEvent(payload.room_id, payload.event); break; @@ -755,19 +740,13 @@ export default class MatrixChat extends React.PureComponent { case 'accept_cookies': SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, true); SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false); - - this.setState({ - showCookieBar: false, - }); + hideAnalyticsToast(); Analytics.enable(); break; case 'reject_cookies': SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false); SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false); - - this.setState({ - showCookieBar: false, - }); + hideAnalyticsToast(); break; } }; @@ -835,19 +814,6 @@ export default class MatrixChat extends React.PureComponent { }); } - // TODO: Move to RoomViewStore - private viewIndexedRoom(roomIndex: number) { - const allRooms = RoomListSorter.mostRecentActivityFirst( - MatrixClientPeg.get().getRooms(), - ); - if (allRooms[roomIndex]) { - dis.dispatch({ - action: 'view_room', - room_id: allRooms[roomIndex].roomId, - }); - } - } - // switch view to the given room // // @param {Object} roomInfo Object containing data about the room to be joined @@ -926,9 +892,20 @@ export default class MatrixChat extends React.PureComponent { }); } - private viewGroup(payload) { + private async viewGroup(payload) { const groupId = payload.group_id; + + // Wait for the first sync to complete + if (!this.firstSyncComplete) { + if (!this.firstSyncPromise) { + console.warn('Cannot view a group before first sync. group_id:', groupId); + return; + } + await this.firstSyncPromise.promise; + } + this.setState({ + view: Views.LOGGED_IN, currentGroupId: groupId, currentGroupIsNew: payload.group_is_new, }); @@ -1012,9 +989,9 @@ export default class MatrixChat extends React.PureComponent { }).close; } - private async createRoom() { + private async createRoom(defaultPublic = false) { const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog'); - const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog); + const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { defaultPublic }); const [shouldCreate, opts] = await modal.finished; if (shouldCreate) { @@ -1091,7 +1068,6 @@ export default class MatrixChat extends React.PureComponent { private leaveRoom(roomId: string) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const roomToLeave = MatrixClientPeg.get().getRoom(roomId); const warnings = this.leaveRoomWarnings(roomId); @@ -1155,6 +1131,21 @@ export default class MatrixChat extends React.PureComponent { }); } + private forgetRoom(roomId: string) { + MatrixClientPeg.get().forget(roomId).then(() => { + // Switch to another room view if we're currently viewing the historical room + if (this.state.currentRoomId === roomId) { + dis.dispatch({ action: "view_next_room" }); + } + }).catch((err) => { + const errCode = err.errcode || _td("unknown error code"); + Modal.createTrackedDialog("Failed to forget room", '', ErrorDialog, { + title: _t("Failed to forget room %(errCode)s", {errCode}), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + }); + } + /** * Starts a chat with the welcome user, if the user doesn't already have one * @returns {string} The room ID of the new room, or null if no room was created @@ -1245,6 +1236,10 @@ export default class MatrixChat extends React.PureComponent { } StorageManager.tryPersistStorage(); + + if (SettingsStore.getValue("showCookieBar") && this.props.config.piwik && navigator.doNotTrack !== "1") { + showAnalyticsToast(this.props.config.piwik && this.props.config.piwik.policyUrl); + } } private showScreenAfterLogin() { @@ -1328,7 +1323,7 @@ export default class MatrixChat extends React.PureComponent { // state (each of which can be 10s of MBs) for each DISJOINT timeline. This is // particularly noticeable when there are lots of 'limited' /sync responses // such as when laptops unsleep. - // https://github.com/vector-im/riot-web/issues/3307#issuecomment-282895568 + // https://github.com/vector-im/element-web/issues/3307#issuecomment-282895568 cli.setCanResetTimelineCallback((roomId) => { console.log("Request to reset timeline in room ", roomId, " viewing:", this.state.currentRoomId); if (roomId !== this.state.currentRoomId) { @@ -1339,7 +1334,7 @@ export default class MatrixChat extends React.PureComponent { // this if we are not scrolled up in the view. To find out, delegate to // the timeline panel. If the timeline panel doesn't exist, then we assume // it is safe to reset the timeline. - if (!this.loggedInView.current || !this.loggedInView.current) { + if (!this.loggedInView.current) { return true; } return this.loggedInView.current.canResetTimelineInRoom(roomId); @@ -1372,10 +1367,13 @@ export default class MatrixChat extends React.PureComponent { this.firstSyncComplete = true; this.firstSyncPromise.resolve(); - dis.dispatch({action: 'focus_composer'}); + if (Notifier.shouldShowToolbar()) { + showNotificationsToast(); + } + + dis.fire(Action.FocusComposer); this.setState({ ready: true, - showNotifierToolbar: Notifier.shouldShowToolbar(), }); }); cli.on('Call.incoming', function(call) { @@ -1396,7 +1394,6 @@ export default class MatrixChat extends React.PureComponent { return; } - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Signed out', '', ErrorDialog, { title: _t('Signed Out'), description: _t('For security, this session has been signed out. Please sign in again.'), @@ -1454,16 +1451,6 @@ export default class MatrixChat extends React.PureComponent { cli.on("Session.logged_out", () => dft.stop()); cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err)); - // TODO: We can remove this once cross-signing is the only way. - // https://github.com/vector-im/riot-web/issues/11908 - const krh = new KeyRequestHandler(cli); - cli.on("crypto.roomKeyRequest", (req) => { - krh.handleKeyRequest(req); - }); - cli.on("crypto.roomKeyRequestCancellation", (req) => { - krh.handleKeyRequestCancellation(req); - }); - cli.on("Room", (room) => { if (MatrixClientPeg.get().isCryptoEnabled()) { const blacklistEnabled = SettingsStore.getValueAt( @@ -1476,19 +1463,20 @@ export default class MatrixChat extends React.PureComponent { } }); cli.on("crypto.warning", (type) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); switch (type) { case 'CRYPTO_WARNING_OLD_VERSION_DETECTED': + const brand = SdkConfig.get().brand; Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, { title: _t('Old cryptography data detected'), description: _t( - "Data from an older version of Riot has been detected. " + + "Data from an older version of %(brand)s has been detected. " + "This will have caused end-to-end cryptography to malfunction " + "in the older version. End-to-end encrypted messages exchanged " + "recently whilst using the older version may not be decryptable " + "in this version. This may also cause messages exchanged with this " + "version to fail. If you experience problems, log out and back in " + "again. To retain message history, export and re-import your keys.", + { brand }, ), }); break; @@ -1534,13 +1522,6 @@ export default class MatrixChat extends React.PureComponent { }); cli.on("crypto.verification.request", request => { - const isFlagOn = SettingsStore.getValue("feature_cross_signing"); - - if (!isFlagOn && !request.channel.deviceId) { - request.cancel({code: "m.invalid_message", reason: "This client has cross-signing disabled"}); - return; - } - if (request.verifier) { const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog"); Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { @@ -1553,7 +1534,7 @@ export default class MatrixChat extends React.PureComponent { icon: "verification", props: {request}, component: sdk.getComponent("toasts.VerificationRequestToast"), - priority: ToastStore.PRIORITY_REALTIME, + priority: 90, }); } }); @@ -1583,9 +1564,7 @@ export default class MatrixChat extends React.PureComponent { // be aware of will be signalled through the room shield // changing colour. More advanced behaviour will come once // we implement more settings. - cli.setGlobalErrorOnUnknownDevices( - !SettingsStore.getValue("feature_cross_signing"), - ); + cli.setGlobalErrorOnUnknownDevices(false); } } @@ -1621,9 +1600,7 @@ export default class MatrixChat extends React.PureComponent { action: 'view_create_room', }); } else if (screen === 'settings') { - dis.dispatch({ - action: 'view_user_settings', - }); + dis.fire(Action.ViewUserSettings); } else if (screen === 'welcome') { dis.dispatch({ action: 'view_welcome_page', @@ -1638,9 +1615,20 @@ export default class MatrixChat extends React.PureComponent { action: 'require_registration', }); } else if (screen === 'directory') { - dis.dispatch({ - action: 'view_room_directory', - }); + dis.fire(Action.ViewRoomDirectory); + } else if (screen === "start_sso" || screen === "start_cas") { + // TODO if logged in, skip SSO + let cli = MatrixClientPeg.get(); + if (!cli) { + const {hsUrl, isUrl} = this.props.serverConfig; + cli = Matrix.createClient({ + baseUrl: hsUrl, + idBaseUrl: isUrl, + }); + } + + const type = screen === "start_sso" ? "sso" : "cas"; + PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin()); } else if (screen === 'groups') { dis.dispatch({ action: 'view_my_groups', @@ -1673,7 +1661,7 @@ export default class MatrixChat extends React.PureComponent { // of the app, we coerce the eventId to be undefined where applicable. if (!eventId) eventId = undefined; - // TODO: Handle encoded room/event IDs: https://github.com/vector-im/riot-web/issues/9149 + // TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149 // FIXME: sort_out caseConsistency const thirdPartyInvite = { @@ -1755,8 +1743,8 @@ export default class MatrixChat extends React.PureComponent { const member = new RoomMember(null, userId); if (!member) { return; } - dis.dispatch({ - action: 'view_user', + dis.dispatch({ + action: Action.ViewUser, member: member, }); } @@ -1829,16 +1817,6 @@ export default class MatrixChat extends React.PureComponent { this.showScreen("settings"); }; - onVersion(current: string, latest: string, releaseNotes?: string) { - this.setState({ - version: current, - newVersion: latest, - hasNewVersion: current !== latest, - newVersionReleaseNotes: releaseNotes, - checkingForUpdate: null, - }); - } - onSendEvent(roomId: string, event: MatrixEvent) { const cli = MatrixClientPeg.get(); if (!cli) { @@ -1863,23 +1841,24 @@ export default class MatrixChat extends React.PureComponent { } else { subtitle = `${this.subTitleStatus} ${subtitle}`; } - document.title = `${SdkConfig.get().brand || 'Riot'} ${subtitle}`; + document.title = `${SdkConfig.get().brand} ${subtitle}`; } updateStatusIndicator(state: string, prevState: string) { - const notifCount = countRoomsWithNotif(MatrixClientPeg.get().getRooms()).count; + const notificationState = RoomNotificationStateStore.instance.globalState; + const numUnreadRooms = notificationState.numUnreadStates; // we know that states === rooms here if (PlatformPeg.get()) { PlatformPeg.get().setErrorStatus(state === 'ERROR'); - PlatformPeg.get().setNotificationCount(notifCount); + PlatformPeg.get().setNotificationCount(numUnreadRooms); } this.subTitleStatus = ''; if (state === "ERROR") { this.subTitleStatus += `[${_t("Offline")}] `; } - if (notifCount > 0) { - this.subTitleStatus += `[${notifCount}]`; + if (numUnreadRooms > 0) { + this.subTitleStatus += `[${numUnreadRooms}]`; } this.setPageSubtitle(); @@ -1909,60 +1888,42 @@ export default class MatrixChat extends React.PureComponent { this.accountPasswordTimer = null; }, 60 * 5 * 1000); - // Wait for the client to be logged in (but not started) - // which is enough to ask the server about account data. - const loggedIn = new Promise(resolve => { - const actionHandlerRef = dis.register(payload => { - if (payload.action !== "on_logged_in") { - return; - } - dis.unregister(actionHandlerRef); - resolve(); - }); - }); - - // Create and start the client in the background - const setLoggedInPromise = Lifecycle.setLoggedIn(credentials); - await loggedIn; + // Create and start the client + await Lifecycle.setLoggedIn(credentials); const cli = MatrixClientPeg.get(); - // We're checking `isCryptoAvailable` here instead of `isCryptoEnabled` - // because the client hasn't been started yet. - const cryptoAvailable = isCryptoAvailable(); - if (!cryptoAvailable) { + const cryptoEnabled = cli.isCryptoEnabled(); + if (!cryptoEnabled) { this.onLoggedIn(); } - this.setState({ pendingInitialSync: true }); - await this.firstSyncPromise.promise; - - if (!cryptoAvailable) { - this.setState({ pendingInitialSync: false }); - return setLoggedInPromise; + const promisesList = [this.firstSyncPromise.promise]; + if (cryptoEnabled) { + // wait for the client to finish downloading cross-signing keys for us so we + // know whether or not we have keys set up on this account + promisesList.push(cli.downloadKeys([cli.getUserId()])); } - // Test for the master cross-signing key in SSSS as a quick proxy for - // whether cross-signing has been set up on the account. - const masterKeyInStorage = !!cli.getAccountData("m.cross_signing.master"); - if (masterKeyInStorage) { - // Auto-enable cross-signing for the new session when key found in - // secret storage. - SettingsStore.setValue("feature_cross_signing", null, SettingLevel.DEVICE, true); + // Now update the state to say we're waiting for the first sync to complete rather + // than for the login to finish. + this.setState({ pendingInitialSync: true }); + + await Promise.all(promisesList); + + if (!cryptoEnabled) { + this.setState({ pendingInitialSync: false }); + return; + } + + const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId()); + if (crossSigningIsSetUp) { this.setStateForNewView({ view: Views.COMPLETE_SECURITY }); - } else if ( - SettingsStore.getValue("feature_cross_signing") && - await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing") - ) { - // This will only work if the feature is set to 'enable' in the config, - // since it's too early in the lifecycle for users to have turned the - // labs flag on. + } else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) { this.setStateForNewView({ view: Views.E2E_SETUP }); } else { this.onLoggedIn(); } this.setState({ pendingInitialSync: false }); - - return setLoggedInPromise; }; // complete security / e2e setup has finished @@ -1970,9 +1931,20 @@ export default class MatrixChat extends React.PureComponent { this.onLoggedIn(); }; - render() { - // console.log(`Rendering MatrixChat with view ${this.state.view}`); + getFragmentAfterLogin() { + let fragmentAfterLogin = ""; + const initialScreenAfterLogin = this.props.initialScreenAfterLogin; + if (initialScreenAfterLogin && + // XXX: workaround for https://github.com/vector-im/element-web/issues/11643 causing a login-loop + !["welcome", "login", "register", "start_sso", "start_cas"].includes(initialScreenAfterLogin.screen) + ) { + fragmentAfterLogin = `/${initialScreenAfterLogin.screen}`; + } + return fragmentAfterLogin; + } + render() { + const fragmentAfterLogin = this.getFragmentAfterLogin(); let view; if (this.state.view === Views.LOADING) { @@ -2028,7 +2000,6 @@ export default class MatrixChat extends React.PureComponent { onCloseAllSettings={this.onCloseAllSettings} onRegistered={this.onRegistered} currentRoomId={this.state.currentRoomId} - showCookieBar={this.state.showCookieBar} /> ); } else { @@ -2052,7 +2023,7 @@ export default class MatrixChat extends React.PureComponent { } } else if (this.state.view === Views.WELCOME) { const Welcome = sdk.getComponent('auth.Welcome'); - view = ; + view = ; } else if (this.state.view === Views.REGISTER) { const Registration = sdk.getComponent('structures.auth.Registration'); view = ( @@ -2091,6 +2062,7 @@ export default class MatrixChat extends React.PureComponent { defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} onForgotPasswordClick={this.onForgotPasswordClick} onServerConfigChange={this.onServerConfigChange} + fragmentAfterLogin={fragmentAfterLogin} {...this.getServerProperties()} /> ); @@ -2100,6 +2072,7 @@ export default class MatrixChat extends React.PureComponent { ); } else { diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 6fbfdb504b..230d136e04 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -29,10 +29,35 @@ import SettingsStore from '../../settings/SettingsStore'; import {_t} from "../../languageHandler"; import {haveTileForEvent} from "../views/rooms/EventTile"; import {textForEvent} from "../../TextForEvent"; +import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = ['m.sticker', 'm.room.message']; +// check if there is a previous event and it has the same sender as this event +// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL +function shouldFormContinuation(prevEvent, mxEvent) { + // sanity check inputs + if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false; + // check if within the max continuation period + if (mxEvent.getTs() - prevEvent.getTs() > CONTINUATION_MAX_INTERVAL) return false; + + // Some events should appear as continuations from previous events of different types. + if (mxEvent.getType() !== prevEvent.getType() && + (!continuedTypes.includes(mxEvent.getType()) || + !continuedTypes.includes(prevEvent.getType()))) return false; + + // Check if the sender is the same and hasn't changed their displayname/avatar between these events + if (mxEvent.sender.userId !== prevEvent.sender.userId || + mxEvent.sender.name !== prevEvent.sender.name || + mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false; + + // if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile + if (!haveTileForEvent(prevEvent)) return false; + + return true; +} + const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() === 'm.room.third_party_invite'; /* (almost) stateless UI component which builds the event tiles in the room timeline. @@ -107,10 +132,14 @@ export default class MessagePanel extends React.Component { // whether to show reactions for an event showReactions: PropTypes.bool, + + // whether to use the irc layout + useIRCLayout: PropTypes.bool, }; - constructor() { - super(); + // Force props to be loaded for useIRCLayout + constructor(props) { + super(props); this.state = { // previous positions the read marker has been in, so we can @@ -317,9 +346,9 @@ export default class MessagePanel extends React.Component { } } - _isUnmounting() { + _isUnmounting = () => { return !this._isMounted; - } + }; // TODO: Implement granular (per-room) hide options _shouldShowEvent(mxEv) { @@ -359,8 +388,11 @@ export default class MessagePanel extends React.Component { } return ( -
  • +
  • { hr }
  • ); @@ -510,39 +542,6 @@ export default class MessagePanel extends React.Component { const isEditing = this.props.editState && this.props.editState.getEvent().getId() === mxEv.getId(); - // is this a continuation of the previous message? - let continuation = false; - - // Some events should appear as continuations from previous events of - // different types. - - const eventTypeContinues = - prevEvent !== null && - continuedTypes.includes(mxEv.getType()) && - continuedTypes.includes(prevEvent.getType()); - - // if there is a previous event and it has the same sender as this event - // and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL - if (prevEvent !== null && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId && - // if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile - haveTileForEvent(prevEvent) && (mxEv.getType() === prevEvent.getType() || eventTypeContinues) && - (mxEv.getTs() - prevEvent.getTs() <= CONTINUATION_MAX_INTERVAL)) { - continuation = true; - } - -/* - // Work out if this is still a continuation, as we are now showing commands - // and /me messages with their own little avatar. The case of a change of - // event type (commands) is handled above, but we need to handle the /me - // messages seperately as they have a msgtype of 'm.emote' but are classed - // as normal messages - if (prevEvent !== null && prevEvent.sender && mxEv.sender - && mxEv.sender.userId === prevEvent.sender.userId - && mxEv.getType() == prevEvent.getType() - && prevEvent.getContent().msgtype === 'm.emote') { - continuation = false; - } -*/ // local echoes have a fake date, which could even be yesterday. Treat them // as 'today' for the date separators. @@ -554,12 +553,15 @@ export default class MessagePanel extends React.Component { } // do we need a date separator since the last event? - if (this._wantsDateSeparator(prevEvent, eventDate)) { + const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate); + if (wantsDateSeparator) { const dateSeparator =
  • ; ret.push(dateSeparator); - continuation = false; } + // is this a continuation of the previous message? + const continuation = !wantsDateSeparator && shouldFormContinuation(prevEvent, mxEv); + const eventId = mxEv.getId(); const highlight = (eventId === this.props.highlightedEventId); @@ -569,12 +571,10 @@ export default class MessagePanel extends React.Component { const readReceipts = this._readReceiptsByEvent[eventId]; - // Dev note: `this._isUnmounting.bind(this)` is important - it ensures that - // the function is run in the context of this class and not EventTile, therefore - // ensuring the right `this._mounted` variable is used by read receipts (which - // don't update their position if we, the MessagePanel, is unmounting). + // use txnId as key if available so that we don't remount during sending ret.push( -
  • @@ -588,7 +588,7 @@ export default class MessagePanel extends React.Component { readReceipts={readReceipts} readReceiptMap={this._readReceiptMap} showUrlPreview={this.props.showUrlPreview} - checkUnmounting={this._isUnmounting.bind(this)} + checkUnmounting={this._isUnmounting} eventSendStatus={mxEv.getAssociatedStatus()} tileShape={this.props.tileShape} isTwelveHour={this.props.isTwelveHour} @@ -597,6 +597,7 @@ export default class MessagePanel extends React.Component { isSelectedEvent={highlight} getRelationsForEvent={this.props.getRelationsForEvent} showReactions={this.props.showReactions} + useIRCLayout={this.props.useIRCLayout} />
  • , @@ -792,6 +793,15 @@ export default class MessagePanel extends React.Component { ); } + let ircResizer = null; + if (this.props.useIRCLayout) { + ircResizer = ; + } + return ( { topSpinner } { this._getEventTiles() } diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index f179cab6ad..7043c7f38a 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -1,6 +1,7 @@ /* Copyright 2017 Vector Creations 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. @@ -19,7 +20,8 @@ import React from 'react'; import createReactClass from 'create-react-class'; import * as sdk from '../../index'; import { _t } from '../../languageHandler'; -import dis from '../../dispatcher'; +import SdkConfig from '../../SdkConfig'; +import dis from '../../dispatcher/dispatcher'; import AccessibleButton from '../views/elements/AccessibleButton'; import MatrixClientContext from "../../contexts/MatrixClientContext"; import AutoHideScrollbar from "./AutoHideScrollbar"; @@ -60,6 +62,7 @@ export default createReactClass({ }, render: function() { + const brand = SdkConfig.get().brand; const Loader = sdk.getComponent("elements.Spinner"); const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader'); const GroupTile = sdk.getComponent("groups.GroupTile"); @@ -77,7 +80,8 @@ export default createReactClass({

    { _t( - "Did you know: you can use communities to filter your Riot.im experience!", + "Did you know: you can use communities to filter your %(brand)s experience!", + { brand }, ) }

    diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx new file mode 100644 index 0000000000..8d415df4dd --- /dev/null +++ b/src/components/structures/NonUrgentToastContainer.tsx @@ -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. +*/ + +import * as React from "react"; +import { ComponentClass } from "../../@types/common"; +import NonUrgentToastStore from "../../stores/NonUrgentToastStore"; +import { UPDATE_EVENT } from "../../stores/AsyncStore"; + +interface IProps { +} + +interface IState { + toasts: ComponentClass[], +} + +export default class NonUrgentToastContainer extends React.PureComponent { + public constructor(props, context) { + super(props, context); + + this.state = { + toasts: NonUrgentToastStore.instance.components, + }; + + NonUrgentToastStore.instance.on(UPDATE_EVENT, this.onUpdateToasts); + } + + public componentWillUnmount() { + NonUrgentToastStore.instance.off(UPDATE_EVENT, this.onUpdateToasts); + } + + private onUpdateToasts = () => { + this.setState({toasts: NonUrgentToastStore.instance.components}); + }; + + public render() { + const toasts = this.state.toasts.map((t, i) => { + return ( +

    + {React.createElement(t, {})} +
    + ); + }); + + return ( +
    + {toasts} +
    + ); + } +} diff --git a/src/components/structures/NotificationPanel.js b/src/components/structures/NotificationPanel.js index c1a0ec9c4b..c1f78cffda 100644 --- a/src/components/structures/NotificationPanel.js +++ b/src/components/structures/NotificationPanel.js @@ -36,6 +36,11 @@ const NotificationPanel = createReactClass({ const TimelinePanel = sdk.getComponent("structures.TimelinePanel"); const Loader = sdk.getComponent("elements.Spinner"); + const emptyState = (
    +

    {_t('You’re all caught up')}

    +

    {_t('You have no visible notifications in this room.')}

    +
    ); + const timelineSet = MatrixClientPeg.get().getNotifTimelineSet(); if (timelineSet) { return ( @@ -46,7 +51,7 @@ const NotificationPanel = createReactClass({ timelineSet={timelineSet} showUrlPreview={false} tileShape="notif" - empty={_t('You have no visible notifications')} + empty={emptyState} />
    ); diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 3fec5aa25f..a4e3254e4c 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -22,14 +22,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import * as sdk from '../../index'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import RateLimitedFunc from '../../ratelimitedfunc'; import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker'; import GroupStore from '../../stores/GroupStore'; -import SettingsStore from "../../settings/SettingsStore"; -import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases"; +import {RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases"; import RightPanelStore from "../../stores/RightPanelStore"; import MatrixClientContext from "../../contexts/MatrixClientContext"; +import {Action} from "../../dispatcher/actions"; export default class RightPanel extends React.Component { static get propTypes() { @@ -75,8 +75,8 @@ export default class RightPanel extends React.Component { const userForPanel = this._getUserForPanel(); if (this.props.groupId) { if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) { - dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.GroupMemberList}); - return RIGHT_PANEL_PHASES.GroupMemberList; + dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList}); + return RightPanelPhases.GroupMemberList; } return rps.groupPanelPhase; } else if (userForPanel) { @@ -98,11 +98,11 @@ export default class RightPanel extends React.Component { ) { return rps.roomPanelPhase; } - return RIGHT_PANEL_PHASES.RoomMemberInfo; + return RightPanelPhases.RoomMemberInfo; } else { if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.roomPanelPhase)) { - dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.RoomMemberList}); - return RIGHT_PANEL_PHASES.RoomMemberList; + dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList}); + return RightPanelPhases.RoomMemberList; } return rps.roomPanelPhase; } @@ -149,7 +149,7 @@ export default class RightPanel extends React.Component { onInviteToGroupButtonClick() { showGroupInviteDialog(this.props.groupId).then(() => { this.setState({ - phase: RIGHT_PANEL_PHASES.GroupMemberList, + phase: RightPanelPhases.GroupMemberList, }); }); } @@ -165,9 +165,9 @@ export default class RightPanel extends React.Component { return; } // redraw the badge on the membership list - if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList && member.roomId === this.props.roomId) { + if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.roomId) { this._delayedUpdate(); - } else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo && member.roomId === this.props.roomId && + } else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.roomId && member.userId === this.state.member.userId) { // refresh the member info (e.g. new power level) this._delayedUpdate(); @@ -175,7 +175,7 @@ export default class RightPanel extends React.Component { } onAction(payload) { - if (payload.action === "after_right_panel_phase_change") { + if (payload.action === Action.AfterRightPanelPhaseChange) { this.setState({ phase: payload.phase, groupRoomId: payload.groupRoomId, @@ -188,113 +188,88 @@ export default class RightPanel extends React.Component { } } + onCloseUserInfo = () => { + // XXX: There are three different ways of 'closing' this panel depending on what state + // things are in... this knows far more than it should do about the state of the rest + // of the app and is generally a bit silly. + if (this.props.user) { + // If we have a user prop then we're displaying a user from the 'user' page type + // in LoggedInView, so need to change the page type to close the panel (we switch + // to the home page which is not obviously the correct thing to do, but I'm not sure + // anything else is - we could hide the close button altogether?) + dis.dispatch({ + action: "view_home_page", + }); + } else { + // Otherwise we have got our user from RoomViewStore which means we're being shown + // within a room/group, so go back to the member panel if we were in the encryption panel, + // or the member list if we were in the member panel... phew. + dis.dispatch({ + action: Action.ViewUser, + member: this.state.phase === RightPanelPhases.EncryptionPanel ? this.state.member : null, + }); + } + }; + render() { const MemberList = sdk.getComponent('rooms.MemberList'); - const MemberInfo = sdk.getComponent('rooms.MemberInfo'); const UserInfo = sdk.getComponent('right_panel.UserInfo'); const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo'); const NotificationPanel = sdk.getComponent('structures.NotificationPanel'); const FilePanel = sdk.getComponent('structures.FilePanel'); const GroupMemberList = sdk.getComponent('groups.GroupMemberList'); - const GroupMemberInfo = sdk.getComponent('groups.GroupMemberInfo'); const GroupRoomList = sdk.getComponent('groups.GroupRoomList'); const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo'); let panel =
    ; switch (this.state.phase) { - case RIGHT_PANEL_PHASES.RoomMemberList: + case RightPanelPhases.RoomMemberList: if (this.props.roomId) { panel = ; } break; - case RIGHT_PANEL_PHASES.GroupMemberList: + case RightPanelPhases.GroupMemberList: if (this.props.groupId) { panel = ; } break; - case RIGHT_PANEL_PHASES.GroupRoomList: + case RightPanelPhases.GroupRoomList: panel = ; break; - case RIGHT_PANEL_PHASES.RoomMemberInfo: - case RIGHT_PANEL_PHASES.EncryptionPanel: - if (SettingsStore.getValue("feature_cross_signing")) { - const onClose = () => { - // XXX: There are three different ways of 'closing' this panel depending on what state - // things are in... this knows far more than it should do about the state of the rest - // of the app and is generally a bit silly. - if (this.props.user) { - // If we have a user prop then we're displaying a user from the 'user' page type - // in LoggedInView, so need to change the page type to close the panel (we switch - // to the home page which is not obviosuly the correct thing to do, but I'm not sure - // anything else is - we could hide the close button altogether?) - dis.dispatch({ - action: "view_home_page", - }); - } else { - // Otherwise we have got our user from RoomViewStore which means we're being shown - // within a room, so go back to the member panel if we were in the encryption panel, - // or the member list if we were in the member panel... phew. - dis.dispatch({ - action: "view_user", - member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? - this.state.member : null, - }); - } - }; - panel = ; - } else { - panel = ; - } + case RightPanelPhases.RoomMemberInfo: + case RightPanelPhases.EncryptionPanel: + panel = ; break; - case RIGHT_PANEL_PHASES.Room3pidMemberInfo: + case RightPanelPhases.Room3pidMemberInfo: panel = ; break; - case RIGHT_PANEL_PHASES.GroupMemberInfo: - if (SettingsStore.getValue("feature_cross_signing")) { - const onClose = () => { - dis.dispatch({ - action: "view_user", - member: null, - }); - }; - panel = ; - } else { - panel = ( - - ); - } + case RightPanelPhases.GroupMemberInfo: + panel = ; break; - case RIGHT_PANEL_PHASES.GroupRoomInfo: + case RightPanelPhases.GroupRoomInfo: panel = ; break; - case RIGHT_PANEL_PHASES.NotificationPanel: + case RightPanelPhases.NotificationPanel: panel = ; break; - case RIGHT_PANEL_PHASES.FilePanel: + case RightPanelPhases.FilePanel: panel = ; break; } diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 0b07c10c8a..5b12dae7df 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -1,7 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -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,11 +20,12 @@ import React from 'react'; import createReactClass from 'create-react-class'; import {MatrixClientPeg} from "../../MatrixClientPeg"; import * as sdk from "../../index"; -import dis from "../../dispatcher"; +import dis from "../../dispatcher/dispatcher"; import Modal from "../../Modal"; import { linkifyAndSanitizeHtml } from '../../HtmlUtils'; import PropTypes from 'prop-types'; import { _t } from '../../languageHandler'; +import SdkConfig from '../../SdkConfig'; import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils'; import Analytics from '../../Analytics'; import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; @@ -74,7 +75,7 @@ export default createReactClass({ this.protocols = response; this.setState({protocolsLoading: false}); }, (err) => { - console.warn(`error loading thirdparty protocols: ${err}`); + console.warn(`error loading third party protocols: ${err}`); this.setState({protocolsLoading: false}); if (MatrixClientPeg.get().isGuest()) { // Guests currently aren't allowed to use this API, so @@ -83,10 +84,12 @@ export default createReactClass({ return; } track('Failed to get protocol list from homeserver'); + const brand = SdkConfig.get().brand; this.setState({ error: _t( - 'Riot failed to get the protocol list from the homeserver. ' + + '%(brand)s failed to get the protocol list from the homeserver. ' + 'The homeserver may be too old to support third party networks.', + { brand }, ), }); }); @@ -173,12 +176,13 @@ export default createReactClass({ console.error("Failed to get publicRooms: %s", JSON.stringify(err)); track('Failed to get public room list'); + const brand = SdkConfig.get().brand; this.setState({ loading: false, - error: - `${_t('Riot failed to get the public room list.')} ` + - `${(err && err.message) ? err.message : _t('The homeserver may be unavailable or overloaded.')}` - , + error: ( + _t('%(brand)s failed to get the public room list.', { brand }) + + (err && err.message) ? err.message : _t('The homeserver may be unavailable or overloaded.') + ), }); }); }, @@ -199,7 +203,7 @@ export default createReactClass({ let desc; if (alias) { - desc = _t('Delete the room alias %(alias)s and remove %(name)s from the directory?', {alias: alias, name: name}); + desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name}); } else { desc = _t('Remove %(name)s from the directory?', {name: name}); } @@ -216,7 +220,7 @@ export default createReactClass({ MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => { if (!alias) return; - step = _t('delete the alias.'); + step = _t('delete the address.'); return MatrixClientPeg.get().deleteAlias(alias); }).then(() => { modal.close(); @@ -314,9 +318,10 @@ export default createReactClass({ const fields = protocolName ? this._getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) : null; if (!fields) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const brand = SdkConfig.get().brand; Modal.createTrackedDialog('Unable to join network', '', ErrorDialog, { title: _t('Unable to join network'), - description: _t('Riot does not know how to join a room on this network'), + description: _t('%(brand)s does not know how to join a room on this network', { brand }), }); return; } @@ -367,7 +372,10 @@ export default createReactClass({ onCreateRoomClick: function(room) { this.props.onFinished(); - dis.dispatch({action: 'view_create_room'}); + dis.dispatch({ + action: 'view_create_room', + public: true, + }); }, showRoomAlias: function(alias, autoJoin=false) { diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx new file mode 100644 index 0000000000..f6b8d42c30 --- /dev/null +++ b/src/components/structures/RoomSearch.tsx @@ -0,0 +1,185 @@ +/* +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 React from "react"; +import { createRef } from "react"; +import classNames from "classnames"; +import defaultDispatcher from "../../dispatcher/dispatcher"; +import { _t } from "../../languageHandler"; +import { ActionPayload } from "../../dispatcher/payloads"; +import { throttle } from 'lodash'; +import { Key } from "../../Keyboard"; +import AccessibleButton from "../views/elements/AccessibleButton"; +import { Action } from "../../dispatcher/actions"; +import RoomListStore from "../../stores/room-list/RoomListStore"; +import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition"; + +interface IProps { + isMinimized: boolean; + onVerticalArrow(ev: React.KeyboardEvent): void; + onEnter(ev: React.KeyboardEvent): boolean; +} + +interface IState { + query: string; + focused: boolean; +} + +export default class RoomSearch extends React.PureComponent { + private dispatcherRef: string; + private inputRef: React.RefObject = createRef(); + private searchFilter: NameFilterCondition = new NameFilterCondition(); + + constructor(props: IProps) { + super(props); + + this.state = { + query: "", + focused: false, + }; + + this.dispatcherRef = defaultDispatcher.register(this.onAction); + } + + public componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { + if (prevState.query !== this.state.query) { + const hadSearch = !!this.searchFilter.search.trim(); + const haveSearch = !!this.state.query.trim(); + this.searchFilter.search = this.state.query; + if (!hadSearch && haveSearch) { + // started a new filter - add the condition + RoomListStore.instance.addFilter(this.searchFilter); + } else if (hadSearch && !haveSearch) { + // cleared a filter - remove the condition + RoomListStore.instance.removeFilter(this.searchFilter); + } // else the filter hasn't changed enough for us to care here + } + } + + public componentWillUnmount() { + defaultDispatcher.unregister(this.dispatcherRef); + } + + private onAction = (payload: ActionPayload) => { + if (payload.action === 'view_room' && payload.clear_search) { + this.clearInput(); + } else if (payload.action === 'focus_room_filter' && this.inputRef.current) { + this.inputRef.current.focus(); + } + }; + + private clearInput = () => { + if (!this.inputRef.current) return; + this.inputRef.current.value = ""; + this.onChange(); + }; + + private openSearch = () => { + defaultDispatcher.dispatch({action: "show_left_panel"}); + defaultDispatcher.dispatch({action: "focus_room_filter"}); + }; + + private onChange = () => { + if (!this.inputRef.current) return; + this.setState({query: this.inputRef.current.value}); + }; + + private onFocus = (ev: React.FocusEvent) => { + this.setState({focused: true}); + ev.target.select(); + }; + + private onBlur = (ev: React.FocusEvent) => { + this.setState({focused: false}); + }; + + private onKeyDown = (ev: React.KeyboardEvent) => { + if (ev.key === Key.ESCAPE) { + this.clearInput(); + defaultDispatcher.fire(Action.FocusComposer); + } else if (ev.key === Key.ARROW_UP || ev.key === Key.ARROW_DOWN) { + this.props.onVerticalArrow(ev); + } else if (ev.key === Key.ENTER) { + const shouldClear = this.props.onEnter(ev); + if (shouldClear) { + // wrap in set immediate to delay it so that we don't clear the filter & then change room + setImmediate(() => { + this.clearInput(); + }); + } + } + }; + + public render(): React.ReactNode { + const classes = classNames({ + 'mx_RoomSearch': true, + 'mx_RoomSearch_hasQuery': this.state.query, + 'mx_RoomSearch_focused': this.state.focused, + 'mx_RoomSearch_minimized': this.props.isMinimized, + }); + + const inputClasses = classNames({ + 'mx_RoomSearch_input': true, + 'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused, + }); + + let icon = ( +
    + ); + let input = ( + + ); + let clearButton = ( + + ); + + if (this.props.isMinimized) { + icon = ( + + ); + input = null; + clearButton = null; + } + + return ( +
    + {icon} + {input} + {clearButton} +
    + ); + } +} diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 639f38a119..3171dbccbe 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -24,9 +24,9 @@ import { _t, _td } from '../../languageHandler'; import * as sdk from '../../index'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import Resend from '../../Resend'; -import * as cryptodevices from '../../cryptodevices'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils'; +import {Action} from "../../dispatcher/actions"; const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; @@ -126,25 +126,14 @@ export default createReactClass({ }); }, - _onSendWithoutVerifyingClick: function() { - cryptodevices.getUnknownDevicesForRoom(MatrixClientPeg.get(), this.props.room).then((devices) => { - cryptodevices.markAllDevicesKnown(MatrixClientPeg.get(), devices); - Resend.resendUnsentEvents(this.props.room); - }); - }, - _onResendAllClick: function() { Resend.resendUnsentEvents(this.props.room); - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); }, _onCancelAllClick: function() { Resend.cancelUnsentEvents(this.props.room); - dis.dispatch({action: 'focus_composer'}); - }, - - _onShowDevicesClick: function() { - cryptodevices.showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room); + dis.fire(Action.FocusComposer); }, _onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) { @@ -184,7 +173,7 @@ export default createReactClass({ if (this.props.hasActiveCall) { const TintableSvg = sdk.getComponent("elements.TintableSvg"); return ( - + ); } @@ -213,82 +202,65 @@ export default createReactClass({ if (!unsentMessages.length) return null; let title; - let content; - const hasUDE = unsentMessages.some((m) => { - return m.error && m.error.name === "UnknownDeviceError"; - }); - - if (hasUDE) { - title = _t("Message not sent due to unknown sessions being present"); - content = _t( - "Show sessions, send anyway or cancel.", + let consentError = null; + let resourceLimitError = null; + for (const m of unsentMessages) { + if (m.error && m.error.errcode === 'M_CONSENT_NOT_GIVEN') { + consentError = m.error; + break; + } else if (m.error && m.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { + resourceLimitError = m.error; + break; + } + } + if (consentError) { + title = _t( + "You can't send any messages until you review and agree to " + + "our terms and conditions.", {}, { - 'showSessionsText': (sub) => { sub }, - 'sendAnywayText': (sub) => { sub }, - 'cancelText': (sub) => { sub }, + 'consentLink': (sub) => + + { sub } + , }, ); + } else if (resourceLimitError) { + title = messageForResourceLimitError( + resourceLimitError.data.limit_type, + resourceLimitError.data.admin_contact, { + 'monthly_active_user': _td( + "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " + + "Please contact your service administrator to continue using the service.", + ), + '': _td( + "Your message wasn't sent because this homeserver has exceeded a resource limit. " + + "Please contact your service administrator to continue using the service.", + ), + }); + } else if ( + unsentMessages.length === 1 && + unsentMessages[0].error && + unsentMessages[0].error.data && + unsentMessages[0].error.data.error + ) { + title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error; } else { - let consentError = null; - let resourceLimitError = null; - for (const m of unsentMessages) { - if (m.error && m.error.errcode === 'M_CONSENT_NOT_GIVEN') { - consentError = m.error; - break; - } else if (m.error && m.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { - resourceLimitError = m.error; - break; - } - } - if (consentError) { - title = _t( - "You can't send any messages until you review and agree to " + - "our terms and conditions.", - {}, - { - 'consentLink': (sub) => - - { sub } - , - }, - ); - } else if (resourceLimitError) { - title = messageForResourceLimitError( - resourceLimitError.data.limit_type, - resourceLimitError.data.admin_contact, { - 'monthly_active_user': _td( - "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " + - "Please contact your service administrator to continue using the service.", - ), - '': _td( - "Your message wasn't sent because this homeserver has exceeded a resource limit. " + - "Please contact your service administrator to continue using the service.", - ), - }); - } else if ( - unsentMessages.length === 1 && - unsentMessages[0].error && - unsentMessages[0].error.data && - unsentMessages[0].error.data.error - ) { - title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error; - } else { - title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length }); - } - content = _t("%(count)s Resend all or cancel all now. " + - "You can also select individual messages to resend or cancel.", - { count: unsentMessages.length }, - { - 'resendText': (sub) => - { sub }, - 'cancelText': (sub) => - { sub }, - }, - ); + title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length }); } + const content = _t("%(count)s Resend all or cancel all " + + "now. You can also select individual messages to resend or cancel.", + { count: unsentMessages.length }, + { + 'resendText': (sub) => + { sub }, + 'cancelText': (sub) => + { sub }, + }, + ); + return
    diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js deleted file mode 100644 index 1e3e15b4ec..0000000000 --- a/src/components/structures/RoomSubList.js +++ /dev/null @@ -1,460 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2018, 2019 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 React, {createRef} from 'react'; -import classNames from 'classnames'; -import * as sdk from '../../index'; -import dis from '../../dispatcher'; -import * as Unread from '../../Unread'; -import * as RoomNotifs from '../../RoomNotifs'; -import * as FormattingUtils from '../../utils/FormattingUtils'; -import IndicatorScrollbar from './IndicatorScrollbar'; -import {Key} from '../../Keyboard'; -import { Group } from 'matrix-js-sdk'; -import PropTypes from 'prop-types'; -import RoomTile from "../views/rooms/RoomTile"; -import LazyRenderList from "../views/elements/LazyRenderList"; -import {_t} from "../../languageHandler"; -import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex"; -import toRem from "../../utils/rem"; - -// turn this on for drop & drag console debugging galore -const debug = false; - -export default class RoomSubList extends React.PureComponent { - static displayName = 'RoomSubList'; - static debug = debug; - - static propTypes = { - list: PropTypes.arrayOf(PropTypes.object).isRequired, - label: PropTypes.string.isRequired, - tagName: PropTypes.string, - addRoomLabel: PropTypes.string, - - // passed through to RoomTile and used to highlight room with `!` regardless of notifications count - isInvite: PropTypes.bool, - - startAsHidden: PropTypes.bool, - showSpinner: PropTypes.bool, // true to show a spinner if 0 elements when expanded - collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed? - onHeaderClick: PropTypes.func, - incomingCall: PropTypes.object, - extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles - forceExpand: PropTypes.bool, - }; - - static defaultProps = { - onHeaderClick: function() { - }, // NOP - extraTiles: [], - isInvite: false, - }; - - static getDerivedStateFromProps(props, state) { - return { - listLength: props.list.length, - scrollTop: props.list.length === state.listLength ? state.scrollTop : 0, - }; - } - - constructor(props) { - super(props); - - this.state = { - hidden: this.props.startAsHidden || false, - // some values to get LazyRenderList starting - scrollerHeight: 800, - scrollTop: 0, - // React 16's getDerivedStateFromProps(props, state) doesn't give the previous props so - // we have to store the length of the list here so we can see if it's changed or not... - listLength: null, - }; - - this._header = createRef(); - this._subList = createRef(); - this._scroller = createRef(); - this._headerButton = createRef(); - } - - componentDidMount() { - this.dispatcherRef = dis.register(this.onAction); - } - - componentWillUnmount() { - dis.unregister(this.dispatcherRef); - } - - // The header is collapsible if it is hidden or not stuck - // The dataset elements are added in the RoomList _initAndPositionStickyHeaders method - isCollapsibleOnClick() { - const stuck = this._header.current.dataset.stuck; - if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) { - return true; - } else { - return false; - } - } - - onAction = (payload) => { - switch (payload.action) { - case 'on_room_read': - // XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched, - // but this is no longer true, so we must do it here (and can apply the small - // optimisation of checking that we care about the room being read). - // - // Ultimately we need to transition to a state pushing flow where something - // explicitly notifies the components concerned that the notif count for a room - // has change (e.g. a Flux store). - if (this.props.list.some((r) => r.roomId === payload.roomId)) { - this.forceUpdate(); - } - break; - - case 'view_room': - if (this.state.hidden && !this.props.forceExpand && payload.show_room_tile && - this.props.list.some((r) => r.roomId === payload.room_id) - ) { - this.toggle(); - } - } - }; - - toggle = () => { - if (this.isCollapsibleOnClick()) { - // The header isCollapsible, so the click is to be interpreted as collapse and truncation logic - const isHidden = !this.state.hidden; - this.setState({hidden: isHidden}, () => { - this.props.onHeaderClick(isHidden); - }); - } else { - // The header is stuck, so the click is to be interpreted as a scroll to the header - this.props.onHeaderClick(this.state.hidden, this._header.current.dataset.originalPosition); - } - }; - - onClick = (ev) => { - this.toggle(); - }; - - onHeaderKeyDown = (ev) => { - switch (ev.key) { - case Key.ARROW_LEFT: - // On ARROW_LEFT collapse the room sublist - if (!this.state.hidden && !this.props.forceExpand) { - this.onClick(); - } - ev.stopPropagation(); - break; - case Key.ARROW_RIGHT: { - ev.stopPropagation(); - if (this.state.hidden && !this.props.forceExpand) { - // sublist is collapsed, expand it - this.onClick(); - } else if (!this.props.forceExpand) { - // sublist is expanded, go to first room - const element = this._subList.current && this._subList.current.querySelector(".mx_RoomTile"); - if (element) { - element.focus(); - } - } - break; - } - } - }; - - onKeyDown = (ev) => { - switch (ev.key) { - // On ARROW_LEFT go to the sublist header - case Key.ARROW_LEFT: - ev.stopPropagation(); - this._headerButton.current.focus(); - break; - // Consume ARROW_RIGHT so it doesn't cause focus to get sent to composer - case Key.ARROW_RIGHT: - ev.stopPropagation(); - } - }; - - onRoomTileClick = (roomId, ev) => { - dis.dispatch({ - action: 'view_room', - show_room_tile: true, // to make sure the room gets scrolled into view - room_id: roomId, - clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)), - }); - }; - - _updateSubListCount = () => { - // Force an update by setting the state to the current state - // Doing it this way rather than using forceUpdate(), so that the shouldComponentUpdate() - // method is honoured - this.setState(this.state); - }; - - makeRoomTile = (room) => { - return 0} - notificationCount={RoomNotifs.getUnreadNotificationCount(room)} - isInvite={this.props.isInvite} - refreshSubList={this._updateSubListCount} - incomingCall={null} - onClick={this.onRoomTileClick} - />; - }; - - _onNotifBadgeClick = (e) => { - // prevent the roomsublist collapsing - e.preventDefault(); - e.stopPropagation(); - const room = this.props.list.find(room => RoomNotifs.getRoomHasBadge(room)); - if (room) { - dis.dispatch({ - action: 'view_room', - room_id: room.roomId, - }); - } - }; - - _onInviteBadgeClick = (e) => { - // prevent the roomsublist collapsing - e.preventDefault(); - e.stopPropagation(); - // switch to first room in sortedList as that'll be the top of the list for the user - if (this.props.list && this.props.list.length > 0) { - dis.dispatch({ - action: 'view_room', - room_id: this.props.list[0].roomId, - }); - } else if (this.props.extraTiles && this.props.extraTiles.length > 0) { - // Group Invites are different in that they are all extra tiles and not rooms - // XXX: this is a horrible special case because Group Invite sublist is a hack - if (this.props.extraTiles[0].props && this.props.extraTiles[0].props.group instanceof Group) { - dis.dispatch({ - action: 'view_group', - group_id: this.props.extraTiles[0].props.group.groupId, - }); - } - } - }; - - onAddRoom = (e) => { - e.stopPropagation(); - if (this.props.onAddRoom) this.props.onAddRoom(); - }; - - _getHeaderJsx(isCollapsed) { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton'); - const subListNotifications = !this.props.isInvite ? - RoomNotifs.aggregateNotificationCount(this.props.list) : - {count: 0, highlight: true}; - const subListNotifCount = subListNotifications.count; - const subListNotifHighlight = subListNotifications.highlight; - - // When collapsed, allow a long hover on the header to show user - // the full tag name and room count - let title; - if (this.props.collapsed) { - title = this.props.label; - } - - let incomingCall; - if (this.props.incomingCall) { - // We can assume that if we have an incoming call then it is for this list - const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox"); - incomingCall = - ; - } - - const len = this.props.list.length + this.props.extraTiles.length; - let chevron; - if (len) { - const chevronClasses = classNames({ - 'mx_RoomSubList_chevron': true, - 'mx_RoomSubList_chevronRight': isCollapsed, - 'mx_RoomSubList_chevronDown': !isCollapsed, - }); - chevron = (
    ); - } - - return - {({onFocus, isActive, ref}) => { - const tabIndex = isActive ? 0 : -1; - - let badge; - if (!this.props.collapsed) { - const badgeClasses = classNames({ - 'mx_RoomSubList_badge': true, - 'mx_RoomSubList_badgeHighlight': subListNotifHighlight, - }); - // Wrap the contents in a div and apply styles to the child div so that the browser default outline works - if (subListNotifCount > 0) { - badge = ( - -
    - { FormattingUtils.formatCount(subListNotifCount) } -
    -
    - ); - } else if (this.props.isInvite && this.props.list.length) { - // no notifications but highlight anyway because this is an invite badge - badge = ( - -
    - { this.props.list.length } -
    -
    - ); - } - } - - let addRoomButton; - if (this.props.onAddRoom) { - addRoomButton = ( - - ); - } - - return ( -
    - - { chevron } - {this.props.label} - { incomingCall } - - { badge } - { addRoomButton } -
    - ); - } } -
    ; - } - - checkOverflow = () => { - if (this._scroller.current) { - this._scroller.current.checkOverflow(); - } - }; - - setHeight = (height) => { - if (this._subList.current) { - this._subList.current.style.height = toRem(height); - } - this._updateLazyRenderHeight(height); - }; - - _updateLazyRenderHeight(height) { - this.setState({scrollerHeight: height}); - } - - _onScroll = () => { - this.setState({scrollTop: this._scroller.current.getScrollTop()}); - }; - - _canUseLazyListRendering() { - // for now disable lazy rendering as they are already rendered tiles - // not rooms like props.list we pass to LazyRenderList - return !this.props.extraTiles || !this.props.extraTiles.length; - } - - render() { - const len = this.props.list.length + this.props.extraTiles.length; - const isCollapsed = this.state.hidden && !this.props.forceExpand; - - const subListClasses = classNames({ - "mx_RoomSubList": true, - "mx_RoomSubList_hidden": len && isCollapsed, - "mx_RoomSubList_nonEmpty": len && !isCollapsed, - }); - - let content; - if (len) { - if (isCollapsed) { - // no body - } else if (this._canUseLazyListRendering()) { - content = ( - - - - ); - } else { - const roomTiles = this.props.list.map(r => this.makeRoomTile(r)); - const tiles = roomTiles.concat(this.props.extraTiles); - content = ( - - { tiles } - - ); - } - } else { - if (this.props.showSpinner && !isCollapsed) { - const Loader = sdk.getComponent("elements.Spinner"); - content = ; - } - } - - return ( -
    - { this._getHeaderJsx(isCollapsed) } - { content } -
    - ); - } -} diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 69a2e54a2c..a79e5b0aa8 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -34,27 +34,29 @@ import ContentMessages from '../../ContentMessages'; import Modal from '../../Modal'; import * as sdk from '../../index'; import CallHandler from '../../CallHandler'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import Tinter from '../../Tinter'; import rate_limited_func from '../../ratelimitedfunc'; import * as ObjectUtils from '../../ObjectUtils'; import * as Rooms from '../../Rooms'; -import eventSearch from '../../Searching'; +import eventSearch, {searchPagination} from '../../Searching'; -import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard'; +import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard'; import MainSplit from './MainSplit'; import RightPanel from './RightPanel'; import RoomViewStore from '../../stores/RoomViewStore'; import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; import WidgetEchoStore from '../../stores/WidgetEchoStore'; -import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; +import SettingsStore from "../../settings/SettingsStore"; import AccessibleButton from "../views/elements/AccessibleButton"; import RightPanelStore from "../../stores/RightPanelStore"; import {haveTileForEvent} from "../views/rooms/EventTile"; import RoomContext from "../../contexts/RoomContext"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { shieldStatusForRoom } from '../../utils/ShieldUtils'; +import {Action} from "../../dispatcher/actions"; +import {SettingLevel} from "../../settings/SettingLevel"; const DEBUG = false; let debuglog = function() {}; @@ -164,6 +166,10 @@ export default createReactClass({ canReact: false, canReply: false, + + useIRCLayout: SettingsStore.getValue("useIRCLayout"), + + matrixClientIsReady: this.context && this.context.isInitialSyncComplete(), }; }, @@ -193,6 +199,8 @@ export default createReactClass({ this._roomView = createRef(); this._searchResultsPanel = createRef(); + + this._layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange); }, _onReadReceiptsChange: function() { @@ -232,7 +240,8 @@ export default createReactClass({ initialEventId: RoomViewStore.getInitialEventId(), isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), forwardingEvent: RoomViewStore.getForwardingEvent(), - shouldPeek: RoomViewStore.shouldPeek(), + // we should only peek once we have a ready client + shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(), showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId), showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), }; @@ -242,7 +251,7 @@ export default createReactClass({ this.context.stopPeeking(); } - // Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307 + // Temporary logging to diagnose https://github.com/vector-im/element-web/issues/4307 console.log( 'RVS update:', newState.roomId, @@ -532,6 +541,14 @@ export default createReactClass({ // no need to do this as Dir & Settings are now overlays. It just burnt CPU. // console.log("Tinter.tint from RoomView.unmount"); // Tinter.tint(); // reset colourscheme + + SettingsStore.unwatchSetting(this._layoutWatcherRef); + }, + + onLayoutChange: function() { + this.setState({ + useIRCLayout: SettingsStore.getValue("useIRCLayout"), + }); }, _onRightPanelStoreUpdate: function() { @@ -588,6 +605,18 @@ export default createReactClass({ handled = true; } break; + case Key.PAGE_UP: + if (!ev.altKey && !ev.ctrlKey && ev.shiftKey && !ev.metaKey) { + this.jumpToReadMarker(); + handled = true; + } + break; + case Key.U.toUpperCase(): + if (isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev) && ev.shiftKey) { + dis.dispatch({ action: "upload_file" }) + handled = true; + } + break; } if (handled) { @@ -669,6 +698,16 @@ export default createReactClass({ }); } break; + case 'sync_state': + if (!this.state.matrixClientIsReady) { + this.setState({ + matrixClientIsReady: this.context && this.context.isInitialSyncComplete(), + }, () => { + // send another "initial" RVS update to trigger peeking if needed + this._onRoomViewStoreUpdate(true); + }); + } + break; } }, @@ -842,15 +881,6 @@ export default createReactClass({ }); return; } - if (!SettingsStore.getValue("feature_cross_signing")) { - room.hasUnverifiedDevices().then((hasUnverifiedDevices) => { - this.setState({ - e2eStatus: hasUnverifiedDevices ? "warning" : "verified", - }); - }); - debuglog("e2e check is warning/verified only as cross-signing is off"); - return; - } /* At this point, the user has encryption on and cross-signing on */ this.setState({ @@ -1007,8 +1037,7 @@ export default createReactClass({ if (this.state.searchResults.next_batch) { debuglog("requesting more search results"); - const searchPromise = this.context.backPaginateRoomEventsSearch( - this.state.searchResults); + const searchPromise = searchPagination(this.state.searchResults); return this._handleSearchResult(searchPromise); } else { debuglog("no more search results"); @@ -1049,7 +1078,7 @@ export default createReactClass({ // Do this by indicating our intention to join // XXX: ILAG is disabled for now, - // see https://github.com/vector-im/riot-web/issues/8222 + // see https://github.com/vector-im/element-web/issues/8222 dis.dispatch({action: 'require_registration'}); // dis.dispatch({ // action: 'will_join', @@ -1134,7 +1163,7 @@ export default createReactClass({ ev.dataTransfer.files, this.state.room.roomId, this.context, ); this.setState({ draggingFile: false }); - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); }, onDragLeaveOrEnd: function(ev) { @@ -1285,6 +1314,14 @@ export default createReactClass({ const mxEv = result.context.getEvent(); const roomId = mxEv.getRoomId(); const room = this.context.getRoom(roomId); + if (!room) { + // if we do not have the room in js-sdk stores then hide it as we cannot easily show it + // As per the spec, an all rooms search can create this condition, + // it happens with Seshat but not Synapse. + // It will make the result count not match the displayed count. + console.log("Hiding search result from an unknown room", roomId); + continue; + } if (!haveTileForEvent(mxEv)) { // XXX: can this ever happen? It will make the result count @@ -1293,16 +1330,9 @@ export default createReactClass({ } if (this.state.searchScope === 'All') { - if (roomId != lastRoomId) { - - // XXX: if we've left the room, we might not know about - // it. We should tell the js sdk to go and find out about - // it. But that's not an issue currently, as synapse only - // returns results for rooms we're joined to. - const roomName = room ? room.name : _t("Unknown room %(roomId)s", { roomId: roomId }); - + if (roomId !== lastRoomId) { ret.push(
  • -

    { _t("Room") }: { roomName }

    +

    { _t("Room") }: { room.name }

  • ); lastRoomId = roomId; } @@ -1340,7 +1370,7 @@ export default createReactClass({ event: null, }); } - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); }, onLeaveClick: function() { @@ -1351,15 +1381,9 @@ export default createReactClass({ }, onForgetClick: function() { - this.context.forget(this.state.room.roomId).then(function() { - dis.dispatch({ action: 'view_next_room' }); - }, function(err) { - const errCode = err.errcode || _t("unknown error code"); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, { - title: _t("Error"), - description: _t("Failed to forget room %(errCode)s", { errCode: errCode }), - }); + dis.dispatch({ + action: 'forget_room', + room_id: this.state.room.roomId, }); }, @@ -1429,9 +1453,7 @@ export default createReactClass({ // using /leave rather than /join. In the short term though, we // just ignore them. // https://github.com/vector-im/vector-web/issues/1134 - dis.dispatch({ - action: 'view_room_directory', - }); + dis.fire(Action.ViewRoomDirectory); }, onSearchClick: function() { @@ -1451,7 +1473,7 @@ export default createReactClass({ // jump down to the bottom of this room, where new events are arriving jumpToLiveTimeline: function() { this._messagePanel.jumpToLiveTimeline(); - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); }, // jump up to wherever our read marker is @@ -1651,14 +1673,16 @@ export default createReactClass({ const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary"); if (!this.state.room) { - const loading = this.state.roomLoading || this.state.peekLoading; + const loading = !this.state.matrixClientIsReady || this.state.roomLoading || this.state.peekLoading; if (loading) { + // Assume preview loading if we don't have a ready client or a room ID (still resolving the alias) + const previewLoading = !this.state.matrixClientIsReady || !this.state.roomId || this.state.peekLoading; return (
    - ; } @@ -1899,25 +1929,29 @@ export default createReactClass({ } if (inCall) { - let zoomButton; let voiceMuteButton; let videoMuteButton; + let zoomButton; let videoMuteButton; if (call.type === "video") { zoomButton = (
    - +
    ); videoMuteButton =
    - + width="" height="27" />
    ; } - voiceMuteButton = + const voiceMuteButton =
    -
    ; @@ -1929,7 +1963,6 @@ export default createReactClass({ { videoMuteButton } { zoomButton } { statusBar } -
    ; } @@ -1944,8 +1977,9 @@ export default createReactClass({ searchResultsPanel = (
    ); } else { searchResultsPanel = ( - @@ -1965,6 +1999,13 @@ export default createReactClass({ highlightedEventId = this.state.initialEventId; } + const messagePanelClassNames = classNames( + "mx_RoomView_messagePanel", + { + "mx_IRCLayout": this.state.useIRCLayout, + "mx_GroupLayout": !this.state.useIRCLayout, + }); + // console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview); const messagePanel = ( ); let topUnreadMessagesBar = null; @@ -2001,6 +2043,7 @@ export default createReactClass({ if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) { const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton'); jumpToBottom = ( 0} numUnreadMessages={this.state.numUnreadMessages} onScrollToBottomClick={this.jumpToLiveTimeline} />); diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 4f44c1a169..51113f4f56 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -144,6 +144,11 @@ export default createReactClass({ /* resizeNotifier: ResizeNotifier to know when middle column has changed size */ resizeNotifier: PropTypes.object, + + /* fixedChildren: allows for children to be passed which are rendered outside + * of the wrapper + */ + fixedChildren: PropTypes.node, }, getDefaultProps: function() { @@ -643,7 +648,9 @@ export default createReactClass({ if (scrollState.stuckAtBottom) { const sn = this._getScrollNode(); - sn.scrollTop = sn.scrollHeight; + if (sn.scrollTop !== sn.scrollHeight) { + sn.scrollTop = sn.scrollHeight; + } } else if (scrollState.trackedScrollToken) { const itemlist = this._itemlist.current; const trackedNode = this._getTrackedNode(); @@ -652,7 +659,10 @@ export default createReactClass({ const bottomDiff = newBottomOffset - scrollState.bottomOffset; this._bottomGrowth += bottomDiff; scrollState.bottomOffset = newBottomOffset; - itemlist.style.height = `${this._getListHeight()}px`; + const newHeight = `${this._getListHeight()}px`; + if (itemlist.style.height !== newHeight) { + itemlist.style.height = newHeight; + } debuglog("balancing height because messages below viewport grew by", bottomDiff); } } @@ -689,12 +699,16 @@ export default createReactClass({ const height = Math.max(minHeight, contentHeight); this._pages = Math.ceil(height / PAGE_SIZE); this._bottomGrowth = 0; - const newHeight = this._getListHeight(); + const newHeight = `${this._getListHeight()}px`; const scrollState = this.scrollState; if (scrollState.stuckAtBottom) { - itemlist.style.height = `${newHeight}px`; - sn.scrollTop = sn.scrollHeight; + if (itemlist.style.height !== newHeight) { + itemlist.style.height = newHeight; + } + if (sn.scrollTop !== sn.scrollHeight){ + sn.scrollTop = sn.scrollHeight; + } debuglog("updateHeight to", newHeight); } else if (scrollState.trackedScrollToken) { const trackedNode = this._getTrackedNode(); @@ -704,7 +718,9 @@ export default createReactClass({ // the currently filled piece of the timeline if (trackedNode) { const oldTop = trackedNode.offsetTop; - itemlist.style.height = `${newHeight}px`; + if (itemlist.style.height !== newHeight) { + itemlist.style.height = newHeight; + } const newTop = trackedNode.offsetTop; const topDiff = newTop - oldTop; // important to scroll by a relative amount as @@ -881,6 +897,7 @@ export default createReactClass({ return ( + { this.props.fixedChildren }
      { this.props.children } diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index 0f3f8a6be9..7e9d290bce 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -19,7 +19,7 @@ import React, {createRef} from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { Key } from '../../Keyboard'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import { throttle } from 'lodash'; import AccessibleButton from '../../components/views/elements/AccessibleButton'; import classNames from 'classnames'; diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index c0e0e58db8..704dbf8832 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -27,25 +27,20 @@ import { ReactNode } from "react"; * Represents a tab for the TabbedView. */ export class Tab { - public label: string; - public icon: string; - public body: React.ReactNode; - /** * Creates a new tab. - * @param {string} tabLabel The untranslated tab label. - * @param {string} tabIconClass The class for the tab icon. This should be a simple mask. - * @param {React.ReactNode} tabJsx The JSX for the tab container. + * @param {string} id The tab's ID. + * @param {string} label The untranslated tab label. + * @param {string} icon The class for the tab icon. This should be a simple mask. + * @param {React.ReactNode} body The JSX for the tab container. */ - constructor(tabLabel: string, tabIconClass: string, tabJsx: React.ReactNode) { - this.label = tabLabel; - this.icon = tabIconClass; - this.body = tabJsx; + constructor(public id: string, public label: string, public icon: string, public body: React.ReactNode) { } } interface IProps { tabs: Tab[]; + initialTabId?: string; } interface IState { @@ -53,16 +48,17 @@ interface IState { } export default class TabbedView extends React.Component { - static propTypes = { - // The tabs to show - tabs: PropTypes.arrayOf(PropTypes.instanceOf(Tab)).isRequired, - }; - constructor(props: IProps) { super(props); + let activeTabIndex = 0; + if (props.initialTabId) { + const tabIndex = props.tabs.findIndex(t => t.id === props.initialTabId); + if (tabIndex >= 0) activeTabIndex = tabIndex; + } + this.state = { - activeTabIndex: 0, + activeTabIndex, }; } diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 6642cce098..4f8a051e62 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -22,7 +22,7 @@ import TagOrderStore from '../../stores/TagOrderStore'; import GroupActions from '../../actions/GroupActions'; import * as sdk from '../../index'; -import dis from '../../dispatcher'; +import dis from '../../dispatcher/dispatcher'; import { _t } from '../../languageHandler'; import { Droppable } from 'react-beautiful-dnd'; @@ -141,7 +141,7 @@ const TagPanel = createReactClass({ - - -
    ); - }, -}); -export default TagPanelButtons; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 6a08cd78eb..2313b60ab1 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -29,7 +29,7 @@ import {MatrixClientPeg} from "../../MatrixClientPeg"; import * as ObjectUtils from "../../ObjectUtils"; import UserActivity from "../../UserActivity"; import Modal from "../../Modal"; -import dis from "../../dispatcher"; +import dis from "../../dispatcher/dispatcher"; import * as sdk from "../../index"; import { Key } from '../../Keyboard'; import Timer from '../../utils/Timer'; @@ -112,6 +112,9 @@ const TimelinePanel = createReactClass({ // whether to show reactions for an event showReactions: PropTypes.bool, + + // whether to use the irc layout + useIRCLayout: PropTypes.bool, }, statics: { @@ -795,6 +798,9 @@ const TimelinePanel = createReactClass({ readMarkerVisible: false, }); } + + // Send the updated read marker (along with read receipt) to the server + this.sendReadReceipt(); }, @@ -1180,7 +1186,7 @@ const TimelinePanel = createReactClass({ if (!timeline) { // Somehow, it seems to be possible for live events to not have // a timeline, even though that should not happen. :( - // https://github.com/vector-im/riot-web/issues/12120 + // https://github.com/vector-im/element-web/issues/12120 console.warn( `Event ${events[i].getId()} in room ${room.roomId} is live, ` + `but it does not have a timeline`, @@ -1447,6 +1453,7 @@ const TimelinePanel = createReactClass({ getRelationsForEvent={this.getRelationsForEvent} editState={this.state.editState} showReactions={this.props.showReactions} + useIRCLayout={this.props.useIRCLayout} /> ); }, diff --git a/src/components/structures/ToastContainer.js b/src/components/structures/ToastContainer.tsx similarity index 69% rename from src/components/structures/ToastContainer.js rename to src/components/structures/ToastContainer.tsx index 283fbdd96a..84473031fa 100644 --- a/src/components/structures/ToastContainer.js +++ b/src/components/structures/ToastContainer.tsx @@ -15,14 +15,21 @@ limitations under the License. */ import * as React from "react"; -import { _t } from '../../languageHandler'; -import ToastStore from "../../stores/ToastStore"; +import ToastStore, {IToast} from "../../stores/ToastStore"; import classNames from "classnames"; -export default class ToastContainer extends React.Component { - constructor() { - super(); - this.state = {toasts: ToastStore.sharedInstance().getToasts()}; +interface IState { + toasts: IToast[]; + countSeen: number; +} + +export default class ToastContainer extends React.Component<{}, IState> { + constructor(props, context) { + super(props, context); + this.state = { + toasts: ToastStore.sharedInstance().getToasts(), + countSeen: ToastStore.sharedInstance().getCountSeen(), + }; // Start listening here rather than in componentDidMount because // toasts may dismiss themselves in their didMount if they find @@ -36,7 +43,10 @@ export default class ToastContainer extends React.Component { } _onToastStoreUpdate = () => { - this.setState({toasts: ToastStore.sharedInstance().getToasts()}); + this.setState({ + toasts: ToastStore.sharedInstance().getToasts(), + countSeen: ToastStore.sharedInstance().getCountSeen(), + }); }; render() { @@ -50,14 +60,21 @@ export default class ToastContainer extends React.Component { "mx_Toast_hasIcon": icon, [`mx_Toast_icon_${icon}`]: icon, }); - const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null; + + let countIndicator; + if (isStacked || this.state.countSeen > 0) { + countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`; + } const toastProps = Object.assign({}, props, { key, toastKey: key, }); toast = (
    -

    {title}{countIndicator}

    +
    +

    {title}

    + {countIndicator} +
    {React.createElement(component, toastProps)}
    ); } diff --git a/src/components/structures/TopLeftMenuButton.js b/src/components/structures/TopLeftMenuButton.js deleted file mode 100644 index ebd7aaae89..0000000000 --- a/src/components/structures/TopLeftMenuButton.js +++ /dev/null @@ -1,157 +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. -*/ - -import React from 'react'; -import PropTypes from 'prop-types'; -import TopLeftMenu from '../views/context_menus/TopLeftMenu'; -import BaseAvatar from '../views/avatars/BaseAvatar'; -import {MatrixClientPeg} from '../../MatrixClientPeg'; -import * as Avatar from '../../Avatar'; -import { _t } from '../../languageHandler'; -import dis from "../../dispatcher"; -import {ContextMenu, ContextMenuButton} from "./ContextMenu"; - -const AVATAR_SIZE = 28; - -export default class TopLeftMenuButton extends React.Component { - static propTypes = { - collapsed: PropTypes.bool.isRequired, - }; - - static displayName = 'TopLeftMenuButton'; - - constructor() { - super(); - this.state = { - menuDisplayed: false, - profileInfo: null, - }; - } - - async _getProfileInfo() { - const cli = MatrixClientPeg.get(); - const userId = cli.getUserId(); - const profileInfo = await cli.getProfileInfo(userId); - const avatarUrl = Avatar.avatarUrlForUser( - {avatarUrl: profileInfo.avatar_url}, - AVATAR_SIZE, AVATAR_SIZE, "crop"); - - return { - userId, - name: profileInfo.displayname, - avatarUrl, - }; - } - - async componentDidMount() { - this._dispatcherRef = dis.register(this.onAction); - - try { - const profileInfo = await this._getProfileInfo(); - this.setState({profileInfo}); - } catch (ex) { - console.log("could not fetch profile"); - console.error(ex); - } - } - - componentWillUnmount() { - dis.unregister(this._dispatcherRef); - } - - onAction = (payload) => { - // For accessibility - if (payload.action === "toggle_top_left_menu") { - if (this._buttonRef) this._buttonRef.click(); - } - }; - - _getDisplayName() { - if (MatrixClientPeg.get().isGuest()) { - return _t("Guest"); - } else if (this.state.profileInfo) { - return this.state.profileInfo.name; - } else { - return MatrixClientPeg.get().getUserId(); - } - } - - openMenu = (e) => { - e.preventDefault(); - e.stopPropagation(); - this.setState({ menuDisplayed: true }); - }; - - closeMenu = () => { - this.setState({ - menuDisplayed: false, - }); - }; - - render() { - const cli = MatrixClientPeg.get().getUserId(); - - const name = this._getDisplayName(); - let nameElement; - let chevronElement; - if (!this.props.collapsed) { - nameElement =
    - { name } -
    ; - chevronElement = ; - } - - let contextMenu; - if (this.state.menuDisplayed) { - const elementRect = this._buttonRef.getBoundingClientRect(); - - contextMenu = ( - - - - ); - } - - return - this._buttonRef = r} - label={_t("Your profile")} - isExpanded={this.state.menuDisplayed} - > - - { nameElement } - { chevronElement } - - - { contextMenu } - ; - } -} diff --git a/src/components/structures/UploadBar.js b/src/components/structures/UploadBar.js index 1aec63f04e..421d1d79a7 100644 --- a/src/components/structures/UploadBar.js +++ b/src/components/structures/UploadBar.js @@ -19,7 +19,7 @@ import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import ContentMessages from '../../ContentMessages'; -import dis from "../../dispatcher"; +import dis from "../../dispatcher/dispatcher"; import filesize from "filesize"; import { _t } from '../../languageHandler'; diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx new file mode 100644 index 0000000000..e782618872 --- /dev/null +++ b/src/components/structures/UserMenu.tsx @@ -0,0 +1,344 @@ +/* +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, { createRef } from "react"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; +import defaultDispatcher from "../../dispatcher/dispatcher"; +import { ActionPayload } from "../../dispatcher/payloads"; +import { Action } from "../../dispatcher/actions"; +import { _t } from "../../languageHandler"; +import { ContextMenuButton } from "./ContextMenu"; +import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog"; +import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; +import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog"; +import Modal from "../../Modal"; +import LogoutDialog from "../views/dialogs/LogoutDialog"; +import SettingsStore from "../../settings/SettingsStore"; +import {getCustomTheme} from "../../theme"; +import {getHostingLink} from "../../utils/HostingLink"; +import {ButtonEvent} from "../views/elements/AccessibleButton"; +import SdkConfig from "../../SdkConfig"; +import {getHomePageUrl} from "../../utils/pages"; +import { OwnProfileStore } from "../../stores/OwnProfileStore"; +import { UPDATE_EVENT } from "../../stores/AsyncStore"; +import BaseAvatar from '../views/avatars/BaseAvatar'; +import classNames from "classnames"; +import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; +import { SettingLevel } from "../../settings/SettingLevel"; +import IconizedContextMenu, { + IconizedContextMenuOption, + IconizedContextMenuOptionList +} from "../views/context_menus/IconizedContextMenu"; + +interface IProps { + isMinimized: boolean; +} + +type PartialDOMRect = Pick; + +interface IState { + contextMenuPosition: PartialDOMRect; + isDarkTheme: boolean; +} + +export default class UserMenu extends React.Component { + private dispatcherRef: string; + private themeWatcherRef: string; + private buttonRef: React.RefObject = createRef(); + + constructor(props: IProps) { + super(props); + + this.state = { + contextMenuPosition: null, + isDarkTheme: this.isUserOnDarkTheme(), + }; + + OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); + } + + private get hasHomePage(): boolean { + return !!getHomePageUrl(SdkConfig.get()); + } + + public componentDidMount() { + this.dispatcherRef = defaultDispatcher.register(this.onAction); + this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged); + } + + public componentWillUnmount() { + if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef); + if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); + OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); + } + + private isUserOnDarkTheme(): boolean { + const theme = SettingsStore.getValue("theme"); + if (theme.startsWith("custom-")) { + return getCustomTheme(theme.substring("custom-".length)).is_dark; + } + return theme === "dark"; + } + + private onProfileUpdate = async () => { + // the store triggered an update, so force a layout update. We don't + // have any state to store here for that to magically happen. + this.forceUpdate(); + }; + + private onThemeChanged = () => { + this.setState({isDarkTheme: this.isUserOnDarkTheme()}); + }; + + private onAction = (ev: ActionPayload) => { + if (ev.action !== Action.ToggleUserMenu) return; // not interested + + if (this.state.contextMenuPosition) { + this.setState({contextMenuPosition: null}); + } else { + if (this.buttonRef.current) this.buttonRef.current.click(); + } + }; + + private onOpenMenuClick = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + const target = ev.target as HTMLButtonElement; + this.setState({contextMenuPosition: target.getBoundingClientRect()}); + }; + + private onContextMenu = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + this.setState({ + contextMenuPosition: { + left: ev.clientX, + top: ev.clientY, + width: 20, + height: 0, + }, + }); + }; + + private onCloseMenu = () => { + this.setState({contextMenuPosition: null}); + }; + + private onSwitchThemeClick = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + // Disable system theme matching if the user hits this button + SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false); + + const newTheme = this.state.isDarkTheme ? "light" : "dark"; + SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab + }; + + private onSettingsOpen = (ev: ButtonEvent, tabId: string) => { + ev.preventDefault(); + ev.stopPropagation(); + + const payload: OpenToTabPayload = {action: Action.ViewUserSettings, initialTabId: tabId}; + defaultDispatcher.dispatch(payload); + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onShowArchived = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + // TODO: Archived room view: https://github.com/vector-im/element-web/issues/14038 + // Note: You'll need to uncomment the button too. + console.log("TODO: Show archived rooms"); + }; + + private onProvideFeedback = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog); + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onSignOutClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog); + this.setState({contextMenuPosition: null}); // also close the menu + }; + + private onHomeClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + defaultDispatcher.dispatch({action: 'view_home_page'}); + }; + + private renderContextMenu = (): React.ReactNode => { + if (!this.state.contextMenuPosition) return null; + + let hostingLink; + const signupLink = getHostingLink("user-context-menu"); + if (signupLink) { + hostingLink = ( +
    + {_t( + "Upgrade to your own domain", {}, + { + a: sub => ( + {sub} + ), + }, + )} +
    + ); + } + + let homeButton = null; + if (this.hasHomePage) { + homeButton = ( + + ); + } + + return +
    +
    + + {OwnProfileStore.instance.displayName} + + + {MatrixClientPeg.get().getUserId()} + +
    + + {_t("Switch + +
    + {hostingLink} + + {homeButton} + this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)} + /> + this.onSettingsOpen(e, USER_SECURITY_TAB)} + /> + this.onSettingsOpen(e, null)} + /> + {/* */} + + + + + +
    ; + }; + + public render() { + const avatarSize = 32; // should match border-radius of the avatar + + let name = {OwnProfileStore.instance.displayName}; + let buttons = ( + + {/* masked image in CSS */} + + ); + if (this.props.isMinimized) { + name = null; + buttons = null; + } + + const classes = classNames({ + 'mx_UserMenu': true, + 'mx_UserMenu_minimized': this.props.isMinimized, + }); + + return ( + + +
    + + + + {name} + {buttons} +
    +
    + {this.renderContextMenu()} +
    + ); + } +} diff --git a/src/components/structures/UserView.js b/src/components/structures/UserView.js index c4fba137cc..694592af88 100644 --- a/src/components/structures/UserView.js +++ b/src/components/structures/UserView.js @@ -22,6 +22,7 @@ import {MatrixClientPeg} from "../../MatrixClientPeg"; import * as sdk from "../../index"; import Modal from '../../Modal'; import { _t } from '../../languageHandler'; +import HomePage from "./HomePage"; export default class UserView extends React.Component { static get propTypes() { @@ -42,7 +43,10 @@ export default class UserView extends React.Component { } componentDidUpdate(prevProps) { - if (prevProps.userId !== this.props.userId) { + // XXX: We shouldn't need to null check the userId here, but we declare + // it as optional and MatrixChat sometimes fires in a way which results + // in an NPE when we try to update the profile info. + if (prevProps.userId !== this.props.userId && this.props.userId) { this._loadProfileInfo(); } } @@ -76,7 +80,7 @@ export default class UserView extends React.Component { const RightPanel = sdk.getComponent('structures.RightPanel'); const MainSplit = sdk.getComponent('structures.MainSplit'); const panel = ; - return (
    ); + return (); } else { return (
    ); } diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 5d3cb69417..3bc363f863 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -285,7 +285,7 @@ export default createReactClass({ // We'd like to rely on new props coming in via `onServerConfigChange` // so that we know the servers have definitely updated before clearing // the busy state. In the case of a full MXID that resolves to the same - // HS as Riot's default HS though, there may not be any server change. + // HS as Element's default HS though, there may not be any server change. // To avoid this trap, we clear busy here. For cases where the server // actually has changed, `_initLoginLogic` will be called and manages // busy state for its own liveness check. @@ -355,7 +355,8 @@ export default createReactClass({ ev.preventDefault(); ev.stopPropagation(); const ssoKind = step === 'm.login.sso' ? 'sso' : 'cas'; - PlatformPeg.get().startSingleSignOn(this._loginLogic.createTemporaryClient(), ssoKind); + PlatformPeg.get().startSingleSignOn(this._loginLogic.createTemporaryClient(), ssoKind, + this.props.fragmentAfterLogin); } else { // Don't intercept - just go through to the register page this.onRegisterClick(ev); @@ -614,8 +615,8 @@ export default createReactClass({ } // XXX: This link does *not* have a target="_blank" because single sign-on relies on // redirecting the user back to a URI once they're logged in. On the web, this means - // we use the same window and redirect back to riot. On electron, this actually - // opens the SSO page in the electron app itself due to + // we use the same window and redirect back to Element. On Electron, this actually + // opens the SSO page in the Electron app itself due to // https://github.com/electron/electron/issues/8841 and so happens to work. // If this bug gets fixed, it will break SSO since it will open the SSO page in the // user's browser, let them log into their SSO provider, then redirect their browser @@ -628,7 +629,9 @@ export default createReactClass({ + loginType={loginType} + fragmentAfterLogin={this.props.fragmentAfterLogin} + />
    ); }, diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index c74f6ed6e3..13e48f6287 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -32,7 +32,7 @@ import * as Lifecycle from '../../../Lifecycle'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import AuthPage from "../../views/auth/AuthPage"; import Login from "../../../Login"; -import dis from "../../../dispatcher"; +import dis from "../../../dispatcher/dispatcher"; // Phases // Show controls to configure server details @@ -243,10 +243,14 @@ export default createReactClass({ }); }; try { - await this._makeRegisterRequest({}); - // This should never succeed since we specified an empty - // auth object. - console.log("Expecting 401 from register request but got success!"); + // We do the first registration request ourselves to discover whether we need to + // do SSO instead. If we've already started the UI Auth process though, we don't + // need to. + if (!this.state.doingUIAuth) { + await this._makeRegisterRequest(null); + // This should never succeed since we specified no auth object. + console.log("Expecting 401 from register request but got success!"); + } } catch (e) { if (e.httpStatus === 401) { this.setState({ @@ -258,7 +262,7 @@ export default createReactClass({ // the user off to the login page to figure their account out. try { const loginLogic = new Login(hsUrl, isUrl, null, { - defaultDeviceDisplayName: "riot login check", // We shouldn't ever be used + defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used }); const flows = await loginLogic.getFlows(); const hasSsoFlow = flows.find(f => f.type === 'm.login.sso' || f.type === 'm.login.cas'); @@ -267,6 +271,7 @@ export default createReactClass({ dis.dispatch({action: 'start_login'}); } else { this.setState({ + serverErrorIsFatal: true, // fatal because user cannot continue on this server errorText: _t("Registration has been disabled on this homeserver."), // add empty flows array to get rid of spinner flows: [], @@ -373,7 +378,7 @@ export default createReactClass({ } if (response.access_token) { - const cli = await this.props.onLoggedIn({ + await this.props.onLoggedIn({ userId: response.user_id, deviceId: response.device_id, homeserverUrl: this.state.matrixClient.getHomeserverUrl(), @@ -381,7 +386,7 @@ export default createReactClass({ accessToken: response.access_token, }, this.state.formVals.password); - this._setupPushers(cli); + this._setupPushers(); // we're still busy until we get unmounted: don't show the registration form again newState.busy = true; } else { @@ -392,10 +397,11 @@ export default createReactClass({ this.setState(newState); }, - _setupPushers: function(matrixClient) { + _setupPushers: function() { if (!this.props.brand) { return Promise.resolve(); } + const matrixClient = MatrixClientPeg.get(); return matrixClient.getPushers().then((resp)=>{ const pushers = resp.pushers; for (let i = 0; i < pushers.length; ++i) { diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index 26534c6e02..6d090936e5 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; +import SdkConfig from '../../../SdkConfig'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import * as sdk from '../../../index'; import { @@ -28,6 +29,14 @@ import { PHASE_FINISHED, } from '../../../stores/SetupEncryptionStore'; +function keyHasPassphrase(keyInfo) { + return ( + keyInfo.passphrase && + keyInfo.passphrase.salt && + keyInfo.passphrase.iterations + ); +} + export default class SetupEncryptionBody extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, @@ -108,6 +117,23 @@ export default class SetupEncryptionBody extends React.Component { member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)} />; } else if (phase === PHASE_INTRO) { + const store = SetupEncryptionStore.sharedInstance(); + let recoveryKeyPrompt; + if (store.keyInfo && keyHasPassphrase(store.keyInfo)) { + recoveryKeyPrompt = _t("Use Recovery Key or Passphrase"); + } else if (store.keyInfo) { + recoveryKeyPrompt = _t("Use Recovery Key"); + } + + let useRecoveryKeyButton; + if (recoveryKeyPrompt) { + useRecoveryKeyButton = + {recoveryKeyPrompt} + ; + } + + const brand = SdkConfig.get().brand; + return (

    {_t( @@ -115,25 +141,24 @@ export default class SetupEncryptionBody extends React.Component { "granting it access to encrypted messages.", )}

    {_t( - "This requires the latest Riot on your other devices:", + "This requires the latest %(brand)s on your other devices:", + { brand }, )}

    -
    Riot Web
    -
    Riot Desktop
    +
    {_t("%(brand)s Web", { brand })}
    +
    {_t("%(brand)s Desktop", { brand })}
    -
    Riot iOS
    -
    Riot X for Android
    +
    {_t("%(brand)s iOS", { brand })}
    +
    {_t("%(brand)s Android", { brand })}

    {_t("or another cross-signing capable Matrix client")}

    - - {_t("Use Recovery Passphrase or Key")} - + {useRecoveryKeyButton} {_t("Skip")} diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js index 08ab7e8a61..a539c8c9ee 100644 --- a/src/components/structures/auth/SoftLogout.js +++ b/src/components/structures/auth/SoftLogout.js @@ -18,13 +18,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import {_t} from '../../../languageHandler'; import * as sdk from '../../../index'; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import * as Lifecycle from '../../../Lifecycle'; import Modal from '../../../Modal'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {sendLoginRequest} from "../../../Login"; import AuthPage from "../../views/auth/AuthPage"; import SSOButton from "../../views/elements/SSOButton"; +import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform"; const LOGIN_VIEW = { LOADING: 1, @@ -43,7 +44,7 @@ const FLOWS_TO_VIEWS = { export default class SoftLogout extends React.Component { static propTypes = { // Query parameters from MatrixChat - realQueryParams: PropTypes.object, // {homeserver, identityServer, loginToken} + realQueryParams: PropTypes.object, // {loginToken} // Called when the SSO login completes onTokenLoginCompleted: PropTypes.func, @@ -71,7 +72,7 @@ export default class SoftLogout extends React.Component { this._initLogin(); - MatrixClientPeg.get().flagAllGroupSessionsForBackup().then(remaining => { + MatrixClientPeg.get().countSessionsNeedingBackup().then(remaining => { this.setState({keyBackupNeeded: remaining > 0}); }); } @@ -90,7 +91,7 @@ export default class SoftLogout extends React.Component { async _initLogin() { const queryParams = this.props.realQueryParams; - const hasAllParams = queryParams && queryParams['homeserver'] && queryParams['loginToken']; + const hasAllParams = queryParams && queryParams['loginToken']; if (hasAllParams) { this.setState({loginView: LOGIN_VIEW.LOADING}); this.trySsoLogin(); @@ -157,8 +158,8 @@ export default class SoftLogout extends React.Component { async trySsoLogin() { this.setState({busy: true}); - const hsUrl = this.props.realQueryParams['homeserver']; - const isUrl = this.props.realQueryParams['identityServer'] || MatrixClientPeg.get().getIdentityServerUrl(); + const hsUrl = localStorage.getItem(SSO_HOMESERVER_URL_KEY); + const isUrl = localStorage.getItem(SSO_ID_SERVER_URL_KEY) || MatrixClientPeg.get().getIdentityServerUrl(); const loginType = "m.login.token"; const loginParams = { token: this.props.realQueryParams['loginToken'], @@ -244,7 +245,9 @@ export default class SoftLogout extends React.Component {

    {introText}

    + loginType={this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"} + fragmentAfterLogin={this.props.fragmentAfterLogin} + />
    ); } diff --git a/src/components/views/auth/CountryDropdown.js b/src/components/views/auth/CountryDropdown.js index 14a974668b..37b1967c48 100644 --- a/src/components/views/auth/CountryDropdown.js +++ b/src/components/views/auth/CountryDropdown.js @@ -19,7 +19,7 @@ import PropTypes from 'prop-types'; import * as sdk from '../../../index'; -import { COUNTRIES } from '../../../phonenumber'; +import {COUNTRIES, getEmojiFlag} from '../../../phonenumber'; import SdkConfig from "../../../SdkConfig"; import { _t } from "../../../languageHandler"; @@ -80,7 +80,7 @@ export default class CountryDropdown extends React.Component { } _flagImgForIso2(iso2) { - return ; + return
    { getEmojiFlag(iso2) }
    ; } _getShortOption(iso2) { diff --git a/src/components/views/auth/CustomServerDialog.js b/src/components/views/auth/CustomServerDialog.js index 024951e6c0..7b2c8f88aa 100644 --- a/src/components/views/auth/CustomServerDialog.js +++ b/src/components/views/auth/CustomServerDialog.js @@ -1,6 +1,6 @@ /* Copyright 2015, 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. @@ -18,11 +18,13 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; +import SdkConfig from '../../../SdkConfig'; export default createReactClass({ displayName: 'CustomServerDialog', render: function() { + const brand = SdkConfig.get().brand; return (
    @@ -32,8 +34,9 @@ export default createReactClass({

    {_t( "You can use the custom server options to sign into other " + "Matrix servers by specifying a different homeserver URL. This " + - "allows you to use this app with an existing Matrix account on a " + + "allows you to use %(brand)s with an existing Matrix account on a " + "different homeserver.", + { brand }, )}

    diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index 327451be17..7080eb3602 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -109,7 +109,7 @@ export const PasswordAuthEntry = createReactClass({ this.props.submitAuthDict({ type: PasswordAuthEntry.LOGIN_TYPE, // TODO: Remove `user` once servers support proper UIA - // See https://github.com/vector-im/riot-web/issues/10312 + // See https://github.com/vector-im/element-web/issues/10312 user: this.props.matrixClient.credentials.userId, identifier: { type: "m.id.user", @@ -355,6 +355,7 @@ export const TermsAuthEntry = createReactClass({ allChecked = allChecked && checked; checkboxes.push( + // XXX: replace with StyledCheckbox