Merge branch 'develop' into uhoreg/keytar_logging

This commit is contained in:
Hubert Chathi 2020-07-28 17:45:39 -04:00
commit 0cf10bb69a
251 changed files with 9427 additions and 10889 deletions

View file

@ -11,111 +11,36 @@ const path = require('path');
const matrixJsSdkPath = path.join(path.dirname(require.resolve('matrix-js-sdk')), '..'); const matrixJsSdkPath = path.join(path.dirname(require.resolve('matrix-js-sdk')), '..');
module.exports = { module.exports = {
extends: ["matrix-org", "matrix-org/react-legacy"],
parser: "babel-eslint", parser: "babel-eslint",
extends: [matrixJsSdkPath + "/.eslintrc.js"],
plugins: [ env: {
"react", browser: true,
"react-hooks", node: true,
"flowtype", },
"babel"
],
globals: { globals: {
LANGUAGES_FILE: "readonly", LANGUAGES_FILE: "readonly",
}, },
env: {
es6: true,
},
parserOptions: {
ecmaFeatures: {
jsx: true,
legacyDecorators: true,
}
},
rules: { rules: {
// eslint's built in no-invalid-this rule breaks with class properties // Things we do that break the ideal style
"no-invalid-this": "off", "no-constant-condition": "off",
// so we replace it with a version that is class property aware "prefer-promise-reject-errors": "off",
"babel/no-invalid-this": "error", "no-async-promise-executor": "off",
"quotes": "off",
// We appear to follow this most of the time, so let's enforce it instead "indent": "off",
// 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
// <Element prop={ consideredError} prop={notConsideredError} />
//
// 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",
},
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",
}
}],
}; };

View file

@ -1,3 +1,125 @@
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 <mx-reply>, 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) 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) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.10.0...v2.10.1)

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "2.10.1", "version": "3.0.0",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -45,126 +45,127 @@
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:all", "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: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\"", "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: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:types": "tsc --noEmit --jsx react",
"lint:style": "stylelint 'res/css/**/*.scss'", "lint:style": "stylelint 'res/css/**/*.scss'",
"test": "jest", "test": "jest",
"test:e2e": "./test/end-to-end-tests/run.sh --riot-url http://localhost:8080" "test:e2e": "./test/end-to-end-tests/run.sh --riot-url http://localhost:8080"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.8.3", "@babel/runtime": "^7.10.5",
"await-lock": "^2.0.1", "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-encrypt-attachment": "^0.3.0",
"browser-request": "^0.3.3", "browser-request": "^0.3.3",
"classnames": "^2.1.2", "classnames": "^2.2.6",
"commonmark": "^0.28.1", "commonmark": "^0.29.1",
"counterpart": "^0.18.0", "counterpart": "^0.18.6",
"create-react-class": "^15.6.0", "create-react-class": "^15.6.3",
"diff-dom": "^4.1.3", "diff-dom": "^4.1.6",
"diff-match-patch": "^1.0.4", "diff-match-patch": "^1.0.5",
"emojibase-data": "^5.0.1", "emojibase-data": "^5.0.1",
"emojibase-regex": "^4.0.1", "emojibase-regex": "^4.0.1",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"file-saver": "^1.3.3", "file-saver": "^1.3.8",
"filesize": "3.5.6", "filesize": "3.6.1",
"flux": "2.1.1", "flux": "2.1.1",
"focus-visible": "^5.0.2", "focus-visible": "^5.1.0",
"fuse.js": "^2.2.0", "fuse.js": "^2.7.4",
"gfm.css": "^1.1.1", "gfm.css": "^1.1.2",
"glob-to-regexp": "^0.4.1", "glob-to-regexp": "^0.4.1",
"highlight.js": "^9.15.8", "highlight.js": "^10.1.2",
"html-entities": "^1.2.1", "html-entities": "^1.3.1",
"is-ip": "^2.0.0", "is-ip": "^2.0.0",
"linkifyjs": "^2.1.6", "linkifyjs": "^2.1.9",
"lodash": "^4.17.14", "lodash": "^4.17.19",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"minimist": "^1.2.0", "minimist": "^1.2.5",
"pako": "^1.0.5", "pako": "^1.0.11",
"parse5": "^5.1.1", "parse5": "^5.1.1",
"png-chunks-extract": "^1.0.0", "png-chunks-extract": "^1.0.0",
"project-name-generator": "^2.1.7", "project-name-generator": "^2.1.7",
"prop-types": "^15.5.8", "prop-types": "^15.7.2",
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
"qs": "^6.6.0", "qs": "^6.9.4",
"re-resizable": "^6.5.2", "re-resizable": "^6.5.4",
"react": "^16.9.0", "react": "^16.13.1",
"react-beautiful-dnd": "^4.0.1", "react-beautiful-dnd": "^4.0.1",
"react-dom": "^16.9.0", "react-dom": "^16.13.1",
"react-focus-lock": "^2.2.1", "react-focus-lock": "^2.4.1",
"react-transition-group": "^4.4.1", "react-transition-group": "^4.4.1",
"resize-observer-polyfill": "^1.5.0", "resize-observer-polyfill": "^1.5.1",
"sanitize-html": "^1.18.4", "sanitize-html": "^1.27.1",
"text-encoding-utf-8": "^1.0.1", "text-encoding-utf-8": "^1.0.2",
"url": "^0.11.0", "url": "^0.11.0",
"velocity-animate": "^1.5.2", "velocity-animate": "^1.5.2",
"what-input": "^5.2.6", "what-input": "^5.2.10",
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.7.5", "@babel/cli": "^7.10.5",
"@babel/core": "^7.7.5", "@babel/core": "^7.10.5",
"@babel/plugin-proposal-class-properties": "^7.7.4", "@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-decorators": "^7.7.4", "@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-proposal-export-default-from": "^7.7.4", "@babel/plugin-proposal-export-default-from": "^7.10.4",
"@babel/plugin-proposal-numeric-separator": "^7.7.4", "@babel/plugin-proposal-numeric-separator": "^7.10.4",
"@babel/plugin-proposal-object-rest-spread": "^7.7.4", "@babel/plugin-proposal-object-rest-spread": "^7.10.4",
"@babel/plugin-transform-flow-comments": "^7.7.4", "@babel/plugin-transform-flow-comments": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.8.3", "@babel/plugin-transform-runtime": "^7.10.5",
"@babel/preset-env": "^7.7.6", "@babel/preset-env": "^7.10.4",
"@babel/preset-flow": "^7.7.4", "@babel/preset-flow": "^7.10.4",
"@babel/preset-react": "^7.7.4", "@babel/preset-react": "^7.10.4",
"@babel/preset-typescript": "^7.7.4", "@babel/preset-typescript": "^7.10.4",
"@babel/register": "^7.7.4", "@babel/register": "^7.10.5",
"@peculiar/webcrypto": "^1.0.22", "@peculiar/webcrypto": "^1.1.2",
"@types/classnames": "^2.2.10", "@types/classnames": "^2.2.10",
"@types/counterpart": "^0.18.1", "@types/counterpart": "^0.18.1",
"@types/flux": "^3.1.9", "@types/flux": "^3.1.9",
"@types/linkifyjs": "^2.1.3", "@types/linkifyjs": "^2.1.3",
"@types/lodash": "^4.14.152", "@types/lodash": "^4.14.158",
"@types/modernizr": "^3.5.3", "@types/modernizr": "^3.5.3",
"@types/node": "^12.12.41", "@types/node": "^12.12.51",
"@types/qrcode": "^1.3.4", "@types/qrcode": "^1.3.4",
"@types/react": "^16.9", "@types/react": "^16.9",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^16.9.8",
"@types/react-transition-group": "^4.4.0", "@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "^1.23.3", "@types/sanitize-html": "^1.23.3",
"@types/zxcvbn": "^4.4.0", "@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", "babel-jest": "^24.9.0",
"chokidar": "^3.3.1", "chokidar": "^3.4.1",
"concurrently": "^4.0.1", "concurrently": "^4.1.2",
"enzyme": "^3.10.0", "enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.1", "enzyme-adapter-react-16": "^1.15.2",
"eslint": "^5.12.0", "eslint": "7.5.0",
"eslint-config-google": "^0.7.1", "eslint-config-google": "^0.14.0",
"eslint-plugin-babel": "^5.2.1", "eslint-config-matrix-org": "^0.1.2",
"eslint-plugin-flowtype": "^2.30.0", "eslint-plugin-babel": "^5.3.1",
"eslint-plugin-jest": "^23.0.4", "eslint-plugin-flowtype": "^2.50.3",
"eslint-plugin-react": "^7.7.0", "eslint-plugin-jest": "^23.18.0",
"eslint-plugin-react-hooks": "^2.0.1", "eslint-plugin-react": "^7.20.3",
"estree-walker": "^0.5.0", "eslint-plugin-react-hooks": "^2.5.1",
"estree-walker": "^0.9.0",
"file-loader": "^3.0.1", "file-loader": "^3.0.1",
"flow-parser": "^0.57.3", "flow-parser": "0.57.3",
"glob": "^5.0.14", "glob": "^5.0.15",
"jest": "^24.9.0", "jest": "^24.9.0",
"jest-canvas-mock": "^2.2.0", "jest-canvas-mock": "^2.2.0",
"lolex": "^5.1.2", "lolex": "^5.1.2",
"matrix-mock-request": "^1.2.3", "matrix-mock-request": "^1.2.3",
"matrix-react-test-utils": "^0.2.2", "matrix-react-test-utils": "^0.2.2",
"react-test-renderer": "^16.9.0", "react-test-renderer": "^16.13.1",
"rimraf": "^2.4.3", "rimraf": "^2.7.1",
"source-map-loader": "^0.2.3", "source-map-loader": "^0.2.4",
"stylelint": "^9.10.1", "stylelint": "^9.10.1",
"stylelint-config-standard": "^18.2.0", "stylelint-config-standard": "^18.3.0",
"stylelint-scss": "^3.9.0", "stylelint-scss": "^3.18.0",
"tslint": "^5.20.1", "typescript": "^3.9.7",
"typescript": "^3.7.3", "walk": "^2.3.14",
"walk": "^2.3.9", "webpack": "^4.43.0",
"webpack": "^4.20.2", "webpack-cli": "^3.3.12"
"webpack-cli": "^3.1.1"
}, },
"jest": { "jest": {
"testMatch": [ "testMatch": [

View file

@ -226,7 +226,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
} }
#mx_theme_tertiaryAccentColor { #mx_theme_tertiaryAccentColor {
color: $roomsublist-label-bg-color; color: $tertiary-accent-color;
} }
/* Expected z-indexes for dialogs: /* Expected z-indexes for dialogs:

View file

@ -12,7 +12,6 @@
@import "./structures/_HeaderButtons.scss"; @import "./structures/_HeaderButtons.scss";
@import "./structures/_HomePage.scss"; @import "./structures/_HomePage.scss";
@import "./structures/_LeftPanel.scss"; @import "./structures/_LeftPanel.scss";
@import "./structures/_LeftPanel2.scss";
@import "./structures/_MainSplit.scss"; @import "./structures/_MainSplit.scss";
@import "./structures/_MatrixChat.scss"; @import "./structures/_MatrixChat.scss";
@import "./structures/_MyGroups.scss"; @import "./structures/_MyGroups.scss";
@ -21,14 +20,12 @@
@import "./structures/_RoomDirectory.scss"; @import "./structures/_RoomDirectory.scss";
@import "./structures/_RoomSearch.scss"; @import "./structures/_RoomSearch.scss";
@import "./structures/_RoomStatusBar.scss"; @import "./structures/_RoomStatusBar.scss";
@import "./structures/_RoomSubList.scss";
@import "./structures/_RoomView.scss"; @import "./structures/_RoomView.scss";
@import "./structures/_ScrollPanel.scss"; @import "./structures/_ScrollPanel.scss";
@import "./structures/_SearchBox.scss"; @import "./structures/_SearchBox.scss";
@import "./structures/_TabbedView.scss"; @import "./structures/_TabbedView.scss";
@import "./structures/_TagPanel.scss"; @import "./structures/_TagPanel.scss";
@import "./structures/_ToastContainer.scss"; @import "./structures/_ToastContainer.scss";
@import "./structures/_TopLeftMenuButton.scss";
@import "./structures/_UploadBar.scss"; @import "./structures/_UploadBar.scss";
@import "./structures/_UserMenu.scss"; @import "./structures/_UserMenu.scss";
@import "./structures/_ViewSource.scss"; @import "./structures/_ViewSource.scss";
@ -108,7 +105,6 @@
@import "./views/elements/_IconButton.scss"; @import "./views/elements/_IconButton.scss";
@import "./views/elements/_ImageView.scss"; @import "./views/elements/_ImageView.scss";
@import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_InlineSpinner.scss";
@import "./views/elements/_InteractiveTooltip.scss";
@import "./views/elements/_ManageIntegsButton.scss"; @import "./views/elements/_ManageIntegsButton.scss";
@import "./views/elements/_PowerSelector.scss"; @import "./views/elements/_PowerSelector.scss";
@import "./views/elements/_ProgressBar.scss"; @import "./views/elements/_ProgressBar.scss";
@ -168,7 +164,6 @@
@import "./views/rooms/_EventTile.scss"; @import "./views/rooms/_EventTile.scss";
@import "./views/rooms/_GroupLayout.scss"; @import "./views/rooms/_GroupLayout.scss";
@import "./views/rooms/_IRCLayout.scss"; @import "./views/rooms/_IRCLayout.scss";
@import "./views/rooms/_InviteOnlyIcon.scss";
@import "./views/rooms/_JumpToBottomButton.scss"; @import "./views/rooms/_JumpToBottomButton.scss";
@import "./views/rooms/_LinkPreviewWidget.scss"; @import "./views/rooms/_LinkPreviewWidget.scss";
@import "./views/rooms/_MemberInfo.scss"; @import "./views/rooms/_MemberInfo.scss";
@ -181,23 +176,18 @@
@import "./views/rooms/_PresenceLabel.scss"; @import "./views/rooms/_PresenceLabel.scss";
@import "./views/rooms/_ReplyPreview.scss"; @import "./views/rooms/_ReplyPreview.scss";
@import "./views/rooms/_RoomBreadcrumbs.scss"; @import "./views/rooms/_RoomBreadcrumbs.scss";
@import "./views/rooms/_RoomBreadcrumbs2.scss";
@import "./views/rooms/_RoomDropTarget.scss";
@import "./views/rooms/_RoomHeader.scss"; @import "./views/rooms/_RoomHeader.scss";
@import "./views/rooms/_RoomList.scss"; @import "./views/rooms/_RoomList.scss";
@import "./views/rooms/_RoomList2.scss";
@import "./views/rooms/_RoomPreviewBar.scss"; @import "./views/rooms/_RoomPreviewBar.scss";
@import "./views/rooms/_RoomRecoveryReminder.scss"; @import "./views/rooms/_RoomRecoveryReminder.scss";
@import "./views/rooms/_RoomSublist2.scss"; @import "./views/rooms/_RoomSublist.scss";
@import "./views/rooms/_RoomTile.scss"; @import "./views/rooms/_RoomTile.scss";
@import "./views/rooms/_RoomTile2.scss";
@import "./views/rooms/_RoomTileIcon.scss"; @import "./views/rooms/_RoomTileIcon.scss";
@import "./views/rooms/_RoomUpgradeWarningBar.scss"; @import "./views/rooms/_RoomUpgradeWarningBar.scss";
@import "./views/rooms/_SearchBar.scss"; @import "./views/rooms/_SearchBar.scss";
@import "./views/rooms/_SendMessageComposer.scss"; @import "./views/rooms/_SendMessageComposer.scss";
@import "./views/rooms/_Stickers.scss"; @import "./views/rooms/_Stickers.scss";
@import "./views/rooms/_TopUnreadMessagesBar.scss"; @import "./views/rooms/_TopUnreadMessagesBar.scss";
@import "./views/rooms/_UserOnlineDot.scss";
@import "./views/rooms/_WhoIsTypingTile.scss"; @import "./views/rooms/_WhoIsTypingTile.scss";
@import "./views/settings/_AvatarSetting.scss"; @import "./views/settings/_AvatarSetting.scss";
@import "./views/settings/_CrossSigningPanel.scss"; @import "./views/settings/_CrossSigningPanel.scss";
@ -228,6 +218,4 @@
@import "./views/verification/_VerificationShowSas.scss"; @import "./views/verification/_VerificationShowSas.scss";
@import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallContainer.scss";
@import "./views/voip/_CallView.scss"; @import "./views/voip/_CallView.scss";
@import "./views/voip/_CallView2.scss";
@import "./views/voip/_IncomingCallbox.scss";
@import "./views/voip/_VideoView.scss"; @import "./views/voip/_VideoView.scss";

View file

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// TODO: Update design for custom tags to match new designs
.mx_LeftPanel_tagPanelContainer { .mx_LeftPanel_tagPanelContainer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -50,7 +52,7 @@ limitations under the License.
background-color: $accent-color-alt; background-color: $accent-color-alt;
width: 5px; width: 5px;
position: absolute; position: absolute;
left: -15px; left: -9px;
border-radius: 0 3px 3px 0; border-radius: 0 3px 3px 0;
top: 2px; // 10 [padding-top] - (56 - 40)/2 top: 12px; // just feels right (see comment above about designs needing to be updated)
} }

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,164 +14,171 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_LeftPanel_container { $tagPanelWidth: 56px; // only applies in this file, used for calculations
display: flex;
/* LeftPanel 260px */
min-width: 260px;
max-width: 50%;
flex: 0 0 auto;
}
.mx_LeftPanel_container.collapsed {
min-width: unset;
/* Collapsed LeftPanel 50px */
flex: 0 0 50px;
}
.mx_LeftPanel_container.collapsed.mx_LeftPanel_container_hasTagPanel {
/* TagPanel 70px + Collapsed LeftPanel 50px */
flex: 0 0 120px;
}
.mx_LeftPanel_tagPanelContainer {
flex: 0 0 70px;
height: 100%;
}
.mx_LeftPanel_hideButton {
position: absolute;
top: 10px;
right: 0px;
padding: 8px;
cursor: pointer;
}
.mx_LeftPanel { .mx_LeftPanel {
flex: 1; background-color: $roomlist-bg-color;
overflow-x: hidden; min-width: 260px;
display: flex; max-width: 50%;
flex-direction: column;
min-height: 0;
}
.mx_LeftPanel .mx_AppTile_mini { // Create a row-based flexbox for the TagPanel and the room list
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 {
display: flex; display: flex;
.mx_SearchBox { .mx_LeftPanel_tagPanelContainer {
flex: 1 1 0; flex-grow: 0;
min-width: 0; flex-shrink: 0;
margin: 4px 9px 1px 9px; flex-basis: $tagPanelWidth;
} height: 100%;
}
.mx_LeftPanel_explore { // Create another flexbox so the TagPanel fills the container
flex: 0 0 50%; display: flex;
overflow: hidden;
transition: flex-basis 0.2s;
box-sizing: border-box;
&.mx_LeftPanel_explore_hidden { // TagPanel handles its own CSS
flex-basis: 0;
} }
.mx_AccessibleButton { &:not(.mx_LeftPanel_hasTagPanel) {
font-size: $font-14px; .mx_LeftPanel_roomListContainer {
margin: 4px 0 1px 9px; width: 100%;
padding: 9px; }
padding-left: 42px; }
font-weight: 600;
color: $notice-secondary-color;
position: relative;
border-radius: 4px;
&:hover { // Note: The 'room list' in this context is actually everything that isn't the tag
background-color: $primary-bg-color; // 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 { .mx_LeftPanel_breadcrumbsContainer {
cursor: pointer; overflow-y: hidden;
mask: url('$(res)/img/explore.svg'); overflow-x: scroll;
mask-repeat: no-repeat; margin: 12px 12px 0 12px;
mask-position: center center; flex: 0 0 auto;
content: ""; // Create yet another flexbox, this time within the row, to ensure items stay
left: 14px; // aligned correctly. This is also a row-based flexbox.
top: 10px; display: flex;
width: 16px; align-items: center;
height: 16px;
background-color: $notice-secondary-color; &.mx_IndicatorScrollbar_leftOverflow {
position: absolute; 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;
}
}
} }
} }
} }

View file

