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/CHANGELOG.md b/CHANGELOG.md index 63702de38b..d944d58f36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,757 @@ +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) 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/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/src/stores/room-list/README.md b/docs/room-list-store.md similarity index 63% rename from src/stores/room-list/README.md rename to docs/room-list-store.md index f4a56130ca..53f0527209 100644 --- a/src/stores/room-list/README.md +++ b/docs/room-list-store.md @@ -2,20 +2,31 @@ 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 behaviour of the room list whereas the sorting -algorithm determines how rooms get ordered within tags affected by the list algorithm. +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. -Behaviour of the room list takes the shape of determining what features the room list supports, as well -as determining where and when to apply the sorting algorithm in a tag. The importance algorithm, which -is described later in this doc, is an example of an algorithm which makes heavy behavioural changes -to the room list. 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. +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 @@ -70,33 +81,33 @@ Conveniently, each tag gets ordered by those categories as presented: red rooms 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-sub-list fashion. This should result in the red rooms (for example) +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 -The algorithm also has a concept of a 'sticky' room which is the room the user is currently viewing. -The sticky room will remain in position on the room list regardless of other factors going on as typically -clicking on a room will cause it to change categories into 'idle'. This is done by preserving N rooms -above the selected room at all times, where N is the number of rooms above the selected rooms when it was -selected. +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. -For example, if the user has 3 red rooms and selects the middle room, they will always see exactly one -room above their selection at all times. If they receive another notification, and the tag ordering is -specified as Recent, they'll see the new notification go to the top position, and the one that was previously -there fall behind the sticky room. +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. -The sticky room's category is technically 'idle' while being viewed and is explicitly pulled out of the -tag sorting algorithm's input as it must maintain its position in the list. When the user moves to another -room, the previous sticky room gets recalculated to determine which category it needs to be in as the user -could have been scrolled up while new messages were received. +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. -Further, 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. +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 @@ -128,13 +139,13 @@ 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` superclass is also -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 `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/package.json b/package.json index 93d59a4fa6..61a9a21815 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.7.1", + "version": "3.0.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -45,121 +45,125 @@ "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", + "@babel/runtime": "^7.10.5", "await-lock": "^2.0.1", - "blueimp-canvas-to-blob": "^3.5.0", + "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", + "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", - "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", - "react-resizable": "^1.10.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", + "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/lodash": "^4.14.152", + "@types/linkifyjs": "^2.1.3", + "@types/lodash": "^4.14.158", "@types/modernizr": "^3.5.3", - "@types/node": "^12.12.41", + "@types/node": "^12.12.51", "@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", - "babel-eslint": "^10.0.3", + "@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": [ diff --git a/res/css/_common.scss b/res/css/_common.scss index ebeeb381e6..f2d3a0e54b 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 { @@ -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; @@ -428,6 +427,10 @@ 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 @@ -581,3 +584,111 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { // So it fits in the space provided by the page max-width: 120px; } + +// 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_compact { + .mx_IconizedContextMenu_optionList > * { + padding: 8px 16px 8px 11px; + } + } +} + +@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; + } +} diff --git a/res/css/_components.scss b/res/css/_components.scss index 6995795117..1f86bb67a6 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -15,19 +15,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"; @@ -46,7 +47,9 @@ @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/_MessageContextMenu.scss"; @import "./views/context_menus/_RoomTileContextMenu.scss"; @import "./views/context_menus/_StatusMessageContextMenu.scss"; @@ -68,10 +71,12 @@ @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss"; +@import "./views/dialogs/_RebrandDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomSettingsDialogBridges.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; @import "./views/dialogs/_RoomUpgradeWarningDialog.scss"; +@import "./views/dialogs/_ServerOfflineDialog.scss"; @import "./views/dialogs/_SetEmailDialog.scss"; @import "./views/dialogs/_SetMxIdDialog.scss"; @import "./views/dialogs/_SetPasswordDialog.scss"; @@ -102,7 +107,6 @@ @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"; @@ -115,6 +119,7 @@ @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"; @@ -140,7 +145,6 @@ @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"; @@ -163,32 +167,31 @@ @import "./views/rooms/_EventTile.scss"; @import "./views/rooms/_GroupLayout.scss"; @import "./views/rooms/_IRCLayout.scss"; -@import "./views/rooms/_InviteOnlyIcon.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; @import "./views/rooms/_LinkPreviewWidget.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/_ReplyTile.scss"; @import "./views/rooms/_RoomBreadcrumbs.scss"; -@import "./views/rooms/_RoomDropTarget.scss"; @import "./views/rooms/_RoomHeader.scss"; @import "./views/rooms/_RoomList.scss"; @import "./views/rooms/_RoomPreviewBar.scss"; @import "./views/rooms/_RoomRecoveryReminder.scss"; -@import "./views/rooms/_RoomSublist2.scss"; +@import "./views/rooms/_RoomSublist.scss"; @import "./views/rooms/_RoomTile.scss"; +@import "./views/rooms/_RoomTileIcon.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"; @@ -216,7 +219,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 2d7ab67e40..caa3a452b0 100644 --- a/res/css/_font-sizes.scss +++ b/res/css/_font-sizes.scss @@ -14,59 +14,60 @@ See the License for the specific language governing permissions and limitations under the License. */ -$font-1px: 0.067rem; -$font-1-5px: 0.100rem; -$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.3333333rem; -$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/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..50b01b4a14 100644 --- a/res/css/structures/_FilePanel.scss +++ b/res/css/structures/_FilePanel.scss @@ -109,3 +109,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 899824bc57..db531cf088 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,172 +14,171 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_LeftPanel_container { - display: flex; - /* LeftPanel 260px */ - min-width: 260px; - max-width: 50%; - flex: 0 0 auto; -} - -// TODO: Remove temporary indicator of new room list implementation. -// This border is meant to visually distinguish between the two components when the -// user has turned on the new room list implementation, at least until the designs -// themselves give it away. -.mx_LeftPanel2 .mx_LeftPanel { - border-left: 5px #e26dff solid; -} - -.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_expanded + .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: 28px; + height: 28px; + border-radius: 20px; + background-color: $roomlist-button-bg-color; + position: relative; + margin-left: 8px; + + &::before { + content: ''; + position: absolute; + top: 6px; + left: 6px; + width: 16px; + height: 16px; + mask-image: url('$(res)/img/feather-customised/compass.svg'); + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + } + } + } + + .mx_LeftPanel_roomListWrapper { + overflow: hidden; + margin-top: 10px; // so we're not up against the search/filter + + &.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..aee7b5a154 100644 --- a/res/css/structures/_MainSplit.scss +++ b/res/css/structures/_MainSplit.scss @@ -21,8 +21,20 @@ 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; + + &: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 05c703ab6d..af6f6c79e9 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -66,7 +66,7 @@ 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; @@ -78,3 +78,24 @@ limitations under the License. */ 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 561ab1446f..715a94fe2c 100644 --- a/res/css/structures/_NotificationPanel.scss +++ b/res/css/structures/_NotificationPanel.scss @@ -99,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 600871e071..120f44db90 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -19,10 +19,16 @@ limitations under the License. overflow-x: hidden; flex: 0 0 auto; position: relative; - min-width: 264px; - max-width: 50%; 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 { @@ -44,22 +50,20 @@ 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; + top: 4px; // center with parent of 32px + left: 4px; // center with parent of 32px + height: 24px; + width: 24px; background-color: $rightpanel-button-color; mask-repeat: no-repeat; mask-size: contain; @@ -67,38 +71,46 @@ limitations under the License. } .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; + } +} + +.mx_RightPanel_headerButton:not(.mx_RightPanel_headerButton_highlight) { + &:hover { + background: rgba($accent-color, 0.1); + + &::before { + background-color: $accent-color; + } + } } .mx_RightPanel_headerButton_badge { @@ -132,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..39a3dee30b --- /dev/null +++ b/res/css/structures/_RoomSearch.scss @@ -0,0 +1,81 @@ +/* +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: 20px; + background-color: $roomlist-button-bg-color; + 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/feather-customised/search-input.svg'); + mask-repeat: no-repeat; + background: $primary-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: $primary-fg-color !important; // !important to override default app-wide styles + } + } + + &.mx_RoomSearch_expanded { + .mx_RoomSearch_clearButton { + width: 16px; + height: 16px; + mask-image: url('$(res)/img/feather-customised/x.svg'); + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-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 1f8443e395..78e8326772 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: 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; @@ -159,7 +157,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 2916c4ffdc..e798e4ac52 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -48,14 +48,17 @@ limitations under the License. padding: 8px; &.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 { @@ -63,8 +66,22 @@ 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; + } + } + + &.mx_Toast_icon_element_logo::after { + background-image: url("$(res)/img/element-logo.svg"); } .mx_Toast_title, .mx_Toast_body { diff --git a/res/css/structures/_TopLeftMenuButton.scss b/res/css/structures/_TopLeftMenuButton.scss deleted file mode 100644 index 8d2e36bcd6..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: $font-22px; - 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..81a10ee1d0 --- /dev/null +++ b/res/css/structures/_UserMenu.scss @@ -0,0 +1,201 @@ +/* +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_UserMenu_contextMenu_redRow { + .mx_AccessibleButton { + padding-top: 16px; + padding-bottom: 16px; + color: $warning-color !important; // !important to override styles from context menu + } + + .mx_IconizedContextMenu_icon::before { + background-color: $warning-color; + } + } + + .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 120da4c4f1..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 { @@ -146,3 +151,12 @@ limitations under the License. .mx_AuthBody_spinner { margin: 1em 0; } + +@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 index d1b8c47d00..bf8e7f4438 100644 --- a/res/css/views/auth/_PassphraseField.scss +++ b/res/css/views/auth/_PassphraseField.scss @@ -18,16 +18,6 @@ $PassphraseStrengthHigh: $accent-color; $PassphraseStrengthMedium: $username-variant5-color; $PassphraseStrengthLow: $notice-primary-color; -@define-mixin ProgressBarColour $colour { - color: $colour; - &::-moz-progress-bar { - background-color: $colour; - } - &::-webkit-progress-value { - background-color: $colour; - } -} - progress.mx_PassphraseField_progress { appearance: none; width: 100%; @@ -36,15 +26,7 @@ progress.mx_PassphraseField_progress { position: absolute; top: -12px; - border-radius: 2px; - &::-moz-progress-bar { - border-radius: 2px; - } - &::-webkit-progress-bar, - &::-webkit-progress-value { - border-radius: 2px; - } - + @mixin ProgressBarBorderRadius "2px"; @mixin ProgressBarColour $PassphraseStrengthLow; &[value="2"], &[value="3"] { @mixin ProgressBarColour $PassphraseStrengthMedium; diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.scss b/res/css/views/avatars/_DecoratedRoomAvatar.scss new file mode 100644 index 0000000000..48d72131b5 --- /dev/null +++ b/res/css/views/avatars/_DecoratedRoomAvatar.scss @@ -0,0 +1,34 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// XXX: We shouldn't be using TemporaryTile anywhere - delete it. +.mx_DecoratedRoomAvatar, .mx_TemporaryTile { + position: relative; + + .mx_RoomTileIcon { + position: absolute; + bottom: 0; + right: 0; + } + + .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/_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/_RebrandDialog.scss b/res/css/views/dialogs/_RebrandDialog.scss new file mode 100644 index 0000000000..534584ae2a --- /dev/null +++ b/res/css/views/dialogs/_RebrandDialog.scss @@ -0,0 +1,64 @@ +/* +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_RebrandDialog { + text-align: center; + + a:link, + a:hover, + a:visited { + @mixin mx_Dialog_link; + } + + .mx_Dialog_buttons { + margin-top: 43px; + text-align: center; + } +} + +.mx_RebrandDialog_body { + width: 550px; + margin-left: auto; + margin-right: auto; +} + +.mx_RebrandDialog_logoContainer { + margin-top: 35px; + margin-bottom: 20px; + display: flex; + align-items: center; + justify-content: center; +} + +.mx_RebrandDialog_logo { + margin-left: 28px; + margin-right: 28px; + width: 64px; + height: 64px; +} + +.mx_RebrandDialog_chevron::after { + content: ''; + display: inline-block; + width: 30px; + height: 30px; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background-color: $muted-fg-color; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + transform: rotate(-90deg); +} 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 e3d2ae8306..d2fe98e8f9 100644 --- a/res/css/views/dialogs/_ShareDialog.scss +++ b/res/css/views/dialogs/_ShareDialog.scss @@ -55,7 +55,7 @@ limitations under the License. margin-left: 5px; width: 20px; height: 20px; - background-repeat: none; + background-repeat: no-repeat; } .mx_ShareDialog_split { diff --git a/res/css/views/dialogs/_UserSettingsDialog.scss b/res/css/views/dialogs/_UserSettingsDialog.scss index 7adcc58c4e..bd472710ea 100644 --- a/res/css/views/dialogs/_UserSettingsDialog.scss +++ b/res/css/views/dialogs/_UserSettingsDialog.scss @@ -18,41 +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/feather-customised/brush.svg'); + 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/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 63e5a3de09..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; @@ -73,33 +126,42 @@ limitations under the License. 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/_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/_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/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/_StyledCheckbox.scss b/res/css/views/elements/_StyledCheckbox.scss index 14081f1e99..60f1bf0277 100644 --- a/res/css/views/elements/_StyledCheckbox.scss +++ b/res/css/views/elements/_StyledCheckbox.scss @@ -24,7 +24,9 @@ limitations under the License. align-items: flex-start; input[type=checkbox] { - display: none; + appearance: none; + margin: 0; + padding: 0; & + label { display: flex; @@ -48,6 +50,8 @@ limitations under the License. border-radius: $border-radius; img { + display: none; + height: 100%; width: 100%; filter: invert(100%); @@ -57,10 +61,24 @@ limitations under the License. &: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; + } } } diff --git a/res/css/views/elements/_StyledRadioButton.scss b/res/css/views/elements/_StyledRadioButton.scss new file mode 100644 index 0000000000..ffa1337ebb --- /dev/null +++ b/res/css/views/elements/_StyledRadioButton.scss @@ -0,0 +1,117 @@ +/* +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; + + 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; + } + } + + &: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/_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/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 9f3971ecf0..e3ccd99611 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -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/_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 1b1bab67bc..6be417f631 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -81,7 +81,7 @@ $AppsDrawerBodyHeight: 273px; margin: 0; padding: 0; border: 5px solid $widget-menu-bar-bg-color; - border-radius: 4px; + border-radius: 8px; } .mx_AppTile_mini { @@ -98,6 +98,7 @@ $AppsDrawerBodyHeight: 273px; .mx_AppTile_mini .mx_AppTile_persistedWrapper { height: 114px; + 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 40a80f17bb..2a2191b799 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; @@ -45,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 { @@ -73,7 +75,7 @@ limitations under the License. /* 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 { @@ -111,7 +113,7 @@ limitations under the License. .mx_EventTile_line, .mx_EventTile_reply { position: relative; - padding-left: 65px; /* left gutter */ + padding-left: $left-gutter; border-radius: 4px; } @@ -182,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; } @@ -328,29 +330,67 @@ limitations under the License. .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; } @@ -392,10 +432,6 @@ limitations under the License. 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 { @@ -403,15 +439,15 @@ limitations under the License. } .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, @@ -500,7 +536,7 @@ limitations under the License. display: inline-block; visibility: hidden; cursor: pointer; - top: 6px; + top: 8px; right: 6px; width: 19px; height: 19px; @@ -532,7 +568,6 @@ limitations under the License. .mx_EventTile_content .markdown-body a { color: $accent-color-alt; - text-decoration: underline; } .mx_EventTile_content .markdown-body .hljs { @@ -572,3 +607,14 @@ limitations under the License. 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 index de4a5538cd..2b447be44a 100644 --- a/res/css/views/rooms/_GroupLayout.scss +++ b/res/css/views/rooms/_GroupLayout.scss @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -$left-gutter: 65px; +$left-gutter: 64px; .mx_GroupLayout { .mx_EventTile { @@ -108,12 +108,12 @@ $left-gutter: 65px; top: 27px; } - .mx_EventTile_continuation .mx_EventTile_readAvatars, - .mx_EventTile_emote .mx_EventTile_readAvatars { + &.mx_EventTile_continuation .mx_EventTile_readAvatars, + &.mx_EventTile_emote .mx_EventTile_readAvatars { top: 5px; } - .mx_EventTile_info .mx_EventTile_readAvatars { + &.mx_EventTile_info .mx_EventTile_readAvatars { top: 4px; } diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index a8eb35eeed..ed60c220e7 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -43,6 +43,10 @@ $irc-line-height: $font-18px; > .mx_EventTile_msgOption { order: 5; flex-shrink: 0; + + .mx_EventTile_readAvatars { + top: 0.2rem; // ($irc-line-height - avatar height) / 2 + } } > .mx_SenderProfile { @@ -78,7 +82,7 @@ $irc-line-height: $font-18px; align-items: center; // Need to use important to override the js provided height and width values. - > .mx_BaseAvatar, .mx_BaseAvatar > * { + > .mx_BaseAvatar, > .mx_BaseAvatar > * { height: $font-14px !important; width: $font-14px !important; font-size: $font-10px !important; @@ -93,13 +97,19 @@ $irc-line-height: $font-18px; } > .mx_EventTile_e2eIcon { - position: relative; + position: absolute; right: unset; left: unset; + top: 0; + padding: 0; - order: 3; + flex-shrink: 0; flex-grow: 0; + + height: $font-18px; + + background-position: center; } .mx_EventTile_line { @@ -111,11 +121,6 @@ $irc-line-height: $font-18px; } } - .mx_EvenTile_line .mx_MessageActionBar, - .mx_EvenTile_line .mx_ReplyThread_wrapper { - display: block; - } - .mx_EventTile_reply { order: 4; } @@ -176,9 +181,11 @@ $irc-line-height: $font-18px; > span { display: flex; - > .mx_SenderProfile_name { + > .mx_SenderProfile_name, + > .mx_SenderProfile_aux { overflow: hidden; text-overflow: ellipsis; + min-width: var(--name-width); } } } 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/_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 c1cda7bf24..ec95403262 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -20,7 +20,7 @@ limitations under the License. margin: auto; border-top: 1px solid $primary-hairline-color; position: relative; - padding-left: 84px; + padding-left: 82px; } .mx_MessageComposer_replaced_wrapper { @@ -60,7 +60,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 +76,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 { @@ -196,30 +196,35 @@ limitations under the License. mask-size: contain; mask-position: center; } + + &.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/feather-customised/emoji3.custom.svg'); + mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg'); } .mx_MessageComposer_stickers::before { - mask-image: url('$(res)/img/feather-customised/sticker.custom.svg'); + mask-image: url('$(res)/img/element-icons/room/composer/sticker.svg'); } .mx_MessageComposer_formatting { 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 08fbd27808..229b4291db 100644 --- a/res/css/views/rooms/_ReplyPreview.scss +++ b/res/css/views/rooms/_ReplyPreview.scss @@ -22,9 +22,10 @@ limitations under the License. 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..ba46100ea6 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; + top: 4px; // center with parent of 32px + left: 4px; // center with parent of 32px + height: 24px; + width: 24px; background-color: $roomheader-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 c23c19699d..89ab85e146 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. @@ -15,56 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_RoomList.mx_RoomList2 { - overflow-y: auto; -} - .mx_RoomList { - /* take up remaining space below TopLeftMenu */ - flex: 1; - min-height: 0; - overflow-y: hidden; -} - -.mx_RoomList .mx_ResizeHandle { - // needed so the z-index takes effect - position: relative; -} - -/* 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 { - 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; -} - -.mx_RoomList_headerButtons { - position: absolute; - right: 60px; + padding-right: 7px; // width of the scrollbar, to line things up } 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..d3c9b79c69 --- /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/feather-customised/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/riot-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 7f93da0bbf..f22228602d 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,214 +14,222 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Note: the room tile expects to be in a flexbox column container .mx_RoomTile { - display: flex; - flex-direction: row; - align-items: center; - cursor: pointer; - height: 34px; - margin: 0; - padding: 0 8px 0 10px; - position: relative; - - .mx_RoomTile_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; - display: flex; -} - -.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_iconCheck::before { + mask-image: url('$(res)/img/element-icons/roomlist/checkmark.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_contextMenu_redRow { + .mx_AccessibleButton { + color: $warning-color !important; // !important to override styles from context menu + } -.mx_RoomTile_unread, .mx_RoomTile_highlight { - .mx_RoomTile_name { - font-weight: 600; - color: $roomtile-selected-color; + .mx_IconizedContextMenu_icon::before { + background-color: $warning-color; + } + } + + .mx_RoomTile_contextMenu_activeRow { + &.mx_AccessibleButton, .mx_AccessibleButton { + color: $accent-color !important; // !important to override styles from context menu + } + + .mx_IconizedContextMenu_icon::before { + background-color: $accent-color; + } + } + + .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_RoomTile_iconStar::before { + mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg'); + } + + .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/_RoomTileIcon.scss b/res/css/views/rooms/_RoomTileIcon.scss new file mode 100644 index 0000000000..2f3afdd446 --- /dev/null +++ b/res/css/views/rooms/_RoomTileIcon.scss @@ -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. +*/ + +.mx_RoomTileIcon { + width: 12px; + height: 12px; + border-radius: 12px; + background-color: $roomlist-bg-color; // to match the room list itself +} + +.mx_RoomTileIcon_globe::before { + content: ''; + width: 8px; + height: 8px; + top: 2px; + left: 2px; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + mask-image: url('$(res)/img/globe.svg'); +} + +.mx_RoomTileIcon_offline::before { + content: ''; + width: 8px; + height: 8px; + top: 2px; + left: 2px; + position: absolute; + border-radius: 8px; + background-color: $presence-offline; +} + +.mx_RoomTileIcon_online::before { + content: ''; + width: 8px; + height: 8px; + top: 2px; + left: 2px; + position: absolute; + border-radius: 8px; + background-color: $presence-online; +} + +.mx_RoomTileIcon_away::before { + content: ''; + width: 8px; + height: 8px; + top: 2px; + left: 2px; + position: absolute; + border-radius: 8px; + background-color: $presence-away; +} 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/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index e82ae3c575..94983a60bf 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -15,31 +15,216 @@ limitations under the License. */ .mx_AppearanceUserSettingsTab_fontSlider, -.mx_AppearanceUserSettingsTab_themeSection .mx_Field, -.mx_AppearanceUserSettingsTab_fontScaling .mx_Field { +.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: $font-slider-bg-color; + 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/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/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/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/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/element-logo.svg b/res/img/element-logo.svg new file mode 100644 index 0000000000..2cd11ed193 --- /dev/null +++ b/res/img/element-logo.svg @@ -0,0 +1,6 @@ + + + + + + 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/brush.svg b/res/img/feather-customised/brush.svg deleted file mode 100644 index d7f2738629..0000000000 --- a/res/img/feather-customised/brush.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - 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/compass.svg b/res/img/feather-customised/compass.svg new file mode 100644 index 0000000000..3296260803 --- /dev/null +++ b/res/img/feather-customised/compass.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/img/feather-customised/emoji3.custom.svg b/res/img/feather-customised/emoji3.custom.svg deleted file mode 100644 index d91ba1c132..0000000000 --- a/res/img/feather-customised/emoji3.custom.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - 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/sticker.custom.svg b/res/img/feather-customised/sticker.custom.svg deleted file mode 100644 index 691e3b3925..0000000000 --- a/res/img/feather-customised/sticker.custom.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - 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/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/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..cc22bc6e66 --- /dev/null +++ b/res/img/globe.svg @@ -0,0 +1,6 @@ + + + + + + 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/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/riot-logo.svg b/res/img/riot-logo.svg new file mode 100644 index 0000000000..ac1e547234 --- /dev/null +++ b/res/img/riot-logo.svg @@ -0,0 +1,6 @@ + + + + + + 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 9fb36ef1a3..e39bb29044 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,7 +39,8 @@ $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; @@ -44,10 +49,10 @@ $selected-color: $room-highlight-color; $event-selected-color: $header-panel-bg-color; // 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,7 +96,8 @@ $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; @@ -102,16 +108,26 @@ $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-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,8 +196,14 @@ $breadcrumb-placeholder-bg-color: #272c35; $user-tile-hover-bg-color: $header-panel-bg-color; -// FontSlider colors -$font-slider-bg-color: $room-highlight-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! ***** 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..7ecfcf13d9 --- /dev/null +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -0,0 +1,273 @@ +// 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; +$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; +$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; +$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-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..3465aa307e --- /dev/null +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -0,0 +1,372 @@ +// 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; +$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; +$roomheader-button-color: #91a1c0; +$groupheader-button-color: #91a1c0; +$rightpanel-button-color: #91a1c0; +$composer-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-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/icon_copy_message.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..0744347826 --- /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 `riot-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 78fe2a74c5..e67bcdf89a 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; @@ -80,7 +77,7 @@ $selected-color: $secondary-accent-color; $event-selected-color: $header-panel-bg-color; // 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,8 +154,9 @@ $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; @@ -167,35 +165,44 @@ $composer-button-color: #91A1C0; $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: #fff; // Buttons include the filter box, explore button, and sublist buttons +$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; // ******************** @@ -270,10 +277,10 @@ $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; @@ -296,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; @@ -307,7 +315,13 @@ $breadcrumb-placeholder-bg-color: #e8eef5; $user-tile-hover-bg-color: $header-panel-bg-color; // FontSlider colors -$font-slider-bg-color: rgba($input-darker-bg-color, 0.2); +$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! ***** 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/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/gen-i18n.js b/scripts/gen-i18n.js index a1823cdf50..c30ac62e3b 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; } diff --git a/src/@types/common.ts b/src/@types/common.ts index 26e5317aa3..b887bd4090 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -14,6 +14,11 @@ 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 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 ffd3277892..6510c02160 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -19,7 +19,13 @@ import ContentMessages from "../ContentMessages"; import { IMatrixClientPeg } from "../MatrixClientPeg"; import ToastStore from "../stores/ToastStore"; import DeviceListener from "../DeviceListener"; -import { RoomListStore2 } from "../stores/room-list/RoomListStore2"; +import RebrandListener from "../RebrandListener"; +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"; declare global { interface Window { @@ -29,15 +35,21 @@ declare global { init: () => Promise; }; - mx_ContentMessages: ContentMessages; - mx_ToastStore: ToastStore; - mx_DeviceListener: DeviceListener; - mx_RoomListStore2: RoomListStore2; + mxContentMessages: ContentMessages; + mxToastStore: ToastStore; + mxDeviceListener: DeviceListener; + mxRebrandListener: RebrandListener; + mxRoomListStore: RoomListStoreClass; + mxRoomListLayoutStore: RoomListLayoutStore; + mxPlatformPeg: PlatformPeg; + mxIntegrationManagers: typeof IntegrationManagers; + singletonModalManager: ModalManager; + mxSettingsStore: SettingsStore; } // workaround for https://github.com/microsoft/TypeScript/issues/30933 interface ObjectConstructor { - fromEntries?(xs: [string|number|symbol, any][]): object + fromEntries?(xs: [string|number|symbol, any][]): object; } interface Document { @@ -45,6 +57,10 @@ declare global { hasStorageAccess?: () => Promise; } + interface Navigator { + userLanguage?: string; + } + interface StorageEstimate { usageDetails?: {[key: string]: number}; } 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.js index d6fbb460b5..b7695d401d 100644 --- a/src/ActiveRoomObserver.js +++ b/src/ActiveRoomObserver.js @@ -27,7 +27,7 @@ import RoomViewStore from './stores/RoomViewStore'; */ class ActiveRoomObserver { constructor() { - this._listeners = {}; + this._listeners = {}; // key=roomId, value=function(isActive:boolean) this._activeRoomId = RoomViewStore.getRoomId(); // TODO: We could self-destruct when the last listener goes away, or at least @@ -35,6 +35,10 @@ class ActiveRoomObserver { this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate.bind(this)); } + get activeRoomId(): string { + return this._activeRoomId; + } + addListener(roomId, listener) { if (!this._listeners[roomId]) this._listeners[roomId] = []; this._listeners[roomId].push(listener); @@ -51,23 +55,23 @@ class ActiveRoomObserver { } } - _emit(roomId) { + _emit(roomId, isActive: boolean) { if (!this._listeners[roomId]) return; for (const l of this._listeners[roomId]) { - l.call(); + l.call(null, isActive); } } _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); } } 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/Avatar.js b/src/Avatar.js index 2cb90eaea6..d76ea6f2c4 100644 --- a/src/Avatar.js +++ b/src/Avatar.js @@ -82,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.ts b/src/BasePlatform.ts index 520c3fbe46..acf72a986c 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -25,8 +25,8 @@ import {CheckUpdatesPayload} from "./dispatcher/payloads/CheckUpdatesPayload"; import {Action} from "./dispatcher/actions"; import {hideToast as hideUpdateToast} from "./toasts/UpdateToast"; -export const HOMESERVER_URL_KEY = "mx_hs_url"; -export const ID_SERVER_URL_KEY = "mx_is_url"; +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", @@ -53,6 +53,10 @@ export default abstract class BasePlatform { 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': @@ -150,7 +154,7 @@ export default abstract class BasePlatform { abstract 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. @@ -221,7 +225,7 @@ export default abstract class BasePlatform { setLanguage(preferredLangs: string[]) {} - getSSOCallbackUrl(fragmentAfterLogin: string): URL { + protected getSSOCallbackUrl(fragmentAfterLogin: string): URL { const url = new URL(window.location.href); url.hash = fragmentAfterLogin || ""; return url; @@ -235,9 +239,9 @@ export default abstract class BasePlatform { */ 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(HOMESERVER_URL_KEY, mxClient.getHomeserverUrl()); + localStorage.setItem(SSO_HOMESERVER_URL_KEY, mxClient.getHomeserverUrl()); if (mxClient.getIdentityServerUrl()) { - localStorage.setItem(ID_SERVER_URL_KEY, 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 diff --git a/src/CallHandler.js b/src/CallHandler.js index 4414bce457..d5e058ef1e 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -62,10 +62,11 @@ import Matrix from 'matrix-js-sdk'; 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 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.tsx b/src/ContentMessages.tsx index 25445b1c74..6f55a75d0c 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -386,7 +386,7 @@ export default class ContentMessages { const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); if (isQuoting) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const {finished} = Modal.createTrackedDialog('Upload Reply Warning', '', QuestionDialog, { + const {finished} = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, { title: _t('Replying With Files'), description: (
{_t( @@ -397,7 +397,7 @@ export default class ContentMessages { hasCancelButton: true, button: _t("Continue"), }); - const [shouldUpload]: [boolean] = await finished; + const [shouldUpload] = await finished; if (!shouldUpload) return; } @@ -420,12 +420,12 @@ export default class ContentMessages { if (tooBigFiles.length > 0) { const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog"); - const {finished} = Modal.createTrackedDialog('Upload Failure', '', UploadFailureDialog, { + const {finished} = Modal.createTrackedDialog<[boolean]>('Upload Failure', '', UploadFailureDialog, { badFiles: tooBigFiles, totalFiles: files.length, contentMessages: this, }); - const [shouldContinue]: [boolean] = await finished; + const [shouldContinue] = await finished; if (!shouldContinue) return; } @@ -437,12 +437,12 @@ export default class ContentMessages { for (let i = 0; i < okFiles.length; ++i) { const file = okFiles[i]; if (!uploadAll) { - const {finished} = Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, { + const {finished} = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation', '', UploadConfirmDialog, { file, currentIndex: i, totalFiles: okFiles.length, }); - const [shouldContinue, shouldUploadAll]: [boolean, boolean] = await finished; + const [shouldContinue, shouldUploadAll] = await finished; if (!shouldContinue) break; if (shouldUploadAll) { uploadAll = true; @@ -621,9 +621,9 @@ export default class ContentMessages { } static sharedInstance() { - if (window.mx_ContentMessages === undefined) { - window.mx_ContentMessages = new ContentMessages(); + if (window.mxContentMessages === undefined) { + window.mxContentMessages = new ContentMessages(); } - return window.mx_ContentMessages; + return window.mxContentMessages; } } diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index c37d0f8bf5..a584a69d35 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; }, diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index e73b56416b..a37521118f 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -17,16 +17,16 @@ limitations under the License. import {MatrixClientPeg} from './MatrixClientPeg'; import { hideToast as hideBulkUnverifiedSessionsToast, - showToast as showBulkUnverifiedSessionsToast + showToast as showBulkUnverifiedSessionsToast, } from "./toasts/BulkUnverifiedSessionsToast"; import { hideToast as hideSetupEncryptionToast, Kind as SetupKind, - showToast as showSetupEncryptionToast + showToast as showSetupEncryptionToast, } from "./toasts/SetupEncryptionToast"; import { hideToast as hideUnverifiedSessionsToast, - showToast as showUnverifiedSessionsToast + showToast as showUnverifiedSessionsToast, } from "./toasts/UnverifiedSessionToast"; import {privateShouldBeEncrypted} from "./createRoom"; @@ -48,8 +48,8 @@ export default class DeviceListener { private displayingToastsForDeviceIds = new Set(); static sharedInstance() { - if (!window.mx_DeviceListener) window.mx_DeviceListener = new DeviceListener(); - return window.mx_DeviceListener; + if (!window.mxDeviceListener) window.mxDeviceListener = new DeviceListener(); + return window.mxDeviceListener; } start() { @@ -119,26 +119,26 @@ export default class DeviceListener { // 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: @@ -152,11 +152,11 @@ export default class DeviceListener { ) { 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 diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index 102afa6bf1..1b4aa19ebf 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -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 = [ @@ -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/HtmlUtils.js b/src/HtmlUtils.tsx similarity index 82% rename from src/HtmlUtils.js rename to src/HtmlUtils.tsx index 34e9e55d25..77a9579f2c 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 @@ -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/Lifecycle.js b/src/Lifecycle.js index 96cefaf593..2bebe22f14 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -40,8 +40,12 @@ import ToastStore from "./stores/ToastStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import {Mjolnir} from "./mjolnir/Mjolnir"; import DeviceListener from "./DeviceListener"; +import RebrandListener from "./RebrandListener"; import {Jitsi} from "./widgets/Jitsi"; -import {HOMESERVER_URL_KEY, ID_SERVER_URL_KEY} from "./BasePlatform"; +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 @@ -164,8 +168,8 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) { return Promise.resolve(false); } - const homeserver = localStorage.getItem(HOMESERVER_URL_KEY); - const identityServer = localStorage.getItem(ID_SERVER_URL_KEY); + 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); @@ -302,6 +306,11 @@ async function _restoreFromLocalStorage(opts) { } 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({ @@ -360,6 +369,12 @@ export async function setLoggedIn(credentials) { ? 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); } @@ -497,6 +512,14 @@ function _persistCredentialsToLocalStorage(credentials) { 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". // @@ -624,6 +647,8 @@ async function startMatrixClient(startSyncing=true) { // Now that we have a MatrixClientPeg, update the Jitsi info await Jitsi.getInstance().start(); + RebrandListener.sharedInstance().start(); + // dispatch that we finished starting up to wire up any other bits // of the matrix client that cannot be set prior to starting up. dis.dispatch({action: 'client_started'}); @@ -685,6 +710,7 @@ export function stopMatrixClient(unsetClient=true) { IntegrationManagers.sharedInstance().stopWatching(); Mjolnir.sharedInstance().stop(); DeviceListener.sharedInstance().stop(); + RebrandListener.sharedInstance().stop(); if (DMRoomMap.shared()) DMRoomMap.shared().stop(); EventIndexPeg.stop(); const cli = MatrixClientPeg.get(); 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..d312b7c5bd 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -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.ts b/src/MatrixClientPeg.ts index bc550c1935..be16f5fe10 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -35,13 +35,13 @@ import { crossSigningCallbacks } from './CrossSigningManager'; import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; export interface IMatrixClientCreds { - homeserverUrl: string, - identityServerUrl: string, - userId: string, - deviceId: string, - accessToken: string, - guest: boolean, - pickleKey?: string, + homeserverUrl: string; + identityServerUrl: string; + userId: string; + deviceId: string; + accessToken: string; + guest: boolean; + pickleKey?: string; } // TODO: Move this to the js-sdk @@ -256,7 +256,7 @@ class _MatrixClientPeg implements IMatrixClientPeg { deviceId: creds.deviceId, pickleKey: creds.pickleKey, timelineSupport: true, - forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false), + forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer'), fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), verificationMethods: [ verificationMethods.SAS, diff --git a/src/Modal.js b/src/Modal.tsx similarity index 50% rename from src/Modal.js rename to src/Modal.tsx index 9b9f190d58..b744dbacf4 100644 --- a/src/Modal.js +++ b/src/Modal.tsx @@ -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. @@ -17,6 +18,8 @@ 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'; @@ -25,36 +28,48 @@ 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; +interface IModal { + elem: React.ReactNode; + className?: string; + beforeClosePromise?: Promise; + closeReason?: string; + onBeforeClose?(reason?: string): Promise; + onFinished(...args: T): void; + close(...args: T): void; +} - // 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 - } */ - ]; +interface IHandle { + finished: Promise; + close(...args: T): void; +} - this.onBackgroundClick = this.onBackgroundClick.bind(this); - } +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; +} - hasDialogs() { - return this._priorityModal || this._staticModal || this._modals.length > 0; - } +interface IOptions { + onBeforeClose?: IModal["onBeforeClose"]; +} - getOrCreateContainer() { +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) { @@ -66,7 +81,7 @@ class ModalManager { return container; } - getOrCreateStaticContainer() { + private static getOrCreateStaticContainer() { let container = document.getElementById(STATIC_DIALOG_CONTAINER_ID); if (!container) { @@ -78,63 +93,99 @@ class ModalManager { return container; } - createTrackedDialog(analyticsAction, analyticsInfo, ...rest) { + 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); + return this.createDialog(...rest); } - appendTrackedDialog(analyticsAction, analyticsInfo, ...rest) { + public appendTrackedDialog( + analyticsAction: string, + analyticsInfo: string, + ...rest: Parameters + ) { Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); - return this.appendDialog(...rest); + return this.appendDialog(...rest); } - createDialog(Element, ...rest) { - return this.createDialogAsync(Promise.resolve(Element), ...rest); + public createDialog( + Element: React.ComponentType, + ...rest: ParametersWithoutFirst + ) { + return this.createDialogAsync(Promise.resolve(Element), ...rest); } - appendDialog(Element, ...rest) { - return this.appendDialogAsync(Promise.resolve(Element), ...rest); + public appendDialog( + Element: React.ComponentType, + ...rest: ParametersWithoutFirst + ) { + return this.appendDialogAsync(Promise.resolve(Element), ...rest); } - createTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) { + public createTrackedDialogAsync( + analyticsAction: string, + analyticsInfo: string, + ...rest: Parameters + ) { Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); - return this.createDialogAsync(...rest); + return this.createDialogAsync(...rest); } - appendTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) { + public appendTrackedDialogAsync( + analyticsAction: string, + analyticsInfo: string, + ...rest: Parameters + ) { Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); - return this.appendDialogAsync(...rest); + return this.appendDialogAsync(...rest); } - _buildModal(prom, props, className, options) { - const modal = {}; + 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); + 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++; + 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.elem = ; modal.close = closeDialog; - modal.closeReason = null; return {modal, closeDialog, onFinishedProm}; } - _getCloseFn(modal, props) { - const deferred = defer(); - return [async (...args) => { + 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) { @@ -147,26 +198,26 @@ class ModalManager { } deferred.resolve(args); if (props && props.onFinished) props.onFinished.apply(null, args); - const i = this._modals.indexOf(modal); + const i = this.modals.indexOf(modal); if (i >= 0) { - this._modals.splice(i, 1); + this.modals.splice(i, 1); } - if (this._priorityModal === modal) { - this._priorityModal = null; + if (this.priorityModal === modal) { + this.priorityModal = null; // XXX: This is destructive - this._modals = []; + this.modals = []; } - if (this._staticModal === modal) { - this._staticModal = null; + if (this.staticModal === modal) { + this.staticModal = null; // XXX: This is destructive - this._modals = []; + this.modals = []; } - this._reRender(); + this.reRender(); }, deferred.promise]; } @@ -207,38 +258,49 @@ class ModalManager { * @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); + 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; + this.priorityModal = modal; } else if (isStaticModal) { // This is intentionally destructive - this._staticModal = modal; + this.staticModal = modal; } else { - this._modals.unshift(modal); + this.modals.unshift(modal); } - this._reRender(); + this.reRender(); return { close: closeDialog, finished: onFinishedProm, }; } - appendDialogAsync(prom, props, className) { - const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, {}); + private appendDialogAsync( + prom: Promise, + props?: IProps, + className?: string + ): IHandle { + const {modal, closeDialog, onFinishedProm} = this.buildModal(prom, props, className, {}); - this._modals.push(modal); - this._reRender(); + this.modals.push(modal); + this.reRender(); return { close: closeDialog, finished: onFinishedProm, }; } - onBackgroundClick() { - const modal = this._getCurrentModal(); + private onBackgroundClick = () => { + const modal = this.getCurrentModal(); if (!modal) { return; } @@ -249,21 +311,21 @@ class ModalManager { modal.closeReason = "backgroundClick"; modal.close(); modal.closeReason = null; + }; + + private getCurrentModal(): IModal { + return this.priorityModal ? this.priorityModal : (this.modals[0] || this.staticModal); } - _getCurrentModal() { - return this._priorityModal ? this._priorityModal : (this._modals[0] || this._staticModal); - } - - _reRender() { - if (this._modals.length === 0 && !this._priorityModal && !this._staticModal) { + private 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()); + ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer()); + ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer()); return; } @@ -274,49 +336,48 @@ class ModalManager { action: 'aria_hide_main_app', }); - if (this._staticModal) { - const classes = "mx_Dialog_wrapper mx_Dialog_staticWrapper " - + (this._staticModal.className ? this._staticModal.className : ''); + if (this.staticModal) { + const classes = classNames("mx_Dialog_wrapper mx_Dialog_staticWrapper", this.staticModal.className); const staticDialog = (
- { this._staticModal.elem } + { this.staticModal.elem }
-
+
); - ReactDOM.render(staticDialog, this.getOrCreateStaticContainer()); + ReactDOM.render(staticDialog, ModalManager.getOrCreateStaticContainer()); } else { // This is safe to call repeatedly if we happen to do that - ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer()); + ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer()); } - const modal = this._getCurrentModal(); - if (modal !== this._staticModal) { - const classes = "mx_Dialog_wrapper " - + (this._staticModal ? "mx_Dialog_wrapperWithStaticUnder " : '') - + (modal.className ? modal.className : ''); + 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, this.getOrCreateContainer()); + ReactDOM.render(dialog, ModalManager.getOrCreateContainer()); } else { // This is safe to call repeatedly if we happen to do that - ReactDOM.unmountComponentAtNode(this.getOrCreateContainer()); + ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer()); } } } -if (!global.singletonModalManager) { - global.singletonModalManager = new ModalManager(); +if (!window.singletonModalManager) { + window.singletonModalManager = new ModalManager(); } -export default global.singletonModalManager; +export default window.singletonModalManager; diff --git a/src/Notifier.js b/src/Notifier.js index cd328ba565..2ed302267e 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -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,7 +17,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClientPeg} from './MatrixClientPeg'; +import { MatrixClientPeg } from './MatrixClientPeg'; +import SdkConfig from './SdkConfig'; import PlatformPeg from './PlatformPeg'; import * as TextForEvent from './TextForEvent'; import Analytics from './Analytics'; @@ -25,10 +27,11 @@ 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: @@ -122,7 +125,7 @@ const Notifier = { } }, - getSoundForRoom: async function(roomId) { + getSoundForRoom: function(roomId) { // We do no caching here because the SDK caches setting // and the browser will cache the sound. const content = SettingsStore.getValue("notificationSound", roomId); @@ -151,7 +154,7 @@ const Notifier = { }, _playAudioNotification: async function(ev, room) { - const sound = await this.getSoundForRoom(room.roomId); + const sound = this.getSoundForRoom(room.roomId); console.log(`Got sound ${sound && sound.name || "default"} for ${room.roomId}`); try { @@ -226,10 +229,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'), 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/RebrandListener.tsx b/src/RebrandListener.tsx new file mode 100644 index 0000000000..47b883cf35 --- /dev/null +++ b/src/RebrandListener.tsx @@ -0,0 +1,184 @@ +/* +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 SdkConfig from "./SdkConfig"; +import ToastStore from "./stores/ToastStore"; +import GenericToast from "./components/views/toasts/GenericToast"; +import RebrandDialog from "./components/views/dialogs/RebrandDialog"; +import { RebrandDialogKind } from "./components/views/dialogs/RebrandDialog"; +import Modal from './Modal'; +import { _t } from './languageHandler'; + +const TOAST_KEY = 'rebrand'; +const NAG_INTERVAL = 24 * 60 * 60 * 1000; + +function getRedirectUrl(url): string { + const redirectUrl = new URL(url); + redirectUrl.hash = ''; + + if (SdkConfig.get()['redirectToNewBrandUrl']) { + const newUrl = new URL(SdkConfig.get()['redirectToNewBrandUrl']); + if (url.hostname !== newUrl.hostname || url.pathname !== newUrl.pathname) { + redirectUrl.hostname = newUrl.hostname; + redirectUrl.pathname = newUrl.pathname; + return redirectUrl.toString(); + } + return null; + } else if (url.hostname === 'riot.im') { + if (url.pathname.startsWith('/app')) { + redirectUrl.hostname = 'app.element.io'; + redirectUrl.pathname = '/'; + } else if (url.pathname.startsWith('/staging')) { + redirectUrl.hostname = 'staging.element.io'; + redirectUrl.pathname = '/'; + } else if (url.pathname.startsWith('/develop')) { + redirectUrl.hostname = 'develop.element.io'; + redirectUrl.pathname = '/'; + } + + return redirectUrl.href; + } else if (url.hostname.endsWith('.riot.im')) { + redirectUrl.hostname = url.hostname.substr(0, url.hostname.length - '.riot.im'.length) + '.element.io'; + return redirectUrl.href; + } else { + return null; + } +} + +/** + * Shows toasts informing the user that the name of the app has changed and, + * potentially, that they should head to a different URL and log in there + */ +export default class RebrandListener { + private _reshowTimer?: number; + private nagAgainAt?: number = null; + + static sharedInstance() { + if (!window.mxRebrandListener) window.mxRebrandListener = new RebrandListener(); + return window.mxRebrandListener; + } + + constructor() { + this._reshowTimer = null; + } + + start() { + this.recheck(); + } + + stop() { + if (this._reshowTimer) { + clearTimeout(this._reshowTimer); + this._reshowTimer = null; + } + } + + onNagToastLearnMore = async () => { + const [doneClicked] = await Modal.createDialog(RebrandDialog, { + kind: RebrandDialogKind.NAG, + targetUrl: getRedirectUrl(window.location), + }).finished; + if (doneClicked) { + // open in new tab: they should come back here & log out + window.open(getRedirectUrl(window.location), '_blank'); + } + + // whatever the user clicks, we go away & nag again after however long: + // If they went to the new URL, we want to nag them to log out if they + // come back to this tab, and if they clicked, 'remind me later' we want + // to, well, remind them later. + this.nagAgainAt = Date.now() + NAG_INTERVAL; + this.recheck(); + }; + + onOneTimeToastLearnMore = async () => { + const [doneClicked] = await Modal.createDialog(RebrandDialog, { + kind: RebrandDialogKind.ONE_TIME, + }).finished; + if (doneClicked) { + localStorage.setItem('mx_rename_dialog_dismissed', 'true'); + this.recheck(); + } + }; + + onOneTimeToastDismiss = async () => { + localStorage.setItem('mx_rename_dialog_dismissed', 'true'); + this.recheck(); + }; + + onNagTimerFired = () => { + this._reshowTimer = null; + this.nagAgainAt = null; + this.recheck(); + }; + + private async recheck() { + // There are two types of toast/dialog we show: a 'one time' informing the user that + // the app is now called a different thing but no action is required from them (they + // may need to look for a different name name/icon to launch the app but don't need to + // log in again) and a nag toast where they need to log in to the app on a different domain. + let nagToast = false; + let oneTimeToast = false; + + if (getRedirectUrl(window.location)) { + if (!this.nagAgainAt) { + // if we have redirectUrl, show the nag toast + nagToast = true; + } + } else { + // otherwise we show the 'one time' toast / dialog + const renameDialogDismissed = localStorage.getItem('mx_rename_dialog_dismissed'); + if (renameDialogDismissed !== 'true') { + oneTimeToast = true; + } + } + + if (nagToast || oneTimeToast) { + let description; + let rejectLabel = null; + let onReject = null; + if (nagToast) { + description = _t("Use your account to sign in to the latest version"); + } else { + description = _t("We’re excited to announce Riot is now Element"); + rejectLabel = _t("Dismiss"); + onReject = this.onOneTimeToastDismiss; + } + + ToastStore.sharedInstance().addOrReplaceToast({ + key: TOAST_KEY, + title: _t("Riot is now Element!"), + icon: 'element_logo', + props: { + description, + acceptLabel: _t("Learn More"), + onAccept: nagToast ? this.onNagToastLearnMore : this.onOneTimeToastLearnMore, + rejectLabel, + onReject, + }, + component: GenericToast, + priority: 20, + }); + } else { + ToastStore.sharedInstance().dismissToast(TOAST_KEY); + } + + if (!this._reshowTimer && this.nagAgainAt) { + // XXX: Our build system picks up NodeJS bindings when we need browser bindings. + this._reshowTimer = setTimeout(this.onNagTimerFired, (this.nagAgainAt - Date.now()) + 100) as any as number; + } + } +} 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/res/css/views/rooms/_UserOnlineDot.scss b/src/RoomNotifsTypes.ts similarity index 69% rename from res/css/views/rooms/_UserOnlineDot.scss rename to src/RoomNotifsTypes.ts index f9da8648ed..0e7093e434 100644 --- a/res/css/views/rooms/_UserOnlineDot.scss +++ b/src/RoomNotifsTypes.ts @@ -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,11 @@ 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; -} +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 315c2d86f4..b33aa57359 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -244,16 +244,17 @@ 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 15798ae3b1..ad3dc7002a 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -43,6 +43,7 @@ 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 { @@ -118,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); } @@ -400,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( @@ -420,9 +423,9 @@ export const Commands = [ )}

, button: _t("Continue"), }, - )); + ); - finished = finished.then(([useDefault]: any) => { + prom = finished.then(([useDefault]) => { if (useDefault) { useDefaultIdentityServer(); return; @@ -434,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") { @@ -495,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', @@ -661,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(); @@ -691,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(); @@ -731,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(args); + 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)); } } @@ -1047,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/WidgetMessaging.js b/src/WidgetMessaging.js index 3b3926b724..c89a0ceeeb 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -96,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 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/res/css/views/rooms/_RoomSublist2.scss b/src/accessibility/roving/types.ts similarity index 81% rename from res/css/views/rooms/_RoomSublist2.scss rename to src/accessibility/roving/types.ts index 9ab1785566..f0a43e5fb8 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/src/accessibility/roving/types.ts @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -@import "../../../../node_modules/react-resizable/css/styles.css"; +import {RefObject} from "react"; -.mx_RoomList2 .mx_RoomSubList_labelContainer { - z-index: 12; -} +export type Ref = RefObject; + +export type FocusHandler = () => void; diff --git a/src/actions/RoomListActions.ts b/src/actions/RoomListActions.ts index e15e1b0c65..88946ee26f 100644 --- a/src/actions/RoomListActions.ts +++ b/src/actions/RoomListActions.ts @@ -16,7 +16,6 @@ limitations under the License. */ import { asyncAction } from './actionCreators'; -import { TAG_DM } from '../stores/RoomListStore'; import Modal from '../Modal'; import * as Rooms from '../Rooms'; import { _t } from '../languageHandler'; @@ -24,7 +23,9 @@ 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 { RoomListStoreTempProxy } from "../stores/room-list/RoomListStoreTempProxy"; +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 { /** @@ -51,9 +52,9 @@ export default class RoomListActions { let metaData = null; // Is the tag ordered manually? - if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { - const lists = RoomListStoreTempProxy.getRoomLists(); - const newList = [...lists[newTag]]; + 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); @@ -81,11 +82,11 @@ export default class RoomListActions { const roomId = room.roomId; // Evil hack to get DMs behaving - if ((oldTag === undefined && newTag === TAG_DM) || - (oldTag === TAG_DM && newTag === undefined) + if ((oldTag === undefined && newTag === DefaultTagID.DM) || + (oldTag === DefaultTagID.DM && newTag === undefined) ) { return Rooms.guessAndSetDMRoom( - room, newTag === TAG_DM, + room, newTag === DefaultTagID.DM, ).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to set direct chat tag " + err); @@ -102,12 +103,12 @@ export default class RoomListActions { // but we avoid ever doing a request with TAG_DM. // // if we moved lists, remove the old tag - if (oldTag && oldTag !== TAG_DM && + if (oldTag && oldTag !== DefaultTagID.DM && hasChangedSubLists ) { const promiseToDelete = matrixClient.deleteRoomTag( roomId, oldTag, - ).catch(function (err) { + ).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, { @@ -120,14 +121,14 @@ export default class RoomListActions { } // if we moved lists or the ordering changed, add the new tag - if (newTag && newTag !== TAG_DM && + 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 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, { diff --git a/src/actions/TagOrderActions.ts b/src/actions/TagOrderActions.ts index bf1820d5d1..c203172874 100644 --- a/src/actions/TagOrderActions.ts +++ b/src/actions/TagOrderActions.ts @@ -22,7 +22,6 @@ 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. @@ -60,7 +59,7 @@ export default class TagOrderActions { // For an optimistic update return {tags, removedTags}; }); - }; + } /** * Creates an action thunk that will do an asynchronous request to diff --git a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js index ec4b88f759..de50feaedb 100644 --- a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js @@ -20,9 +20,10 @@ import PropTypes from 'prop-types'; 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. 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/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index d7b79c2cfa..4cef817a38 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -26,20 +26,27 @@ 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. +// 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 * Secret Storage in account data. @@ -70,6 +77,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { passPhraseConfirm: '', copied: false, downloaded: false, + setPassphrase: false, backupInfo: null, backupSigStatus: null, // does the server offer a UI auth flow with just m.login.password @@ -77,8 +85,8 @@ 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(); @@ -110,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, @@ -152,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) => { @@ -176,7 +203,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { if (successful) { this.setState({ copied: true, - phase: PHASE_KEEPITSAFE, }); } } @@ -189,7 +215,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.setState({ downloaded: true, - phase: PHASE_KEEPITSAFE, }); } @@ -259,22 +284,15 @@ export default class CreateSecretStorageDialog extends React.PureComponent { await cli.bootstrapSecretStorage({ authUploadDeviceSigningKeys: this._doBootstrapUIAuth, createSecretStorageKey: async () => this._recoveryKey, - setupNewKeyBackup: this.state.useKeyBackup, + setupNewKeyBackup: true, setupNewSecretStorage: true, }); - if (!this.state.useKeyBackup && this.state.backupInfo) { - // If the user is resetting their cross-signing keys and doesn't want - // key backup (but had it enabled before), delete the key backup as it's - // no longer valid. - console.log("Deleting invalid key backup (secrets have been reset; key backup not requested)"); - await cli.deleteKeyBackupVersion(this.state.backupInfo.version); - } } else { await cli.bootstrapSecretStorage({ 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 @@ -286,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({ @@ -342,22 +358,16 @@ 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) => { @@ -384,6 +394,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.setState({ copied: false, downloaded: false, + setPassphrase: true, phase: PHASE_SHOWKEY, }); } @@ -397,12 +408,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); } - _onKeepItSafeBackClick = () => { - this.setState({ - phase: PHASE_SHOWKEY, - }); - } - _onPassPhraseValidate = (result) => { this.setState({ passPhraseValid: result.valid, @@ -427,13 +432,55 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); } + _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'); const Field = sdk.getComponent('views.elements.Field'); let authPrompt; @@ -446,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} />
; @@ -474,7 +521,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { hasCancel={false} primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword} > - @@ -482,14 +529,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _renderPhasePassPhrase() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch'); - 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.", )}

@@ -508,11 +551,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { />
- - + >{_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; @@ -566,7 +596,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
; } - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

{_t( "Enter your recovery passphrase a second time to confirm it.", @@ -592,7 +621,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { disabled={this.state.passPhrase !== this.state.passPhraseConfirm} > @@ -600,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}
; } @@ -671,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")}

@@ -684,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 ''; } @@ -741,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")}

@@ -760,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; @@ -772,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/Components.tsx b/src/autocomplete/Components.tsx index 0ee0088f02..6ac2f4db14 100644 --- a/src/autocomplete/Components.tsx +++ b/src/autocomplete/Components.tsx @@ -46,7 +46,7 @@ export const TextualCompletion = forwardRef((props }); interface IPillCompletionProps extends ITextualCompletionProps { - children?: React.ReactNode, + children?: React.ReactNode; } export const PillCompletion = forwardRef((props, ref) => { diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index c4dd3ec9cc..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; diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index 2c1899d813..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 @@ -47,14 +45,10 @@ interface IOptions { */ 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); @@ -79,57 +73,78 @@ export default class QueryMatcher { // 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._keys); + 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 0d8aac4218..f14fa3bbfa 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, 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..f1bd297730 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", +} + +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 2753d5c4da..a79bdafeb5 100644 --- a/src/components/structures/CustomRoomTagPanel.js +++ b/src/components/structures/CustomRoomTagPanel.js @@ -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/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/HomePage.tsx b/src/components/structures/HomePage.tsx index ff8d35a114..7414a44f11 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -22,9 +22,10 @@ import { _t } from "../../languageHandler"; import SdkConfig from "../../SdkConfig"; import * as sdk from "../../index"; 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,7 +46,7 @@ const HomePage = () => { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); return
- Riot + {config.brand

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

{ _t("Liberate your communication") }

diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index 05ad4f7561..27f7fbb301 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -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/LeftPanel.js b/src/components/structures/LeftPanel.js deleted file mode 100644 index 05cd97df2a..0000000000 --- a/src/components/structures/LeftPanel.js +++ /dev/null @@ -1,305 +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/dispatcher'; -import * as VectorConferenceHandler from '../../VectorConferenceHandler'; -import SettingsStore from '../../settings/SettingsStore'; -import {_t} from "../../languageHandler"; -import Analytics from "../../Analytics"; -import {Action} from "../../dispatcher/actions"; - - -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.fire(Action.FocusComposer); - } - 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 = (); - } - - const roomList = ; - - 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..bc17bbe23f --- /dev/null +++ b/src/components/structures/LeftPanel.tsx @@ -0,0 +1,428 @@ +/* +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"; + +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.isFeatureEnabled("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/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx deleted file mode 100644 index c9a4948539..0000000000 --- a/src/components/structures/LeftPanel2.tsx +++ /dev/null @@ -1,154 +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 * as React from "react"; -import TagPanel from "./TagPanel"; -import classNames from "classnames"; -import dis from "../../dispatcher/dispatcher"; -import AccessibleButton from "../views/elements/AccessibleButton"; -import { _t } from "../../languageHandler"; -import SearchBox from "./SearchBox"; -import RoomList2 from "../views/rooms/RoomList2"; -import TopLeftMenuButton from "./TopLeftMenuButton"; -import { Action } from "../../dispatcher/actions"; - -/******************************************************************* - * CAUTION * - ******************************************************************* - * This is a work in progress implementation and isn't complete or * - * even useful as a component. Please avoid using it until this * - * warning disappears. * - *******************************************************************/ - -interface IProps { - // TODO: Support collapsed state -} - -interface IState { - searchExpanded: boolean; - searchFilter: string; // TODO: Move search into room list? -} - -export default class LeftPanel2 extends React.Component { - // TODO: Properly support TagPanel - // TODO: Properly support searching/filtering - // TODO: Properly support breadcrumbs - // TODO: Properly support TopLeftMenu (User Settings) - // TODO: a11y - // TODO: actually make this useful in general (match design proposals) - // TODO: Fadable support (is this still needed?) - - constructor(props: IProps) { - super(props); - - this.state = { - searchExpanded: false, - searchFilter: "", - }; - } - - private onSearch = (term: string): void => { - this.setState({searchFilter: term}); - }; - - private onSearchCleared = (source: string): void => { - if (source === "keyboard") { - dis.fire(Action.FocusComposer); - } - this.setState({searchExpanded: false}); - } - - private onSearchFocus = (): void => { - this.setState({searchExpanded: true}); - }; - - private onSearchBlur = (event: FocusEvent): void => { - const target = event.target as HTMLInputElement; - if (target.value.length === 0) { - this.setState({searchExpanded: false}); - } - } - - public render(): React.ReactNode { - const tagPanel = ( -
- -
- ); - - const exploreButton = ( -
- dis.dispatch({action: 'view_room_directory'})}> - {_t("Explore")} - -
- ); - - const searchBox = ( {/*TODO*/}} - onSearch={this.onSearch} - onCleared={this.onSearchCleared} - onFocus={this.onSearchFocus} - onBlur={this.onSearchBlur} - collapsed={false}/>); // TODO: Collapsed support - - // TODO: Improve props for RoomList2 - const roomList = {/*TODO*/}} - resizeNotifier={null} - collapsed={false} - searchFilter={this.state.searchFilter} - onFocus={() => {/*TODO*/}} - onBlur={() => {/*TODO*/}} - />; - - // TODO: Breadcrumbs - // TODO: Conference handling / calls - - const containerClasses = classNames({ - "mx_LeftPanel_container": true, - "mx_fadable": true, - "collapsed": false, // TODO: Collapsed support - "mx_LeftPanel_container_hasTagPanel": true, // TODO: TagPanel support - "mx_fadable_faded": false, - "mx_LeftPanel2": true, // TODO: Remove flag when RoomList2 ships (used as an indicator) - }); - - return ( -
- {tagPanel} - -
- ); - } -} diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 0504e3a76a..48669a3721 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'; @@ -41,7 +40,6 @@ import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts"; import HomePage from "./HomePage"; import ResizeNotifier from "../../utils/ResizeNotifier"; import PlatformPeg from "../../PlatformPeg"; -import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy"; import { DefaultTagID } from "../../stores/room-list/models"; import { showToast as showSetPasswordToast, @@ -52,7 +50,12 @@ import { hideToast as hideServerLimitToast } from "../../toasts/ServerLimitToast"; import { Action } from "../../dispatcher/actions"; -import LeftPanel2 from "./LeftPanel2"; +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. @@ -123,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 = { @@ -146,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) { @@ -177,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(); @@ -194,6 +202,7 @@ class LoggedInView extends React.PureComponent { 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(); } @@ -263,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 && @@ -300,8 +310,8 @@ class LoggedInView extends React.PureComponent { }; onRoomStateEvents = (ev, state) => { - const roomLists = RoomListStoreTempProxy.getRoomLists(); - if (roomLists[DefaultTagID.ServerNotice] && roomLists[DefaultTagID.ServerNotice].some(r => r.roomId === ev.getRoomId())) { + const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice]; + if (serverNoticeList && serverNoticeList.some(r => r.roomId === ev.getRoomId())) { this._updateServerNoticeEvents(); } }; @@ -320,11 +330,11 @@ class LoggedInView extends React.PureComponent { } _updateServerNoticeEvents = async () => { - const roomLists = RoomListStoreTempProxy.getRoomLists(); - if (!roomLists[DefaultTagID.ServerNotice]) return []; + const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice]; + if (!serverNoticeList) return []; const events = []; - for (const room of roomLists[DefaultTagID.ServerNotice]) { + for (const room of serverNoticeList) { const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", ""); if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue; @@ -402,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; @@ -452,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; @@ -469,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, }); @@ -480,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; @@ -615,7 +609,6 @@ 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'); @@ -669,19 +662,12 @@ class LoggedInView extends React.PureComponent { bodyClasses += ' mx_MatrixChat_useCompactLayout'; } - let leftPanel = ( + const leftPanel = ( ); - if (SettingsStore.isFeatureEnabled("feature_new_room_list")) { - // TODO: Supply props like collapsed and disabled to LeftPanel2 - leftPanel = ( - - ); - } return ( @@ -702,6 +688,8 @@ class LoggedInView extends React.PureComponent {
+ + ); } 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 69f91047b7..a66d4c043f 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -18,10 +18,11 @@ limitations under the License. */ 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"; -import { isCryptoAvailable } from 'matrix-js-sdk/src/crypto'; // 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 @@ -49,15 +50,14 @@ import PageTypes from '../../PageTypes'; import { getHomePageUrl } from '../../utils/pages'; import createRoom from "../../createRoom"; -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 AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; import DMRoomMap from '../../utils/DMRoomMap'; -import { countRoomsWithNotif } from '../../RoomNotifs'; import ThemeWatcher from "../../settings/watchers/ThemeWatcher"; import { FontWatcher } from '../../settings/watchers/FontWatcher'; import { storeRoomAliasInCache } from '../../RoomAliasCache'; @@ -72,6 +72,10 @@ import { 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 { @@ -150,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 { @@ -458,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 @@ -552,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'), @@ -594,19 +600,16 @@ 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 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(); @@ -620,7 +623,7 @@ export default class MatrixChat extends React.PureComponent { 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); @@ -673,12 +676,16 @@ export default class MatrixChat extends React.PureComponent { 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': { @@ -807,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 @@ -1074,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); @@ -1138,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 @@ -1386,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.'), @@ -1456,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; @@ -1607,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', @@ -1822,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(); @@ -1868,42 +1888,35 @@ 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) { + // 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 (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) { this.setStateForNewView({ view: Views.E2E_SETUP }); @@ -1911,8 +1924,6 @@ export default class MatrixChat extends React.PureComponent { this.onLoggedIn(); } this.setState({ pendingInitialSync: false }); - - return setLoggedInPromise; }; // complete security / e2e setup has finished @@ -1920,17 +1931,20 @@ export default class MatrixChat extends React.PureComponent { this.onLoggedIn(); }; - render() { - // console.log(`Rendering MatrixChat with view ${this.state.view}`); - + getFragmentAfterLogin() { let fragmentAfterLogin = ""; - if (this.props.initialScreenAfterLogin && + const initialScreenAfterLogin = this.props.initialScreenAfterLogin; + if (initialScreenAfterLogin && // XXX: workaround for https://github.com/vector-im/riot-web/issues/11643 causing a login-loop - !["welcome", "login", "register"].includes(this.props.initialScreenAfterLogin.screen) + !["welcome", "login", "register", "start_sso", "start_cas"].includes(initialScreenAfterLogin.screen) ) { - fragmentAfterLogin = `/${this.props.initialScreenAfterLogin.screen}`; + fragmentAfterLogin = `/${initialScreenAfterLogin.screen}`; } + return fragmentAfterLogin; + } + render() { + const fragmentAfterLogin = this.getFragmentAfterLogin(); let view; if (this.state.view === Views.LOADING) { @@ -2009,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 = ( diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index d11fee6360..230d136e04 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -346,9 +346,9 @@ export default class MessagePanel extends React.Component { } } - _isUnmounting() { + _isUnmounting = () => { return !this._isMounted; - } + }; // TODO: Implement granular (per-room) hide options _shouldShowEvent(mxEv) { @@ -388,8 +388,11 @@ export default class MessagePanel extends React.Component { } return ( -
  • +
  • { hr }
  • ); @@ -568,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( -
  • @@ -587,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} diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index e2a3d6e71f..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,6 +20,7 @@ import React from 'react'; import createReactClass from 'create-react-class'; import * as sdk from '../../index'; import { _t } from '../../languageHandler'; +import SdkConfig from '../../SdkConfig'; import dis from '../../dispatcher/dispatcher'; import AccessibleButton from '../views/elements/AccessibleButton'; import MatrixClientContext from "../../contexts/MatrixClientContext"; @@ -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 776130e709..a4e3254e4c 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -26,7 +26,7 @@ import dis from '../../dispatcher/dispatcher'; import RateLimitedFunc from '../../ratelimitedfunc'; import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker'; import GroupStore from '../../stores/GroupStore'; -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"; @@ -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, @@ -206,7 +206,7 @@ export default class RightPanel extends React.Component { // or the member list if we were in the member panel... phew. dis.dispatch({ action: Action.ViewUser, - member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? this.state.member : null, + member: this.state.phase === RightPanelPhases.EncryptionPanel ? this.state.member : null, }); } }; @@ -225,21 +225,21 @@ export default class RightPanel extends React.Component { 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: + case RightPanelPhases.RoomMemberInfo: + case RightPanelPhases.EncryptionPanel: panel = ; break; - case RIGHT_PANEL_PHASES.Room3pidMemberInfo: + case RightPanelPhases.Room3pidMemberInfo: panel = ; break; - case RIGHT_PANEL_PHASES.GroupMemberInfo: + 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 acaf66b206..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. @@ -25,6 +25,7 @@ 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.') + ), }); }); }, @@ -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; } diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx new file mode 100644 index 0000000000..69504e9ab8 --- /dev/null +++ b/src/components/structures/RoomSearch.tsx @@ -0,0 +1,184 @@ +/* +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_expanded': this.state.query || 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 65d062cfaa..3171dbccbe 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -173,7 +173,7 @@ export default createReactClass({ if (this.props.hasActiveCall) { const TintableSvg = sdk.getComponent("elements.TintableSvg"); return ( - + ); } diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js deleted file mode 100644 index 090f3de22a..0000000000 --- a/src/components/structures/RoomSubList.js +++ /dev/null @@ -1,496 +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/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 {toPx} from "../../utils/units"; - -// turn this on for drop & drag console debugging galore -const debug = false; - -class RoomTileErrorBoundary extends React.PureComponent { - constructor(props) { - super(props); - - this.state = { - error: null, - }; - } - - static getDerivedStateFromError(error) { - // Side effects are not permitted here, so we only update the state so - // that the next render shows an error message. - return { error }; - } - - componentDidCatch(error, { componentStack }) { - // Browser consoles are better at formatting output when native errors are passed - // in their own `console.error` invocation. - console.error(error); - console.error( - "The above error occured while React was rendering the following components:", - componentStack, - ); - } - - render() { - if (this.state.error) { - return (
    - {this.props.roomId} -
    ); - } else { - return this.props.children; - } - } -} - -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 = toPx(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 0ff997ee09..f585a97fde 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -39,7 +39,7 @@ 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 {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard'; @@ -48,7 +48,7 @@ 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"; @@ -56,6 +56,7 @@ 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() {}; @@ -166,7 +167,7 @@ export default createReactClass({ canReact: false, canReply: false, - useIRCLayout: SettingsStore.getValue("feature_irc_ui"), + useIRCLayout: SettingsStore.getValue("useIRCLayout"), matrixClientIsReady: this.context && this.context.isInitialSyncComplete(), }; @@ -199,7 +200,7 @@ export default createReactClass({ this._roomView = createRef(); this._searchResultsPanel = createRef(); - this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange); + this._layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange); }, _onReadReceiptsChange: function() { @@ -546,7 +547,7 @@ export default createReactClass({ onLayoutChange: function() { this.setState({ - useIRCLayout: SettingsStore.getValue("feature_irc_ui"), + useIRCLayout: SettingsStore.getValue("useIRCLayout"), }); }, @@ -1036,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"); @@ -1314,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 @@ -1322,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; } @@ -1380,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, }); }, @@ -1458,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() { @@ -1821,6 +1814,7 @@ export default createReactClass({ ); const showRoomRecoveryReminder = ( + this.context.isCryptoEnabled() && SettingsStore.getValue("showRoomRecoveryReminder") && this.context.isRoomEncrypted(this.state.room.roomId) && this.context.getKeyBackupEnabled() === false @@ -1934,25 +1928,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 =
    -
    ; @@ -1964,7 +1962,6 @@ export default createReactClass({ { videoMuteButton } { zoomButton } { statusBar } -
    ; } @@ -1979,8 +1976,9 @@ export default createReactClass({ searchResultsPanel = (
    ); } else { searchResultsPanel = ( - @@ -2044,6 +2042,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 cb0114b243..51113f4f56 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -648,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(); @@ -657,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); } } @@ -694,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(); @@ -709,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 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/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 95dc42fcee..d469a41cc8 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -798,6 +798,9 @@ const TimelinePanel = createReactClass({ readMarkerVisible: false, }); } + + // Send the updated read marker (along with read receipt) to the server + this.sendReadReceipt(); }, diff --git a/src/components/structures/TopLeftMenuButton.js b/src/components/structures/TopLeftMenuButton.js deleted file mode 100644 index 234dc661f9..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/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/UserMenu.tsx b/src/components/structures/UserMenu.tsx new file mode 100644 index 0000000000..3f2e387ccb --- /dev/null +++ b/src/components/structures/UserMenu.tsx @@ -0,0 +1,357 @@ +/* +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 { ChevronFace, ContextMenu, ContextMenuButton, MenuItem } 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"; + +interface IProps { + isMinimized: boolean; +} + +type PartialDOMRect = Pick; + +interface IState { + contextMenuPosition: PartialDOMRect; + isDarkTheme: boolean; +} + +interface IMenuButtonProps { + iconClassName: string; + label: string; + onClick(ev: ButtonEvent); +} + +const MenuButton: React.FC = ({iconClassName, label, onClick}) => { + return + + {label} + ; +}; + +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/riot-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/auth/Registration.js b/src/components/structures/auth/Registration.js index 6349614d72..3b5f5676dc 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -378,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(), @@ -386,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 { @@ -397,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..5b1f025dfb 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 X for 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 a2824b63a3..a539c8c9ee 100644 --- a/src/components/structures/auth/SoftLogout.js +++ b/src/components/structures/auth/SoftLogout.js @@ -25,7 +25,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {sendLoginRequest} from "../../../Login"; import AuthPage from "../../views/auth/AuthPage"; import SSOButton from "../../views/elements/SSOButton"; -import {HOMESERVER_URL_KEY, ID_SERVER_URL_KEY} from "../../../BasePlatform"; +import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform"; const LOGIN_VIEW = { LOADING: 1, @@ -72,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}); }); } @@ -158,8 +158,8 @@ export default class SoftLogout extends React.Component { async trySsoLogin() { this.setState({busy: true}); - const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY); - const isUrl = localStorage.getItem(ID_SERVER_URL_KEY) || 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'], 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/LanguageSelector.js b/src/components/views/auth/LanguageSelector.js index 83db5d225b..0738ee43e4 100644 --- a/src/components/views/auth/LanguageSelector.js +++ b/src/components/views/auth/LanguageSelector.js @@ -16,10 +16,11 @@ limitations under the License. import SdkConfig from "../../../SdkConfig"; import {getCurrentLanguage} from "../../../languageHandler"; -import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import SettingsStore from "../../../settings/SettingsStore"; import PlatformPeg from "../../../PlatformPeg"; import * as sdk from '../../../index'; import React from 'react'; +import {SettingLevel} from "../../../settings/SettingLevel"; function onChange(newLang) { if (getCurrentLanguage() !== newLang) { diff --git a/src/components/views/auth/ModularServerConfig.js b/src/components/views/auth/ModularServerConfig.js index 591c30ee7c..28fd16379d 100644 --- a/src/components/views/auth/ModularServerConfig.js +++ b/src/components/views/auth/ModularServerConfig.js @@ -23,8 +23,8 @@ import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils"; import * as ServerType from '../../views/auth/ServerTypeSelector'; import ServerConfig from "./ServerConfig"; -const MODULAR_URL = 'https://modular.im/services/matrix-hosting-riot' + - '?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication'; +const MODULAR_URL = 'https://element.io/matrix-services' + + '?utm_source=element-web&utm_medium=web&utm_campaign=element-web-authentication'; // TODO: TravisR - Can this extend ServerConfig for most things? @@ -95,10 +95,10 @@ export default class ModularServerConfig extends ServerConfig { return (
    -

    {_t("Your Modular server")}

    +

    {_t("Your server")}

    {_t( - "Enter the location of your Modular homeserver. It may use your own " + - "domain name or be a subdomain of modular.im.", + "Enter the location of your Element Matrix Services homeserver. It may use your own " + + "domain name or be a subdomain of element.io.", {}, { a: sub => {sub} diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx index f09791ce26..2f5064447e 100644 --- a/src/components/views/auth/PassphraseField.tsx +++ b/src/components/views/auth/PassphraseField.tsx @@ -118,7 +118,7 @@ class PassphraseField extends PureComponent { value={this.props.value} onChange={this.props.onChange} onValidate={this.onValidate} - /> + />; } } diff --git a/src/components/views/auth/ServerTypeSelector.js b/src/components/views/auth/ServerTypeSelector.js index a8a1dda968..71e7ac7f0e 100644 --- a/src/components/views/auth/ServerTypeSelector.js +++ b/src/components/views/auth/ServerTypeSelector.js @@ -22,8 +22,8 @@ import classnames from 'classnames'; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import {makeType} from "../../../utils/TypeUtils"; -const MODULAR_URL = 'https://modular.im/services/matrix-hosting-riot' + - '?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication'; +const MODULAR_URL = 'https://element.io/matrix-services' + + '?utm_source=element-web&utm_medium=web&utm_campaign=element-web-authentication'; export const FREE = 'Free'; export const PREMIUM = 'Premium'; @@ -45,7 +45,7 @@ export const TYPES = { PREMIUM: { id: PREMIUM, label: () => _t('Premium'), - logo: () => , + logo: () => , description: () => _t('Premium hosting for organisations Learn more', {}, { a: sub => {sub} diff --git a/src/components/views/auth/Welcome.js b/src/components/views/auth/Welcome.js index 91ba368f70..5a30a02490 100644 --- a/src/components/views/auth/Welcome.js +++ b/src/components/views/auth/Welcome.js @@ -18,9 +18,7 @@ import React from 'react'; import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import AuthPage from "./AuthPage"; -import * as Matrix from "matrix-js-sdk"; import {_td} from "../../../languageHandler"; -import PlatformPeg from "../../../PlatformPeg"; // translatable strings for Welcome pages _td("Sign in with SSO"); @@ -39,15 +37,6 @@ export default class Welcome extends React.PureComponent { pageUrl = 'welcome.html'; } - const {hsUrl, isUrl} = this.props.serverConfig; - const tmpClient = Matrix.createClient({ - baseUrl: hsUrl, - idBaseUrl: isUrl, - }); - const plaf = PlatformPeg.get(); - const callbackUrl = plaf.getSSOCallbackUrl(tmpClient.getHomeserverUrl(), tmpClient.getIdentityServerUrl(), - this.props.fragmentAfterLogin); - return (
    @@ -55,8 +44,8 @@ export default class Welcome extends React.PureComponent { className="mx_WelcomePage" url={pageUrl} replaceMap={{ - "$riot:ssoUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "sso"), - "$riot:casUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "cas"), + "$riot:ssoUrl": "#/start_sso", + "$riot:casUrl": "#/start_cas", }} /> diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.tsx similarity index 75% rename from src/components/views/avatars/BaseAvatar.js rename to src/components/views/avatars/BaseAvatar.tsx index 508691e5fd..4c6fde19eb 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -18,7 +18,7 @@ limitations under the License. */ import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; -import PropTypes from 'prop-types'; +import classNames from 'classnames'; import * as AvatarLogic from '../../../Avatar'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; @@ -26,9 +26,25 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; import {toPx} from "../../../utils/units"; -const useImageUrl = ({url, urls}) => { - const [imageUrls, setUrls] = useState([]); - const [urlsIndex, setIndex] = useState(); +interface IProps { + name: string; // The name (first initial used as default) + idName?: string; // ID for generating hash colours + title?: string; // onHover title text + url?: string; // highest priority of them all, shortcut to set in urls[0] + urls?: string[]; // [highest_priority, ... , lowest_priority] + width?: number; + height?: number; + // XXX: resizeMethod not actually used. + resizeMethod?: string; + defaultToInitialLetter?: boolean; // true to add default url + onClick?: React.MouseEventHandler; + inputRef?: React.RefObject; + className?: string; +} + +const useImageUrl = ({url, urls}): [string, () => void] => { + const [imageUrls, setUrls] = useState([]); + const [urlsIndex, setIndex] = useState(); const onError = useCallback(() => { setIndex(i => i + 1); // try the next one @@ -70,19 +86,20 @@ const useImageUrl = ({url, urls}) => { return [imageUrl, onError]; }; -const BaseAvatar = (props) => { +const BaseAvatar = (props: IProps) => { const { name, idName, title, url, urls, - width=40, - height=40, - resizeMethod="crop", // eslint-disable-line no-unused-vars - defaultToInitialLetter=true, + width = 40, + height = 40, + resizeMethod = "crop", // eslint-disable-line no-unused-vars + defaultToInitialLetter = true, onClick, inputRef, + className, ...otherProps } = props; @@ -117,12 +134,12 @@ const BaseAvatar = (props) => { aria-hidden="true" /> ); - if (onClick != null) { + if (onClick) { return ( @@ -132,7 +149,12 @@ const BaseAvatar = (props) => { ); } else { return ( - + { textNode } { imgNode } @@ -140,10 +162,10 @@ const BaseAvatar = (props) => { } } - if (onClick != null) { + if (onClick) { return ( { } else { return ( { } }; -BaseAvatar.displayName = "BaseAvatar"; - -BaseAvatar.propTypes = { - name: PropTypes.string.isRequired, // The name (first initial used as default) - idName: PropTypes.string, // ID for generating hash colours - title: PropTypes.string, // onHover title text - url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0] - urls: PropTypes.array, // [highest_priority, ... , lowest_priority] - width: PropTypes.number, - height: PropTypes.number, - // XXX resizeMethod not actually used. - resizeMethod: PropTypes.string, - defaultToInitialLetter: PropTypes.bool, // true to add default url - onClick: PropTypes.func, - inputRef: PropTypes.oneOfType([ - // Either a function - PropTypes.func, - // Or the instance of a DOM native element - PropTypes.shape({ current: PropTypes.instanceOf(Element) }), - ]), -}; - export default BaseAvatar; +export type BaseAvatarType = React.FC; diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx new file mode 100644 index 0000000000..daf28400f2 --- /dev/null +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -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. +*/ + +import React from 'react'; +import { Room } from "matrix-js-sdk/src/models/room"; + +import { TagID } from '../../../stores/room-list/models'; +import RoomAvatar from "./RoomAvatar"; +import RoomTileIcon from "../rooms/RoomTileIcon"; +import NotificationBadge from '../rooms/NotificationBadge'; +import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; +import { NotificationState } from "../../../stores/notifications/NotificationState"; + +interface IProps { + room: Room; + avatarSize: number; + tag: TagID; + displayBadge?: boolean; + forceCount?: boolean; + oobData?: object; + viewAvatarOnClick?: boolean; +} + +interface IState { + notificationState?: NotificationState; +} + +export default class DecoratedRoomAvatar extends React.PureComponent { + + constructor(props: IProps) { + super(props); + + this.state = { + notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room), + }; + } + + public render(): React.ReactNode { + let badge: React.ReactNode; + if (this.props.displayBadge) { + badge = ; + } + + return
    + + + {badge} +
    ; + } +} diff --git a/src/components/views/avatars/GroupAvatar.js b/src/components/views/avatars/GroupAvatar.tsx similarity index 64% rename from src/components/views/avatars/GroupAvatar.js rename to src/components/views/avatars/GroupAvatar.tsx index 0da57bcb99..e55e2e6fac 100644 --- a/src/components/views/avatars/GroupAvatar.js +++ b/src/components/views/avatars/GroupAvatar.tsx @@ -15,43 +15,36 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -import * as sdk from '../../../index'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import BaseAvatar from './BaseAvatar'; -export default createReactClass({ - displayName: 'GroupAvatar', +export interface IProps { + groupId?: string; + groupName?: string; + groupAvatarUrl?: string; + width?: number; + height?: number; + resizeMethod?: string; + onClick?: React.MouseEventHandler; +} - propTypes: { - groupId: PropTypes.string, - groupName: PropTypes.string, - groupAvatarUrl: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - resizeMethod: PropTypes.string, - onClick: PropTypes.func, - }, +export default class GroupAvatar extends React.Component { + public static defaultProps = { + width: 36, + height: 36, + resizeMethod: 'crop', + }; - getDefaultProps: function() { - return { - width: 36, - height: 36, - resizeMethod: 'crop', - }; - }, - - getGroupAvatarUrl: function() { + getGroupAvatarUrl() { return MatrixClientPeg.get().mxcUrlToHttp( this.props.groupAvatarUrl, this.props.width, this.props.height, this.props.resizeMethod, ); - }, + } - render: function() { - const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); + render() { // extract the props we use from props so we can pass any others through // should consider adding this as a global rule in js-sdk? /*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/ @@ -65,5 +58,5 @@ export default createReactClass({ {...otherProps} /> ); - }, -}); + } +} diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.tsx similarity index 64% rename from src/components/views/avatars/MemberAvatar.js rename to src/components/views/avatars/MemberAvatar.tsx index b763129dd8..1d23d85b0f 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -16,48 +16,50 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -import * as sdk from "../../../index"; import dis from "../../../dispatcher/dispatcher"; import {Action} from "../../../dispatcher/actions"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import BaseAvatar from "./BaseAvatar"; -export default createReactClass({ - displayName: 'MemberAvatar', +interface IProps { + // TODO: replace with correct type + member: any; + fallbackUserId: string; + width: number; + height: number; + resizeMethod: string; + // The onClick to give the avatar + onClick: React.MouseEventHandler; + // Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser` + viewUserOnClick: boolean; + title: string; +} - propTypes: { - member: PropTypes.object, - fallbackUserId: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - resizeMethod: PropTypes.string, - // The onClick to give the avatar - onClick: PropTypes.func, - // Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser` - viewUserOnClick: PropTypes.bool, - title: PropTypes.string, - }, +interface IState { + name: string; + title: string; + imageUrl?: string; +} - getDefaultProps: function() { - return { - width: 40, - height: 40, - resizeMethod: 'crop', - viewUserOnClick: false, - }; - }, +export default class MemberAvatar extends React.Component { + public static defaultProps = { + width: 40, + height: 40, + resizeMethod: 'crop', + viewUserOnClick: false, + }; - getInitialState: function() { - return this._getState(this.props); - }, + constructor(props: IProps) { + super(props); - // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps: function(nextProps) { - this.setState(this._getState(nextProps)); - }, + this.state = MemberAvatar.getState(props); + } - _getState: function(props) { + public static getDerivedStateFromProps(nextProps: IProps): IState { + return MemberAvatar.getState(nextProps); + } + + private static getState(props: IProps): IState { if (props.member && props.member.name) { return { name: props.member.name, @@ -79,11 +81,9 @@ export default createReactClass({ } else { console.error("MemberAvatar called somehow with null member or fallbackUserId"); } - }, - - render: function() { - const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); + } + render() { let {member, fallbackUserId, onClick, viewUserOnClick, ...otherProps} = this.props; const userId = member ? member.userId : fallbackUserId; @@ -100,5 +100,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/res/css/views/messages/_ReactionsRowButtonTooltip.scss b/src/components/views/avatars/PulsedAvatar.tsx similarity index 67% rename from res/css/views/messages/_ReactionsRowButtonTooltip.scss rename to src/components/views/avatars/PulsedAvatar.tsx index cf4219fcec..94a6c87687 100644 --- a/res/css/views/messages/_ReactionsRowButtonTooltip.scss +++ b/src/components/views/avatars/PulsedAvatar.tsx @@ -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,6 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_ReactionsRowButtonTooltip_reactedWith { - opacity: 0.7; +import React from 'react'; + +interface IProps { } + +const PulsedAvatar: React.FC = (props) => { + return
    + {props.children} +
    ; +}; + +export default PulsedAvatar; \ No newline at end of file diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.tsx similarity index 56% rename from src/components/views/avatars/RoomAvatar.js rename to src/components/views/avatars/RoomAvatar.tsx index a72d318b8d..3317ed3a60 100644 --- a/src/components/views/avatars/RoomAvatar.js +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -13,90 +13,96 @@ WITHOUT 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 createReactClass from 'create-react-class'; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import React from 'react'; +import Room from 'matrix-js-sdk/src/models/room'; +import {getHttpUriForMxc} from 'matrix-js-sdk/src/content-repo'; + +import BaseAvatar from './BaseAvatar'; +import ImageView from '../elements/ImageView'; +import {MatrixClientPeg} from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; -import * as sdk from "../../../index"; import * as Avatar from '../../../Avatar'; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; - -export default createReactClass({ - displayName: 'RoomAvatar', +interface IProps { // Room may be left unset here, but if it is, // oobData.avatarUrl should be set (else there // would be nowhere to get the avatar from) - propTypes: { - room: PropTypes.object, - oobData: PropTypes.object, - width: PropTypes.number, - height: PropTypes.number, - resizeMethod: PropTypes.string, - viewAvatarOnClick: PropTypes.bool, - }, + room?: Room; + // TODO: type when js-sdk has types + oobData?: any; + width?: number; + height?: number; + resizeMethod?: string; + viewAvatarOnClick?: boolean; +} - getDefaultProps: function() { - return { - width: 36, - height: 36, - resizeMethod: 'crop', - oobData: {}, +interface IState { + urls: string[]; +} + +export default class RoomAvatar extends React.Component { + public static defaultProps = { + width: 36, + height: 36, + resizeMethod: 'crop', + oobData: {}, + }; + + constructor(props: IProps) { + super(props); + + this.state = { + urls: RoomAvatar.getImageUrls(this.props), }; - }, + } - getInitialState: function() { - return { - urls: this.getImageUrls(this.props), - }; - }, - - componentDidMount: function() { + public componentDidMount() { MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents); - }, + } - componentWillUnmount: function() { + public componentWillUnmount() { const cli = MatrixClientPeg.get(); if (cli) { cli.removeListener("RoomState.events", this.onRoomStateEvents); } - }, + } - // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps: function(newProps) { - this.setState({ - urls: this.getImageUrls(newProps), - }); - }, + public static getDerivedStateFromProps(nextProps: IProps): IState { + return { + urls: RoomAvatar.getImageUrls(nextProps), + }; + } - onRoomStateEvents: function(ev) { + // TODO: type when js-sdk has types + private onRoomStateEvents = (ev: any) => { if (!this.props.room || ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'm.room.avatar' ) return; this.setState({ - urls: this.getImageUrls(this.props), + urls: RoomAvatar.getImageUrls(this.props), }); - }, + }; - getImageUrls: function(props) { + private static getImageUrls(props: IProps): string[] { return [ getHttpUriForMxc( MatrixClientPeg.get().getHomeserverUrl(), + // Default props don't play nicely with getDerivedStateFromProps + //props.oobData !== undefined ? props.oobData.avatarUrl : {}, props.oobData.avatarUrl, Math.floor(props.width * window.devicePixelRatio), Math.floor(props.height * window.devicePixelRatio), props.resizeMethod, ), // highest priority - this.getRoomAvatarUrl(props), + RoomAvatar.getRoomAvatarUrl(props), ].filter(function(url) { - return (url != null && url != ""); + return (url !== null && url !== ""); }); - }, + } - getRoomAvatarUrl: function(props) { + private static getRoomAvatarUrl(props: IProps): string { if (!props.room) return null; return Avatar.avatarUrlForRoom( @@ -105,35 +111,32 @@ export default createReactClass({ Math.floor(props.height * window.devicePixelRatio), props.resizeMethod, ); - }, + } - onRoomAvatarClick: function() { + private onRoomAvatarClick = () => { const avatarUrl = this.props.room.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), null, null, null, false); - const ImageView = sdk.getComponent("elements.ImageView"); const params = { src: avatarUrl, name: this.props.room.name, }; Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); - }, + }; - render: function() { - const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); - - /*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/ + public render() { const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props; const roomName = room ? room.name : oobData.name; return ( - + onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : null} + /> ); - }, -}); + } +} diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js index ff1a7f1b14..8d690483a8 100644 --- a/src/components/views/context_menus/TagTileContextMenu.js +++ b/src/components/views/context_menus/TagTileContextMenu.js @@ -20,7 +20,6 @@ import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import dis from '../../../dispatcher/dispatcher'; import TagOrderActions from '../../../actions/TagOrderActions'; -import * as sdk from '../../../index'; import {MenuItem} from "../../structures/ContextMenu"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; @@ -54,21 +53,12 @@ export default class TagTileContextMenu extends React.Component { } render() { - const TintableSvg = sdk.getComponent("elements.TintableSvg"); - return
    - - + { _t('View Community') }
    - - + { _t('Hide') }
    ; diff --git a/src/components/views/create_room/CreateRoomButton.js b/src/components/views/create_room/CreateRoomButton.js deleted file mode 100644 index adf3972eff..0000000000 --- a/src/components/views/create_room/CreateRoomButton.js +++ /dev/null @@ -1,44 +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 PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -import { _t } from '../../../languageHandler'; - -export default createReactClass({ - displayName: 'CreateRoomButton', - propTypes: { - onCreateRoom: PropTypes.func, - }, - - getDefaultProps: function() { - return { - onCreateRoom: function() {}, - }; - }, - - onClick: function() { - this.props.onCreateRoom(); - }, - - render: function() { - return ( - - ); - }, -}); diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.js b/src/components/views/dialogs/AskInviteAnywayDialog.js index 120ad8deca..7a12d2bd20 100644 --- a/src/components/views/dialogs/AskInviteAnywayDialog.js +++ b/src/components/views/dialogs/AskInviteAnywayDialog.js @@ -19,8 +19,8 @@ import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; -import {SettingLevel} from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore"; +import {SettingLevel} from "../../../settings/SettingLevel"; export default createReactClass({ propTypes: { diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index e59b6bbaf5..353298032c 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -75,8 +75,12 @@ export default createReactClass({ // If provided, this is used to add a aria-describedby attribute contentId: PropTypes.string, - // optional additional class for the title element - titleClass: PropTypes.string, + // optional additional class for the title element (basically anything that can be passed to classnames) + titleClass: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + PropTypes.arrayOf(PropTypes.string), + ]), }, getDefaultProps: function() { diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.js b/src/components/views/dialogs/CryptoStoreTooNewDialog.js index 4694619601..6336c635e4 100644 --- a/src/components/views/dialogs/CryptoStoreTooNewDialog.js +++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.js @@ -1,5 +1,6 @@ /* 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. @@ -18,9 +19,12 @@ import React from 'react'; import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; +import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; export default (props) => { + const brand = SdkConfig.get().brand; + const _onLogoutClicked = () => { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createTrackedDialog('Logout e2e db too new', '', QuestionDialog, { @@ -28,7 +32,8 @@ export default (props) => { description: _t( "To avoid losing your chat history, you must export your room keys " + "before logging out. You will need to go back to the newer version of " + - "Riot to do this", + "%(brand)s to do this", + { brand }, ), button: _t("Sign out"), focus: false, @@ -42,9 +47,12 @@ export default (props) => { }; const description = - _t("You've previously used a newer version of Riot with this session. " + + _t( + "You've previously used a newer version of %(brand)s with this session. " + "To use this version again with end to end encryption, you will " + - "need to sign out and back in again."); + "need to sign out and back in again.", + { brand }, + ); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index 08817cdfee..a0c5375843 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -19,7 +19,7 @@ import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import SyntaxHighlight from '../elements/SyntaxHighlight'; import { _t } from '../../../languageHandler'; -import { Room } from "matrix-js-sdk"; +import { Room, MatrixEvent } from "matrix-js-sdk"; import Field from "../elements/Field"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; @@ -327,6 +327,8 @@ class RoomStateExplorer extends React.PureComponent { static contextType = MatrixClientContext; + roomStateEvents: Map>; + constructor(props) { super(props); @@ -412,30 +414,26 @@ class RoomStateExplorer extends React.PureComponent { if (this.state.eventType === null) { list = { - Object.keys(this.roomStateEvents).map((evType) => { - const stateGroup = this.roomStateEvents[evType]; - const stateKeys = Object.keys(stateGroup); - + Array.from(this.roomStateEvents.entries()).map(([eventType, allStateKeys]) => { let onClickFn; - if (stateKeys.length === 1 && stateKeys[0] === '') { - onClickFn = this.onViewSourceClick(stateGroup[stateKeys[0]]); + if (allStateKeys.size === 1 && allStateKeys.has("")) { + onClickFn = this.onViewSourceClick(allStateKeys.get("")); } else { - onClickFn = this.browseEventType(evType); + onClickFn = this.browseEventType(eventType); } - return ; }) } ; } else { - const stateGroup = this.roomStateEvents[this.state.eventType]; + const stateGroup = this.roomStateEvents.get(this.state.eventType); list = { - Object.keys(stateGroup).map((stateKey) => { - const ev = stateGroup[stateKey]; + Array.from(stateGroup.entries()).map(([stateKey, ev]) => { return ; diff --git a/src/components/views/dialogs/IntegrationsImpossibleDialog.js b/src/components/views/dialogs/IntegrationsImpossibleDialog.js index f12e4232be..68bedc711d 100644 --- a/src/components/views/dialogs/IntegrationsImpossibleDialog.js +++ b/src/components/views/dialogs/IntegrationsImpossibleDialog.js @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -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 * as sdk from "../../../index"; export default class IntegrationsImpossibleDialog extends React.Component { @@ -29,6 +30,7 @@ export default class IntegrationsImpossibleDialog extends React.Component { }; render() { + const brand = SdkConfig.get().brand; const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -39,8 +41,9 @@ export default class IntegrationsImpossibleDialog extends React.Component {

    {_t( - "Your Riot doesn't allow you to use an Integration Manager to do this. " + + "Your %(brand)s doesn't allow you to use an Integration Manager to do this. " + "Please contact an admin.", + { brand }, )}

    diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 7ac9e21518..4d7a66e957 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -35,8 +35,11 @@ import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../ import {inviteMultipleToRoom} from "../../../RoomInvite"; import {Key} from "../../../Keyboard"; import {Action} from "../../../dispatcher/actions"; -import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy"; import {DefaultTagID} from "../../../stores/room-list/models"; +import RoomListStore from "../../../stores/room-list/RoomListStore"; + +// we have a number of types defined from the Matrix spec which can't reasonably be altered here. +/* eslint-disable camelcase */ export const KIND_DM = "dm"; export const KIND_INVITE = "invite"; @@ -346,8 +349,7 @@ export default class InviteDialog extends React.PureComponent { // Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the // room list doesn't tag the room for the DMRoomMap, but does for the room list. - const taggedRooms = RoomListStoreTempProxy.getRoomLists(); - const dmTaggedRooms = taggedRooms[DefaultTagID.DM]; + const dmTaggedRooms = RoomListStore.instance.orderedLists[DefaultTagID.DM]; const myUserId = MatrixClientPeg.get().getUserId(); for (const dmRoom of dmTaggedRooms) { const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId); diff --git a/src/components/views/dialogs/KeySignatureUploadFailedDialog.js b/src/components/views/dialogs/KeySignatureUploadFailedDialog.js index e59f77fce9..25eb7a90d2 100644 --- a/src/components/views/dialogs/KeySignatureUploadFailedDialog.js +++ b/src/components/views/dialogs/KeySignatureUploadFailedDialog.js @@ -17,6 +17,7 @@ limitations under the License. import React, {useState, useCallback, useRef} from 'react'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; +import SdkConfig from '../../../SdkConfig'; export default function KeySignatureUploadFailedDialog({ failures, @@ -65,9 +66,10 @@ export default function KeySignatureUploadFailedDialog({ let body; if (!success && !cancelled && continuation && retry > 0) { const reason = causes.get(source) || defaultCause; + const brand = SdkConfig.get().brand; body = (
    -

    {_t("Riot encountered an error during upload of:")}

    +

    {_t("%(brand)s encountered an error during upload of:", { brand })}

    {reason}

    {retrying && }
    {JSON.stringify(failures, null, 2)}
    diff --git a/src/components/views/dialogs/LazyLoadingDisabledDialog.js b/src/components/views/dialogs/LazyLoadingDisabledDialog.js index d128d8dedd..cae9510742 100644 --- a/src/components/views/dialogs/LazyLoadingDisabledDialog.js +++ b/src/components/views/dialogs/LazyLoadingDisabledDialog.js @@ -1,5 +1,6 @@ /* 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. @@ -17,17 +18,28 @@ limitations under the License. import React from 'react'; import QuestionDialog from './QuestionDialog'; import { _t } from '../../../languageHandler'; +import SdkConfig from '../../../SdkConfig'; export default (props) => { - const description1 = - _t("You've previously used Riot on %(host)s with lazy loading of members enabled. " + - "In this version lazy loading is disabled. " + - "As the local cache is not compatible between these two settings, " + - "Riot needs to resync your account.", - {host: props.host}); - const description2 = _t("If the other version of Riot is still open in another tab, " + - "please close it as using Riot on the same host with both " + - "lazy loading enabled and disabled simultaneously will cause issues."); + const brand = SdkConfig.get().brand; + const description1 = _t( + "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. " + + "In this version lazy loading is disabled. " + + "As the local cache is not compatible between these two settings, " + + "%(brand)s needs to resync your account.", + { + brand, + host: props.host, + }, + ); + const description2 = _t( + "If the other version of %(brand)s is still open in another tab, " + + "please close it as using %(brand)s on the same host with both " + + "lazy loading enabled and disabled simultaneously will cause issues.", + { + brand, + }, + ); return ( { + const brand = SdkConfig.get().brand; const description = - _t("Riot now uses 3-5x less memory, by only loading information about other users" - + " when needed. Please wait whilst we resynchronise with the server!"); + _t( + "%(brand)s now uses 3-5x less memory, by only loading information " + + "about other users when needed. Please wait whilst we resynchronise " + + "with the server!", + { brand }, + ); return ({description}
    } button={_t("OK")} onFinished={props.onFinished} diff --git a/src/components/views/dialogs/RebrandDialog.tsx b/src/components/views/dialogs/RebrandDialog.tsx new file mode 100644 index 0000000000..79b4b69a4a --- /dev/null +++ b/src/components/views/dialogs/RebrandDialog.tsx @@ -0,0 +1,116 @@ +/* +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 * as PropTypes from 'prop-types'; +import BaseDialog from './BaseDialog'; +import { _t } from '../../../languageHandler'; +import DialogButtons from '../elements/DialogButtons'; + +export enum RebrandDialogKind { + NAG, + ONE_TIME, +} + +interface IProps { + onFinished: (bool) => void; + kind: RebrandDialogKind; + targetUrl?: string; +} + +export default class RebrandDialog extends React.PureComponent { + private onDoneClick = () => { + this.props.onFinished(true); + }; + + private onGoToElementClick = () => { + this.props.onFinished(true); + }; + + private onRemindMeLaterClick = () => { + this.props.onFinished(false); + }; + + private getPrettyTargetUrl() { + const u = new URL(this.props.targetUrl); + let ret = u.host; + if (u.pathname !== '/') ret += u.pathname; + return ret; + } + + getBodyText() { + if (this.props.kind === RebrandDialogKind.NAG) { + return _t( + "Use your account to sign in to the latest version of the app at
    ", {}, + { + a: sub => {this.getPrettyTargetUrl()}, + }, + ); + } else { + return _t( + "You’re already signed in and good to go here, but you can also grab the latest " + + "versions of the app on all platforms at element.io/get-started.", {}, + { + a: sub => {sub}, + }, + ); + } + } + + getDialogButtons() { + if (this.props.kind === RebrandDialogKind.NAG) { + return ; + } else { + return ; + } + } + + render() { + return +
    {this.getBodyText()}
    +
    + Riot Logo + + Element Logo +
    +
    + {_t( + "Learn more at element.io/previously-riot", {}, { + a: sub => {sub}, + } + )} +
    + {this.getDialogButtons()} +
    ; + } +} diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index c2b98cd9f3..7ad1001f75 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -30,6 +30,13 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg"; import dis from "../../../dispatcher/dispatcher"; import SettingsStore from "../../../settings/SettingsStore"; +export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB"; +export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB"; +export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB"; +export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB"; +export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB"; +export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB"; + export default class RoomSettingsDialog extends React.Component { static propTypes = { roomId: PropTypes.string.isRequired, @@ -56,21 +63,25 @@ export default class RoomSettingsDialog extends React.Component { const tabs = []; tabs.push(new Tab( + ROOM_GENERAL_TAB, _td("General"), "mx_RoomSettingsDialog_settingsIcon", , )); tabs.push(new Tab( + ROOM_SECURITY_TAB, _td("Security & Privacy"), "mx_RoomSettingsDialog_securityIcon", , )); tabs.push(new Tab( + ROOM_ROLES_TAB, _td("Roles & Permissions"), "mx_RoomSettingsDialog_rolesIcon", , )); tabs.push(new Tab( + ROOM_NOTIFICATIONS_TAB, _td("Notifications"), "mx_RoomSettingsDialog_notificationsIcon", , @@ -78,6 +89,7 @@ export default class RoomSettingsDialog extends React.Component { if (SettingsStore.isFeatureEnabled("feature_bridge_state")) { tabs.push(new Tab( + ROOM_BRIDGES_TAB, _td("Bridges"), "mx_RoomSettingsDialog_bridgesIcon", , @@ -85,6 +97,7 @@ export default class RoomSettingsDialog extends React.Component { } tabs.push(new Tab( + ROOM_ADVANCED_TAB, _td("Advanced"), "mx_RoomSettingsDialog_warningIcon", , diff --git a/src/components/views/dialogs/RoomUpgradeWarningDialog.js b/src/components/views/dialogs/RoomUpgradeWarningDialog.js index 02534c5b35..c83528c5ba 100644 --- a/src/components/views/dialogs/RoomUpgradeWarningDialog.js +++ b/src/components/views/dialogs/RoomUpgradeWarningDialog.js @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -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 * as sdk from "../../../index"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; @@ -63,6 +64,7 @@ export default class RoomUpgradeWarningDialog extends React.Component { }; render() { + const brand = SdkConfig.get().brand; const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -96,8 +98,11 @@ export default class RoomUpgradeWarningDialog extends React.Component {

    {_t( "This usually only affects how the room is processed on the server. If you're " + - "having problems with your Riot, please report a bug.", - {}, { + "having problems with your %(brand)s, please report a bug.", + { + brand, + }, + { "a": (sub) => { return {sub}; }, diff --git a/src/components/views/dialogs/ServerOfflineDialog.tsx b/src/components/views/dialogs/ServerOfflineDialog.tsx new file mode 100644 index 0000000000..f6767dcb8d --- /dev/null +++ b/src/components/views/dialogs/ServerOfflineDialog.tsx @@ -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. +*/ + +import * as React from 'react'; +import BaseDialog from './BaseDialog'; +import { _t } from '../../../languageHandler'; +import { EchoStore } from "../../../stores/local-echo/EchoStore"; +import { formatTime } from "../../../DateUtils"; +import SettingsStore from "../../../settings/SettingsStore"; +import { RoomEchoContext } from "../../../stores/local-echo/RoomEchoContext"; +import RoomAvatar from "../avatars/RoomAvatar"; +import { TransactionStatus } from "../../../stores/local-echo/EchoTransaction"; +import Spinner from "../elements/Spinner"; +import AccessibleButton from "../elements/AccessibleButton"; +import { UPDATE_EVENT } from "../../../stores/AsyncStore"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; + +interface IProps { + onFinished: (bool) => void; +} + +export default class ServerOfflineDialog extends React.PureComponent { + public componentDidMount() { + EchoStore.instance.on(UPDATE_EVENT, this.onEchosUpdated); + } + + public componentWillUnmount() { + EchoStore.instance.off(UPDATE_EVENT, this.onEchosUpdated); + } + + private onEchosUpdated = () => { + this.forceUpdate(); // no state to worry about + }; + + private renderTimeline(): React.ReactElement[] { + return EchoStore.instance.contexts.map((c, i) => { + if (!c.firstFailedTime) return null; // not useful + if (!(c instanceof RoomEchoContext)) throw new Error("Cannot render unknown context: " + c); + const header = ( +

    + + {c.room.name} +
    + ); + const entries = c.transactions + .filter(t => t.status === TransactionStatus.Error || t.didPreviouslyFail) + .map((t, j) => { + let button = ; + if (t.status === TransactionStatus.Error) { + button = ( + t.run()}>{_t("Resend")} + ); + } + return ( +
    + + {t.auditName} + + {button} +
    + ); + }); + return ( +
    +
    + {formatTime(c.firstFailedTime, SettingsStore.getValue("showTwelveHourTimestamps"))} +
    +
    + {header} + {entries} +
    +
    + ) + }); + } + + public render() { + let timeline = this.renderTimeline().filter(c => !!c); // remove nulls for next check + if (timeline.length === 0) { + timeline = [
    {_t("You're all caught up.")}
    ]; + } + + const serverName = MatrixClientPeg.getHomeserverName(); + return +
    +

    {_t( + "Your server isn't responding to some of your requests. " + + "Below are some of the most likely reasons.", + )}

    +
      +
    • {_t("The server (%(serverName)s) took too long to respond.", {serverName})}
    • +
    • {_t("Your firewall or anti-virus is blocking the request.")}
    • +
    • {_t("A browser extension is preventing the request.")}
    • +
    • {_t("The server is offline.")}
    • +
    • {_t("The server has denied your request.")}
    • +
    • {_t("Your area is experiencing difficulties connecting to the internet.")}
    • +
    • {_t("A connection error occurred while trying to contact the server.")}
    • +
    • {_t("The server is not configured to indicate what the problem is (CORS).")}
    • +
    +
    +

    {_t("Recent changes that have not yet been received")}

    + {timeline} +
    +
    ; + } +} diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js index 935faf0cad..3706172085 100644 --- a/src/components/views/dialogs/SessionRestoreErrorDialog.js +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js @@ -1,6 +1,7 @@ /* Copyright 2017 Vector Creations 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. @@ -56,6 +57,7 @@ export default createReactClass({ }, render: function() { + const brand = SdkConfig.get().brand; const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -94,9 +96,10 @@ export default createReactClass({

    { _t("We encountered an error trying to restore your previous session.") }

    { _t( - "If you have previously used a more recent version of Riot, your session " + + "If you have previously used a more recent version of %(brand)s, your session " + "may be incompatible with this version. Close this window and return " + "to the more recent version.", + { brand }, ) }

    { _t( diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index 4592d921a9..1f1a8d1523 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -33,9 +33,21 @@ import * as sdk from "../../../index"; import SdkConfig from "../../../SdkConfig"; import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab"; +export const USER_GENERAL_TAB = "USER_GENERAL_TAB"; +export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB"; +export const USER_FLAIR_TAB = "USER_FLAIR_TAB"; +export const USER_NOTIFICATIONS_TAB = "USER_NOTIFICATIONS_TAB"; +export const USER_PREFERENCES_TAB = "USER_PREFERENCES_TAB"; +export const USER_VOICE_TAB = "USER_VOICE_TAB"; +export const USER_SECURITY_TAB = "USER_SECURITY_TAB"; +export const USER_LABS_TAB = "USER_LABS_TAB"; +export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB"; +export const USER_HELP_TAB = "USER_HELP_TAB"; + export default class UserSettingsDialog extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, + initialTabId: PropTypes.string, }; constructor() { @@ -63,42 +75,50 @@ export default class UserSettingsDialog extends React.Component { const tabs = []; tabs.push(new Tab( + USER_GENERAL_TAB, _td("General"), "mx_UserSettingsDialog_settingsIcon", , )); tabs.push(new Tab( + USER_APPEARANCE_TAB, _td("Appearance"), "mx_UserSettingsDialog_appearanceIcon", , )); tabs.push(new Tab( + USER_FLAIR_TAB, _td("Flair"), "mx_UserSettingsDialog_flairIcon", , )); tabs.push(new Tab( + USER_NOTIFICATIONS_TAB, _td("Notifications"), "mx_UserSettingsDialog_bellIcon", , )); tabs.push(new Tab( + USER_PREFERENCES_TAB, _td("Preferences"), "mx_UserSettingsDialog_preferencesIcon", , )); tabs.push(new Tab( + USER_VOICE_TAB, _td("Voice & Video"), "mx_UserSettingsDialog_voiceIcon", , )); tabs.push(new Tab( + USER_SECURITY_TAB, _td("Security & Privacy"), "mx_UserSettingsDialog_securityIcon", , )); if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) { tabs.push(new Tab( + USER_LABS_TAB, _td("Labs"), "mx_UserSettingsDialog_labsIcon", , @@ -106,12 +126,14 @@ export default class UserSettingsDialog extends React.Component { } if (this.state.mjolnirEnabled) { tabs.push(new Tab( + USER_MJOLNIR_TAB, _td("Ignored users"), "mx_UserSettingsDialog_mjolnirIcon", , )); } tabs.push(new Tab( + USER_HELP_TAB, _td("Help & About"), "mx_UserSettingsDialog_helpIcon", , @@ -127,7 +149,7 @@ export default class UserSettingsDialog extends React.Component {

    - +
    ); diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js index 162cb4736a..42a5304f13 100644 --- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js +++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js @@ -17,10 +17,11 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import {_t} from "../../../languageHandler"; -import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import SettingsStore from "../../../settings/SettingsStore"; import * as sdk from "../../../index"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import WidgetUtils from "../../../utils/WidgetUtils"; +import {SettingLevel} from "../../../settings/SettingLevel"; export default class WidgetOpenIDPermissionsDialog extends React.Component { static propTypes = { diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index e2ceadfbb9..5c01a6907f 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -15,13 +15,24 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { debounce } from 'lodash'; +import classNames from 'classnames'; import React from 'react'; import PropTypes from "prop-types"; import * as sdk from '../../../../index'; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; +import Field from '../../elements/Field'; +import AccessibleButton from '../../elements/AccessibleButton'; import { _t } from '../../../../languageHandler'; -import { accessSecretStorage } from '../../../../CrossSigningManager'; + +// Maximum acceptable size of a key file. It's 59 characters including the spaces we encode, +// so this should be plenty and allow for people putting extra whitespace in the file because +// maybe that's a thing people would do? +const KEY_FILE_MAX_SIZE = 128; + +// Don't shout at the user that their key is invalid every time they type a key: wait a short time +const VALIDATION_THROTTLE_MS = 200; /* * Access Secure Secret Storage by requesting the user's passphrase. @@ -36,9 +47,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent { constructor(props) { super(props); + + this._fileUpload = React.createRef(); + this.state = { recoveryKey: "", - recoveryKeyValid: false, + recoveryKeyValid: null, + recoveryKeyCorrect: null, + recoveryKeyFileError: null, forceRecoveryKey: false, passPhrase: '', keyMatches: null, @@ -55,18 +71,89 @@ export default class AccessSecretStorageDialog extends React.PureComponent { }); } - _onResetRecoveryClick = () => { - // Re-enter the access flow, but resetting storage this time around. - this.props.onFinished(false); - accessSecretStorage(() => {}, /* forceReset = */ true); + _validateRecoveryKeyOnChange = debounce(() => { + this._validateRecoveryKey(); + }, VALIDATION_THROTTLE_MS); + + async _validateRecoveryKey() { + if (this.state.recoveryKey === '') { + this.setState({ + recoveryKeyValid: null, + recoveryKeyCorrect: null, + }); + return; + } + + try { + const cli = MatrixClientPeg.get(); + const decodedKey = cli.keyBackupKeyFromRecoveryKey(this.state.recoveryKey); + const correct = await cli.checkSecretStorageKey( + decodedKey, this.props.keyInfo, + ); + this.setState({ + recoveryKeyValid: true, + recoveryKeyCorrect: correct, + }); + } catch (e) { + this.setState({ + recoveryKeyValid: false, + recoveryKeyCorrect: false, + }); + } } _onRecoveryKeyChange = (e) => { this.setState({ recoveryKey: e.target.value, - recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value), - keyMatches: null, + recoveryKeyFileError: null, }); + + // also clear the file upload control so that the user can upload the same file + // the did before (otherwise the onchange wouldn't fire) + if (this._fileUpload.current) this._fileUpload.current.value = null; + + // We don't use Field's validation here because a) we want it in a separate place rather + // than in a tooltip and b) we want it to display feedback based on the uploaded file + // as well as the text box. Ideally we would refactor Field's validation logic so we could + // re-use some of it. + this._validateRecoveryKeyOnChange(); + } + + _onRecoveryKeyFileChange = async e => { + if (e.target.files.length === 0) return; + + const f = e.target.files[0]; + + if (f.size > KEY_FILE_MAX_SIZE) { + this.setState({ + recoveryKeyFileError: true, + recoveryKeyCorrect: false, + recoveryKeyValid: false, + }); + } else { + const contents = await f.text(); + // test it's within the base58 alphabet. We could be more strict here, eg. require the + // right number of characters, but it's really just to make sure that what we're reading is + // text because we'll put it in the text field. + if (/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\s]+$/.test(contents)) { + this.setState({ + recoveryKeyFileError: null, + recoveryKey: contents.trim(), + }); + this._validateRecoveryKey(); + } else { + this.setState({ + recoveryKeyFileError: true, + recoveryKeyCorrect: false, + recoveryKeyValid: false, + recoveryKey: '', + }); + } + } + } + + _onRecoveryKeyFileUploadClick = () => { + this._fileUpload.current.click(); } _onPassPhraseNext = async (e) => { @@ -106,6 +193,20 @@ export default class AccessSecretStorageDialog extends React.PureComponent { }); } + getKeyValidationText() { + if (this.state.recoveryKeyFileError) { + return _t("Wrong file type"); + } else if (this.state.recoveryKeyCorrect) { + return _t("Looks good!"); + } else if (this.state.recoveryKeyValid) { + return _t("Wrong Recovery Key"); + } else if (this.state.recoveryKeyValid === null) { + return ''; + } else { + return _t("Invalid Recovery Key"); + } + } + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); @@ -118,10 +219,12 @@ export default class AccessSecretStorageDialog extends React.PureComponent { let content; let title; + let titleClass; if (hasPassphrase && !this.state.forceRecoveryKey) { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - title = _t("Enter recovery passphrase"); + title = _t("Security Phrase"); + titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_securePhraseTitle']; let keyStatus; if (this.state.keyMatches === false) { @@ -137,12 +240,15 @@ export default class AccessSecretStorageDialog extends React.PureComponent { content =

    {_t( - "Warning: You should only do this on a trusted computer.", {}, - { b: sub => {sub} }, - )}

    -

    {_t( - "Access your secure message history and your cross-signing " + - "identity for verifying other sessions by entering your recovery passphrase.", + "Enter your Security Phrase or to continue.", {}, + { + button: s => + {s} + , + }, )}

    @@ -153,10 +259,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent { value={this.state.passPhrase} autoFocus={true} autoComplete="new-password" + placeholder={_t("Security Phrase")} /> {keyStatus} - {_t( - "If you've forgotten your recovery passphrase you can "+ - "use your recovery key or " + - "set up new recovery options." - , {}, { - button1: s => - {s} - , - button2: s => - {s} - , - })}
    ; } else { - title = _t("Enter recovery key"); + title = _t("Security Key"); + titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_secureBackupTitle']; const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - let keyStatus; - if (this.state.recoveryKey.length === 0) { - keyStatus =
    ; - } else if (this.state.keyMatches === false) { - keyStatus =
    - {"\uD83D\uDC4E "}{_t( - "Unable to access secret storage. " + - "Please verify that you entered the correct recovery key.", - )} -
    ; - } else if (this.state.recoveryKeyValid) { - keyStatus =
    - {"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")} -
    ; - } else { - keyStatus =
    - {"\uD83D\uDC4E "}{_t("Not a valid recovery key")} -
    ; - } + const feedbackClasses = classNames({ + 'mx_AccessSecretStorageDialog_recoveryKeyFeedback': true, + 'mx_AccessSecretStorageDialog_recoveryKeyFeedback_valid': this.state.recoveryKeyCorrect === true, + 'mx_AccessSecretStorageDialog_recoveryKeyFeedback_invalid': this.state.recoveryKeyCorrect === false, + }); + const recoveryKeyFeedback =
    + {this.getKeyValidationText()} +
    ; content =
    -

    {_t( - "Warning: You should only do this on a trusted computer.", {}, - { b: sub => {sub} }, - )}

    -

    {_t( - "Access your secure message history and your cross-signing " + - "identity for verifying other sessions by entering your recovery key.", - )}

    +

    {_t("Use your Security Key to continue.")}

    -
    - - {keyStatus} + +
    +
    + +
    + + {_t("or")} + +
    + + + {_t("Upload")} + +
    +
    + {recoveryKeyFeedback} - {_t( - "If you've forgotten your recovery key you can "+ - "." - , {}, { - button: s => - {s} - , - })}
    ; } @@ -252,6 +333,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
    {content} diff --git a/src/components/views/elements/AccessibleButton.js b/src/components/views/elements/AccessibleButton.tsx similarity index 67% rename from src/components/views/elements/AccessibleButton.js rename to src/components/views/elements/AccessibleButton.tsx index d708a44ab2..ae822204df 100644 --- a/src/components/views/elements/AccessibleButton.js +++ b/src/components/views/elements/AccessibleButton.tsx @@ -15,9 +15,36 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; import {Key} from '../../../Keyboard'; +import classnames from 'classnames'; + +export type ButtonEvent = React.MouseEvent | React.KeyboardEvent; + +/** + * children: React's magic prop. Represents all children given to the element. + * element: (optional) The base element type. "div" by default. + * onClick: (required) Event handler for button activation. Should be + * implemented exactly like a normal onClick handler. + */ +interface IProps extends React.InputHTMLAttributes { + inputRef?: React.Ref; + element?: string; + // The kind of button, similar to how Bootstrap works. + // See available classes for AccessibleButton for options. + kind?: string; + // The ARIA role + role?: string; + // The tabIndex + tabIndex?: number; + disabled?: boolean; + className?: string; + onClick?(e?: ButtonEvent): void; +} + +interface IAccessibleButtonProps extends React.InputHTMLAttributes { + ref?: React.Ref; +} /** * AccessibleButton is a generic wrapper for any element that should be treated @@ -27,11 +54,19 @@ import {Key} from '../../../Keyboard'; * @param {Object} props react element properties * @returns {Object} rendered react */ -export default function AccessibleButton(props) { - const {element, onClick, children, kind, disabled, ...restProps} = props; - +export default function AccessibleButton({ + element, + onClick, + children, + kind, + disabled, + inputRef, + className, + ...restProps +}: IProps) { + const newProps: IAccessibleButtonProps = restProps; if (!disabled) { - restProps.onClick = onClick; + newProps.onClick = onClick; // We need to consume enter onKeyDown and space onKeyUp // otherwise we are risking also activating other keyboard focusable elements // that might receive focus as a result of the AccessibleButtonClick action @@ -39,7 +74,7 @@ export default function AccessibleButton(props) { // And divs which we report as role button to assistive technologies. // Browsers handle space and enter keypresses differently and we are only adjusting to the // inconsistencies here - restProps.onKeyDown = function(e) { + newProps.onKeyDown = (e) => { if (e.key === Key.ENTER) { e.stopPropagation(); e.preventDefault(); @@ -50,7 +85,7 @@ export default function AccessibleButton(props) { e.preventDefault(); } }; - restProps.onKeyUp = function(e) { + newProps.onKeyUp = (e) => { if (e.key === Key.SPACE) { e.stopPropagation(); e.preventDefault(); @@ -64,57 +99,26 @@ export default function AccessibleButton(props) { } // Pass through the ref - used for keyboard shortcut access to some buttons - restProps.ref = restProps.inputRef; - delete restProps.inputRef; + newProps.ref = inputRef; - restProps.className = (restProps.className ? restProps.className + " " : "") + "mx_AccessibleButton"; - - if (kind) { - // We apply a hasKind class to maintain backwards compatibility with - // buttons which might not know about kind and break - restProps.className += " mx_AccessibleButton_hasKind mx_AccessibleButton_kind_" + kind; - } - - if (disabled) { - restProps.className += " mx_AccessibleButton_disabled"; - restProps["aria-disabled"] = true; - } + newProps.className = classnames( + "mx_AccessibleButton", + className, + { + "mx_AccessibleButton_hasKind": kind, + [`mx_AccessibleButton_kind_${kind}`]: kind, + "mx_AccessibleButton_disabled": disabled, + }, + ); + // React.createElement expects InputHTMLAttributes return React.createElement(element, restProps, children); } -/** - * children: React's magic prop. Represents all children given to the element. - * element: (optional) The base element type. "div" by default. - * onClick: (required) Event handler for button activation. Should be - * implemented exactly like a normal onClick handler. - */ -AccessibleButton.propTypes = { - children: PropTypes.node, - inputRef: PropTypes.oneOfType([ - // Either a function - PropTypes.func, - // Or the instance of a DOM native element - PropTypes.shape({ current: PropTypes.instanceOf(Element) }), - ]), - element: PropTypes.string, - onClick: PropTypes.func.isRequired, - - // The kind of button, similar to how Bootstrap works. - // See available classes for AccessibleButton for options. - kind: PropTypes.string, - // The ARIA role - role: PropTypes.string, - // The tabIndex - tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - - disabled: PropTypes.bool, -}; - AccessibleButton.defaultProps = { element: 'div', role: 'button', - tabIndex: "0", + tabIndex: 0, }; AccessibleButton.displayName = "AccessibleButton"; diff --git a/src/components/views/elements/AccessibleTooltipButton.js b/src/components/views/elements/AccessibleTooltipButton.tsx similarity index 57% rename from src/components/views/elements/AccessibleTooltipButton.js rename to src/components/views/elements/AccessibleTooltipButton.tsx index 6c84c6ab7e..3546f62359 100644 --- a/src/components/views/elements/AccessibleTooltipButton.js +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -16,21 +16,28 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; +import classNames from 'classnames'; import AccessibleButton from "./AccessibleButton"; -import * as sdk from "../../../index"; +import Tooltip from './Tooltip'; -export default class AccessibleTooltipButton extends React.PureComponent { - static propTypes = { - ...AccessibleButton.propTypes, - // The tooltip to render on hover - title: PropTypes.string.isRequired, - }; +interface ITooltipProps extends React.ComponentProps { + title: string; + tooltip?: React.ReactNode; + tooltipClassName?: string; +} - state = { - hover: false, - }; +interface IState { + hover: boolean; +} + +export default class AccessibleTooltipButton extends React.PureComponent { + constructor(props: ITooltipProps) { + super(props); + this.state = { + hover: false, + }; + } onMouseOver = () => { this.setState({ @@ -38,25 +45,27 @@ export default class AccessibleTooltipButton extends React.PureComponent { }); }; - onMouseOut = () => { + onMouseLeave = () => { this.setState({ hover: false, }); }; render() { - const Tooltip = sdk.getComponent("elements.Tooltip"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - - const {title, children, ...props} = this.props; + const {title, tooltip, children, tooltipClassName, ...props} = this.props; const tip = this.state.hover ? :
    ; return ( - + { children } { tip } diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js index 36af5059fc..e5ea2e5d20 100644 --- a/src/components/views/elements/AddressTile.js +++ b/src/components/views/elements/AddressTile.js @@ -58,18 +58,6 @@ export default createReactClass({ imgUrls.push(require("../../../../res/img/icon-email-user.svg")); } - // Removing networks for now as they're not really supported - /* - var network; - if (this.props.networkUrl !== "") { - network = ( -
    - -
    - ); - } - */ - const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const TintableSvg = sdk.getComponent("elements.TintableSvg"); diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.js index b96001b106..ec8bffc32f 100644 --- a/src/components/views/elements/AppPermission.js +++ b/src/components/views/elements/AppPermission.js @@ -1,7 +1,7 @@ /* Copyright 2017 Vector Creations Ltd Copyright 2018, 2019 New Vector 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. @@ -21,6 +21,7 @@ import PropTypes from 'prop-types'; import url from 'url'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; +import SdkConfig from '../../../SdkConfig'; import WidgetUtils from "../../../utils/WidgetUtils"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; @@ -76,6 +77,7 @@ export default class AppPermission extends React.Component { } render() { + const brand = SdkConfig.get().brand; const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); @@ -96,7 +98,7 @@ export default class AppPermission extends React.Component {
  • {_t("Your avatar URL")}
  • {_t("Your user ID")}
  • {_t("Your theme")}
  • -
  • {_t("Riot URL")}
  • +
  • {_t("%(brand)s URL", { brand })}
  • {_t("Room ID")}
  • {_t("Widget ID")}
  • diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 527436b0e4..d0fc56743f 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -29,16 +29,19 @@ import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; import AppPermission from './AppPermission'; import AppWarning from './AppWarning'; -import MessageSpinner from './MessageSpinner'; +import Spinner from './Spinner'; import WidgetUtils from '../../../utils/WidgetUtils'; import dis from '../../../dispatcher/dispatcher'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import classNames from 'classnames'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; -import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import SettingsStore from "../../../settings/SettingsStore"; import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu"; import PersistedElement from "./PersistedElement"; import {WidgetType} from "../../../widgets/WidgetType"; +import {Capability} from "../../../widgets/WidgetApi"; +import {sleep} from "../../../utils/promise"; +import {SettingLevel} from "../../../settings/SettingLevel"; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ENABLE_REACT_PERF = false; @@ -341,23 +344,37 @@ export default class AppTile extends React.Component { /** * Ends all widget interaction, such as cancelling calls and disabling webcams. * @private + * @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed. */ _endWidgetActions() { - // HACK: This is a really dirty way to ensure that Jitsi cleans up - // its hold on the webcam. Without this, the widget holds a media - // stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351 - if (this._appFrame.current) { - // In practice we could just do `+= ''` to trick the browser - // into thinking the URL changed, however I can foresee this - // being optimized out by a browser. Instead, we'll just point - // the iframe at a page that is reasonably safe to use in the - // event the iframe doesn't wink away. - // This is relative to where the Riot instance is located. - this._appFrame.current.src = 'about:blank'; + let terminationPromise; + + if (this._hasCapability(Capability.ReceiveTerminate)) { + // Wait for widget to terminate within a timeout + const timeout = 2000; + const messaging = ActiveWidgetStore.getWidgetMessaging(this.props.app.id); + terminationPromise = Promise.race([messaging.terminate(), sleep(timeout)]); + } else { + terminationPromise = Promise.resolve(); } - // Delete the widget from the persisted store for good measure. - PersistedElement.destroyElement(this._persistKey); + return terminationPromise.finally(() => { + // HACK: This is a really dirty way to ensure that Jitsi cleans up + // its hold on the webcam. Without this, the widget holds a media + // stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351 + if (this._appFrame.current) { + // In practice we could just do `+= ''` to trick the browser + // into thinking the URL changed, however I can foresee this + // being optimized out by a browser. Instead, we'll just point + // the iframe at a page that is reasonably safe to use in the + // event the iframe doesn't wink away. + // This is relative to where the Riot instance is located. + this._appFrame.current.src = 'about:blank'; + } + + // Delete the widget from the persisted store for good measure. + PersistedElement.destroyElement(this._persistKey); + }); } /* If user has permission to modify widgets, delete the widget, @@ -381,12 +398,12 @@ export default class AppTile extends React.Component { } this.setState({deleting: true}); - this._endWidgetActions(); - - WidgetUtils.setRoomWidget( - this.props.room.roomId, - this.props.app.id, - ).catch((e) => { + this._endWidgetActions().then(() => { + return WidgetUtils.setRoomWidget( + this.props.room.roomId, + this.props.app.id, + ); + }).catch((e) => { console.error('Failed to delete widget', e); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -669,6 +686,17 @@ export default class AppTile extends React.Component { } _onPopoutWidgetClick() { + // Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them + // twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop). + if (WidgetType.JITSI.matches(this.props.app.type) && this.props.show) { + this._endWidgetActions().then(() => { + if (this._appFrame.current) { + // Reload iframe + this._appFrame.current.src = this._getRenderedUrl(); + this.setState({}); + } + }); + } // Using Object.assign workaround as the following opens in a new window instead of a new tab. // window.open(this._getPopoutUrl(), '_blank', 'noopener=yes'); Object.assign(document.createElement('a'), @@ -677,6 +705,7 @@ export default class AppTile extends React.Component { _onReloadWidgetClick() { // Reload iframe in this way to avoid cross-origin restrictions + // eslint-disable-next-line no-self-assign this._appFrame.current.src = this._appFrame.current.src; } @@ -713,7 +742,7 @@ export default class AppTile extends React.Component { if (this.props.show) { const loadingElement = (
    - +
    ); if (!this.state.hasPermissionToLoad) { diff --git a/src/components/views/elements/CreateRoomButton.js b/src/components/views/elements/CreateRoomButton.js deleted file mode 100644 index 1410bdabdb..0000000000 --- a/src/components/views/elements/CreateRoomButton.js +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import * as sdk from '../../../index'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; - -const CreateRoomButton = function(props) { - const ActionButton = sdk.getComponent('elements.ActionButton'); - return ( - - ); -}; - -CreateRoomButton.propTypes = { - size: PropTypes.string, - tooltip: PropTypes.bool, -}; - -export default CreateRoomButton; diff --git a/src/components/views/elements/Draggable.tsx b/src/components/views/elements/Draggable.tsx index 3096ac42f7..3397fd901c 100644 --- a/src/components/views/elements/Draggable.tsx +++ b/src/components/views/elements/Draggable.tsx @@ -17,20 +17,20 @@ limitations under the License. import React from 'react'; interface IProps { - className: string, - dragFunc: (currentLocation: ILocationState, event: MouseEvent) => ILocationState, - onMouseUp: (event: MouseEvent) => void, + className: string; + dragFunc: (currentLocation: ILocationState, event: MouseEvent) => ILocationState; + onMouseUp: (event: MouseEvent) => void; } interface IState { - onMouseMove: (event: MouseEvent) => void, - onMouseUp: (event: MouseEvent) => void, - location: ILocationState, + onMouseMove: (event: MouseEvent) => void; + onMouseUp: (event: MouseEvent) => void; + location: ILocationState; } export interface ILocationState { - currentX: number, - currentY: number, + currentX: number; + currentY: number; } export default class Draggable extends React.Component { @@ -58,13 +58,13 @@ export default class Draggable extends React.Component { document.addEventListener("mousemove", this.state.onMouseMove); document.addEventListener("mouseup", this.state.onMouseUp); - } + }; private onMouseUp = (event: MouseEvent): void => { document.removeEventListener("mousemove", this.state.onMouseMove); document.removeEventListener("mouseup", this.state.onMouseUp); this.props.onMouseUp(event); - } + }; private onMouseMove(event: MouseEvent): void { const newLocation = this.props.dragFunc(this.state.location, event); @@ -75,7 +75,7 @@ export default class Draggable extends React.Component { } render() { - return
    + return
    ; } } \ No newline at end of file diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.js index 50d5a3d10f..34e53906a2 100644 --- a/src/components/views/elements/EditableItemList.js +++ b/src/components/views/elements/EditableItemList.js @@ -16,7 +16,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {_t} from '../../../languageHandler.js'; +import {_t} from '../../../languageHandler'; import Field from "./Field"; import AccessibleButton from "./AccessibleButton"; diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx new file mode 100644 index 0000000000..7d8b774955 --- /dev/null +++ b/src/components/views/elements/EventTilePreview.tsx @@ -0,0 +1,129 @@ +/* +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 classnames from 'classnames'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; + +import * as Avatar from '../../../Avatar'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import EventTile from '../rooms/EventTile'; + +interface IProps { + /** + * The text to be displayed in the message preview + */ + message: string; + + /** + * Whether to use the irc layout or not + */ + useIRCLayout: boolean; + + /** + * classnames to apply to the wrapper of the preview + */ + className: string; +} + +interface IState { + userId: string; + displayname: string; + avatar_url: string; +} + +const AVATAR_SIZE = 32; + +export default class EventTilePreview extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + userId: "@erim:fink.fink", + displayname: "Erimayas Fink", + avatar_url: null, + }; + } + + async componentDidMount() { + // Fetch current user data + const client = MatrixClientPeg.get(); + const userId = client.getUserId(); + const profileInfo = await client.getProfileInfo(userId); + const avatar_url = Avatar.avatarUrlForUser( + {avatarUrl: profileInfo.avatar_url}, + AVATAR_SIZE, AVATAR_SIZE, "crop"); + + this.setState({ + userId, + displayname: profileInfo.displayname, + avatar_url, + }); + + } + + private fakeEvent({userId, displayname, avatar_url}: IState) { + // Fake it till we make it + const event = new MatrixEvent(JSON.parse(`{ + "type": "m.room.message", + "sender": "${userId}", + "content": { + "m.new_content": { + "msgtype": "m.text", + "body": "${this.props.message}", + "displayname": "${displayname}", + "avatar_url": "${avatar_url}" + }, + "msgtype": "m.text", + "body": "${this.props.message}", + "displayname": "${displayname}", + "avatar_url": "${avatar_url}" + }, + "unsigned": { + "age": 97 + }, + "event_id": "$9999999999999999999999999999999999999999999", + "room_id": "!999999999999999999:matrix.org" + }`)); + + // Fake it more + event.sender = { + name: displayname, + userId: userId, + getAvatarUrl: (..._) => { + return avatar_url; + }, + }; + + return event; + } + + public render() { + const event = this.fakeEvent(this.state); + + let className = classnames( + this.props.className, + { + "mx_IRCLayout": this.props.useIRCLayout, + "mx_GroupLayout": !this.props.useIRCLayout, + } + ); + + return
    + +
    ; + } +} diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 771d2182ea..d9fd59dc11 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes} from 'react'; import classNames from 'classnames'; import * as sdk from '../../../index'; import { debounce } from 'lodash'; -import {IFieldState, IValidationResult} from "../elements/Validation"; +import {IFieldState, IValidationResult} from "./Validation"; // Invoke validation from user input (when typing, etc.) at most once every N ms. const VALIDATION_THROTTLE_MS = 200; @@ -29,60 +29,78 @@ function getId() { return `${BASE_ID}_${count++}`; } -interface IProps extends React.InputHTMLAttributes { +interface IProps { // The field's ID, which binds the input and label together. Immutable. - id?: string, - // The element to create. Defaults to "input". - // To define options for a select, use - element?: "input" | "select" | "textarea", + id?: string; // The field's type (when used as an ). Defaults to "text". - type?: string, + type?: string; // id of a element for suggestions - list?: string, + list?: string; // The field's label string. - label?: string, + label?: string; // The field's placeholder string. Defaults to the label. - placeholder?: string, - // The field's value. - // This is a controlled component, so the value is required. - value: string, + placeholder?: string; // Optional component to include inside the field before the input. - prefixComponent?: React.ReactNode, + prefixComponent?: React.ReactNode; // Optional component to include inside the field after the input. - postfixComponent?: React.ReactNode, + postfixComponent?: React.ReactNode; // The callback called whenever the contents of the field // changes. Returns an object with `valid` boolean field // and a `feedback` react component field to provide feedback // to the user. - onValidate?: (input: IFieldState) => Promise, + onValidate?: (input: IFieldState) => Promise; // If specified, overrides the value returned by onValidate. - flagInvalid?: boolean, + forceValidity?: boolean; // If specified, contents will appear as a tooltip on the element and // validation feedback tooltips will be suppressed. - tooltipContent?: React.ReactNode, + tooltipContent?: React.ReactNode; + // If specified the tooltip will be shown regardless of feedback + forceTooltipVisible?: boolean; // If specified alongside tooltipContent, the class name to apply to the // tooltip itself. - tooltipClassName?: string, + tooltipClassName?: string; // If specified, an additional class name to apply to the field container - className?: string, + className?: string; // All other props pass through to the . } -interface IState { - valid: boolean, - feedback: React.ReactNode, - feedbackVisible: boolean, - focused: boolean, +interface IInputProps extends IProps, InputHTMLAttributes { + // The element to create. Defaults to "input". + element?: "input"; + // The input's value. This is a controlled component, so the value is required. + value: string; } -export default class Field extends React.PureComponent { +interface ISelectProps extends IProps, SelectHTMLAttributes { + // To define options for a select, use + element: "select"; + // The select's value. This is a controlled component, so the value is required. + value: string; +} + +interface ITextareaProps extends IProps, TextareaHTMLAttributes { + element: "textarea"; + // The textarea's value. This is a controlled component, so the value is required. + value: string; +} + +type PropShapes = IInputProps | ISelectProps | ITextareaProps; + +interface IState { + valid: boolean; + feedback: React.ReactNode; + feedbackVisible: boolean; + focused: boolean; +} + +export default class Field extends React.PureComponent { private id: string; private input: HTMLInputElement; - private static defaultProps = { + public static readonly defaultProps = { element: "input", type: "text", - } + }; /* * This was changed from throttle to debounce: this is more traditional for @@ -185,7 +203,7 @@ export default class Field extends React.PureComponent { public render() { const { element, prefixComponent, postfixComponent, className, onValidate, children, - tooltipContent, flagInvalid, tooltipClassName, list, ...inputProps} = this.props; + tooltipContent, forceValidity, tooltipClassName, list, ...inputProps} = this.props; // Set some defaults for the element const ref = input => this.input = input; @@ -210,15 +228,15 @@ export default class Field extends React.PureComponent { postfixContainer = {postfixComponent}; } - const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined; + const hasValidationFlag = forceValidity !== null && forceValidity !== undefined; const fieldClasses = classNames("mx_Field", `mx_Field_${this.props.element}`, className, { // If we have a prefix element, leave the label always at the top left and // don't animate it, as it looks a bit clunky and would add complexity to do // properly. mx_Field_labelAlwaysTopLeft: prefixComponent, - mx_Field_valid: onValidate && this.state.valid === true, + mx_Field_valid: hasValidationFlag ? forceValidity : onValidate && this.state.valid === true, mx_Field_invalid: hasValidationFlag - ? flagInvalid + ? !forceValidity : onValidate && this.state.valid === false, }); @@ -226,11 +244,11 @@ export default class Field extends React.PureComponent { const Tooltip = sdk.getComponent("elements.Tooltip"); let fieldTooltip; if (tooltipContent || this.state.feedback) { - const addlClassName = tooltipClassName ? tooltipClassName : ''; fieldTooltip = ; } diff --git a/src/components/views/elements/IRCTimelineProfileResizer.tsx b/src/components/views/elements/IRCTimelineProfileResizer.tsx index 596d46bf36..1098d0293e 100644 --- a/src/components/views/elements/IRCTimelineProfileResizer.tsx +++ b/src/components/views/elements/IRCTimelineProfileResizer.tsx @@ -15,20 +15,21 @@ limitations under the License. */ import React from 'react'; -import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import SettingsStore from "../../../settings/SettingsStore"; import Draggable, {ILocationState} from './Draggable'; +import { SettingLevel } from "../../../settings/SettingLevel"; interface IProps { // Current room - roomId: string, - minWidth: number, - maxWidth: number, -}; + roomId: string; + minWidth: number; + maxWidth: number; +} interface IState { - width: number, - IRCLayoutRoot: HTMLElement, -}; + width: number; + IRCLayoutRoot: HTMLElement; +} export default class IRCTimelineProfileResizer extends React.Component { constructor(props: IProps) { @@ -37,20 +38,19 @@ export default class IRCTimelineProfileResizer extends React.Component this.updateCSSWidth(this.state.width)) + }, () => this.updateCSSWidth(this.state.width)); } private dragFunc = (location: ILocationState, event: React.MouseEvent): ILocationState => { const offset = event.clientX - location.currentX; const newWidth = this.state.width + offset; - console.log({offset}) // If we're trying to go smaller than min width, don't. if (newWidth < this.props.minWidth) { return location; @@ -69,8 +69,8 @@ export default class IRCTimelineProfileResizer extends React.Component + return ; } -}; \ No newline at end of file +} diff --git a/src/components/views/elements/InlineSpinner.js b/src/components/views/elements/InlineSpinner.js index ad70471d89..89b5e6f19d 100644 --- a/src/components/views/elements/InlineSpinner.js +++ b/src/components/views/elements/InlineSpinner.js @@ -16,6 +16,8 @@ limitations under the License. import React from "react"; import createReactClass from 'create-react-class'; +import {_t} from "../../../languageHandler"; +import SettingsStore from "../../../settings/SettingsStore"; export default createReactClass({ displayName: 'InlineSpinner', @@ -25,9 +27,22 @@ export default createReactClass({ const h = this.props.h || 16; const imgClass = this.props.imgClassName || ""; + let imageSource; + if (SettingsStore.isFeatureEnabled('feature_new_spinner')) { + imageSource = require("../../../../res/img/spinner.svg"); + } else { + imageSource = require("../../../../res/img/spinner.gif"); + } + return (
    - +
    ); }, diff --git a/src/components/views/elements/InteractiveTooltip.js b/src/components/views/elements/InteractiveTooltip.js deleted file mode 100644 index 7f5f24a094..0000000000 --- a/src/components/views/elements/InteractiveTooltip.js +++ /dev/null @@ -1,336 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -const InteractiveTooltipContainerId = "mx_InteractiveTooltip_Container"; - -// If the distance from tooltip to window edge is below this value, the tooltip -// will flip around to the other side of the target. -const MIN_SAFE_DISTANCE_TO_WINDOW_EDGE = 20; - -function getOrCreateContainer() { - let container = document.getElementById(InteractiveTooltipContainerId); - - if (!container) { - container = document.createElement("div"); - container.id = InteractiveTooltipContainerId; - document.body.appendChild(container); - } - - return container; -} - -function isInRect(x, y, rect) { - const { top, right, bottom, left } = rect; - return x >= left && x <= right && y >= top && y <= bottom; -} - -/** - * Returns the positive slope of the diagonal of the rect. - * - * @param {DOMRect} rect - * @return {integer} - */ -function getDiagonalSlope(rect) { - const { top, right, bottom, left } = rect; - return (bottom - top) / (right - left); -} - -function isInUpperLeftHalf(x, y, rect) { - const { bottom, left } = rect; - // Negative slope because Y values grow downwards and for this case, the - // diagonal goes from larger to smaller Y values. - const diagonalSlope = getDiagonalSlope(rect) * -1; - return isInRect(x, y, rect) && (y <= bottom + diagonalSlope * (x - left)); -} - -function isInLowerRightHalf(x, y, rect) { - const { bottom, left } = rect; - // Negative slope because Y values grow downwards and for this case, the - // diagonal goes from larger to smaller Y values. - const diagonalSlope = getDiagonalSlope(rect) * -1; - return isInRect(x, y, rect) && (y >= bottom + diagonalSlope * (x - left)); -} - -function isInUpperRightHalf(x, y, rect) { - const { top, left } = rect; - // Positive slope because Y values grow downwards and for this case, the - // diagonal goes from smaller to larger Y values. - const diagonalSlope = getDiagonalSlope(rect) * 1; - return isInRect(x, y, rect) && (y <= top + diagonalSlope * (x - left)); -} - -function isInLowerLeftHalf(x, y, rect) { - const { top, left } = rect; - // Positive slope because Y values grow downwards and for this case, the - // diagonal goes from smaller to larger Y values. - const diagonalSlope = getDiagonalSlope(rect) * 1; - return isInRect(x, y, rect) && (y >= top + diagonalSlope * (x - left)); -} - -/* - * This style of tooltip takes a "target" element as its child and centers the - * tooltip along one edge of the target. - */ -export default class InteractiveTooltip extends React.Component { - static propTypes = { - // Content to show in the tooltip - content: PropTypes.node.isRequired, - // Function to call when visibility of the tooltip changes - onVisibilityChange: PropTypes.func, - // flag to forcefully hide this tooltip - forceHidden: PropTypes.bool, - }; - - constructor() { - super(); - - this.state = { - contentRect: null, - visible: false, - }; - } - - componentDidUpdate() { - // Whenever this passthrough component updates, also render the tooltip - // in a separate DOM tree. This allows the tooltip content to participate - // the normal React rendering cycle: when this component re-renders, the - // tooltip content re-renders. - // Once we upgrade to React 16, this could be done a bit more naturally - // using the portals feature instead. - this.renderTooltip(); - } - - componentWillUnmount() { - document.removeEventListener("mousemove", this.onMouseMove); - } - - collectContentRect = (element) => { - // We don't need to clean up when unmounting, so ignore - if (!element) return; - - this.setState({ - contentRect: element.getBoundingClientRect(), - }); - } - - collectTarget = (element) => { - this.target = element; - } - - canTooltipFitAboveTarget() { - const { contentRect } = this.state; - const targetRect = this.target.getBoundingClientRect(); - const targetTop = targetRect.top + window.pageYOffset; - return ( - !contentRect || - (targetTop - contentRect.height > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE) - ); - } - - onMouseMove = (ev) => { - const { clientX: x, clientY: y } = ev; - const { contentRect } = this.state; - const targetRect = this.target.getBoundingClientRect(); - - // When moving the mouse from the target to the tooltip, we create a - // safe area that includes the tooltip, the target, and the trapezoid - // ABCD between them: - // ┌───────────┐ - // │ │ - // │ │ - // A └───E───F───┘ B - // V - // ┌─┐ - // │ │ - // C└─┘D - // - // As long as the mouse remains inside the safe area, the tooltip will - // stay open. - const buffer = 50; - if (isInRect(x, y, targetRect)) { - return; - } - if (this.canTooltipFitAboveTarget()) { - const contentRectWithBuffer = { - top: contentRect.top - buffer, - right: contentRect.right + buffer, - bottom: contentRect.bottom, - left: contentRect.left - buffer, - }; - const trapezoidLeft = { - top: contentRect.bottom, - right: targetRect.left, - bottom: targetRect.bottom, - left: contentRect.left - buffer, - }; - const trapezoidCenter = { - top: contentRect.bottom, - right: targetRect.right, - bottom: targetRect.bottom, - left: targetRect.left, - }; - const trapezoidRight = { - top: contentRect.bottom, - right: contentRect.right + buffer, - bottom: targetRect.bottom, - left: targetRect.right, - }; - - if ( - isInRect(x, y, contentRectWithBuffer) || - isInUpperRightHalf(x, y, trapezoidLeft) || - isInRect(x, y, trapezoidCenter) || - isInUpperLeftHalf(x, y, trapezoidRight) - ) { - return; - } - } else { - const contentRectWithBuffer = { - top: contentRect.top, - right: contentRect.right + buffer, - bottom: contentRect.bottom + buffer, - left: contentRect.left - buffer, - }; - const trapezoidLeft = { - top: targetRect.top, - right: targetRect.left, - bottom: contentRect.top, - left: contentRect.left - buffer, - }; - const trapezoidCenter = { - top: targetRect.top, - right: targetRect.right, - bottom: contentRect.top, - left: targetRect.left, - }; - const trapezoidRight = { - top: targetRect.top, - right: contentRect.right + buffer, - bottom: contentRect.top, - left: targetRect.right, - }; - - if ( - isInRect(x, y, contentRectWithBuffer) || - isInLowerRightHalf(x, y, trapezoidLeft) || - isInRect(x, y, trapezoidCenter) || - isInLowerLeftHalf(x, y, trapezoidRight) - ) { - return; - } - } - - this.hideTooltip(); - } - - onTargetMouseOver = (ev) => { - this.showTooltip(); - } - - showTooltip() { - // Don't enter visible state if we haven't collected the target yet - if (!this.target) { - return; - } - this.setState({ - visible: true, - }); - if (this.props.onVisibilityChange) { - this.props.onVisibilityChange(true); - } - document.addEventListener("mousemove", this.onMouseMove); - } - - hideTooltip() { - this.setState({ - visible: false, - }); - if (this.props.onVisibilityChange) { - this.props.onVisibilityChange(false); - } - document.removeEventListener("mousemove", this.onMouseMove); - } - - renderTooltip() { - const { contentRect, visible } = this.state; - if (this.props.forceHidden === true || !visible) { - ReactDOM.render(null, getOrCreateContainer()); - return null; - } - - const targetRect = this.target.getBoundingClientRect(); - - // The window X and Y offsets are to adjust position when zoomed in to page - const targetLeft = targetRect.left + window.pageXOffset; - const targetBottom = targetRect.bottom + window.pageYOffset; - const targetTop = targetRect.top + window.pageYOffset; - - // Place the tooltip above the target by default. If we find that the - // tooltip content would extend past the safe area towards the window - // edge, flip around to below the target. - const position = {}; - let chevronFace = null; - if (this.canTooltipFitAboveTarget()) { - position.bottom = window.innerHeight - targetTop; - chevronFace = "bottom"; - } else { - position.top = targetBottom; - chevronFace = "top"; - } - - // Center the tooltip horizontally with the target's center. - position.left = targetLeft + targetRect.width / 2; - - const chevron =
    ; - - const menuClasses = classNames({ - 'mx_InteractiveTooltip': true, - 'mx_InteractiveTooltip_withChevron_top': chevronFace === 'top', - 'mx_InteractiveTooltip_withChevron_bottom': chevronFace === 'bottom', - }); - - const menuStyle = {}; - if (contentRect) { - menuStyle.left = `-${contentRect.width / 2}px`; - } - - const tooltip =
    -
    - {chevron} - {this.props.content} -
    -
    ; - - ReactDOM.render(tooltip, getOrCreateContainer()); - } - - render() { - // We use `cloneElement` here to append some props to the child content - // without using a wrapper element which could disrupt layout. - return React.cloneElement(this.props.children, { - ref: this.collectTarget, - onMouseOver: this.onTargetMouseOver, - }); - } -} diff --git a/src/components/views/elements/ManageIntegsButton.js b/src/components/views/elements/ManageIntegsButton.js index b631ddee73..ac8a98a94a 100644 --- a/src/components/views/elements/ManageIntegsButton.js +++ b/src/components/views/elements/ManageIntegsButton.js @@ -17,10 +17,10 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import SettingsStore from "../../../settings/SettingsStore"; +import AccessibleTooltipButton from "./AccessibleTooltipButton"; export default class ManageIntegsButton extends React.Component { constructor(props) { @@ -45,9 +45,8 @@ export default class ManageIntegsButton extends React.Component { render() { let integrationsButton =
    ; if (IntegrationManagers.sharedInstance().hasManager()) { - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); integrationsButton = ( - Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,6 +23,7 @@ import { _t } from '../../../languageHandler'; import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import * as sdk from "../../../index"; import {MatrixEvent} from "matrix-js-sdk"; +import {isValid3pidInvite} from "../../../RoomInvite"; export default createReactClass({ displayName: 'MemberEventListSummary', @@ -284,6 +285,9 @@ export default createReactClass({ _getTransition: function(e) { if (e.mxEvent.getType() === 'm.room.third_party_invite') { // Handle 3pid invites the same as invites so they get bundled together + if (!isValid3pidInvite(e.mxEvent)) { + return 'invite_withdrawal'; + } return 'invited'; } diff --git a/src/components/views/elements/MessageSpinner.js b/src/components/views/elements/MessageSpinner.js deleted file mode 100644 index 1775fdd4d7..0000000000 --- a/src/components/views/elements/MessageSpinner.js +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import createReactClass from 'create-react-class'; - -export default createReactClass({ - displayName: 'MessageSpinner', - - render: function() { - const w = this.props.w || 32; - const h = this.props.h || 32; - const imgClass = this.props.imgClassName || ""; - const msg = this.props.msg || "Loading..."; - return ( -
    -
    { msg }
      - -
    - ); - }, -}); diff --git a/src/components/views/elements/ProgressBar.js b/src/components/views/elements/ProgressBar.js deleted file mode 100644 index 045731ba38..0000000000 --- a/src/components/views/elements/ProgressBar.js +++ /dev/null @@ -1,39 +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 PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; - -export default createReactClass({ - displayName: 'ProgressBar', - propTypes: { - value: PropTypes.number, - max: PropTypes.number, - }, - - render: function() { - // Would use an HTML5 progress tag but if that doesn't animate if you - // use the HTML attributes rather than styles - const progressStyle = { - width: ((this.props.value / this.props.max) * 100)+"%", - }; - return ( -
    - ); - }, -}); diff --git a/src/components/views/elements/ProgressBar.tsx b/src/components/views/elements/ProgressBar.tsx new file mode 100644 index 0000000000..90832e5006 --- /dev/null +++ b/src/components/views/elements/ProgressBar.tsx @@ -0,0 +1,28 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; + +interface IProps { + value: number; + max: number; +} + +const ProgressBar: React.FC = ({value, max}) => { + return ; +}; + +export default ProgressBar; diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index f237521839..6c038a0ddb 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -27,6 +27,7 @@ import escapeHtml from "escape-html"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { getUserNameColorClass } from "../../../utils/FormattingUtils"; import {Action} from "../../../dispatcher/actions"; +import sanitizeHtml from "sanitize-html"; // This component does no cycle detection, simply because the only way to make such a cycle would be to // craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would @@ -92,7 +93,21 @@ export default class ReplyThread extends React.Component { // Part of Replies fallback support static stripHTMLReply(html) { - return html.replace(/^[\s\S]+?<\/mx-reply>/, ''); + // Sanitize the original HTML for inclusion in . We allow + // any HTML, since the original sender could use special tags that we + // don't recognize, but want to pass along to any recipients who do + // recognize them -- recipients should be sanitizing before displaying + // anyways. However, we sanitize to 1) remove any mx-reply, so that we + // don't generate a nested mx-reply, and 2) make sure that the HTML is + // properly formatted (e.g. tags are closed where necessary) + return sanitizeHtml( + html, + { + allowedTags: false, // false means allow everything + allowedAttributes: false, + exclusiveFilter: (frame) => frame.tag === "mx-reply", + }, + ); } // Part of Replies fallback support @@ -102,15 +117,19 @@ export default class ReplyThread extends React.Component { let {body, formatted_body: html} = ev.getContent(); if (this.getParentEventId(ev)) { if (body) body = this.stripPlainReply(body); - if (html) html = this.stripHTMLReply(html); } if (!body) body = ""; // Always ensure we have a body, for reasons. - // Escape the body to use as HTML below. - // We also run a nl2br over the result to fix the fallback representation. We do this - // after converting the text to safe HTML to avoid user-provided BR's from being converted. - if (!html) html = escapeHtml(body).replace(/\n/g, '
    '); + if (html) { + // sanitize the HTML before we put it in an + html = this.stripHTMLReply(html); + } else { + // Escape the body to use as HTML below. + // We also run a nl2br over the result to fix the fallback representation. We do this + // after converting the text to safe HTML to avoid user-provided BR's from being converted. + html = escapeHtml(body).replace(/\n/g, '
    '); + } // dev note: do not rely on `body` being safe for HTML usage below. diff --git a/src/components/views/elements/RoomDirectoryButton.js b/src/components/views/elements/RoomDirectoryButton.js index d0bff4beeb..e9de6f8d15 100644 --- a/src/components/views/elements/RoomDirectoryButton.js +++ b/src/components/views/elements/RoomDirectoryButton.js @@ -18,11 +18,12 @@ import React from 'react'; import * as sdk from '../../../index'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; +import {Action} from "../../../dispatcher/actions"; const RoomDirectoryButton = function(props) { const ActionButton = sdk.getComponent('elements.ActionButton'); return ( - - {label} - -
    - ); - }, -}); diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx new file mode 100644 index 0000000000..03e91fac62 --- /dev/null +++ b/src/components/views/elements/SettingsFlag.tsx @@ -0,0 +1,104 @@ +/* +Copyright 2017 Travis Ralston +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import SettingsStore from "../../../settings/SettingsStore"; +import { _t } from '../../../languageHandler'; +import ToggleSwitch from "./ToggleSwitch"; +import StyledCheckbox from "./StyledCheckbox"; +import { SettingLevel } from "../../../settings/SettingLevel"; + +interface IProps { + // The setting must be a boolean + name: string; + level: SettingLevel; + roomId?: string; // for per-room settings + label?: string; // untranslated + isExplicit?: boolean; + // XXX: once design replaces all toggles make this the default + useCheckbox?: boolean; + disabled?: boolean; + onChange?(checked: boolean): void; +} + +interface IState { + value: boolean; +} + +export default class SettingsFlag extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + value: SettingsStore.getValueAt( + this.props.level, + this.props.name, + this.props.roomId, + this.props.isExplicit, + ), + }; + } + + private onChange = async (checked: boolean) => { + await this.save(checked); + this.setState({ value: checked }); + if (this.props.onChange) this.props.onChange(checked); + }; + + private checkBoxOnChange = (e: React.ChangeEvent) => { + this.onChange(e.target.checked); + }; + + private save = async (val?: boolean) => { + await SettingsStore.setValue( + this.props.name, + this.props.roomId, + this.props.level, + val !== undefined ? val : this.state.value, + ); + }; + + public render() { + const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level); + + let label = this.props.label; + if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level); + else label = _t(label); + + if (this.props.useCheckbox) { + return + {label} + ; + } else { + return ( +
    + {label} + +
    + ); + } + } +} diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index f76a4684d3..a88c581d07 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -65,9 +65,9 @@ export default class Slider extends React.Component { const intervalWidth = 1 / (values.length - 1); - const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue) + const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue); - return 100 * (closest - 1 + linearInterpolation) * intervalWidth + return 100 * (closest - 1 + linearInterpolation) * intervalWidth; } @@ -87,7 +87,7 @@ export default class Slider extends React.Component { selection =

    -
    +
    ; } return
    @@ -115,13 +115,13 @@ export default class Slider extends React.Component { interface IDotProps { // Callback for behavior onclick - onClick: () => void, + onClick: () => void; // Whether the dot should appear active - active: boolean, + active: boolean; // The label on the dot - label: string, + label: string; // Whether the slider is disabled disabled: boolean; @@ -129,7 +129,7 @@ interface IDotProps { class Dot extends React.PureComponent { render(): React.ReactNode { - let className = "mx_Slider_dot" + let className = "mx_Slider_dot"; if (!this.props.disabled && this.props.active) { className += " mx_Slider_dotActive"; } diff --git a/src/components/views/elements/Spinner.js b/src/components/views/elements/Spinner.js index b1fe97d5d2..033d4d13f4 100644 --- a/src/components/views/elements/Spinner.js +++ b/src/components/views/elements/Spinner.js @@ -16,19 +16,36 @@ limitations under the License. */ import React from "react"; -import createReactClass from 'create-react-class'; +import PropTypes from "prop-types"; +import {_t} from "../../../languageHandler"; +import SettingsStore from "../../../settings/SettingsStore"; -export default createReactClass({ - displayName: 'Spinner', +const Spinner = ({w = 32, h = 32, imgClassName, message}) => { + let imageSource; + if (SettingsStore.isFeatureEnabled('feature_new_spinner')) { + imageSource = require("../../../../res/img/spinner.svg"); + } else { + imageSource = require("../../../../res/img/spinner.gif"); + } - render: function() { - const w = this.props.w || 32; - const h = this.props.h || 32; - const imgClass = this.props.imgClassName || ""; - return ( -
    - -
    - ); - }, -}); + return ( +
    + { message &&
    { message}
     
    } + +
    + ); +}; +Spinner.propTypes = { + w: PropTypes.number, + h: PropTypes.number, + imgClassName: PropTypes.string, + message: PropTypes.node, +}; + +export default Spinner; diff --git a/src/components/views/elements/StyledCheckbox.tsx b/src/components/views/elements/StyledCheckbox.tsx index 341f59d5da..be983828ff 100644 --- a/src/components/views/elements/StyledCheckbox.tsx +++ b/src/components/views/elements/StyledCheckbox.tsx @@ -30,7 +30,7 @@ export default class StyledCheckbox extends React.PureComponent public static readonly defaultProps = { className: "", - } + }; constructor(props: IProps) { super(props); @@ -51,6 +51,6 @@ export default class StyledCheckbox extends React.PureComponent { this.props.children }
    - + ; } } \ No newline at end of file diff --git a/src/components/views/elements/StyledRadioButton.tsx b/src/components/views/elements/StyledRadioButton.tsx new file mode 100644 index 0000000000..2efd084861 --- /dev/null +++ b/src/components/views/elements/StyledRadioButton.tsx @@ -0,0 +1,51 @@ +/* +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 classnames from 'classnames'; + +interface IProps extends React.InputHTMLAttributes { + outlined?: boolean; +} + +interface IState { +} + +export default class StyledRadioButton extends React.PureComponent { + public static readonly defaultProps = { + className: '', + }; + + public render() { + const { children, className, disabled, outlined, ...otherProps } = this.props; + const _className = classnames( + 'mx_RadioButton', + className, + { + "mx_RadioButton_disabled": disabled, + "mx_RadioButton_enabled": !disabled, + "mx_RadioButton_checked": this.props.checked, + "mx_RadioButton_outlined": outlined, + }); + return
    { _t(customVariables[row[0]].expl) }{_t( + customVariables[row[0]].expl, + customVariables[row[0]].getTextVariables ? + customVariables[row[0]].getTextVariables() : + null, + )}{ row[1] }