@ -1,197 +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.
*/
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
$tagPanelWidth: 56px; // only applies in this file, used for calculations
.mx_LeftPanel2 {
background-color: $roomlist2-bg-color;
min-width: 260px;
max-width: 50%;
// Create a row-based flexbox for the TagPanel and the room list
display: flex;
.mx_LeftPanel2_tagPanelContainer {
flex-grow: 0;
flex-shrink: 0;
flex-basis: $tagPanelWidth;
height: 100%;
// Create another flexbox so the TagPanel fills the container
display: flex;
// TagPanel handles its own CSS
}
&:not(.mx_LeftPanel2_hasTagPanel) {
.mx_LeftPanel2_roomListContainer {
width: 100%;
}
}
// 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_LeftPanel2_roomListContainer {
width: calc(100% - $tagPanelWidth);
background-color: $roomlist2-bg-color;
// Create another flexbox (this time a column) for the room list components
display: flex;
flex-direction: column;
.mx_LeftPanel2_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;
}
.mx_LeftPanel2_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_LeftPanel2_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_LeftPanel2_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_LeftPanel2_exploreButton {
width: 28px;
height: 28px;
border-radius: 20px;
background-color: $roomlist2-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_LeftPanel2_roomListWrapper {
// Create a flexbox to ensure the containing items cause appropriate overflow.
display: flex;
flex-grow: 1;
overflow: hidden;
min-height: 0;
margin-top: 10px; // so we're not up against the search/filter
&.mx_LeftPanel2_roomListWrapper_stickyBottom {
padding-bottom: 32px;
}
&.mx_LeftPanel2_roomListWrapper_stickyTop {
padding-top: 32px;
}
}
.mx_LeftPanel2_actualRoomListContainer {
flex-grow: 1; // fill the available space
overflow-y: auto;
width: 100%;
max-width: 100%;
position: relative; // for sticky headers
// Create a flexbox to trick the layout engine
display: flex;
}
}
// These styles override the defaults for the minimized (66px) layout
&.mx_LeftPanel2_minimized {
min-width: unset;
// We have to forcefully set the width to override the resizer's style attribute.
&.mx_LeftPanel2_hasTagPanel {
width: calc(68px + $tagPanelWidth) !important;
}
&:not(.mx_LeftPanel2_hasTagPanel) {
width: 68px !important;
}
.mx_LeftPanel2_roomListContainer {
width: 68px;
.mx_LeftPanel2_filterContainer {
// Organize the flexbox into a centered column layout
flex-direction: column;
justify-content: center;
.mx_LeftPanel2_exploreButton {
margin-left: 0;
margin-top: 8px;
background-color: transparent;
}
}
}
}
}

View file

@ -21,8 +21,20 @@ limitations under the License.
height: 100%; height: 100%;
} }
// move hit area 5px to the right so it doesn't overlap with the timeline scrollbar .mx_MainSplit > .mx_RightPanel_ResizeWrapper {
.mx_MainSplit > .mx_ResizeHandle.mx_ResizeHandle_horizontal { padding: 5px;
margin: 0 -10px 0 0;
padding: 0 10px 0 0; &: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;
}
} }

View file

@ -66,7 +66,7 @@ limitations under the License.
} }
/* not the left panel, and not the resize handle, so the roomview/groupview/... */ /* not the left panel, and not the resize handle, so the roomview/groupview/... */
.mx_MatrixChat > :not(.mx_LeftPanel_container):not(.mx_LeftPanel2):not(.mx_ResizeHandle) { .mx_MatrixChat > :not(.mx_LeftPanel):not(.mx_ResizeHandle) {
background-color: $primary-bg-color; background-color: $primary-bg-color;
flex: 1 1 0; flex: 1 1 0;
@ -78,3 +78,24 @@ limitations under the License.
*/ */
height: 100%; 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;
}
}

View file

@ -19,13 +19,12 @@ limitations under the License.
overflow-x: hidden; overflow-x: hidden;
flex: 0 0 auto; flex: 0 0 auto;
position: relative; position: relative;
min-width: 264px;
max-width: 50%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-radius: 8px; border-radius: 8px;
margin: 5px;
padding: 4px 0; padding: 4px 0;
box-sizing: border-box;
height: 100%;
.mx_RoomView_MessageList { .mx_RoomView_MessageList {
padding: 14px 18px; // top and bottom is 4px smaller to balance with the padding set above padding: 14px 18px; // top and bottom is 4px smaller to balance with the padding set above

View file

@ -18,7 +18,7 @@ limitations under the License.
.mx_RoomSearch { .mx_RoomSearch {
flex: 1; flex: 1;
border-radius: 20px; border-radius: 20px;
background-color: $roomlist2-button-bg-color; background-color: $roomlist-button-bg-color;
height: 28px; height: 28px;
padding: 2px; padding: 2px;

View file

@ -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;
}
}

View file

@ -157,7 +157,7 @@ limitations under the License.
font-weight: 600; font-weight: 600;
font-size: $font-14px; font-size: $font-14px;
padding: 0 5px; padding: 0 5px;
background-color: $roomtile-name-color; background-color: $muted-fg-color;
} }
.mx_TagTile_badgeHighlight { .mx_TagTile_badgeHighlight {

View file

@ -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;
}

View file

@ -24,7 +24,7 @@ limitations under the License.
right: 0; right: 0;
} }
.mx_NotificationBadge, .mx_RoomTile2_badgeContainer { .mx_NotificationBadge, .mx_RoomTile_badgeContainer {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;

View file

@ -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
}
}

View file

@ -34,7 +34,7 @@ limitations under the License.
.mx_MatrixChat > .mx_ResizeHandle.mx_ResizeHandle_horizontal { .mx_MatrixChat > .mx_ResizeHandle.mx_ResizeHandle_horizontal {
margin: 0 -10px 0 0; margin: 0 -10px 0 0;
padding: 0 10px 0 0; padding: 0 8px 0 0;
} }
.mx_ResizeHandle > div { .mx_ResizeHandle > div {

View file

@ -51,21 +51,27 @@ limitations under the License.
.mx_Tooltip { .mx_Tooltip {
display: none; display: none;
position: fixed; position: fixed;
border: 1px solid $menu-border-color;
border-radius: 8px; border-radius: 8px;
box-shadow: 4px 4px 12px 0 $menu-box-shadow-color; box-shadow: 4px 4px 12px 0 $menu-box-shadow-color;
background-color: $menu-bg-color;
z-index: 6000; // Higher than context menu so tooltips can be used everywhere z-index: 6000; // Higher than context menu so tooltips can be used everywhere
padding: 10px; padding: 10px;
pointer-events: none; pointer-events: none;
line-height: $font-14px; line-height: $font-14px;
font-size: $font-12px; font-size: $font-12px;
font-weight: 500; font-weight: 500;
color: $primary-fg-color;
max-width: 200px; max-width: 200px;
word-break: break-word; word-break: break-word;
margin-right: 50px; 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 { &.mx_Tooltip_visible {
animation: mx_fadein 0.2s forwards; animation: mx_fadein 0.2s forwards;
} }
@ -75,16 +81,15 @@ limitations under the License.
} }
} }
.mx_Tooltip_timeline { // These tooltips use an older style with a chevron
&.mx_Tooltip { .mx_Field_tooltip {
background-color: $inverted-bg-color; background-color: $menu-bg-color;
color: $accent-fg-color; color: $primary-fg-color;
border: 0; border: 1px solid $menu-border-color;
text-align: center; text-align: unset;
.mx_Tooltip_chevron { .mx_Tooltip_chevron {
display: none; display: unset;
}
} }
} }

View file

@ -17,4 +17,5 @@ limitations under the License.
.mx_MessageTimestamp { .mx_MessageTimestamp {
color: $event-timestamp-color; color: $event-timestamp-color;
font-size: $font-10px; font-size: $font-10px;
font-variant-numeric: tabular-nums;
} }

View file

@ -16,7 +16,6 @@ limitations under the License.
.mx_ReactionsRowButton { .mx_ReactionsRowButton {
display: inline-flex; display: inline-flex;
height: 20px;
line-height: $font-21px; line-height: $font-21px;
margin-right: 6px; margin-right: 6px;
padding: 0 6px; padding: 0 6px;
@ -35,11 +34,6 @@ limitations under the License.
border-color: $reaction-row-button-selected-border-color; 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 { .mx_ReactionsRowButton_content {
max-width: 100px; max-width: 100px;
overflow: hidden; overflow: hidden;

View file

@ -15,28 +15,45 @@ limitations under the License.
*/ */
.mx_cryptoEvent { .mx_cryptoEvent {
display: grid; display: grid;
grid-template-columns: 24px minmax(0, 1fr) min-content; grid-template-columns: 24px minmax(0, 1fr) min-content;
&.mx_cryptoEvent_icon::before,
&.mx_cryptoEvent_icon::after { &.mx_cryptoEvent_icon::after {
grid-column: 1; grid-column: 1;
grid-row: 1 / 3; grid-row: 1 / 3;
width: 16px; width: 16px;
height: 16px; height: 16px;
content: ""; content: "";
background-image: url("$(res)/img/e2e/normal.svg"); top: 0;
background-repeat: no-repeat; bottom: 0;
background-size: 100%; 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; 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 { &.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 { &.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 { .mx_cryptoEvent_title, .mx_cryptoEvent_subtitle, .mx_cryptoEvent_state {

View file

@ -98,6 +98,7 @@ $AppsDrawerBodyHeight: 273px;
.mx_AppTile_mini .mx_AppTile_persistedWrapper { .mx_AppTile_mini .mx_AppTile_persistedWrapper {
height: 114px; height: 114px;
min-width: 300px;
} }
.mx_AppTileMenuBar { .mx_AppTileMenuBar {

View file

@ -42,7 +42,7 @@ limitations under the License.
// white infill for the transparency // white infill for the transparency
.mx_E2EIcon::before { .mx_E2EIcon::before {
background-color: #ffffff; background-color: #ffffff;
mask: url('$(res)/img/e2e/normal.svg'); mask-image: url('$(res)/img/e2e/normal.svg');
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center; mask-position: center;
mask-size: 90%; mask-size: 90%;

View file

@ -355,7 +355,7 @@ $left-gutter: 64px;
&::before { &::before {
background-color: #ffffff; background-color: #ffffff;
mask: url('$(res)/img/e2e/normal.svg'); mask-image: url('$(res)/img/e2e/normal.svg');
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center; mask-position: center;
mask-size: 90%; mask-size: 90%;

View file

@ -181,7 +181,8 @@ $irc-line-height: $font-18px;
> span { > span {
display: flex; display: flex;
> .mx_SenderProfile_name { > .mx_SenderProfile_name,
> .mx_SenderProfile_aux {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
min-width: var(--name-width); min-width: var(--name-width);

View file

@ -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;
}
}

View file

@ -41,8 +41,8 @@ limitations under the License.
// with text-align in parent // with text-align in parent
display: inline-block; display: inline-block;
padding: 0 4px; padding: 0 4px;
color: $roomtile-badge-fg-color; color: $accent-fg-color;
background-color: $roomtile-name-color; background-color: $muted-fg-color;
} }
.mx_JumpToBottomButton_highlight .mx_JumpToBottomButton_badge { .mx_JumpToBottomButton_highlight .mx_JumpToBottomButton_badge {
@ -56,7 +56,7 @@ limitations under the License.
border-radius: 19px; border-radius: 19px;
box-sizing: border-box; box-sizing: border-box;
background: $primary-bg-color; background: $primary-bg-color;
border: 1.3px solid $roomtile-name-color; border: 1.3px solid $muted-fg-color;
cursor: pointer; cursor: pointer;
} }
@ -70,5 +70,5 @@ limitations under the License.
mask: url('$(res)/img/icon-jump-to-bottom.svg'); mask: url('$(res)/img/icon-jump-to-bottom.svg');
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: 9px 14px; mask-position: 9px 14px;
background: $roomtile-name-color; background: $muted-fg-color;
} }

View file

@ -70,15 +70,11 @@ limitations under the License.
} }
} }
.mx_MemberList_wrapper { .mx_MemberList_wrapper {
padding: 10px; padding: 10px;
} }
.mx_MemberList_invite {
.mx_MemberList_invite,
.mx_RightPanel_invite {
flex: 0 0 auto; flex: 0 0 auto;
position: relative; position: relative;
background-color: $button-bg-color; background-color: $button-bg-color;
@ -88,11 +84,6 @@ limitations under the License.
justify-content: center; justify-content: center;
color: $button-fg-color; color: $button-fg-color;
font-weight: 600; font-weight: 600;
.mx_RightPanel_icon {
padding-right: 5px;
padding-top: 2px;
}
} }
.mx_MemberList_invite.mx_AccessibleButton_disabled { .mx_MemberList_invite.mx_AccessibleButton_disabled {
@ -107,3 +98,11 @@ limitations under the License.
background-size: 20px; background-size: 20px;
padding: 8px 0 8px 25px; 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');
}

View file

@ -27,7 +27,7 @@ limitations under the License.
// ^- The count is an element floating within that. // ^- The count is an element floating within that.
&.mx_NotificationBadge_visible { &.mx_NotificationBadge_visible {
background-color: $roomtile2-default-badge-bg-color; background-color: $roomtile-default-badge-bg-color;
// Create a flexbox to order the count a bit easier // Create a flexbox to order the count a bit easier
display: flex; display: flex;

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,98 +15,42 @@ limitations under the License.
*/ */
.mx_RoomBreadcrumbs { .mx_RoomBreadcrumbs {
position: relative; width: 100%;
height: 42px;
padding: 8px; // Create a flexbox for the crumbs
padding-bottom: 0;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: flex-start;
// 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%;
}
.mx_RoomBreadcrumbs_crumb { .mx_RoomBreadcrumbs_crumb {
margin-left: 4px; margin-right: 8px;
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;
width: 32px; width: 32px;
transform: scale(1);
} }
.mx_RoomBreadcrumbs_preAnimate { // These classes come from the CSSTransition component. There's many more classes we
width: 0; // could care about, but this is all we worried about for now. The animation works by
transform: scale(0); // 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 { .mx_RoomBreadcrumbs_placeholder {
opacity: 0.5; font-weight: 600;
} font-size: $font-14px;
line-height: 32px; // specifically to match the height this is not scaled
// Note: we have to manually control the gradient and stuff, but the IndicatorScrollbar height: 32px;
// 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_Tooltip {
margin-left: -42px;
margin-top: -42px;
}

View file

@ -1,68 +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.
*/
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
.mx_RoomBreadcrumbs2 {
width: 100%;
// Create a flexbox for the crumbs
display: flex;
flex-direction: row;
align-items: flex-start;
.mx_RoomBreadcrumbs2_crumb {
margin-right: 8px;
width: 32px;
}
// 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_RoomBreadcrumbs2-enter {
margin-left: -40px; // 32px for the avatar, 8px for the margin
}
&.mx_RoomBreadcrumbs2-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_RoomBreadcrumbs2_placeholder {
font-weight: 600;
font-size: $font-14px;
line-height: 32px; // specifically to match the height this is not scaled
height: 32px;
}
}
.mx_RoomBreadcrumbs2_Tooltip {
margin-left: -42px;
margin-top: -42px;
&.mx_Tooltip {
background-color: $inverted-bg-color;
color: $accent-fg-color;
border: 0;
.mx_Tooltip_chevron {
display: none;
}
}
}

View file

@ -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;
}

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. limitations under the License.
*/ */
.mx_RoomList.mx_RoomList2 {
overflow-y: auto;
}
.mx_RoomList { .mx_RoomList {
/* take up remaining space below TopLeftMenu */ padding-right: 7px; // width of the scrollbar, to line things up
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;
} }

View file

@ -1,27 +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.
*/
// TODO: Rename to mx_RoomList during replacement of old component
.mx_RoomList2 {
width: calc(100% - 16px); // 16px of artificial right-side margin (8px is overflowed from the sublists)
// Create a column-based flexbox for the sublists. That's pretty much all we have to
// worry about in this stylesheet.
display: flex;
flex-direction: column;
flex-wrap: nowrap; // let the column overflow
}

View file

@ -58,11 +58,6 @@ limitations under the License.
} }
} }
.mx_RoomPreviewBar_dark {
background-color: $tagpanel-bg-color;
color: $accent-fg-color;
}
.mx_RoomPreviewBar_actions { .mx_RoomPreviewBar_actions {
display: flex; display: flex;
} }

View file

@ -14,20 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367 .mx_RoomSublist {
.mx_RoomSublist2 {
// The sublist is a column of rows, essentially
display: flex;
flex-direction: column;
margin-left: 8px; margin-left: 8px;
margin-bottom: 4px; margin-bottom: 4px;
width: 100%;
flex-shrink: 0; // to convince safari's layout engine the flexbox is fine .mx_RoomSublist_headerContainer {
.mx_RoomSublist2_headerContainer {
// Create a flexbox to make alignment easy // Create a flexbox to make alignment easy
display: flex; display: flex;
align-items: center; align-items: center;
@ -43,13 +34,13 @@ limitations under the License.
// all works by ensuring the header text has a fixed height when sticky so the // 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. // fixed height of the container can maintain the scroll position.
// The combined height must be set in the LeftPanel2 component for sticky headers // The combined height must be set in the LeftPanel component for sticky headers
// to work correctly. // to work correctly.
padding-bottom: 8px; padding-bottom: 8px;
height: 24px; height: 24px;
color: $roomlist2-header-color; color: $roomlist-header-color;
.mx_RoomSublist2_stickable { .mx_RoomSublist_stickable {
flex: 1; flex: 1;
max-width: 100%; max-width: 100%;
@ -61,26 +52,26 @@ limitations under the License.
// to identify when a header is sticky. If we didn't have a consistent sticky class, // 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 // we'd have to do the "is sticky" checks again on click, as clicking the header
// when sticky scrolls instead of collapses the list. // when sticky scrolls instead of collapses the list.
&.mx_RoomSublist2_headerContainer_sticky { &.mx_RoomSublist_headerContainer_sticky {
position: fixed; position: fixed;
height: 32px; // to match the header container height: 32px; // to match the header container
// width set by JS // width set by JS
width: calc(100% - 22px); width: calc(100% - 22px);
} }
&.mx_RoomSublist2_headerContainer_stickyBottom { &.mx_RoomSublist_headerContainer_stickyBottom {
bottom: 0; bottom: 0;
} }
// We don't have a top style because the top is dependent on the room list header's // 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. // height, and is therefore calculated in JS.
// The class, mx_RoomSublist2_headerContainer_stickyTop, is applied though. // The class, mx_RoomSublist_headerContainer_stickyTop, is applied though.
} }
// Sticky Headers End // Sticky Headers End
// *************************** // ***************************
.mx_RoomSublist2_badgeContainer { .mx_RoomSublist_badgeContainer {
// Create another flexbox row because it's super easy to position the badge this way. // Create another flexbox row because it's super easy to position the badge this way.
display: flex; display: flex;
align-items: center; align-items: center;
@ -93,14 +84,14 @@ limitations under the License.
} }
} }
&:not(.mx_RoomSublist2_headerContainer_withAux) { &:not(.mx_RoomSublist_headerContainer_withAux) {
.mx_NotificationBadge { .mx_NotificationBadge {
margin-right: 4px; // just to push it over a bit, aligning it with the other elements margin-right: 4px; // just to push it over a bit, aligning it with the other elements
} }
} }
.mx_RoomSublist2_auxButton, .mx_RoomSublist_auxButton,
.mx_RoomSublist2_menuButton { .mx_RoomSublist_menuButton {
margin-left: 8px; // should be the same as the notification badge margin-left: 8px; // should be the same as the notification badge
position: relative; position: relative;
width: 24px; width: 24px;
@ -122,21 +113,21 @@ limitations under the License.
} }
// Hide the menu button by default // Hide the menu button by default
.mx_RoomSublist2_menuButton { .mx_RoomSublist_menuButton {
visibility: hidden; visibility: hidden;
width: 0; width: 0;
margin: 0; margin: 0;
} }
.mx_RoomSublist2_auxButton::before { .mx_RoomSublist_auxButton::before {
mask-image: url('$(res)/img/feather-customised/plus.svg'); mask-image: url('$(res)/img/feather-customised/plus.svg');
} }
.mx_RoomSublist2_menuButton::before { .mx_RoomSublist_menuButton::before {
mask-image: url('$(res)/img/element-icons/context-menu.svg'); mask-image: url('$(res)/img/element-icons/context-menu.svg');
} }
.mx_RoomSublist2_headerText { .mx_RoomSublist_headerText {
flex: 1; flex: 1;
max-width: calc(100% - 16px); // 16px is the badge width max-width: calc(100% - 16px); // 16px is the badge width
line-height: $font-16px; line-height: $font-16px;
@ -148,7 +139,7 @@ limitations under the License.
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
.mx_RoomSublist2_collapseBtn { .mx_RoomSublist_collapseBtn {
display: inline-block; display: inline-block;
position: relative; position: relative;
width: 12px; width: 12px;
@ -169,7 +160,7 @@ limitations under the License.
mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
} }
&.mx_RoomSublist2_collapseBtn_collapsed::before { &.mx_RoomSublist_collapseBtn_collapsed::before {
mask-image: url('$(res)/img/feather-customised/chevron-right.svg'); mask-image: url('$(res)/img/feather-customised/chevron-right.svg');
} }
} }
@ -181,12 +172,12 @@ limitations under the License.
// when scrolled to the top above the first sublist (whose header can only // 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. // 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. // See also https://github.com/vector-im/riot-web/issues/14429.
&:first-child .mx_RoomSublist2_headerContainer { &:first-child .mx_RoomSublist_headerContainer {
height: 0; height: 0;
padding-bottom: 4px; padding-bottom: 4px;
} }
.mx_RoomSublist2_resizeBox { .mx_RoomSublist_resizeBox {
position: relative; position: relative;
// Create another flexbox column for the tiles // Create another flexbox column for the tiles
@ -194,7 +185,7 @@ limitations under the License.
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
.mx_RoomSublist2_tiles { .mx_RoomSublist_tiles {
flex: 1 0 0; flex: 1 0 0;
overflow: hidden; overflow: hidden;
// need this to be flex otherwise the overflow hidden from above // need this to be flex otherwise the overflow hidden from above
@ -206,18 +197,18 @@ limitations under the License.
mask-image: linear-gradient(0deg, transparent, black 4px); mask-image: linear-gradient(0deg, transparent, black 4px);
} }
.mx_RoomSublist2_resizerHandles_showNButton { .mx_RoomSublist_resizerHandles_showNButton {
flex: 0 0 32px; flex: 0 0 32px;
} }
.mx_RoomSublist2_resizerHandles { .mx_RoomSublist_resizerHandles {
flex: 0 0 4px; flex: 0 0 4px;
} }
// Class name comes from the ResizableBox component // Class name comes from the ResizableBox component
// The hover state needs to use the whole sublist, not just the resizable box, // The hover state needs to use the whole sublist, not just the resizable box,
// so that selector is below and one level higher. // so that selector is below and one level higher.
.mx_RoomSublist2_resizerHandle { .mx_RoomSublist_resizerHandle {
cursor: ns-resize; cursor: ns-resize;
border-radius: 3px; border-radius: 3px;
@ -235,21 +226,21 @@ limitations under the License.
right: calc(50% - 32px) !important; right: calc(50% - 32px) !important;
} }
&:hover, &.mx_RoomSublist2_hasMenuOpen { &:hover, &.mx_RoomSublist_hasMenuOpen {
.mx_RoomSublist2_resizerHandle { .mx_RoomSublist_resizerHandle {
opacity: 0.8; opacity: 0.8;
background-color: $primary-fg-color; background-color: $primary-fg-color;
} }
} }
} }
.mx_RoomSublist2_showNButton { .mx_RoomSublist_showNButton {
cursor: pointer; cursor: pointer;
font-size: $font-13px; font-size: $font-13px;
line-height: $font-18px; line-height: $font-18px;
color: $roomtile2-preview-color; color: $roomtile-preview-color;
// Update the render() function for RoomSublist2 if these change // Update the render() function for RoomSublist if these change
// Update the ListLayout class for minVisibleTiles if these change. // Update the ListLayout class for minVisibleTiles if these change.
height: 24px; height: 24px;
padding-bottom: 4px; padding-bottom: 4px;
@ -258,7 +249,7 @@ limitations under the License.
display: flex; display: flex;
align-items: center; align-items: center;
.mx_RoomSublist2_showNButtonChevron { .mx_RoomSublist_showNButtonChevron {
position: relative; position: relative;
width: 16px; width: 16px;
height: 16px; height: 16px;
@ -267,52 +258,52 @@ limitations under the License.
mask-position: center; mask-position: center;
mask-size: contain; mask-size: contain;
mask-repeat: no-repeat; mask-repeat: no-repeat;
background: $roomtile2-preview-color; background: $roomtile-preview-color;
} }
.mx_RoomSublist2_showMoreButtonChevron { .mx_RoomSublist_showMoreButtonChevron {
mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
} }
.mx_RoomSublist2_showLessButtonChevron { .mx_RoomSublist_showLessButtonChevron {
mask-image: url('$(res)/img/feather-customised/chevron-up.svg'); mask-image: url('$(res)/img/feather-customised/chevron-up.svg');
} }
} }
&.mx_RoomSublist2_hasMenuOpen, &.mx_RoomSublist_hasMenuOpen,
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:focus-within, &:not(.mx_RoomSublist_minimized) > .mx_RoomSublist_headerContainer:focus-within,
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:hover { &:not(.mx_RoomSublist_minimized) > .mx_RoomSublist_headerContainer:hover {
.mx_RoomSublist2_menuButton { .mx_RoomSublist_menuButton {
visibility: visible; visibility: visible;
width: 24px; width: 24px;
margin-left: 8px; margin-left: 8px;
} }
} }
&.mx_RoomSublist2_minimized { &.mx_RoomSublist_minimized {
.mx_RoomSublist2_headerContainer { .mx_RoomSublist_headerContainer {
height: auto; height: auto;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
.mx_RoomSublist2_badgeContainer { .mx_RoomSublist_badgeContainer {
order: 0; order: 0;
align-self: flex-end; align-self: flex-end;
margin-right: 0; margin-right: 0;
} }
.mx_RoomSublist2_stickable { .mx_RoomSublist_stickable {
order: 1; order: 1;
max-width: 100%; max-width: 100%;
} }
.mx_RoomSublist2_auxButton { .mx_RoomSublist_auxButton {
order: 2; order: 2;
visibility: visible; visibility: visible;
width: 32px !important; // !important to override hover styles width: 32px !important; // !important to override hover styles
height: 32px !important; // !important to override hover styles height: 32px !important; // !important to override hover styles
margin-left: 0 !important; // !important to override hover styles margin-left: 0 !important; // !important to override hover styles
background-color: $roomlist2-button-bg-color; background-color: $roomlist-button-bg-color;
margin-top: 8px; margin-top: 8px;
&::before { &::before {
@ -322,25 +313,25 @@ limitations under the License.
} }
} }
.mx_RoomSublist2_resizeBox { .mx_RoomSublist_resizeBox {
align-items: center; align-items: center;
} }
.mx_RoomSublist2_showNButton { .mx_RoomSublist_showNButton {
flex-direction: column; flex-direction: column;
.mx_RoomSublist2_showNButtonChevron { .mx_RoomSublist_showNButtonChevron {
margin-right: 12px; // to center margin-right: 12px; // to center
} }
} }
.mx_RoomSublist2_menuButton { .mx_RoomSublist_menuButton {
height: 16px; height: 16px;
} }
&.mx_RoomSublist2_hasMenuOpen, &.mx_RoomSublist_hasMenuOpen,
& > .mx_RoomSublist2_headerContainer:hover { & > .mx_RoomSublist_headerContainer:hover {
.mx_RoomSublist2_menuButton { .mx_RoomSublist_menuButton {
visibility: visible; visibility: visible;
position: absolute; position: absolute;
bottom: 48px; // align to middle of name, 40px for aux button (with padding) and 8px for alignment bottom: 48px; // align to middle of name, 40px for aux button (with padding) and 8px for alignment
@ -352,7 +343,7 @@ limitations under the License.
// This is the same color as the left panel background because it needs // This is the same color as the left panel background because it needs
// to occlude the sublist title // to occlude the sublist title
background-color: $roomlist2-bg-color; background-color: $roomlist-bg-color;
&::before { &::before {
top: 0; top: 0;
@ -360,8 +351,8 @@ limitations under the License.
} }
} }
&.mx_RoomSublist2_headerContainer:not(.mx_RoomSublist2_headerContainer_withAux) { &.mx_RoomSublist_headerContainer:not(.mx_RoomSublist_headerContainer_withAux) {
.mx_RoomSublist2_menuButton { .mx_RoomSublist_menuButton {
bottom: 8px; // align to the middle of name, 40px less than the `bottom` above. bottom: 8px; // align to the middle of name, 40px less than the `bottom` above.
} }
} }
@ -369,7 +360,7 @@ limitations under the License.
} }
} }
.mx_RoomSublist2_contextMenu { .mx_RoomSublist_contextMenu {
padding: 20px 16px; padding: 20px 16px;
width: 250px; width: 250px;
@ -377,11 +368,11 @@ limitations under the License.
margin-top: 16px; margin-top: 16px;
margin-bottom: 16px; margin-bottom: 16px;
margin-right: 16px; // additional 16px margin-right: 16px; // additional 16px
border: 1px solid $roomsublist2-divider-color; border: 1px solid $roomsublist-divider-color;
opacity: 0.1; opacity: 0.1;
} }
.mx_RoomSublist2_contextMenu_title { .mx_RoomSublist_contextMenu_title {
font-size: $font-15px; font-size: $font-15px;
line-height: $font-20px; line-height: $font-20px;
font-weight: 600; font-weight: 600;
@ -393,6 +384,6 @@ limitations under the License.
} }
} }
.mx_RoomSublist2_addRoomTooltip { .mx_RoomSublist_addRoomTooltip {
margin-top: -3px; margin-top: -3px;
} }

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. limitations under the License.
*/ */
// Note: the room tile expects to be in a flexbox column container
.mx_RoomTile { .mx_RoomTile {
display: flex; margin-bottom: 4px;
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;
padding: 4px; padding: 4px;
width: 24px;
vertical-align: middle;
}
.mx_RoomTile_hasSubtext .mx_RoomTile_avatar { // The tile is also a flexbox row itself
padding-top: 0; display: flex;
vertical-align: super;
}
.mx_RoomTile_dm { &.mx_RoomTile_selected,
display: block; &:hover,
position: absolute; &:focus-within,
bottom: 0; &.mx_RoomTile_hasMenuOpen {
right: -5px; background-color: $roomtile-selected-bg-color;
z-index: 2; border-radius: 8px;
} }
// Note we match .mx_E2EIcon to make sure this matches more tightly than just .mx_DecoratedRoomAvatar, .mx_RoomTile_avatarContainer {
// .mx_E2EIcon on its own margin-right: 8px;
.mx_RoomTile_e2eIcon.mx_E2EIcon { }
height: 14px;
width: 14px;
display: block;
position: absolute;
bottom: -2px;
right: -5px;
z-index: 1;
margin: 0;
}
.mx_RoomTile_name { .mx_RoomTile_nameContainer {
font-size: $font-14px; flex-grow: 1;
padding: 0 4px; min-width: 0; // allow flex to shrink it
color: $roomtile-name-color; margin-right: 8px; // spacing to buttons/badges
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
}
.mx_RoomTile_badge { // Create a new column layout flexbox for the name parts
flex: 0 1 content; display: flex;
border-radius: 0.8em; flex-direction: column;
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;
justify-content: center; 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; 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 { // If the room has an overriden notification setting then we always show the notifications menu button
position: absolute; .mx_RoomTile_notificationsButton.mx_RoomTile_notificationsButton_show {
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 {
display: block; 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, // We use these both in context menus and the room tiles
.mx_RoomTile_badge.mx_RoomTile_badgeUnread { .mx_RoomTile_iconBell::before {
background-color: $roomtile-name-color; 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_contextMenu {
.mx_RoomTile_badge.mx_RoomTile_badgeRed { .mx_RoomTile_contextMenu_redRow {
color: $accent-fg-color; .mx_AccessibleButton {
background-color: $warning-color; color: $warning-color !important; // !important to override styles from context menu
} }
.mx_RoomTile_unread, .mx_RoomTile_highlight { .mx_IconizedContextMenu_icon::before {
.mx_RoomTile_name { background-color: $warning-color;
font-weight: 600; }
color: $roomtile-selected-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;
}

View file

@ -1,241 +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.
*/
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
// Note: the room tile expects to be in a flexbox column container
.mx_RoomTile2 {
margin-bottom: 4px;
padding: 4px;
// The tile is also a flexbox row itself
display: flex;
&.mx_RoomTile2_selected,
&:hover,
&:focus-within,
&.mx_RoomTile2_hasMenuOpen {
background-color: $roomtile2-selected-bg-color;
border-radius: 8px;
}
.mx_DecoratedRoomAvatar, .mx_RoomTile2_avatarContainer {
margin-right: 8px;
}
.mx_RoomTile2_nameContainer {
flex-grow: 1;
min-width: 0; // allow flex to shrink it
margin-right: 8px; // spacing to buttons/badges
// Create a new column layout flexbox for the name parts
display: flex;
flex-direction: column;
justify-content: center;
.mx_RoomTile2_name,
.mx_RoomTile2_messagePreview {
margin: 0 2px;
width: 100%;
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.mx_RoomTile2_name {
font-size: $font-14px;
line-height: $font-18px;
}
.mx_RoomTile2_name.mx_RoomTile2_nameHasUnreadEvents {
font-weight: 600;
}
.mx_RoomTile2_messagePreview {
font-size: $font-13px;
line-height: $font-18px;
color: $roomtile2-preview-color;
}
.mx_RoomTile2_nameWithPreview {
margin-top: -4px; // shift the name up a bit more
}
}
.mx_RoomTile2_notificationsButton {
margin-left: 4px; // spacing between buttons
}
.mx_RoomTile2_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_RoomTile2_menuButton,
.mx_RoomTile2_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;
}
}
// If the room has an overriden notification setting then we always show the notifications menu button
.mx_RoomTile2_notificationsButton.mx_RoomTile2_notificationsButton_show {
display: block;
}
.mx_RoomTile2_menuButton::before {
mask-image: url('$(res)/img/element-icons/context-menu.svg');
}
&:not(.mx_RoomTile2_minimized) {
&:hover,
&:focus-within,
&.mx_RoomTile2_hasMenuOpen {
// Hide the badge container on hover because it'll be a menu button
.mx_RoomTile2_badgeContainer {
width: 0;
height: 0;
display: none;
}
.mx_RoomTile2_notificationsButton,
.mx_RoomTile2_menuButton {
display: block;
}
}
}
&.mx_RoomTile2_minimized {
flex-direction: column;
align-items: center;
position: relative;
.mx_DecoratedRoomAvatar, .mx_RoomTile2_avatarContainer {
margin-right: 0;
}
}
}
// We use these both in context menus and the room tiles
.mx_RoomTile2_iconBell::before {
mask-image: url('$(res)/img/element-icons/notifications.svg');
}
.mx_RoomTile2_iconBellDot::before {
mask-image: url('$(res)/img/element-icons/roomlist/notifications-default.svg');
}
.mx_RoomTile2_iconBellCrossed::before {
mask-image: url('$(res)/img/element-icons/roomlist/notifications-off.svg');
}
.mx_RoomTile2_iconBellMentions::before {
mask-image: url('$(res)/img/element-icons/roomlist/notifications-dm.svg');
}
.mx_RoomTile2_iconCheck::before {
mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg');
}
.mx_RoomTile2_contextMenu {
.mx_RoomTile2_contextMenu_redRow {
.mx_AccessibleButton {
color: $warning-color !important; // !important to override styles from context menu
}
.mx_IconizedContextMenu_icon::before {
background-color: $warning-color;
}
}
.mx_RoomTile2_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_RoomTile2_iconStar::before {
mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg');
}
.mx_RoomTile2_iconFavorite::before {
mask-image: url('$(res)/img/feather-customised/favourites.svg');
}
.mx_RoomTile2_iconArrowDown::before {
mask-image: url('$(res)/img/element-icons/roomlist/low-priority.svg');
}
.mx_RoomTile2_iconSettings::before {
mask-image: url('$(res)/img/element-icons/settings.svg');
}
.mx_RoomTile2_iconSignOut::before {
mask-image: url('$(res)/img/element-icons/leave.svg');
}
}

View file

@ -18,7 +18,7 @@ limitations under the License.
width: 12px; width: 12px;
height: 12px; height: 12px;
border-radius: 12px; border-radius: 12px;
background-color: $roomlist2-bg-color; // to match the room list itself background-color: $roomlist-bg-color; // to match the room list itself
} }
.mx_RoomTileIcon_globe::before { .mx_RoomTileIcon_globe::before {

View file

@ -42,7 +42,7 @@ limitations under the License.
border-radius: 19px; border-radius: 19px;
box-sizing: border-box; box-sizing: border-box;
background: $primary-bg-color; background: $primary-bg-color;
border: 1.3px solid $roomtile-name-color; border: 1.3px solid $muted-fg-color;
cursor: pointer; cursor: pointer;
} }
@ -54,7 +54,7 @@ limitations under the License.
mask-image: url('$(res)/img/icon-jump-to-first-unread.svg'); mask-image: url('$(res)/img/icon-jump-to-first-unread.svg');
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: 9px 13px; mask-position: 9px 13px;
background: $roomtile-name-color; background: $muted-fg-color;
} }
.mx_TopUnreadMessagesBar_markAsRead { .mx_TopUnreadMessagesBar_markAsRead {
@ -62,7 +62,7 @@ limitations under the License.
width: 18px; width: 18px;
height: 18px; height: 18px;
background: $primary-bg-color; background: $primary-bg-color;
border: 1.3px solid $roomtile-name-color; border: 1.3px solid $muted-fg-color;
border-radius: 10px; border-radius: 10px;
margin: 5px auto; margin: 5px auto;
} }
@ -76,5 +76,5 @@ limitations under the License.
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-size: 10px; mask-size: 10px;
mask-position: 4px 4px; mask-position: 4px 4px;
background: $roomtile-name-color; background: $muted-fg-color;
} }

View file

@ -1,23 +0,0 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_UserOnlineDot {
border-radius: 50%;
background-color: $accent-color;
height: 6px;
width: 6px;
display: inline-block;
}

View file

@ -36,12 +36,12 @@ limitations under the License.
} }
} }
.mx_IncomingCallBox2 { .mx_IncomingCallBox {
min-width: 250px; min-width: 250px;
background-color: $primary-bg-color; background-color: $primary-bg-color;
padding: 8px; padding: 8px;
.mx_IncomingCallBox2_CallerInfo { .mx_IncomingCallBox_CallerInfo {
display: flex; display: flex;
direction: row; direction: row;
@ -68,12 +68,12 @@ limitations under the License.
} }
} }
.mx_IncomingCallBox2_buttons { .mx_IncomingCallBox_buttons {
padding: 8px; padding: 8px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
> .mx_IncomingCallBox2_spacer { > .mx_IncomingCallBox_spacer {
width: 8px; width: 8px;
} }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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; background-color: $accent-color;
color: $accent-fg-color; color: $accent-fg-color;
cursor: pointer; cursor: pointer;
text-align: center;
padding: 6px; padding: 6px;
font-weight: bold; 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;
}
} }

View file

@ -1,96 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
.mx_CallView2_voice {
background-color: $accent-color;
color: $accent-fg-color;
cursor: pointer;
padding: 6px;
font-weight: bold;
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_CallView2_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;
}
}

View file

@ -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;
}

View file

@ -108,32 +108,20 @@ $composer-e2e-icon-color: $header-panel-text-primary-color;
// ******************** // ********************
// V2 Room List
// TODO: Remove the 2 from all of these when the new list takes over
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
$roomlist-bg-color: rgba(33, 38, 44, 0.90);
$roomlist-header-color: #8E99A4;
$roomsublist-divider-color: $primary-fg-color;
$roomlist2-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomtile-preview-color: #A9B2BC;
$roomlist2-bg-color: rgba(33, 38, 44, 0.90); $roomtile-default-badge-bg-color: #61708b;
$roomlist2-header-color: #8E99A4; $roomtile-selected-bg-color: rgba(141, 151, 165, 0.2);
$roomsublist2-divider-color: $primary-fg-color;
$roomtile2-preview-color: #A9B2BC;
$roomtile2-default-badge-bg-color: #61708b;
$roomtile2-selected-bg-color: rgba(141, 151, 165, 0.2);
// ******************** // ********************
$notice-secondary-color: $roomlist2-header-color; $notice-secondary-color: $roomlist-header-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);
$panel-divider-color: transparent; $panel-divider-color: transparent;

View file

@ -107,30 +107,19 @@ $composer-e2e-icon-color: $header-panel-text-primary-color;
// ******************** // ********************
// V2 Room List
// TODO: Remove the 2 from all of these when the new list takes over
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$roomlist2-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons
$roomlist2-bg-color: $header-panel-bg-color; $roomlist-bg-color: $header-panel-bg-color;
$roomsublist2-divider-color: $primary-fg-color; $roomsublist-divider-color: $primary-fg-color;
$roomtile2-preview-color: #9e9e9e; $roomtile-preview-color: #9e9e9e;
$roomtile2-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;
$roomtile2-selected-bg-color: #1A1D23; $roomtile-selected-bg-color: #1A1D23;
// ******************** // ********************
$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);
$panel-divider-color: $header-panel-border-color; $panel-divider-color: $header-panel-border-color;
$widget-menu-bar-bg-color: $header-panel-bg-color; $widget-menu-bar-bg-color: $header-panel-bg-color;

View file

@ -174,19 +174,16 @@ $header-divider-color: #91a1c0;
// ******************** // ********************
// V2 Room List
// TODO: Remove the 2 from all of these when the new list takes over
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$roomlist2-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons
$roomlist2-bg-color: $header-panel-bg-color; $roomlist-bg-color: $header-panel-bg-color;
$roomlist2-header-color: $primary-fg-color; $roomlist-header-color: $primary-fg-color;
$roomsublist2-divider-color: $primary-fg-color; $roomsublist-divider-color: $primary-fg-color;
$roomtile2-preview-color: #9e9e9e; $roomtile-preview-color: #9e9e9e;
$roomtile2-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;
$roomtile2-selected-bg-color: #fff; $roomtile-selected-bg-color: #fff;
$presence-online: $accent-color; $presence-online: $accent-color;
$presence-away: #d9b072; $presence-away: #d9b072;
@ -194,13 +191,6 @@ $presence-offline: #e3e8f0;
// ******************** // ********************
$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;
$username-variant1-color: #368bd6; $username-variant1-color: #368bd6;
$username-variant2-color: #ac3ba8; $username-variant2-color: #ac3ba8;
$username-variant3-color: #03b381; $username-variant3-color: #03b381;
@ -210,13 +200,6 @@ $username-variant6-color: #2dc2c5;
$username-variant7-color: #5c56f5; $username-variant7-color: #5c56f5;
$username-variant8-color: #74d12c; $username-variant8-color: #74d12c;
$roomtile-transparent-focused-color: rgba(0, 0, 0, 0.1);
$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: #dee1f3; $panel-divider-color: #dee1f3;
// ******************** // ********************
@ -292,7 +275,7 @@ $progressbar-color: #000;
$room-warning-bg-color: $yellow-background; $room-warning-bg-color: $yellow-background;
$memberstatus-placeholder-color: $roomtile-name-color; $memberstatus-placeholder-color: $muted-fg-color;
$authpage-bg-color: #2e3649; $authpage-bg-color: #2e3649;
$authpage-modal-bg-color: rgba(255, 255, 255, 0.59); $authpage-modal-bg-color: rgba(255, 255, 255, 0.59);

View file

@ -25,7 +25,6 @@ $button-link-fg-color: var(--accent-color);
$button-primary-bg-color: var(--accent-color); $button-primary-bg-color: var(--accent-color);
$input-valid-border-color: var(--accent-color); $input-valid-border-color: var(--accent-color);
$reaction-row-button-selected-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); $tab-label-active-bg-color: var(--accent-color);
$togglesw-on-color: var(--accent-color); $togglesw-on-color: var(--accent-color);
$username-variant3-color: var(--accent-color); $username-variant3-color: var(--accent-color);
@ -40,7 +39,6 @@ $menu-bg-color: var(--timeline-background-color);
$avatar-bg-color: var(--timeline-background-color); $avatar-bg-color: var(--timeline-background-color);
$message-action-bar-bg-color: var(--timeline-background-color); $message-action-bar-bg-color: var(--timeline-background-color);
$primary-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); $togglesw-ball-color: var(--timeline-background-color);
$droptarget-bg-color: var(--timeline-background-color-50pct); //still needs alpha at .5 $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 $authpage-modal-bg-color: var(--timeline-background-color-50pct); //still needs alpha at .59
@ -48,14 +46,13 @@ $roomheader-bg-color: var(--timeline-background-color);
// //
// --roomlist-highlights-color // --roomlist-highlights-color
$roomtile-selected-bg-color: var(--roomlist-highlights-color); $roomtile-selected-bg-color: var(--roomlist-highlights-color);
$roomtile2-selected-bg-color: var(--roomlist-highlights-color);
// //
// --sidebar-color // --sidebar-color
$interactive-tooltip-bg-color: var(--sidebar-color); $interactive-tooltip-bg-color: var(--sidebar-color);
$tagpanel-bg-color: var(--sidebar-color); $tagpanel-bg-color: var(--sidebar-color);
$tooltip-timeline-bg-color: var(--sidebar-color); $tooltip-timeline-bg-color: var(--sidebar-color);
$dialog-backdrop-color: var(--sidebar-color-50pct); $dialog-backdrop-color: var(--sidebar-color-50pct);
$roomlist2-button-bg-color: var(--sidebar-color-15pct); $roomlist-button-bg-color: var(--sidebar-color-15pct);
// //
// --roomlist-background-color // --roomlist-background-color
$header-panel-bg-color: var(--roomlist-background-color); $header-panel-bg-color: var(--roomlist-background-color);
@ -65,12 +62,10 @@ $panel-gradient: var(--roomlist-background-color-0pct), var(--roomlist-backgroun
$dark-panel-bg-color: var(--roomlist-background-color); $dark-panel-bg-color: var(--roomlist-background-color);
$input-lighter-bg-color: var(--roomlist-background-color); $input-lighter-bg-color: var(--roomlist-background-color);
$plinth-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); $secondary-accent-color: var(--roomlist-background-color);
$selected-color: var(--roomlist-background-color); $selected-color: var(--roomlist-background-color);
$widget-menu-bar-bg-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);
$roomlist2-bg-color: var(--roomlist-background-color);
// //
// --timeline-text-color // --timeline-text-color
$message-action-bar-fg-color: var(--timeline-text-color); $message-action-bar-fg-color: var(--timeline-text-color);
@ -87,23 +82,17 @@ $tab-label-fg-color: var(--timeline-text-color);
// was #4e5054 // was #4e5054
$authpage-lang-color: var(--timeline-text-color); $authpage-lang-color: var(--timeline-text-color);
$roomheader-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 // --roomlist-text-secondary-color
$roomsublist-label-fg-color: var(--roomlist-text-secondary-color); $roomtile-preview-color: var(--roomlist-text-secondary-color);
$roomtile-name-color: var(--roomlist-text-secondary-color); $roomlist-header-color: var(--roomlist-text-secondary-color);
$roomtile2-preview-color: var(--roomlist-text-secondary-color); $roomtile-default-badge-bg-color: var(--roomlist-text-secondary-color);
$roomlist2-header-color: var(--roomlist-text-secondary-color);
$roomtile2-default-badge-bg-color: var(--roomlist-text-secondary-color);
// //
// --roomlist-separator-color // --roomlist-separator-color
$input-darker-bg-color: var(--roomlist-separator-color); $input-darker-bg-color: var(--roomlist-separator-color);
$panel-divider-color: var(--roomlist-separator-color);// originally #dee1f3, but close enough $panel-divider-color: var(--roomlist-separator-color);// originally #dee1f3, but close enough
$primary-hairline-color: var(--roomlist-separator-color);// originally #e5e5e5, but close enough $primary-hairline-color: var(--roomlist-separator-color);// originally #e5e5e5, but close enough
$roomsublist2-divider-color: var(--roomlist-separator-color); $roomsublist-divider-color: var(--roomlist-separator-color);
// //
// --timeline-text-secondary-color // --timeline-text-secondary-color
$authpage-secondary-color: var(--timeline-text-secondary-color); $authpage-secondary-color: var(--timeline-text-secondary-color);

View file

@ -1,25 +1,20 @@
/*
* 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. /* 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-face {
font-family: 'Inter'; font-family: 'Inter';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
unicode-range: $inter-unicode-range;
src: url("$(res)/fonts/Inter/Inter-Regular.woff2?v=3.13") format("woff2"), 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"); url("$(res)/fonts/Inter/Inter-Regular.woff?v=3.13") format("woff");
} }
@ -28,6 +23,7 @@
font-style: italic; font-style: italic;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
unicode-range: $inter-unicode-range;
src: url("$(res)/fonts/Inter/Inter-Italic.woff2?v=3.13") format("woff2"), 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"); url("$(res)/fonts/Inter/Inter-Italic.woff?v=3.13") format("woff");
} }
@ -37,6 +33,7 @@
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
unicode-range: $inter-unicode-range;
src: url("$(res)/fonts/Inter/Inter-Medium.woff2?v=3.13") format("woff2"), 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"); url("$(res)/fonts/Inter/Inter-Medium.woff?v=3.13") format("woff");
} }
@ -45,6 +42,7 @@
font-style: italic; font-style: italic;
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
unicode-range: $inter-unicode-range;
src: url("$(res)/fonts/Inter/Inter-MediumItalic.woff2?v=3.13") format("woff2"), 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"); url("$(res)/fonts/Inter/Inter-MediumItalic.woff?v=3.13") format("woff");
} }
@ -54,6 +52,7 @@
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
unicode-range: $inter-unicode-range;
src: url("$(res)/fonts/Inter/Inter-SemiBold.woff2?v=3.13") format("woff2"), 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"); url("$(res)/fonts/Inter/Inter-SemiBold.woff?v=3.13") format("woff");
} }
@ -62,6 +61,7 @@
font-style: italic; font-style: italic;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
unicode-range: $inter-unicode-range;
src: url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff2?v=3.13") format("woff2"), 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"); url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff?v=3.13") format("woff");
} }
@ -71,6 +71,7 @@
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
unicode-range: $inter-unicode-range;
src: url("$(res)/fonts/Inter/Inter-Bold.woff2?v=3.13") format("woff2"), 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"); url("$(res)/fonts/Inter/Inter-Bold.woff?v=3.13") format("woff");
} }
@ -79,6 +80,7 @@
font-style: italic; font-style: italic;
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
unicode-range: $inter-unicode-range;
src: url("$(res)/fonts/Inter/Inter-BoldItalic.woff2?v=3.13") format("woff2"), 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"); url("$(res)/fonts/Inter/Inter-BoldItalic.woff?v=3.13") format("woff");
} }
@ -118,17 +120,3 @@
src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2') format('woff2'); 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; 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');
}
*/

View file

@ -19,8 +19,8 @@ $accent-bg-color: rgba(3, 179, 129, 0.16);
$notice-primary-color: #ff4b55; $notice-primary-color: #ff4b55;
$notice-primary-bg-color: rgba(255, 75, 85, 0.16); $notice-primary-bg-color: rgba(255, 75, 85, 0.16);
$primary-fg-color: #2e2f32; $primary-fg-color: #2e2f32;
$roomlist2-header-color: $primary-fg-color; $roomlist-header-color: $primary-fg-color;
$notice-secondary-color: $roomlist2-header-color; $notice-secondary-color: $roomlist-header-color;
$header-panel-bg-color: #f3f8fd; $header-panel-bg-color: #f3f8fd;
// typical text (dark-on-white in light skin) // typical text (dark-on-white in light skin)
@ -174,32 +174,22 @@ $header-divider-color: #91A1C0;
// ******************** // ********************
// V2 Room List
// TODO: Remove the 2 from all of these when the new list takes over
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$roomlist2-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons
$roomlist2-bg-color: rgba(245, 245, 245, 0.90); $roomlist-bg-color: rgba(245, 245, 245, 0.90);
$roomsublist2-divider-color: $primary-fg-color; $roomsublist-divider-color: $primary-fg-color;
$roomtile2-preview-color: #737D8C; $roomtile-preview-color: #737D8C;
$roomtile2-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;
$roomtile2-selected-bg-color: #FFF; $roomtile-selected-bg-color: #FFF;
$presence-online: $accent-color; $presence-online: $accent-color;
$presence-away: orange; // TODO: Get color $presence-away: #d9b072;
$presence-offline: #E3E8F0; $presence-offline: #E3E8F0;
// ******************** // ********************
$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;
$username-variant1-color: #368bd6; $username-variant1-color: #368bd6;
$username-variant2-color: #ac3ba8; $username-variant2-color: #ac3ba8;
$username-variant3-color: #0DBD8B; $username-variant3-color: #0DBD8B;
@ -209,13 +199,6 @@ $username-variant6-color: #2dc2c5;
$username-variant7-color: #5c56f5; $username-variant7-color: #5c56f5;
$username-variant8-color: #74d12c; $username-variant8-color: #74d12c;
$roomtile-transparent-focused-color: rgba(0, 0, 0, 0.1);
$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: transparent;
// ******************** // ********************
@ -291,7 +274,7 @@ $progressbar-color: #000;
$room-warning-bg-color: $yellow-background; $room-warning-bg-color: $yellow-background;
$memberstatus-placeholder-color: $roomtile-name-color; $memberstatus-placeholder-color: $muted-fg-color;
$authpage-bg-color: #2e3649; $authpage-bg-color: #2e3649;
$authpage-modal-bg-color: rgba(245, 245, 245, 0.90); $authpage-modal-bg-color: rgba(245, 245, 245, 0.90);

View file

@ -5,7 +5,7 @@
// it can be blurred by the tag panel and room list // it can be blurred by the tag panel and room list
@supports (backdrop-filter: none) { @supports (backdrop-filter: none) {
.mx_LeftPanel2 { .mx_LeftPanel {
background-image: var(--avatar-url); background-image: var(--avatar-url);
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
@ -16,12 +16,12 @@
backdrop-filter: blur($tagpanel-background-blur-amount); backdrop-filter: blur($tagpanel-background-blur-amount);
} }
.mx_LeftPanel2 .mx_LeftPanel2_roomListContainer { .mx_LeftPanel .mx_LeftPanel_roomListContainer {
backdrop-filter: blur($roomlist-background-blur-amount); backdrop-filter: blur($roomlist-background-blur-amount);
} }
} }
.mx_RoomSublist2_showNButton { .mx_RoomSublist_showNButton {
background-color: transparent !important; background-color: transparent !important;
} }

View file

@ -20,9 +20,11 @@ import { IMatrixClientPeg } from "../MatrixClientPeg";
import ToastStore from "../stores/ToastStore"; import ToastStore from "../stores/ToastStore";
import DeviceListener from "../DeviceListener"; import DeviceListener from "../DeviceListener";
import RebrandListener from "../RebrandListener"; import RebrandListener from "../RebrandListener";
import { RoomListStore2 } from "../stores/room-list/RoomListStore2"; import { RoomListStoreClass } from "../stores/room-list/RoomListStore";
import { PlatformPeg } from "../PlatformPeg"; import { PlatformPeg } from "../PlatformPeg";
import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore"; import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore";
import {IntegrationManagers} from "../integrations/IntegrationManagers";
import {ModalManager} from "../Modal";
declare global { declare global {
interface Window { interface Window {
@ -32,21 +34,20 @@ declare global {
init: () => Promise<void>; init: () => Promise<void>;
}; };
mx_ContentMessages: ContentMessages; mxContentMessages: ContentMessages;
mx_ToastStore: ToastStore; mxToastStore: ToastStore;
mx_DeviceListener: DeviceListener; mxDeviceListener: DeviceListener;
mx_RebrandListener: RebrandListener; mxRebrandListener: RebrandListener;
mx_RoomListStore2: RoomListStore2; mxRoomListStore: RoomListStoreClass;
mx_RoomListLayoutStore: RoomListLayoutStore; mxRoomListLayoutStore: RoomListLayoutStore;
mxPlatformPeg: PlatformPeg; mxPlatformPeg: PlatformPeg;
mxIntegrationManagers: typeof IntegrationManagers;
// TODO: Remove flag before launch: https://github.com/vector-im/riot-web/issues/14231 singletonModalManager: ModalManager;
mx_LoudRoomListLogging: boolean;
} }
// workaround for https://github.com/microsoft/TypeScript/issues/30933 // workaround for https://github.com/microsoft/TypeScript/issues/30933
interface ObjectConstructor { interface ObjectConstructor {
fromEntries?(xs: [string|number|symbol, any][]): object fromEntries?(xs: [string|number|symbol, any][]): object;
} }
interface Document { interface Document {

View file

@ -386,7 +386,7 @@ export default class ContentMessages {
const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
if (isQuoting) { if (isQuoting) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); 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'), title: _t('Replying With Files'),
description: ( description: (
<div>{_t( <div>{_t(
@ -397,7 +397,7 @@ export default class ContentMessages {
hasCancelButton: true, hasCancelButton: true,
button: _t("Continue"), button: _t("Continue"),
}); });
const [shouldUpload]: [boolean] = await finished; const [shouldUpload] = await finished;
if (!shouldUpload) return; if (!shouldUpload) return;
} }
@ -420,12 +420,12 @@ export default class ContentMessages {
if (tooBigFiles.length > 0) { if (tooBigFiles.length > 0) {
const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog"); const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog");
const {finished} = Modal.createTrackedDialog('Upload Failure', '', UploadFailureDialog, { const {finished} = Modal.createTrackedDialog<[boolean]>('Upload Failure', '', UploadFailureDialog, {
badFiles: tooBigFiles, badFiles: tooBigFiles,
totalFiles: files.length, totalFiles: files.length,
contentMessages: this, contentMessages: this,
}); });
const [shouldContinue]: [boolean] = await finished; const [shouldContinue] = await finished;
if (!shouldContinue) return; if (!shouldContinue) return;
} }
@ -437,12 +437,12 @@ export default class ContentMessages {
for (let i = 0; i < okFiles.length; ++i) { for (let i = 0; i < okFiles.length; ++i) {
const file = okFiles[i]; const file = okFiles[i];
if (!uploadAll) { if (!uploadAll) {
const {finished} = Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, { const {finished} = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation', '', UploadConfirmDialog, {
file, file,
currentIndex: i, currentIndex: i,
totalFiles: okFiles.length, totalFiles: okFiles.length,
}); });
const [shouldContinue, shouldUploadAll]: [boolean, boolean] = await finished; const [shouldContinue, shouldUploadAll] = await finished;
if (!shouldContinue) break; if (!shouldContinue) break;
if (shouldUploadAll) { if (shouldUploadAll) {
uploadAll = true; uploadAll = true;
@ -621,9 +621,9 @@ export default class ContentMessages {
} }
static sharedInstance() { static sharedInstance() {
if (window.mx_ContentMessages === undefined) { if (window.mxContentMessages === undefined) {
window.mx_ContentMessages = new ContentMessages(); window.mxContentMessages = new ContentMessages();
} }
return window.mx_ContentMessages; return window.mxContentMessages;
} }
} }

View file

@ -40,25 +40,16 @@ export class AccessCancelledError extends Error {
} }
} }
async function confirmToDismiss(name) { async function confirmToDismiss() {
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.");
}
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const [sure] = await Modal.createDialog(QuestionDialog, { const [sure] = await Modal.createDialog(QuestionDialog, {
title: _t("Cancel entering passphrase?"), title: _t("Cancel entering passphrase?"),
description, description: _t("Are you sure you want to cancel entering passphrase?"),
danger: true, danger: false,
cancelButton: _t("Enter passphrase"), button: _t("Go Back"),
button: _t("Cancel"), cancelButton: _t("Cancel"),
}).finished; }).finished;
return sure; return !sure;
} }
async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
@ -102,7 +93,7 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
/* options= */ { /* options= */ {
onBeforeClose: async (reason) => { onBeforeClose: async (reason) => {
if (reason === "backgroundClick") { if (reason === "backgroundClick") {
return confirmToDismiss(ssssItemName); return confirmToDismiss();
} }
return true; return true;
}, },

View file

@ -17,16 +17,16 @@ limitations under the License.
import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import { import {
hideToast as hideBulkUnverifiedSessionsToast, hideToast as hideBulkUnverifiedSessionsToast,
showToast as showBulkUnverifiedSessionsToast showToast as showBulkUnverifiedSessionsToast,
} from "./toasts/BulkUnverifiedSessionsToast"; } from "./toasts/BulkUnverifiedSessionsToast";
import { import {
hideToast as hideSetupEncryptionToast, hideToast as hideSetupEncryptionToast,
Kind as SetupKind, Kind as SetupKind,
showToast as showSetupEncryptionToast showToast as showSetupEncryptionToast,
} from "./toasts/SetupEncryptionToast"; } from "./toasts/SetupEncryptionToast";
import { import {
hideToast as hideUnverifiedSessionsToast, hideToast as hideUnverifiedSessionsToast,
showToast as showUnverifiedSessionsToast showToast as showUnverifiedSessionsToast,
} from "./toasts/UnverifiedSessionToast"; } from "./toasts/UnverifiedSessionToast";
import {privateShouldBeEncrypted} from "./createRoom"; import {privateShouldBeEncrypted} from "./createRoom";
@ -48,8 +48,8 @@ export default class DeviceListener {
private displayingToastsForDeviceIds = new Set<string>(); private displayingToastsForDeviceIds = new Set<string>();
static sharedInstance() { static sharedInstance() {
if (!window.mx_DeviceListener) window.mx_DeviceListener = new DeviceListener(); if (!window.mxDeviceListener) window.mxDeviceListener = new DeviceListener();
return window.mx_DeviceListener; return window.mxDeviceListener;
} }
start() { start() {

View file

@ -184,7 +184,7 @@ const transformTags: sanitizeHtml.IOptions["transformTags"] = { // custom to mat
if (typeof attribs.class !== 'undefined') { if (typeof attribs.class !== 'undefined') {
// Filter out all classes other than ones starting with language- for syntax highlighting. // Filter out all classes other than ones starting with language- for syntax highlighting.
const classes = attribs.class.split(/\s/).filter(function(cl) { const classes = attribs.class.split(/\s/).filter(function(cl) {
return cl.startsWith('language-'); return cl.startsWith('language-') && !cl.startsWith('language-_');
}); });
attribs.class = classes.join(' '); attribs.class = classes.join(' ');
} }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import classNames from 'classnames';
import Analytics from './Analytics'; import Analytics from './Analytics';
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import {defer} from './utils/promise'; import {defer} from './utils/promise';
@ -25,36 +28,48 @@ import AsyncWrapper from './AsyncWrapper';
const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
class ModalManager { interface IModal<T extends any[]> {
constructor() { elem: React.ReactNode;
this._counter = 0; className?: string;
beforeClosePromise?: Promise<boolean>;
closeReason?: string;
onBeforeClose?(reason?: string): Promise<boolean>;
onFinished(...args: T): void;
close(...args: T): void;
}
// The modal to prioritise over all others. If this is set, only show interface IHandle<T extends any[]> {
// this modal. Remove all other modals from the stack when this modal finished: Promise<T>;
// is closed. close(...args: T): void;
this._priorityModal = null; }
// The modal to keep open underneath other modals if possible. Useful
// for cases like Settings where the modal should remain open while the
// user is prompted for more information/errors.
this._staticModal = null;
// A list of the modals we have stacked up, with the most recent at [0]
// Neither the static nor priority modal will be in this list.
this._modals = [
/* {
elem: React component for this dialog
onFinished: caller-supplied onFinished callback
className: CSS class for the dialog wrapper div
} */
];
this.onBackgroundClick = this.onBackgroundClick.bind(this); interface IProps<T extends any[]> {
} onFinished?(...args: T): void;
// TODO improve typing here once all Modals are TS and we can exhaustively check the props
[key: string]: any;
}
hasDialogs() { interface IOptions<T extends any[]> {
return this._priorityModal || this._staticModal || this._modals.length > 0; onBeforeClose?: IModal<T>["onBeforeClose"];
} }
getOrCreateContainer() { type ParametersWithoutFirst<T extends (...args: any) => 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<any> = 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<any> = 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<any>[] = [];
private static getOrCreateContainer() {
let container = document.getElementById(DIALOG_CONTAINER_ID); let container = document.getElementById(DIALOG_CONTAINER_ID);
if (!container) { if (!container) {
@ -66,7 +81,7 @@ class ModalManager {
return container; return container;
} }
getOrCreateStaticContainer() { private static getOrCreateStaticContainer() {
let container = document.getElementById(STATIC_DIALOG_CONTAINER_ID); let container = document.getElementById(STATIC_DIALOG_CONTAINER_ID);
if (!container) { if (!container) {
@ -78,63 +93,99 @@ class ModalManager {
return container; return container;
} }
createTrackedDialog(analyticsAction, analyticsInfo, ...rest) { public hasDialogs() {
return this.priorityModal || this.staticModal || this.modals.length > 0;
}
public createTrackedDialog<T extends any[]>(
analyticsAction: string,
analyticsInfo: string,
...rest: Parameters<ModalManager["createDialog"]>
) {
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
return this.createDialog(...rest); return this.createDialog<T>(...rest);
} }
appendTrackedDialog(analyticsAction, analyticsInfo, ...rest) { public appendTrackedDialog<T extends any[]>(
analyticsAction: string,
analyticsInfo: string,
...rest: Parameters<ModalManager["appendDialog"]>
) {
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
return this.appendDialog(...rest); return this.appendDialog<T>(...rest);
} }
createDialog(Element, ...rest) { public createDialog<T extends any[]>(
return this.createDialogAsync(Promise.resolve(Element), ...rest); Element: React.ComponentType,
...rest: ParametersWithoutFirst<ModalManager["createDialogAsync"]>
) {
return this.createDialogAsync<T>(Promise.resolve(Element), ...rest);
} }
appendDialog(Element, ...rest) { public appendDialog<T extends any[]>(
return this.appendDialogAsync(Promise.resolve(Element), ...rest); Element: React.ComponentType,
...rest: ParametersWithoutFirst<ModalManager["appendDialogAsync"]>
) {
return this.appendDialogAsync<T>(Promise.resolve(Element), ...rest);
} }
createTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) { public createTrackedDialogAsync<T extends any[]>(
analyticsAction: string,
analyticsInfo: string,
...rest: Parameters<ModalManager["appendDialogAsync"]>
) {
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
return this.createDialogAsync(...rest); return this.createDialogAsync<T>(...rest);
} }
appendTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) { public appendTrackedDialogAsync<T extends any[]>(
analyticsAction: string,
analyticsInfo: string,
...rest: Parameters<ModalManager["appendDialogAsync"]>
) {
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
return this.appendDialogAsync(...rest); return this.appendDialogAsync<T>(...rest);
} }
_buildModal(prom, props, className, options) { private buildModal<T extends any[]>(
const modal = {}; prom: Promise<React.ComponentType>,
props?: IProps<T>,
className?: string,
options?: IOptions<T>
) {
const modal: IModal<T> = {
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 // never call this from onFinished() otherwise it will loop
const [closeDialog, onFinishedProm] = this._getCloseFn(modal, props); const [closeDialog, onFinishedProm] = this.getCloseFn<T>(modal, props);
// don't attempt to reuse the same AsyncWrapper for different dialogs, // don't attempt to reuse the same AsyncWrapper for different dialogs,
// otherwise we'll get confused. // otherwise we'll get confused.
const modalCount = this._counter++; const modalCount = this.counter++;
// FIXME: If a dialog uses getDefaultProps it clobbers the onFinished // FIXME: If a dialog uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the dialog from a button click! // property set here so you can't close the dialog from a button click!
modal.elem = ( modal.elem = <AsyncWrapper key={modalCount} prom={prom} {...props} onFinished={closeDialog} />;
<AsyncWrapper key={modalCount} prom={prom} {...props}
onFinished={closeDialog} />
);
modal.onFinished = props ? props.onFinished : null;
modal.className = className;
modal.onBeforeClose = options.onBeforeClose;
modal.beforeClosePromise = null;
modal.close = closeDialog; modal.close = closeDialog;
modal.closeReason = null;
return {modal, closeDialog, onFinishedProm}; return {modal, closeDialog, onFinishedProm};
} }
_getCloseFn(modal, props) { private getCloseFn<T extends any[]>(
const deferred = defer(); modal: IModal<T>,
return [async (...args) => { props: IProps<T>
): [IHandle<T>["close"], IHandle<T>["finished"]] {
const deferred = defer<T>();
return [async (...args: T) => {
if (modal.beforeClosePromise) { if (modal.beforeClosePromise) {
await modal.beforeClosePromise; await modal.beforeClosePromise;
} else if (modal.onBeforeClose) { } else if (modal.onBeforeClose) {
@ -147,26 +198,26 @@ class ModalManager {
} }
deferred.resolve(args); deferred.resolve(args);
if (props && props.onFinished) props.onFinished.apply(null, 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) { if (i >= 0) {
this._modals.splice(i, 1); this.modals.splice(i, 1);
} }
if (this._priorityModal === modal) { if (this.priorityModal === modal) {
this._priorityModal = null; this.priorityModal = null;
// XXX: This is destructive // XXX: This is destructive
this._modals = []; this.modals = [];
} }
if (this._staticModal === modal) { if (this.staticModal === modal) {
this._staticModal = null; this.staticModal = null;
// XXX: This is destructive // XXX: This is destructive
this._modals = []; this.modals = [];
} }
this._reRender(); this.reRender();
}, deferred.promise]; }, deferred.promise];
} }
@ -207,38 +258,49 @@ class ModalManager {
* @param {onBeforeClose} options.onBeforeClose a callback to decide whether to close the dialog * @param {onBeforeClose} options.onBeforeClose a callback to decide whether to close the dialog
* @returns {object} Object with 'close' parameter being a function that will close the dialog * @returns {object} Object with 'close' parameter being a function that will close the dialog
*/ */
createDialogAsync(prom, props, className, isPriorityModal, isStaticModal, options = {}) { private createDialogAsync<T extends any[]>(
const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, options); prom: Promise<React.ComponentType>,
props?: IProps<T>,
className?: string,
isPriorityModal = false,
isStaticModal = false,
options: IOptions<T> = {}
): IHandle<T> {
const {modal, closeDialog, onFinishedProm} = this.buildModal<T>(prom, props, className, options);
if (isPriorityModal) { if (isPriorityModal) {
// XXX: This is destructive // XXX: This is destructive
this._priorityModal = modal; this.priorityModal = modal;
} else if (isStaticModal) { } else if (isStaticModal) {
// This is intentionally destructive // This is intentionally destructive
this._staticModal = modal; this.staticModal = modal;
} else { } else {
this._modals.unshift(modal); this.modals.unshift(modal);
} }
this._reRender(); this.reRender();
return { return {
close: closeDialog, close: closeDialog,
finished: onFinishedProm, finished: onFinishedProm,
}; };
} }
appendDialogAsync(prom, props, className) { private appendDialogAsync<T extends any[]>(
const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, {}); prom: Promise<React.ComponentType>,
props?: IProps<T>,
className?: string
): IHandle<T> {
const {modal, closeDialog, onFinishedProm} = this.buildModal<T>(prom, props, className, {});
this._modals.push(modal); this.modals.push(modal);
this._reRender(); this.reRender();
return { return {
close: closeDialog, close: closeDialog,
finished: onFinishedProm, finished: onFinishedProm,
}; };
} }
onBackgroundClick() { private onBackgroundClick = () => {
const modal = this._getCurrentModal(); const modal = this.getCurrentModal();
if (!modal) { if (!modal) {
return; return;
} }
@ -249,21 +311,21 @@ class ModalManager {
modal.closeReason = "backgroundClick"; modal.closeReason = "backgroundClick";
modal.close(); modal.close();
modal.closeReason = null; modal.closeReason = null;
};
private getCurrentModal(): IModal<any> {
return this.priorityModal ? this.priorityModal : (this.modals[0] || this.staticModal);
} }
_getCurrentModal() { private reRender() {
return this._priorityModal ? this._priorityModal : (this._modals[0] || this._staticModal); if (this.modals.length === 0 && !this.priorityModal && !this.staticModal) {
}
_reRender() {
if (this._modals.length === 0 && !this._priorityModal && !this._staticModal) {
// If there is no modal to render, make all of Riot available // If there is no modal to render, make all of Riot available
// to screen reader users again // to screen reader users again
dis.dispatch({ dis.dispatch({
action: 'aria_unhide_main_app', action: 'aria_unhide_main_app',
}); });
ReactDOM.unmountComponentAtNode(this.getOrCreateContainer()); ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer());
ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer()); ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer());
return; return;
} }
@ -274,49 +336,48 @@ class ModalManager {
action: 'aria_hide_main_app', action: 'aria_hide_main_app',
}); });
if (this._staticModal) { if (this.staticModal) {
const classes = "mx_Dialog_wrapper mx_Dialog_staticWrapper " const classes = classNames("mx_Dialog_wrapper mx_Dialog_staticWrapper", this.staticModal.className);
+ (this._staticModal.className ? this._staticModal.className : '');
const staticDialog = ( const staticDialog = (
<div className={classes}> <div className={classes}>
<div className="mx_Dialog"> <div className="mx_Dialog">
{ this._staticModal.elem } { this.staticModal.elem }
</div> </div>
<div className="mx_Dialog_background mx_Dialog_staticBackground" onClick={this.onBackgroundClick}></div> <div className="mx_Dialog_background mx_Dialog_staticBackground" onClick={this.onBackgroundClick} />
</div> </div>
); );
ReactDOM.render(staticDialog, this.getOrCreateStaticContainer()); ReactDOM.render(staticDialog, ModalManager.getOrCreateStaticContainer());
} else { } else {
// This is safe to call repeatedly if we happen to do that // This is safe to call repeatedly if we happen to do that
ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer()); ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer());
} }
const modal = this._getCurrentModal(); const modal = this.getCurrentModal();
if (modal !== this._staticModal) { if (modal !== this.staticModal) {
const classes = "mx_Dialog_wrapper " const classes = classNames("mx_Dialog_wrapper", modal.className, {
+ (this._staticModal ? "mx_Dialog_wrapperWithStaticUnder " : '') mx_Dialog_wrapperWithStaticUnder: this.staticModal,
+ (modal.className ? modal.className : ''); });
const dialog = ( const dialog = (
<div className={classes}> <div className={classes}>
<div className="mx_Dialog"> <div className="mx_Dialog">
{modal.elem} {modal.elem}
</div> </div>
<div className="mx_Dialog_background" onClick={this.onBackgroundClick}></div> <div className="mx_Dialog_background" onClick={this.onBackgroundClick} />
</div> </div>
); );
ReactDOM.render(dialog, this.getOrCreateContainer()); ReactDOM.render(dialog, ModalManager.getOrCreateContainer());
} else { } else {
// This is safe to call repeatedly if we happen to do that // This is safe to call repeatedly if we happen to do that
ReactDOM.unmountComponentAtNode(this.getOrCreateContainer()); ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer());
} }
} }
} }
if (!global.singletonModalManager) { if (!window.singletonModalManager) {
global.singletonModalManager = new ModalManager(); window.singletonModalManager = new ModalManager();
} }
export default global.singletonModalManager; export default window.singletonModalManager;

View file

@ -67,8 +67,8 @@ export default class RebrandListener {
private nagAgainAt?: number = null; private nagAgainAt?: number = null;
static sharedInstance() { static sharedInstance() {
if (!window.mx_RebrandListener) window.mx_RebrandListener = new RebrandListener(); if (!window.mxRebrandListener) window.mxRebrandListener = new RebrandListener();
return window.mx_RebrandListener; return window.mxRebrandListener;
} }
constructor() { constructor() {
@ -114,6 +114,11 @@ export default class RebrandListener {
} }
}; };
onOneTimeToastDismiss = async () => {
localStorage.setItem('mx_rename_dialog_dismissed', 'true');
this.recheck();
};
onNagTimerFired = () => { onNagTimerFired = () => {
this._reshowTimer = null; this._reshowTimer = null;
this.nagAgainAt = null; this.nagAgainAt = null;
@ -143,10 +148,14 @@ export default class RebrandListener {
if (nagToast || oneTimeToast) { if (nagToast || oneTimeToast) {
let description; let description;
let rejectLabel = null;
let onReject = null;
if (nagToast) { if (nagToast) {
description = _t("Use your account to sign in to the latest version"); description = _t("Use your account to sign in to the latest version");
} else { } else {
description = _t("Were excited to announce Riot is now Element"); description = _t("Were excited to announce Riot is now Element");
rejectLabel = _t("Dismiss");
onReject = this.onOneTimeToastDismiss;
} }
ToastStore.sharedInstance().addOrReplaceToast({ ToastStore.sharedInstance().addOrReplaceToast({
@ -157,6 +166,8 @@ export default class RebrandListener {
description, description,
acceptLabel: _t("Learn More"), acceptLabel: _t("Learn More"),
onAccept: nagToast ? this.onNagToastLearnMore : this.onOneTimeToastLearnMore, onAccept: nagToast ? this.onNagToastLearnMore : this.onOneTimeToastLearnMore,
rejectLabel,
onReject,
}, },
component: GenericToast, component: GenericToast,
priority: 20, priority: 20,

View file

@ -34,27 +34,6 @@ export function shouldShowMentionBadge(roomNotifState) {
return MENTION_BADGE_STATES.includes(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) { export function aggregateNotificationCount(rooms) {
return rooms.reduce((result, room) => { return rooms.reduce((result, room) => {
const roomNotifState = getRoomNotifsState(room.roomId); const roomNotifState = getRoomNotifsState(room.roomId);

View file

@ -401,14 +401,16 @@ export const Commands = [
// If we need an identity server but don't have one, things // If we need an identity server but don't have one, things
// get a bit more complex here, but we try to show something // get a bit more complex here, but we try to show something
// meaningful. // meaningful.
let finished = Promise.resolve(); let prom = Promise.resolve();
if ( if (
getAddressType(address) === 'email' && getAddressType(address) === 'email' &&
!MatrixClientPeg.get().getIdentityServerUrl() !MatrixClientPeg.get().getIdentityServerUrl()
) { ) {
const defaultIdentityServerUrl = getDefaultIdentityServerUrl(); const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
if (defaultIdentityServerUrl) { if (defaultIdentityServerUrl) {
({ finished } = Modal.createTrackedDialog('Slash Commands', 'Identity server', const { finished } = Modal.createTrackedDialog<[boolean]>(
'Slash Commands',
'Identity server',
QuestionDialog, { QuestionDialog, {
title: _t("Use an identity server"), title: _t("Use an identity server"),
description: <p>{_t( description: <p>{_t(
@ -421,9 +423,9 @@ export const Commands = [
)}</p>, )}</p>,
button: _t("Continue"), button: _t("Continue"),
}, },
)); );
finished = finished.then(([useDefault]: any) => { prom = finished.then(([useDefault]) => {
if (useDefault) { if (useDefault) {
useDefaultIdentityServer(); useDefaultIdentityServer();
return; return;
@ -435,7 +437,7 @@ export const Commands = [
} }
} }
const inviter = new MultiInviter(roomId); const inviter = new MultiInviter(roomId);
return success(finished.then(() => { return success(prom.then(() => {
return inviter.invite([address]); return inviter.invite([address]);
}).then(() => { }).then(() => {
if (inviter.getCompletionState(address) !== "invited") { if (inviter.getCompletionState(address) !== "invited") {
@ -1049,7 +1051,7 @@ export function parseCommandString(input) {
// trim any trailing whitespace, as it can confuse the parser for // trim any trailing whitespace, as it can confuse the parser for
// IRC-style commands // IRC-style commands
input = input.replace(/\s+$/, ''); 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)*))?$/); const bits = input.match(/^(\S+?)(?: +((.|\n)*))?$/);
let cmd; let cmd;

View file

@ -18,9 +18,9 @@ limitations under the License.
import React from "react"; import React from "react";
import AccessibleButton, {IProps as IAccessibleButtonProps} from "../../components/views/elements/AccessibleButton"; import AccessibleButton from "../../components/views/elements/AccessibleButton";
interface IProps extends IAccessibleButtonProps { interface IProps extends React.ComponentProps<typeof AccessibleButton> {
label?: string; label?: string;
// whether or not the context menu is currently open // whether or not the context menu is currently open
isExpanded: boolean; isExpanded: boolean;

View file

@ -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<typeof AccessibleTooltipButton> {
// whether or not the context menu is currently open
isExpanded: boolean;
}
// Semantic component for representing the AccessibleButton which launches a <ContextMenu />
export const ContextMenuTooltipButton: React.FC<IProps> = ({
isExpanded,
children,
onClick,
onContextMenu,
...props
}) => {
return (
<AccessibleTooltipButton
{...props}
onClick={onClick}
onContextMenu={onContextMenu || onClick}
aria-haspopup={true}
aria-expanded={isExpanded}
>
{ children }
</AccessibleTooltipButton>
);
};

View file

@ -16,7 +16,6 @@ limitations under the License.
*/ */
import { asyncAction } from './actionCreators'; import { asyncAction } from './actionCreators';
import { TAG_DM } from '../stores/RoomListStore';
import Modal from '../Modal'; import Modal from '../Modal';
import * as Rooms from '../Rooms'; import * as Rooms from '../Rooms';
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
@ -24,7 +23,9 @@ import * as sdk from '../index';
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { AsyncActionPayload } from "../dispatcher/payloads"; 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 { export default class RoomListActions {
/** /**
@ -51,9 +52,9 @@ export default class RoomListActions {
let metaData = null; let metaData = null;
// Is the tag ordered manually? // Is the tag ordered manually?
if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { const store = RoomListStore.instance;
const lists = RoomListStoreTempProxy.getRoomLists(); if (newTag && store.getTagSorting(newTag) === SortAlgorithm.Manual) {
const newList = [...lists[newTag]]; const newList = [...store.orderedLists[newTag]];
newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order); newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order);
@ -81,11 +82,11 @@ export default class RoomListActions {
const roomId = room.roomId; const roomId = room.roomId;
// Evil hack to get DMs behaving // Evil hack to get DMs behaving
if ((oldTag === undefined && newTag === TAG_DM) || if ((oldTag === undefined && newTag === DefaultTagID.DM) ||
(oldTag === TAG_DM && newTag === undefined) (oldTag === DefaultTagID.DM && newTag === undefined)
) { ) {
return Rooms.guessAndSetDMRoom( return Rooms.guessAndSetDMRoom(
room, newTag === TAG_DM, room, newTag === DefaultTagID.DM,
).catch((err) => { ).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to set direct chat tag " + err); 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. // but we avoid ever doing a request with TAG_DM.
// //
// if we moved lists, remove the old tag // if we moved lists, remove the old tag
if (oldTag && oldTag !== TAG_DM && if (oldTag && oldTag !== DefaultTagID.DM &&
hasChangedSubLists hasChangedSubLists
) { ) {
const promiseToDelete = matrixClient.deleteRoomTag( const promiseToDelete = matrixClient.deleteRoomTag(
roomId, oldTag, roomId, oldTag,
).catch(function (err) { ).catch(function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to remove tag " + oldTag + " from room: " + err); console.error("Failed to remove tag " + oldTag + " from room: " + err);
Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { 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 we moved lists or the ordering changed, add the new tag
if (newTag && newTag !== TAG_DM && if (newTag && newTag !== DefaultTagID.DM &&
(hasChangedSubLists || metaData) (hasChangedSubLists || metaData)
) { ) {
// metaData is the body of the PUT to set the tag, so it must // metaData is the body of the PUT to set the tag, so it must
// at least be an empty object. // at least be an empty object.
metaData = metaData || {}; 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"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to add tag " + newTag + " to room: " + err); console.error("Failed to add tag " + newTag + " to room: " + err);
Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, {

View file

@ -22,7 +22,6 @@ import { AsyncActionPayload } from "../dispatcher/payloads";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
export default class TagOrderActions { export default class TagOrderActions {
/** /**
* Creates an action thunk that will do an asynchronous request to * Creates an action thunk that will do an asynchronous request to
* move a tag in TagOrderStore to destinationIx. * move a tag in TagOrderStore to destinationIx.

View file

@ -34,7 +34,8 @@ import EMOTICON_REGEX from 'emojibase-regex/emoticon';
const LIMIT = 20; const LIMIT = 20;
// Match for ascii-style ";-)" emoticons or ":wink:" shortcodes provided by emojibase // 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 { interface IEmojiShort {
emoji: IEmoji; emoji: IEmoji;

View file

@ -18,16 +18,15 @@ limitations under the License.
import _at from 'lodash/at'; import _at from 'lodash/at';
import _uniq from 'lodash/uniq'; import _uniq from 'lodash/uniq';
import {removeHiddenChars} from "matrix-js-sdk/src/utils";
function stripDiacritics(str: string): string {
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
interface IOptions<T extends {}> { interface IOptions<T extends {}> {
keys: Array<string | keyof T>; keys: Array<string | keyof T>;
funcs?: Array<(T) => string>; funcs?: Array<(T) => string>;
shouldMatchWordsOnly?: boolean; shouldMatchWordsOnly?: boolean;
shouldMatchPrefix?: boolean; shouldMatchPrefix?: boolean;
// whether to apply unhomoglyph and strip diacritics to fuzz up the search. Defaults to true
fuzzy?: boolean;
} }
/** /**
@ -46,14 +45,10 @@ interface IOptions<T extends {}> {
*/ */
export default class QueryMatcher<T extends Object> { export default class QueryMatcher<T extends Object> {
private _options: IOptions<T>; private _options: IOptions<T>;
private _keys: IOptions<T>["keys"];
private _funcs: Required<IOptions<T>["funcs"]>;
private _items: Map<string, {object: T, keyWeight: number}[]>; private _items: Map<string, {object: T, keyWeight: number}[]>;
constructor(objects: T[], options: IOptions<T> = { keys: [] }) { constructor(objects: T[], options: IOptions<T> = { keys: [] }) {
this._options = options; this._options = options;
this._keys = options.keys;
this._funcs = options.funcs || [];
this.setObjects(objects); this.setObjects(objects);
@ -78,15 +73,17 @@ export default class QueryMatcher<T extends Object> {
// type for their values. We assume that those values who's keys have // type for their values. We assume that those values who's keys have
// been specified will be string. Also, we cannot infer all the // been specified will be string. Also, we cannot infer all the
// types of the keys of the objects at compile. // types of the keys of the objects at compile.
const keyValues = _at<string>(<any>object, this._keys); const keyValues = _at<string>(<any>object, this._options.keys);
for (const f of this._funcs) { if (this._options.funcs) {
keyValues.push(f(object)); for (const f of this._options.funcs) {
keyValues.push(f(object));
}
} }
for (const [index, keyValue] of Object.entries(keyValues)) { for (const [index, keyValue] of Object.entries(keyValues)) {
if (!keyValue) continue; // skip falsy keyValues if (!keyValue) continue; // skip falsy keyValues
const key = stripDiacritics(keyValue).toLowerCase(); const key = this.processQuery(keyValue);
if (!this._items.has(key)) { if (!this._items.has(key)) {
this._items.set(key, []); this._items.set(key, []);
} }
@ -99,7 +96,7 @@ export default class QueryMatcher<T extends Object> {
} }
match(query: string): T[] { match(query: string): T[] {
query = stripDiacritics(query).toLowerCase(); query = this.processQuery(query);
if (this._options.shouldMatchWordsOnly) { if (this._options.shouldMatchWordsOnly) {
query = query.replace(/[^\w]/g, ''); query = query.replace(/[^\w]/g, '');
} }
@ -118,7 +115,7 @@ export default class QueryMatcher<T extends Object> {
const index = resultKey.indexOf(query); const index = resultKey.indexOf(query);
if (index !== -1 && (!this._options.shouldMatchPrefix || index === 0)) { if (index !== -1 && (!this._options.shouldMatchPrefix || index === 0)) {
matches.push( matches.push(
...candidates.map((candidate) => ({index, ...candidate})) ...candidates.map((candidate) => ({index, ...candidate})),
); );
} }
} }
@ -142,4 +139,12 @@ export default class QueryMatcher<T extends Object> {
// Now map the keys to the result objects. Also remove any duplicates. // Now map the keys to the result objects. Also remove any duplicates.
return _uniq(matches.map((match) => match.object)); 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();
}
} }

View file

@ -38,12 +38,13 @@ export default class AutoHideScrollbar extends React.Component {
render() { render() {
return (<div return (<div
ref={this._collectContainerRef} ref={this._collectContainerRef}
style={this.props.style} style={this.props.style}
className={["mx_AutoHideScrollbar", this.props.className].join(" ")} className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
onScroll={this.props.onScroll} onScroll={this.props.onScroll}
onWheel={this.props.onWheel} onWheel={this.props.onWheel}
> tabIndex={this.props.tabIndex}
>
{ this.props.children } { this.props.children }
</div>); </div>);
} }

View file

@ -461,6 +461,7 @@ export function createMenu(ElementClass, props) {
// re-export the semantic helper components for simplicity // re-export the semantic helper components for simplicity
export {ContextMenuButton} from "../../accessibility/context_menu/ContextMenuButton"; export {ContextMenuButton} from "../../accessibility/context_menu/ContextMenuButton";
export {ContextMenuTooltipButton} from "../../accessibility/context_menu/ContextMenuTooltipButton";
export {MenuGroup} from "../../accessibility/context_menu/MenuGroup"; export {MenuGroup} from "../../accessibility/context_menu/MenuGroup";
export {MenuItem} from "../../accessibility/context_menu/MenuItem"; export {MenuItem} from "../../accessibility/context_menu/MenuItem";
export {MenuItemCheckbox} from "../../accessibility/context_menu/MenuItemCheckbox"; export {MenuItemCheckbox} from "../../accessibility/context_menu/MenuItemCheckbox";

View file

@ -72,17 +72,17 @@ class CustomRoomTagTile extends React.Component {
const tag = this.props.tag; const tag = this.props.tag;
const avatarHeight = 40; const avatarHeight = 40;
const className = classNames({ const className = classNames({
CustomRoomTagPanel_tileSelected: tag.selected, "CustomRoomTagPanel_tileSelected": tag.selected,
}); });
const name = tag.name; const name = tag.name;
const badge = tag.badge; const badgeNotifState = tag.badgeNotifState;
let badgeElement; let badgeElement;
if (badge) { if (badgeNotifState) {
const badgeClasses = classNames({ const badgeClasses = classNames({
"mx_TagTile_badge": true, "mx_TagTile_badge": true,
"mx_TagTile_badgeHighlight": badge.highlight, "mx_TagTile_badgeHighlight": badgeNotifState.hasMentions,
}); });
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>); badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badgeNotifState.count)}</div>);
} }
return ( return (

View file

@ -192,7 +192,7 @@ export default class IndicatorScrollbar extends React.Component {
ref={this._collectScrollerComponent} ref={this._collectScrollerComponent}
wrappedRef={this._collectScroller} wrappedRef={this._collectScroller}
onWheel={this.onMouseWheel} onWheel={this.onMouseWheel}
{... this.props} {...this.props}
> >
{ leftOverflowIndicator } { leftOverflowIndicator }
{ this.props.children } { this.props.children }

View file

@ -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 = (<div className="mx_LeftPanel_tagPanelContainer">
<TagPanel />
{ isCustomTagsEnabled ? <CustomRoomTagPanel /> : undefined }
</div>);
}
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 = (
<div className={classNames("mx_LeftPanel_explore", {"mx_LeftPanel_explore_hidden": this.state.searchExpanded})}>
<AccessibleButton onClick={() => dis.fire(Action.ViewRoomDirectory)}>{_t("Explore")}</AccessibleButton>
</div>
);
}
const searchBox = (<SearchBox
className="mx_LeftPanel_filterRooms"
enableRoomSearchFocus={true}
blurredPlaceholder={ _t('Filter') }
placeholder={ _t('Filter rooms…') }
onKeyDown={this._onFilterKeyDown}
onSearch={ this.onSearch }
onCleared={ this.onSearchCleared }
onFocus={this._onSearchFocus}
onBlur={this._onSearchBlur}
collapsed={this.props.collapsed} />);
let breadcrumbs;
if (this.state.breadcrumbs) {
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
}
const roomList = <RoomList
onKeyDown={this._onKeyDown}
onFocus={this._onFocus}
onBlur={this._onBlur}
ref={this.collectRoomList}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ConferenceHandler={VectorConferenceHandler} />;
return (
<div className={containerClasses}>
{ tagPanelContainer }
<aside className="mx_LeftPanel dark-panel">
<TopLeftMenuButton collapsed={this.props.collapsed} />
{ breadcrumbs }
<CallPreview ConferenceHandler={VectorConferenceHandler} />
<div className="mx_LeftPanel_exploreAndFilterRow" onKeyDown={this._onKeyDown} onFocus={this._onFocus} onBlur={this._onBlur}>
{ exploreButton }
{ searchBox }
</div>
{roomList}
</aside>
</div>
);
},
});
export default LeftPanel;

View file

@ -17,25 +17,26 @@ limitations under the License.
import * as React from "react"; import * as React from "react";
import { createRef } from "react"; import { createRef } from "react";
import TagPanel from "./TagPanel"; import TagPanel from "./TagPanel";
import CustomRoomTagPanel from "./CustomRoomTagPanel";
import classNames from "classnames"; import classNames from "classnames";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import RoomList2 from "../views/rooms/RoomList2"; import RoomList from "../views/rooms/RoomList";
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist2"; import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import UserMenu from "./UserMenu"; import UserMenu from "./UserMenu";
import RoomSearch from "./RoomSearch"; import RoomSearch from "./RoomSearch";
import AccessibleButton from "../views/elements/AccessibleButton"; import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2";
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore2"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
import {Key} from "../../Keyboard"; import {Key} from "../../Keyboard";
import IndicatorScrollbar from "../structures/IndicatorScrollbar"; import IndicatorScrollbar from "../structures/IndicatorScrollbar";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367 import { OwnProfileStore } from "../../stores/OwnProfileStore";
import { MatrixClientPeg } from "../../MatrixClientPeg";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
@ -43,7 +44,6 @@ interface IProps {
} }
interface IState { interface IState {
searchFilter: string;
showBreadcrumbs: boolean; showBreadcrumbs: boolean;
showTagPanel: boolean; showTagPanel: boolean;
} }
@ -52,14 +52,15 @@ interface IState {
const cssClasses = [ const cssClasses = [
"mx_RoomSearch_input", "mx_RoomSearch_input",
"mx_RoomSearch_icon", // minimized <RoomSearch /> "mx_RoomSearch_icon", // minimized <RoomSearch />
"mx_RoomSublist2_headerText", "mx_RoomSublist_headerText",
"mx_RoomTile2", "mx_RoomTile",
"mx_RoomSublist2_showNButton", "mx_RoomSublist_showNButton",
]; ];
export default class LeftPanel2 extends React.Component<IProps, IState> { export default class LeftPanel extends React.Component<IProps, IState> {
private listContainerRef: React.RefObject<HTMLDivElement> = createRef(); private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
private tagPanelWatcherRef: string; private tagPanelWatcherRef: string;
private bgImageWatcherRef: string;
private focusedElement = null; private focusedElement = null;
private isDoingStickyHeaders = false; private isDoingStickyHeaders = false;
@ -67,13 +68,15 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
super(props); super(props);
this.state = { this.state = {
searchFilter: "",
showBreadcrumbs: BreadcrumbsStore.instance.visible, showBreadcrumbs: BreadcrumbsStore.instance.visible,
showTagPanel: SettingsStore.getValue('TagPanel.enableTagPanel'), showTagPanel: SettingsStore.getValue('TagPanel.enableTagPanel'),
}; };
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.on(LISTS_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.tagPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
this.setState({showTagPanel: SettingsStore.getValue("TagPanel.enableTagPanel")}); this.setState({showTagPanel: SettingsStore.getValue("TagPanel.enableTagPanel")});
}); });
@ -85,15 +88,13 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
public componentWillUnmount() { public componentWillUnmount() {
SettingsStore.unwatchSetting(this.tagPanelWatcherRef); SettingsStore.unwatchSetting(this.tagPanelWatcherRef);
SettingsStore.unwatchSetting(this.bgImageWatcherRef);
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.off(LISTS_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); this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
} }
private onSearch = (term: string): void => {
this.setState({searchFilter: term});
};
private onExplore = () => { private onExplore = () => {
dis.fire(Action.ViewRoomDirectory); dis.fire(Action.ViewRoomDirectory);
}; };
@ -109,6 +110,20 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
} }
}; };
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) { private handleStickyHeaders(list: HTMLDivElement) {
if (this.isDoingStickyHeaders) return; if (this.isDoingStickyHeaders) return;
this.isDoingStickyHeaders = true; this.isDoingStickyHeaders = true;
@ -121,7 +136,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
private doStickyHeaders(list: HTMLDivElement) { private doStickyHeaders(list: HTMLDivElement) {
const topEdge = list.scrollTop; const topEdge = list.scrollTop;
const bottomEdge = list.offsetHeight + list.scrollTop; const bottomEdge = list.offsetHeight + list.scrollTop;
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist2"); const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist");
const headerRightMargin = 16; // calculated from margins and widths to align with non-sticky tiles const headerRightMargin = 16; // calculated from margins and widths to align with non-sticky tiles
const headerStickyWidth = list.clientWidth - headerRightMargin; const headerStickyWidth = list.clientWidth - headerRightMargin;
@ -137,7 +152,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
let lastTopHeader; let lastTopHeader;
let firstBottomHeader; let firstBottomHeader;
for (const sublist of sublists) { for (const sublist of sublists) {
const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist2_stickable"); const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist_stickable");
header.style.removeProperty("display"); // always clear display:none first header.style.removeProperty("display"); // always clear display:none first
// When an element is <=40% off screen, make it take over // When an element is <=40% off screen, make it take over
@ -173,8 +188,8 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
} }
if (style.stickyTop) { if (style.stickyTop) {
if (!header.classList.contains("mx_RoomSublist2_headerContainer_stickyTop")) { if (!header.classList.contains("mx_RoomSublist_headerContainer_stickyTop")) {
header.classList.add("mx_RoomSublist2_headerContainer_stickyTop"); header.classList.add("mx_RoomSublist_headerContainer_stickyTop");
} }
const newTop = `${list.parentElement.offsetTop}px`; const newTop = `${list.parentElement.offsetTop}px`;
@ -182,8 +197,8 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
header.style.top = newTop; header.style.top = newTop;
} }
} else { } else {
if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyTop")) { if (header.classList.contains("mx_RoomSublist_headerContainer_stickyTop")) {
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop"); header.classList.remove("mx_RoomSublist_headerContainer_stickyTop");
} }
if (header.style.top) { if (header.style.top) {
header.style.removeProperty('top'); header.style.removeProperty('top');
@ -191,18 +206,18 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
} }
if (style.stickyBottom) { if (style.stickyBottom) {
if (!header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) { if (!header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) {
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom"); header.classList.add("mx_RoomSublist_headerContainer_stickyBottom");
} }
} else { } else {
if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) { if (header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) {
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom"); header.classList.remove("mx_RoomSublist_headerContainer_stickyBottom");
} }
} }
if (style.stickyTop || style.stickyBottom) { if (style.stickyTop || style.stickyBottom) {
if (!header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) { if (!header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
header.classList.add("mx_RoomSublist2_headerContainer_sticky"); header.classList.add("mx_RoomSublist_headerContainer_sticky");
} }
const newWidth = `${headerStickyWidth}px`; const newWidth = `${headerStickyWidth}px`;
@ -210,8 +225,8 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
header.style.width = newWidth; header.style.width = newWidth;
} }
} else if (!style.stickyTop && !style.stickyBottom) { } else if (!style.stickyTop && !style.stickyBottom) {
if (header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) { if (header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
header.classList.remove("mx_RoomSublist2_headerContainer_sticky"); header.classList.remove("mx_RoomSublist_headerContainer_sticky");
} }
if (header.style.width) { if (header.style.width) {
header.style.removeProperty('width'); header.style.removeProperty('width');
@ -221,16 +236,16 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
// add appropriate sticky classes to wrapper so it has // add appropriate sticky classes to wrapper so it has
// the necessary top/bottom padding to put the sticky header in // the necessary top/bottom padding to put the sticky header in
const listWrapper = list.parentElement; // .mx_LeftPanel2_roomListWrapper const listWrapper = list.parentElement; // .mx_LeftPanel_roomListWrapper
if (lastTopHeader) { if (lastTopHeader) {
listWrapper.classList.add("mx_LeftPanel2_roomListWrapper_stickyTop"); listWrapper.classList.add("mx_LeftPanel_roomListWrapper_stickyTop");
} else { } else {
listWrapper.classList.remove("mx_LeftPanel2_roomListWrapper_stickyTop"); listWrapper.classList.remove("mx_LeftPanel_roomListWrapper_stickyTop");
} }
if (firstBottomHeader) { if (firstBottomHeader) {
listWrapper.classList.add("mx_LeftPanel2_roomListWrapper_stickyBottom"); listWrapper.classList.add("mx_LeftPanel_roomListWrapper_stickyBottom");
} else { } else {
listWrapper.classList.remove("mx_LeftPanel2_roomListWrapper_stickyBottom"); listWrapper.classList.remove("mx_LeftPanel_roomListWrapper_stickyBottom");
} }
} }
@ -266,7 +281,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
}; };
private onEnter = () => { private onEnter = () => {
const firstRoom = this.listContainerRef.current.querySelector<HTMLDivElement>(".mx_RoomTile2"); const firstRoom = this.listContainerRef.current.querySelector<HTMLDivElement>(".mx_RoomTile");
if (firstRoom) { if (firstRoom) {
firstRoom.click(); firstRoom.click();
return true; // to get the field to clear return true; // to get the field to clear
@ -314,7 +329,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
private renderHeader(): React.ReactNode { private renderHeader(): React.ReactNode {
return ( return (
<div className="mx_LeftPanel2_userHeader"> <div className="mx_LeftPanel_userHeader">
<UserMenu isMinimized={this.props.isMinimized} /> <UserMenu isMinimized={this.props.isMinimized} />
</div> </div>
); );
@ -324,10 +339,13 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
if (this.state.showBreadcrumbs && !this.props.isMinimized) { if (this.state.showBreadcrumbs && !this.props.isMinimized) {
return ( return (
<IndicatorScrollbar <IndicatorScrollbar
className="mx_LeftPanel2_breadcrumbsContainer mx_AutoHideScrollbar" className="mx_LeftPanel_breadcrumbsContainer mx_AutoHideScrollbar"
verticalScrollsHorizontally={true} verticalScrollsHorizontally={true}
// Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order.
tabIndex={-1}
> >
<RoomBreadcrumbs2 /> <RoomBreadcrumbs />
</IndicatorScrollbar> </IndicatorScrollbar>
); );
} }
@ -336,19 +354,18 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
private renderSearchExplore(): React.ReactNode { private renderSearchExplore(): React.ReactNode {
return ( return (
<div <div
className="mx_LeftPanel2_filterContainer" className="mx_LeftPanel_filterContainer"
onFocus={this.onFocus} onFocus={this.onFocus}
onBlur={this.onBlur} onBlur={this.onBlur}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
> >
<RoomSearch <RoomSearch
onQueryUpdate={this.onSearch}
isMinimized={this.props.isMinimized} isMinimized={this.props.isMinimized}
onVerticalArrow={this.onKeyDown} onVerticalArrow={this.onKeyDown}
onEnter={this.onEnter} onEnter={this.onEnter}
/> />
<AccessibleButton <AccessibleTooltipButton
className="mx_LeftPanel2_exploreButton" className="mx_LeftPanel_exploreButton"
onClick={this.onExplore} onClick={this.onExplore}
title={_t("Explore rooms")} title={_t("Explore rooms")}
/> />
@ -358,16 +375,16 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
public render(): React.ReactNode { public render(): React.ReactNode {
const tagPanel = !this.state.showTagPanel ? null : ( const tagPanel = !this.state.showTagPanel ? null : (
<div className="mx_LeftPanel2_tagPanelContainer"> <div className="mx_LeftPanel_tagPanelContainer">
<TagPanel/> <TagPanel/>
{SettingsStore.isFeatureEnabled("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
</div> </div>
); );
const roomList = <RoomList2 const roomList = <RoomList
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
resizeNotifier={null} resizeNotifier={null}
collapsed={false} collapsed={false}
searchFilter={this.state.searchFilter}
onFocus={this.onFocus} onFocus={this.onFocus}
onBlur={this.onBlur} onBlur={this.onBlur}
isMinimized={this.props.isMinimized} isMinimized={this.props.isMinimized}
@ -375,24 +392,24 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
/>; />;
const containerClasses = classNames({ const containerClasses = classNames({
"mx_LeftPanel2": true, "mx_LeftPanel": true,
"mx_LeftPanel2_hasTagPanel": !!tagPanel, "mx_LeftPanel_hasTagPanel": !!tagPanel,
"mx_LeftPanel2_minimized": this.props.isMinimized, "mx_LeftPanel_minimized": this.props.isMinimized,
}); });
const roomListClasses = classNames( const roomListClasses = classNames(
"mx_LeftPanel2_actualRoomListContainer", "mx_LeftPanel_actualRoomListContainer",
"mx_AutoHideScrollbar", "mx_AutoHideScrollbar",
); );
return ( return (
<div className={containerClasses}> <div className={containerClasses}>
{tagPanel} {tagPanel}
<aside className="mx_LeftPanel2_roomListContainer"> <aside className="mx_LeftPanel_roomListContainer">
{this.renderHeader()} {this.renderHeader()}
{this.renderSearchExplore()} {this.renderSearchExplore()}
{this.renderBreadcrumbs()} {this.renderBreadcrumbs()}
<div className="mx_LeftPanel2_roomListWrapper"> <div className="mx_LeftPanel_roomListWrapper">
<div <div
className={roomListClasses} className={roomListClasses}
onScroll={this.onScroll} onScroll={this.onScroll}

View file

@ -40,7 +40,6 @@ import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts";
import HomePage from "./HomePage"; import HomePage from "./HomePage";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import PlatformPeg from "../../PlatformPeg"; import PlatformPeg from "../../PlatformPeg";
import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy";
import { DefaultTagID } from "../../stores/room-list/models"; import { DefaultTagID } from "../../stores/room-list/models";
import { import {
showToast as showSetPasswordToast, showToast as showSetPasswordToast,
@ -51,9 +50,10 @@ import {
hideToast as hideServerLimitToast hideToast as hideServerLimitToast
} from "../../toasts/ServerLimitToast"; } from "../../toasts/ServerLimitToast";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import LeftPanel2 from "./LeftPanel2"; import LeftPanel from "./LeftPanel";
import CallContainer from '../views/voip/CallContainer'; import CallContainer from '../views/voip/CallContainer';
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload"; import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
import RoomListStore from "../../stores/room-list/RoomListStore";
// We need to fetch each pinned message individually (if we don't already have it) // 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. // so each pinned message may trigger a request. Limit the number per room for sanity.
@ -308,8 +308,8 @@ class LoggedInView extends React.Component<IProps, IState> {
}; };
onRoomStateEvents = (ev, state) => { onRoomStateEvents = (ev, state) => {
const roomLists = RoomListStoreTempProxy.getRoomLists(); const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
if (roomLists[DefaultTagID.ServerNotice] && roomLists[DefaultTagID.ServerNotice].some(r => r.roomId === ev.getRoomId())) { if (serverNoticeList && serverNoticeList.some(r => r.roomId === ev.getRoomId())) {
this._updateServerNoticeEvents(); this._updateServerNoticeEvents();
} }
}; };
@ -328,11 +328,11 @@ class LoggedInView extends React.Component<IProps, IState> {
} }
_updateServerNoticeEvents = async () => { _updateServerNoticeEvents = async () => {
const roomLists = RoomListStoreTempProxy.getRoomLists(); const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
if (!roomLists[DefaultTagID.ServerNotice]) return []; if (!serverNoticeList) return [];
const events = []; const events = [];
for (const room of roomLists[DefaultTagID.ServerNotice]) { for (const room of serverNoticeList) {
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", ""); const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue; if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
@ -607,7 +607,6 @@ class LoggedInView extends React.Component<IProps, IState> {
}; };
render() { render() {
const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RoomView = sdk.getComponent('structures.RoomView'); const RoomView = sdk.getComponent('structures.RoomView');
const UserView = sdk.getComponent('structures.UserView'); const UserView = sdk.getComponent('structures.UserView');
const GroupView = sdk.getComponent('structures.GroupView'); const GroupView = sdk.getComponent('structures.GroupView');
@ -661,21 +660,12 @@ class LoggedInView extends React.Component<IProps, IState> {
bodyClasses += ' mx_MatrixChat_useCompactLayout'; bodyClasses += ' mx_MatrixChat_useCompactLayout';
} }
let leftPanel = ( const leftPanel = (
<LeftPanel <LeftPanel
isMinimized={this.props.collapseLhs || false}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/> />
); );
if (SettingsStore.getValue("feature_new_room_list")) {
leftPanel = (
<LeftPanel2
isMinimized={this.props.collapseLhs || false}
resizeNotifier={this.props.resizeNotifier}
/>
);
}
return ( return (
<MatrixClientContext.Provider value={this._matrixClient}> <MatrixClientContext.Provider value={this._matrixClient}>

View file

@ -16,77 +16,24 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import ResizeHandle from '../views/elements/ResizeHandle'; import { Resizable } from 're-resizable';
import {Resizer, FixedDistributor} from '../../resizer';
export default class MainSplit extends React.Component { export default class MainSplit extends React.Component {
constructor(props) { _onResized = (event, direction, refToElement, delta) => {
super(props); window.localStorage.setItem("mx_rhs_size", this._loadSidePanelSize().width + delta.width);
this._setResizeContainerRef = this._setResizeContainerRef.bind(this);
this._onResized = this._onResized.bind(this);
} }
_onResized(size) { _loadSidePanelSize() {
window.localStorage.setItem("mx_rhs_size", size); let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10);
if (this.props.resizeNotifier) {
this.props.resizeNotifier.notifyRightHandleResized();
}
}
_createResizer() { if (isNaN(rhsSize)) {
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 {
rhsSize = 350; rhsSize = 350;
} }
resizer.forHandleAt(0).resize(rhsSize);
resizer.attach(); return {
this.resizer = resizer; height: "100%",
} width: rhsSize,
};
_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;
}
} }
render() { render() {
@ -97,13 +44,29 @@ export default class MainSplit extends React.Component {
let children; let children;
if (hasResizer) { if (hasResizer) {
children = <React.Fragment> children = <Resizable
<ResizeHandle reverse={true} /> defaultSize={this._loadSidePanelSize()}
minWidth={264}
maxWidth="50%"
enable={{
top: false,
right: false,
bottom: false,
left: true,
topRight: false,
bottomRight: false,
bottomLeft: false,
topLeft: false,
}}
onResizeStop={this._onResized}
className="mx_RightPanel_ResizeWrapper"
handleClasses={{left: "mx_RightPanel_ResizeHandle"}}
>
{ panelView } { panelView }
</React.Fragment>; </Resizable>;
} }
return <div className="mx_MainSplit" ref={hasResizer ? this._setResizeContainerRef : undefined}> return <div className="mx_MainSplit">
{ bodyView } { bodyView }
{ children } { children }
</div>; </div>;

View file

@ -58,7 +58,6 @@ import { messageForSyncError } from '../../utils/ErrorUtils';
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
import DMRoomMap from '../../utils/DMRoomMap'; import DMRoomMap from '../../utils/DMRoomMap';
import { countRoomsWithNotif } from '../../RoomNotifs';
import ThemeWatcher from "../../settings/watchers/ThemeWatcher"; import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
import { FontWatcher } from '../../settings/watchers/FontWatcher'; import { FontWatcher } from '../../settings/watchers/FontWatcher';
import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { storeRoomAliasInCache } from '../../RoomAliasCache';
@ -75,6 +74,7 @@ import {
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast"; import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import ErrorDialog from "../views/dialogs/ErrorDialog"; import ErrorDialog from "../views/dialogs/ErrorDialog";
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
@ -1844,21 +1844,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
updateStatusIndicator(state: string, prevState: string) { updateStatusIndicator(state: string, prevState: string) {
// only count visible rooms to not torment the user with notification counts in rooms they can't see const notificationState = RoomNotificationStateStore.instance.globalState;
// it will include highlights from the previous version of the room internally const numUnreadRooms = notificationState.numUnreadStates; // we know that states === rooms here
const notifCount = countRoomsWithNotif(MatrixClientPeg.get().getVisibleRooms()).count;
if (PlatformPeg.get()) { if (PlatformPeg.get()) {
PlatformPeg.get().setErrorStatus(state === 'ERROR'); PlatformPeg.get().setErrorStatus(state === 'ERROR');
PlatformPeg.get().setNotificationCount(notifCount); PlatformPeg.get().setNotificationCount(numUnreadRooms);
} }
this.subTitleStatus = ''; this.subTitleStatus = '';
if (state === "ERROR") { if (state === "ERROR") {
this.subTitleStatus += `[${_t("Offline")}] `; this.subTitleStatus += `[${_t("Offline")}] `;
} }
if (notifCount > 0) { if (numUnreadRooms > 0) {
this.subTitleStatus += `[${notifCount}]`; this.subTitleStatus += `[${numUnreadRooms}]`;
} }
this.setPageSubtitle(); this.setPageSubtitle();

View file

@ -346,9 +346,9 @@ export default class MessagePanel extends React.Component {
} }
} }
_isUnmounting() { _isUnmounting = () => {
return !this._isMounted; return !this._isMounted;
} };
// TODO: Implement granular (per-room) hide options // TODO: Implement granular (per-room) hide options
_shouldShowEvent(mxEv) { _shouldShowEvent(mxEv) {
@ -571,12 +571,10 @@ export default class MessagePanel extends React.Component {
const readReceipts = this._readReceiptsByEvent[eventId]; const readReceipts = this._readReceiptsByEvent[eventId];
// Dev note: `this._isUnmounting.bind(this)` is important - it ensures that // use txnId as key if available so that we don't remount during sending
// 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).
ret.push( ret.push(
<li key={eventId} <li
key={mxEv.getTxnId() || eventId}
ref={this._collectEventNode.bind(this, eventId)} ref={this._collectEventNode.bind(this, eventId)}
data-scroll-tokens={scrollToken} data-scroll-tokens={scrollToken}
> >
@ -590,7 +588,7 @@ export default class MessagePanel extends React.Component {
readReceipts={readReceipts} readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap} readReceiptMap={this._readReceiptMap}
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting.bind(this)} checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.getAssociatedStatus()} eventSendStatus={mxEv.getAssociatedStatus()}
tileShape={this.props.tileShape} tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour} isTwelveHour={this.props.isTwelveHour}

View file

@ -24,9 +24,10 @@ import { throttle } from 'lodash';
import { Key } from "../../Keyboard"; import { Key } from "../../Keyboard";
import AccessibleButton from "../views/elements/AccessibleButton"; import AccessibleButton from "../views/elements/AccessibleButton";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import RoomListStore from "../../stores/room-list/RoomListStore";
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
interface IProps { interface IProps {
onQueryUpdate: (newQuery: string) => void;
isMinimized: boolean; isMinimized: boolean;
onVerticalArrow(ev: React.KeyboardEvent): void; onVerticalArrow(ev: React.KeyboardEvent): void;
onEnter(ev: React.KeyboardEvent): boolean; onEnter(ev: React.KeyboardEvent): boolean;
@ -40,6 +41,7 @@ interface IState {
export default class RoomSearch extends React.PureComponent<IProps, IState> { export default class RoomSearch extends React.PureComponent<IProps, IState> {
private dispatcherRef: string; private dispatcherRef: string;
private inputRef: React.RefObject<HTMLInputElement> = createRef(); private inputRef: React.RefObject<HTMLInputElement> = createRef();
private searchFilter: NameFilterCondition = new NameFilterCondition();
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
@ -52,6 +54,21 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
this.dispatcherRef = defaultDispatcher.register(this.onAction); this.dispatcherRef = defaultDispatcher.register(this.onAction);
} }
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): 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() { public componentWillUnmount() {
defaultDispatcher.unregister(this.dispatcherRef); defaultDispatcher.unregister(this.dispatcherRef);
} }
@ -78,19 +95,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
private onChange = () => { private onChange = () => {
if (!this.inputRef.current) return; if (!this.inputRef.current) return;
this.setState({query: this.inputRef.current.value}); this.setState({query: this.inputRef.current.value});
this.onSearchUpdated();
}; };
// it wants this at the top of the file, but we know better
// tslint:disable-next-line
private onSearchUpdated = throttle(
() => {
// We can't use the state variable because it can lag behind the input.
// The lag is most obvious when deleting/clearing text with the keyboard.
this.props.onQueryUpdate(this.inputRef.current.value);
}, 200, {trailing: true, leading: true},
);
private onFocus = (ev: React.FocusEvent<HTMLInputElement>) => { private onFocus = (ev: React.FocusEvent<HTMLInputElement>) => {
this.setState({focused: true}); this.setState({focused: true});
ev.target.select(); ev.target.select();

View file

@ -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 (<div className="mx_RoomTile mx_RoomTileError">
{this.props.roomId}
</div>);
} 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 <RoomTileErrorBoundary roomId={room.roomId}><RoomTile
room={room}
roomSubList={this}
tagName={this.props.tagName}
key={room.roomId}
collapsed={this.props.collapsed || false}
unread={Unread.doesRoomHaveUnreadMessages(room)}
highlight={this.props.isInvite || RoomNotifs.getUnreadNotificationCount(room, 'highlight') > 0}
notificationCount={RoomNotifs.getUnreadNotificationCount(room)}
isInvite={this.props.isInvite}
refreshSubList={this._updateSubListCount}
incomingCall={null}
onClick={this.onRoomTileClick}
/></RoomTileErrorBoundary>;
};
_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 =
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.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 = (<div className={chevronClasses} />);
}
return <RovingTabIndexWrapper inputRef={this._headerButton}>
{({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 = (
<AccessibleButton
tabIndex={tabIndex}
className={badgeClasses}
onClick={this._onNotifBadgeClick}
aria-label={_t("Jump to first unread room.")}
>
<div>
{ FormattingUtils.formatCount(subListNotifCount) }
</div>
</AccessibleButton>
);
} else if (this.props.isInvite && this.props.list.length) {
// no notifications but highlight anyway because this is an invite badge
badge = (
<AccessibleButton
tabIndex={tabIndex}
className={badgeClasses}
onClick={this._onInviteBadgeClick}
aria-label={_t("Jump to first invite.")}
>
<div>
{ this.props.list.length }
</div>
</AccessibleButton>
);
}
}
let addRoomButton;
if (this.props.onAddRoom) {
addRoomButton = (
<AccessibleTooltipButton
tabIndex={tabIndex}
onClick={this.onAddRoom}
className="mx_RoomSubList_addRoom"
title={this.props.addRoomLabel || _t("Add room")}
/>
);
}
return (
<div className="mx_RoomSubList_labelContainer" title={title} ref={this._header} onKeyDown={this.onHeaderKeyDown}>
<AccessibleButton
onFocus={onFocus}
tabIndex={tabIndex}
inputRef={ref}
onClick={this.onClick}
className="mx_RoomSubList_label"
aria-expanded={!isCollapsed}
role="treeitem"
aria-level="1"
>
{ chevron }
<span>{this.props.label}</span>
{ incomingCall }
</AccessibleButton>
{ badge }
{ addRoomButton }
</div>
);
} }
</RovingTabIndexWrapper>;
}
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 = (
<IndicatorScrollbar ref={this._scroller} className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
<LazyRenderList
scrollTop={this.state.scrollTop }
height={ this.state.scrollerHeight }
renderItem={ this.makeRoomTile }
itemHeight={34}
items={ this.props.list } />
</IndicatorScrollbar>
);
} else {
const roomTiles = this.props.list.map(r => this.makeRoomTile(r));
const tiles = roomTiles.concat(this.props.extraTiles);
content = (
<IndicatorScrollbar ref={this._scroller} className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
{ tiles }
</IndicatorScrollbar>
);
}
} else {
if (this.props.showSpinner && !isCollapsed) {
const Loader = sdk.getComponent("elements.Spinner");
content = <Loader />;
}
}
return (
<div
ref={this._subList}
className={subListClasses}
role="group"
aria-label={this.props.label}
onKeyDown={this.onKeyDown}
>
{ this._getHeaderJsx(isCollapsed) }
{ content }
</div>
);
}
}

View file

@ -648,7 +648,9 @@ export default createReactClass({
if (scrollState.stuckAtBottom) { if (scrollState.stuckAtBottom) {
const sn = this._getScrollNode(); const sn = this._getScrollNode();
sn.scrollTop = sn.scrollHeight; if (sn.scrollTop !== sn.scrollHeight) {
sn.scrollTop = sn.scrollHeight;
}
} else if (scrollState.trackedScrollToken) { } else if (scrollState.trackedScrollToken) {
const itemlist = this._itemlist.current; const itemlist = this._itemlist.current;
const trackedNode = this._getTrackedNode(); const trackedNode = this._getTrackedNode();
@ -657,7 +659,10 @@ export default createReactClass({
const bottomDiff = newBottomOffset - scrollState.bottomOffset; const bottomDiff = newBottomOffset - scrollState.bottomOffset;
this._bottomGrowth += bottomDiff; this._bottomGrowth += bottomDiff;
scrollState.bottomOffset = newBottomOffset; 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); debuglog("balancing height because messages below viewport grew by", bottomDiff);
} }
} }
@ -694,12 +699,16 @@ export default createReactClass({
const height = Math.max(minHeight, contentHeight); const height = Math.max(minHeight, contentHeight);
this._pages = Math.ceil(height / PAGE_SIZE); this._pages = Math.ceil(height / PAGE_SIZE);
this._bottomGrowth = 0; this._bottomGrowth = 0;
const newHeight = this._getListHeight(); const newHeight = `${this._getListHeight()}px`;
const scrollState = this.scrollState; const scrollState = this.scrollState;
if (scrollState.stuckAtBottom) { if (scrollState.stuckAtBottom) {
itemlist.style.height = `${newHeight}px`; if (itemlist.style.height !== newHeight) {
sn.scrollTop = sn.scrollHeight; itemlist.style.height = newHeight;
}
if (sn.scrollTop !== sn.scrollHeight){
sn.scrollTop = sn.scrollHeight;
}
debuglog("updateHeight to", newHeight); debuglog("updateHeight to", newHeight);
} else if (scrollState.trackedScrollToken) { } else if (scrollState.trackedScrollToken) {
const trackedNode = this._getTrackedNode(); const trackedNode = this._getTrackedNode();
@ -709,7 +718,9 @@ export default createReactClass({
// the currently filled piece of the timeline // the currently filled piece of the timeline
if (trackedNode) { if (trackedNode) {
const oldTop = trackedNode.offsetTop; const oldTop = trackedNode.offsetTop;
itemlist.style.height = `${newHeight}px`; if (itemlist.style.height !== newHeight) {
itemlist.style.height = newHeight;
}
const newTop = trackedNode.offsetTop; const newTop = trackedNode.offsetTop;
const topDiff = newTop - oldTop; const topDiff = newTop - oldTop;
// important to scroll by a relative amount as // important to scroll by a relative amount as

View file

@ -1,158 +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";
import {Action} from "../../dispatcher/actions";
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 === Action.ToggleUserMenu) {
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 = <div className="mx_TopLeftMenuButton_name">
{ name }
</div>;
chevronElement = <span className="mx_TopLeftMenuButton_chevron" />;
}
let contextMenu;
if (this.state.menuDisplayed) {
const elementRect = this._buttonRef.getBoundingClientRect();
contextMenu = (
<ContextMenu
chevronFace="none"
left={elementRect.left}
top={elementRect.top + elementRect.height}
onFinished={this.closeMenu}
>
<TopLeftMenu displayName={name} userId={cli} onFinished={this.closeMenu} />
</ContextMenu>
);
}
return <React.Fragment>
<ContextMenuButton
className="mx_TopLeftMenuButton"
onClick={this.openMenu}
inputRef={(r) => this._buttonRef = r}
label={_t("Your profile")}
isExpanded={this.state.menuDisplayed}
>
<BaseAvatar
idName={MatrixClientPeg.get().getUserId()}
name={name}
url={this.state.profileInfo && this.state.profileInfo.avatarUrl}
width={AVATAR_SIZE}
height={AVATAR_SIZE}
resizeMethod="crop"
/>
{ nameElement }
{ chevronElement }
</ContextMenuButton>
{ contextMenu }
</React.Fragment>;
}
}

View file

@ -306,9 +306,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
public render() { public render() {
const avatarSize = 32; // should match border-radius of the avatar const avatarSize = 32; // should match border-radius of the avatar
const {body} = document;
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
body.style.setProperty("--avatar-url", `url('${avatarUrl}')`);
let name = <span className="mx_UserMenu_userName">{OwnProfileStore.instance.displayName}</span>; let name = <span className="mx_UserMenu_userName">{OwnProfileStore.instance.displayName}</span>;
let buttons = ( let buttons = (

View file

@ -134,7 +134,7 @@ const BaseAvatar = (props: IProps) => {
aria-hidden="true" /> aria-hidden="true" />
); );
if (onClick !== null) { if (onClick) {
return ( return (
<AccessibleButton <AccessibleButton
{...otherProps} {...otherProps}
@ -162,7 +162,7 @@ const BaseAvatar = (props: IProps) => {
} }
} }
if (onClick !== null) { if (onClick) {
return ( return (
<AccessibleButton <AccessibleButton
className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)} className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)}
@ -196,4 +196,4 @@ const BaseAvatar = (props: IProps) => {
}; };
export default BaseAvatar; export default BaseAvatar;
export type BaseAvatarType = React.FC<IProps>; export type BaseAvatarType = React.FC<IProps>;

View file

@ -44,7 +44,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
super(props); super(props);
this.state = { this.state = {
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room, this.props.tag), notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room),
}; };
} }

View file

@ -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 (
<button className="mx_CreateRoomButton" onClick={this.onClick}>{ _t("Create Room") }</button>
);
},
});

View file

@ -416,7 +416,7 @@ class RoomStateExplorer extends React.PureComponent {
{ {
Array.from(this.roomStateEvents.entries()).map(([eventType, allStateKeys]) => { Array.from(this.roomStateEvents.entries()).map(([eventType, allStateKeys]) => {
let onClickFn; let onClickFn;
if (allStateKeys.size() === 1 && allStateKeys.has("")) { if (allStateKeys.size === 1 && allStateKeys.has("")) {
onClickFn = this.onViewSourceClick(allStateKeys.get("")); onClickFn = this.onViewSourceClick(allStateKeys.get(""));
} else { } else {
onClickFn = this.browseEventType(eventType); onClickFn = this.browseEventType(eventType);

View file

@ -35,8 +35,11 @@ import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../
import {inviteMultipleToRoom} from "../../../RoomInvite"; import {inviteMultipleToRoom} from "../../../RoomInvite";
import {Key} from "../../../Keyboard"; import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy";
import {DefaultTagID} from "../../../stores/room-list/models"; 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_DM = "dm";
export const KIND_INVITE = "invite"; 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 // 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. // room list doesn't tag the room for the DMRoomMap, but does for the room list.
const taggedRooms = RoomListStoreTempProxy.getRoomLists(); const dmTaggedRooms = RoomListStore.instance.orderedLists[DefaultTagID.DM];
const dmTaggedRooms = taggedRooms[DefaultTagID.DM];
const myUserId = MatrixClientPeg.get().getUserId(); const myUserId = MatrixClientPeg.get().getUserId();
for (const dmRoom of dmTaggedRooms) { for (const dmRoom of dmTaggedRooms) {
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId); const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);

View file

@ -27,7 +27,7 @@ export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Elemen
* onClick: (required) Event handler for button activation. Should be * onClick: (required) Event handler for button activation. Should be
* implemented exactly like a normal onClick handler. * implemented exactly like a normal onClick handler.
*/ */
export interface IProps extends React.InputHTMLAttributes<Element> { interface IProps extends React.InputHTMLAttributes<Element> {
inputRef?: React.Ref<Element>; inputRef?: React.Ref<Element>;
element?: string; element?: string;
// The kind of button, similar to how Bootstrap works. // The kind of button, similar to how Bootstrap works.
@ -118,7 +118,7 @@ export default function AccessibleButton({
AccessibleButton.defaultProps = { AccessibleButton.defaultProps = {
element: 'div', element: 'div',
role: 'button', role: 'button',
tabIndex: "0", tabIndex: 0,
}; };
AccessibleButton.displayName = "AccessibleButton"; AccessibleButton.displayName = "AccessibleButton";

View file

@ -19,10 +19,9 @@ import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import AccessibleButton from "./AccessibleButton"; import AccessibleButton from "./AccessibleButton";
import {IProps} from "./AccessibleButton";
import Tooltip from './Tooltip'; import Tooltip from './Tooltip';
interface ITooltipProps extends IProps { interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
title: string; title: string;
tooltip?: React.ReactNode; tooltip?: React.ReactNode;
tooltipClassName?: string; tooltipClassName?: string;
@ -46,7 +45,7 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
}); });
}; };
onMouseOut = () => { onMouseLeave = () => {
this.setState({ this.setState({
hover: false, hover: false,
}); });
@ -61,7 +60,12 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
label={tooltip || title} label={tooltip || title}
/> : <div />; /> : <div />;
return ( return (
<AccessibleButton {...props} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} aria-label={title}> <AccessibleButton
{...props}
onMouseOver={this.onMouseOver}
onMouseLeave={this.onMouseLeave}
aria-label={title}
>
{ children } { children }
{ tip } { tip }
</AccessibleButton> </AccessibleButton>

View file

@ -704,6 +704,7 @@ export default class AppTile extends React.Component {
_onReloadWidgetClick() { _onReloadWidgetClick() {
// Reload iframe in this way to avoid cross-origin restrictions // 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; this._appFrame.current.src = this._appFrame.current.src;
} }

View file

@ -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 (
<ActionButton action="view_create_room"
mouseOverAction={props.callout ? "callout_create_room" : null}
label={_t("Create new room")}
iconPath={require("../../../../res/img/icons-create-room.svg")}
size={props.size}
tooltip={props.tooltip}
/>
);
};
CreateRoomButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default CreateRoomButton;

View file

@ -248,6 +248,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
tooltipClassName={classNames("mx_Field_tooltip", tooltipClassName)} tooltipClassName={classNames("mx_Field_tooltip", tooltipClassName)}
visible={(this.state.focused && this.props.forceTooltipVisible) || this.state.feedbackVisible} visible={(this.state.focused && this.props.forceTooltipVisible) || this.state.feedbackVisible}
label={tooltipContent || this.state.feedback} label={tooltipContent || this.state.feedback}
forceOnRight
/>; />;
} }

View file

@ -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 = <div className={"mx_InteractiveTooltip_chevron_" + chevronFace} />;
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 = <div className="mx_InteractiveTooltip_wrapper" style={{...position}}>
<div className={menuClasses}
style={menuStyle}
ref={this.collectContentRect}
>
{chevron}
{this.props.content}
</div>
</div>;
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,
});
}
}

View file

@ -17,10 +17,10 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import AccessibleTooltipButton from "./AccessibleTooltipButton";
export default class ManageIntegsButton extends React.Component { export default class ManageIntegsButton extends React.Component {
constructor(props) { constructor(props) {
@ -45,9 +45,8 @@ export default class ManageIntegsButton extends React.Component {
render() { render() {
let integrationsButton = <div />; let integrationsButton = <div />;
if (IntegrationManagers.sharedInstance().hasManager()) { if (IntegrationManagers.sharedInstance().hasManager()) {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
integrationsButton = ( integrationsButton = (
<AccessibleButton <AccessibleTooltipButton
className='mx_RoomHeader_button mx_RoomHeader_manageIntegsButton' className='mx_RoomHeader_button mx_RoomHeader_manageIntegsButton'
title={_t("Manage Integrations")} title={_t("Manage Integrations")}
onClick={this.onManageIntegrations} onClick={this.onManageIntegrations}

View file

@ -27,6 +27,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import escapeHtml from "escape-html"; import escapeHtml from "escape-html";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions"; 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 // 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 // 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 // Part of Replies fallback support
static stripHTMLReply(html) { static stripHTMLReply(html) {
return html.replace(/^<mx-reply>[\s\S]+?<\/mx-reply>/, ''); // Sanitize the original HTML for inclusion in <mx-reply>. 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 // Part of Replies fallback support
@ -102,15 +117,19 @@ export default class ReplyThread extends React.Component {
let {body, formatted_body: html} = ev.getContent(); let {body, formatted_body: html} = ev.getContent();
if (this.getParentEventId(ev)) { if (this.getParentEventId(ev)) {
if (body) body = this.stripPlainReply(body); if (body) body = this.stripPlainReply(body);
if (html) html = this.stripHTMLReply(html);
} }
if (!body) body = ""; // Always ensure we have a body, for reasons. if (!body) body = ""; // Always ensure we have a body, for reasons.
// Escape the body to use as HTML below. if (html) {
// We also run a nl2br over the result to fix the fallback representation. We do this // sanitize the HTML before we put it in an <mx-reply>
// after converting the text to safe HTML to avoid user-provided BR's from being converted. html = this.stripHTMLReply(html);
if (!html) html = escapeHtml(body).replace(/\n/g, '<br/>'); } 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, '<br/>');
}
// dev note: do not rely on `body` being safe for HTML usage below. // dev note: do not rely on `body` being safe for HTML usage below.

View file

@ -29,6 +29,7 @@ import FlairStore from '../../../stores/FlairStore';
import GroupStore from '../../../stores/GroupStore'; import GroupStore from '../../../stores/GroupStore';
import TagOrderStore from '../../../stores/TagOrderStore'; import TagOrderStore from '../../../stores/TagOrderStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AccessibleButton from "./AccessibleButton";
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents // A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
// a thing to click on for the user to filter the visible rooms in the RoomList to: // a thing to click on for the user to filter the visible rooms in the RoomList to:
@ -114,7 +115,7 @@ export default createReactClass({
this.setState({ hover: true }); this.setState({ hover: true });
}, },
onMouseOut: function() { onMouseLeave: function() {
this.setState({ hover: false }); this.setState({ hover: false });
}, },
@ -151,11 +152,14 @@ export default createReactClass({
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>); badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>);
} }
// FIXME: this ought to use AccessibleButton for a11y but that causes onMouseOut/onMouseOver to fire too much
const contextButton = this.state.hover || this.props.menuDisplayed ? const contextButton = this.state.hover || this.props.menuDisplayed ?
<div className="mx_TagTile_context_button" onClick={this.openMenu} ref={this.props.contextMenuButtonRef}> <AccessibleButton
className="mx_TagTile_context_button"
onClick={this.openMenu}
inputRef={this.props.contextMenuButtonRef}
>
{"\u00B7\u00B7\u00B7"} {"\u00B7\u00B7\u00B7"}
</div> : <div ref={this.props.contextMenuButtonRef} />; </AccessibleButton> : <div ref={this.props.contextMenuButtonRef} />;
const AccessibleTooltipButton = sdk.getComponent("elements.AccessibleTooltipButton"); const AccessibleTooltipButton = sdk.getComponent("elements.AccessibleTooltipButton");
@ -168,7 +172,7 @@ export default createReactClass({
<div <div
className="mx_TagTile_avatar" className="mx_TagTile_avatar"
onMouseOver={this.onMouseOver} onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut} onMouseLeave={this.onMouseLeave}
> >
<BaseAvatar <BaseAvatar
name={name} name={name}

View file

@ -37,7 +37,7 @@ export default class TextWithTooltip extends React.Component {
this.setState({hover: true}); this.setState({hover: true});
}; };
onMouseOut = () => { onMouseLeave = () => {
this.setState({hover: false}); this.setState({hover: false});
}; };
@ -45,13 +45,12 @@ export default class TextWithTooltip extends React.Component {
const Tooltip = sdk.getComponent("elements.Tooltip"); const Tooltip = sdk.getComponent("elements.Tooltip");
return ( return (
<span onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} className={this.props.class}> <span onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={this.props.class}>
{this.props.children} {this.props.children}
<Tooltip {this.state.hover && <Tooltip
label={this.props.tooltip} label={this.props.tooltip}
visible={this.state.hover}
tooltipClassName={this.props.tooltipClass} tooltipClassName={this.props.tooltipClass}
className={"mx_TextWithTooltip_tooltip"} /> className={"mx_TextWithTooltip_tooltip"} /> }
</span> </span>
); );
} }

View file

@ -18,18 +18,15 @@ limitations under the License.
*/ */
import React, { Component } from 'react'; import React, {Component, CSSProperties} from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import dis from '../../../dispatcher/dispatcher';
import classNames from 'classnames'; import classNames from 'classnames';
import { ViewTooltipPayload } from '../../../dispatcher/payloads/ViewTooltipPayload';
import { Action } from '../../../dispatcher/actions';
const MIN_TOOLTIP_HEIGHT = 25; const MIN_TOOLTIP_HEIGHT = 25;
interface IProps { interface IProps {
// Class applied to the element used to position the tooltip // Class applied to the element used to position the tooltip
className: string; className?: string;
// Class applied to the tooltip itself // Class applied to the tooltip itself
tooltipClassName?: string; tooltipClassName?: string;
// Whether the tooltip is visible or hidden. // Whether the tooltip is visible or hidden.
@ -38,6 +35,7 @@ interface IProps {
visible?: boolean; visible?: boolean;
// the react element to put into the tooltip // the react element to put into the tooltip
label: React.ReactNode; label: React.ReactNode;
forceOnRight?: boolean;
} }
export default class Tooltip extends React.Component<IProps> { export default class Tooltip extends React.Component<IProps> {
@ -68,18 +66,12 @@ export default class Tooltip extends React.Component<IProps> {
// Remove the wrapper element, as the tooltip has finished using it // Remove the wrapper element, as the tooltip has finished using it
public componentWillUnmount() { public componentWillUnmount() {
dis.dispatch<ViewTooltipPayload>({
action: Action.ViewTooltip,
tooltip: null,
parent: null,
});
ReactDOM.unmountComponentAtNode(this.tooltipContainer); ReactDOM.unmountComponentAtNode(this.tooltipContainer);
document.body.removeChild(this.tooltipContainer); document.body.removeChild(this.tooltipContainer);
window.removeEventListener('scroll', this.renderTooltip, true); window.removeEventListener('scroll', this.renderTooltip, true);
} }
private updatePosition(style: {[key: string]: any}) { private updatePosition(style: CSSProperties) {
const parentBox = this.parent.getBoundingClientRect(); const parentBox = this.parent.getBoundingClientRect();
let offset = 0; let offset = 0;
if (parentBox.height > MIN_TOOLTIP_HEIGHT) { if (parentBox.height > MIN_TOOLTIP_HEIGHT) {
@ -89,8 +81,14 @@ export default class Tooltip extends React.Component<IProps> {
// we need so that we're still centered. // we need so that we're still centered.
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT); offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
} }
style.top = (parentBox.top - 2) + window.pageYOffset + offset; style.top = (parentBox.top - 2) + window.pageYOffset + offset;
style.left = 6 + parentBox.right + window.pageXOffset; if (!this.props.forceOnRight && parentBox.right > window.innerWidth / 2) {
style.right = window.innerWidth - parentBox.right - window.pageXOffset - 8;
} else {
style.left = parentBox.right + window.pageXOffset + 6;
}
return style; return style;
} }
@ -99,7 +97,6 @@ export default class Tooltip extends React.Component<IProps> {
// positioned, also taking into account any window zoom // positioned, also taking into account any window zoom
// NOTE: The additional 6 pixels for the left position, is to take account of the // NOTE: The additional 6 pixels for the left position, is to take account of the
// tooltips chevron // tooltips chevron
const parent = ReactDOM.findDOMNode(this).parentNode as Element;
const style = this.updatePosition({}); const style = this.updatePosition({});
// Hide the entire container when not visible. This prevents flashing of the tooltip // Hide the entire container when not visible. This prevents flashing of the tooltip
// if it is not meant to be visible on first mount. // if it is not meant to be visible on first mount.
@ -119,19 +116,12 @@ export default class Tooltip extends React.Component<IProps> {
// Render the tooltip manually, as we wish it not to be rendered within the parent // Render the tooltip manually, as we wish it not to be rendered within the parent
this.tooltip = ReactDOM.render<Element>(tooltip, this.tooltipContainer); this.tooltip = ReactDOM.render<Element>(tooltip, this.tooltipContainer);
// Tell the roomlist about us so it can manipulate us if it wishes
dis.dispatch<ViewTooltipPayload>({
action: Action.ViewTooltip,
tooltip: this.tooltip,
parent: parent,
});
}; };
public render() { public render() {
// Render a placeholder // Render a placeholder
return ( return (
<div className={this.props.className} > <div className={this.props.className}>
</div> </div>
); );
} }

View file

@ -34,7 +34,7 @@ export default createReactClass({
}); });
}, },
onMouseOut: function() { onMouseLeave: function() {
this.setState({ this.setState({
hover: false, hover: false,
}); });
@ -48,7 +48,7 @@ export default createReactClass({
label={this.props.helpText} label={this.props.helpText}
/> : <div />; /> : <div />;
return ( return (
<div className="mx_TooltipButton" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} > <div className="mx_TooltipButton" onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave}>
? ?
{ tip } { tip }
</div> </div>

Some files were not shown because too many files have changed in this diff Show more