+ Thank you, you've already accepted the license. +
+ {% else %} ++ Please accept the license! +
+ + {% endif %} + + \ No newline at end of file diff --git a/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html b/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html new file mode 100644 index 0000000000..abe27d87ca --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html @@ -0,0 +1,9 @@ + + + +Danke schon
+ + \ No newline at end of file diff --git a/cypress/support/index.ts b/cypress/support/index.ts new file mode 100644 index 0000000000..9901ef4cb8 --- /dev/null +++ b/cypress/support/index.ts @@ -0,0 +1,3 @@ +// Empty file to prevent cypress from recreating a helpful example +// file on every run (their example file doesn't use semicolons and +// so fails our lint rules). diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 0000000000..85239e1a2a --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "es2016", + "lib": ["es2020", "dom"], + "types": ["cypress"], + "moduleResolution": "node" + }, + "include": ["**/*.ts"] +} diff --git a/docs/skinning.md b/docs/skinning.md index 229bc78372..0186186c24 100644 --- a/docs/skinning.md +++ b/docs/skinning.md @@ -1,71 +1,18 @@ # Skinning -The react-sdk can be skinned to replace presentation components, CSS, or -other relevant parts of the SDK. Typically consumers will replace entire -components and get the ability for custom CSS as a result. +Skinning in the context of the react-sdk is component replacement rather than CSS. This means you can override (replace) +any accessible component in the project to implement custom behaviour, look & feel, etc. Depending on your approach, +overriding CSS classes to apply custom styling is also possible, though harder to do. -This doc isn't exhaustive on how skinning works, though it should cover -some of the more complicated parts such as component replacement. +At present, the react-sdk offers no stable interface for components - this means properties and state can and do change +at any time without notice. Once we determine the react-sdk to be stable enough to use as a proper SDK, we will adjust +this policy. In the meantime, skinning is done completely at your own risk. -## Loading a skin +The approach you take is up to you - we suggest using a module replacement plugin, as found in +[webpack](https://webpack.js.org/plugins/normal-module-replacement-plugin/), though you're free to use whichever build +system works for you. The react-sdk does not have any particular functions to call to load skins, so simply replace or +extend the components/stores/etc you're after and build. As a reminder though, this is done completely at your own risk +as we cannot guarantee a stable interface at this time. -1. Generate a `component-index.js` (preferably using the tools that the react-sdk -exposes). This can typically be done with a npm script like `"reskindex -h src/header"`. -2. In your app's entry point, add something like this code: - ```javascript - import {loadSkin} from "matrix-react-sdk"; - loadSkin(import("component-index").components); - // The rest of your imports go under this. - ``` -3. Import the remainder of the SDK and bootstrap your app. - -It is extremely important that you **do not** import anything else from the -SDK prior to loading your skin as otherwise the skin might not work. Loading -the skin should be one of the first things your app does, if not the very -first thing. - -Additionally, **do not** provide `loadSkin` with the react-sdk components -themselves otherwise the app might explode. The SDK is already aware of its -components and doesn't need to be told. - -## Replacing components - -Components that replace the react-sdk ones MUST have a `replaces` static -key on the component's class to describe which component it overrides. For -example, if your `VectorAuthPage` component is meant to replace the react-sdk -`AuthPage` component then you'd add `static replaces = 'views.auth.AuthPage';` -to the `VectorAuthPage` class. - -Other than that, the skin just needs to be loaded normally as mentioned above. -Consumers of the SDK likely will not be interested in the rest of this section. - -### SDK developer notes - -Components in the react-sdk MUST be decorated with the `@replaceableComponent` -function. For components that can't use the decorator, they must use a -variation that provides similar functionality. The decorator gives consumers -an opportunity to load skinned components by abusing import ordering and -behaviour. - -Decorators are executed at import time which is why we can abuse the import -ordering behaviour: importing `loadSkin` doesn't trigger any components to -be imported, allowing the consumer to specify a skin. When the consumer does -import a component (for example, `MatrixChat`), it starts to pull in all the -components via `import` statements. When the components get pulled in the -decorator checks with the skinned components to see if it should be replacing -the component being imported. The decorator then effectively replaces the -components when needed by specifying the skinned component as an override for -the SDK's component, which should in theory override critical functions like -`render()` and lifecycle event handlers. - -The decorator also means that older usage of `getComponent()` is no longer -required because components should be replaced by the decorator. Eventually -the react-sdk should only have one usage of `getComponent()`: the decorator. - -The decorator assumes that if `getComponent()` returns null that there is -no skinned version of the component and continues on using the SDK's component. -In previous versions of the SDK, the function would throw an error instead -because it also expected the skin to list the SDK's components as well, however -that is no longer possible due to the above. - -In short, components should always be `import`ed. +Taking a look at [element-web](https://github.com/vector-im/element-web)'s approach to skinning may be worthwhile, as it +overrides some relatively simple components. diff --git a/docs/slate-formats.md b/docs/slate-formats.md deleted file mode 100644 index 7bb2fc9c5f..0000000000 --- a/docs/slate-formats.md +++ /dev/null @@ -1,88 +0,0 @@ -Guide to data types used by the Slate-based Rich Text Editor ------------------------------------------------------------- - -We always store the Slate editor state in its Value form. - -The schema for the Value is the same whether the editor is in MD or rich text mode, and is currently (rather arbitrarily) -dictated by the schema expected by slate-md-serializer, simply because it was the only bit of the pipeline which -has opinions on the schema. (slate-html-serializer lets you define how to serialize whatever schema you like). - -The BLOCK_TAGS and MARK_TAGS give the mapping from HTML tags to the schema's node types (for blocks, which describe -block content like divs, and marks, which describe inline formatted sections like spans). - -We use as the parent tag for the message (XXX: although some tags are technically not allowed to be nested within p's) - -Various conversions are performed as content is moved between HTML, MD, and plaintext representations of HTML and MD. - -The primitives used are: - - * Markdown.js - models commonmark-formatted MD strings (as entered by the composer in MD mode) - * toHtml() - renders them to HTML suitable for sending on the wire - * isPlainText() - checks whether the parsed MD contains anything other than simple text. - * toPlainText() - renders MD to plain text in order to remove backslashes. Only works if the MD is already plaintext (otherwise it just emits HTML) - - * slate-html-serializer - * converts Values to HTML (serialising) using our schema rules - * converts HTML to Values (deserialising) using our schema rules - - * slate-md-serializer - * converts rich Values to MD strings (serialising) but using a non-commonmark generic MD dialect. - * This should use commonmark, but we use the serializer here for expedience rather than writing a commonmark one. - - * slate-plain-serializer - * converts Values to plain text strings (serialising them) by concatenating the strings together - * converts Values from plain text strings (deserialiasing them). - * Used to initialise the editor by deserializing "" into a Value. Apparently this is the idiomatic way to initialise a blank editor. - * Used (as a bodge) to turn a rich text editor into a MD editor, when deserialising the converted MD string of the editor into a value - - * PlainWithPillsSerializer - * A fork of slate-plain-serializer which is aware of Pills (hence the name) and Emoji. - * It can be configured to output Pills as: - * "plain": Pills are rendered via their 'completion' text - e.g. 'Matthew'; used for sending messages) - * "md": Pills are rendered as MD, e.g. [Matthew](https://matrix.to/#/@matthew:matrix.org) ) - * "id": Pills are rendered as IDs, e.g. '@matthew:matrix.org' (used for authoring / commands) - * Emoji nodes are converted to inline utf8 emoji. - -The actual conversion transitions are: - - * Quoting: - * The message being quoted is taken as HTML - * ...and deserialised into a Value - * ...and then serialised into MD via slate-md-serializer if the editor is in MD mode - - * Roundtripping between MD and rich text editor mode - * From MD to richtext (mdToRichEditorState): - * Serialise the MD-format Value to a MD string (converting pills to MD) with PlainWithPillsSerializer in 'md' mode - * Convert that MD string to HTML via Markdown.js - * Deserialise that Value to HTML via slate-html-serializer - * From richtext to MD (richToMdEditorState): - * Serialise the richtext-format Value to a MD string with slate-md-serializer (XXX: this should use commonmark) - * Deserialise that to a plain text value via slate-plain-serializer - - * Loading history in one format into an editor which is in the other format - * Uses the same functions as for roundtripping - - * Scanning the editor for a slash command - * If the editor is a single line node starting with /, then serialize it to a string with PlainWithPillsSerializer in 'id' mode - So that pills get converted to IDs suitable for commands being passed around - - * Sending messages - * In RT mode: - * If there is rich content, serialize the RT-format Value to HTML body via slate-html-serializer - * Serialize the RT-format Value to the plain text fallback via PlainWithPillsSerializer in 'plain' mode - * In MD mode: - * Serialize the MD-format Value into an MD string with PlainWithPillsSerializer in 'md' mode - * Parse the string with Markdown.js - * If it contains no formatting: - * Send as plaintext (as taken from Markdown.toPlainText()) - * Otherwise - * Send as HTML (as taken from Markdown.toHtml()) - * Serialize the RT-format Value to the plain text fallback via PlainWithPillsSerializer in 'plain' mode - - * Pasting HTML - * Deserialize HTML to a RT Value via slate-html-serializer - * In RT mode, insert it straight into the editor as a fragment - * In MD mode, serialise it to an MD string via slate-md-serializer and then insert the string into the editor as a fragment. - -The various scenarios and transitions could be drawn into a pretty diagram if one felt the urge, but hopefully the above -gives sufficient detail on how it's all meant to work. \ No newline at end of file diff --git a/package.json b/package.json index 82f4116272..d132189054 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.42.4", + "version": "3.43.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -22,9 +22,6 @@ "README.md", "package.json" ], - "bin": { - "reskindex": "scripts/reskindex.js" - }, "main": "./lib/index.ts", "matrix_src_main": "./src/index.ts", "matrix_lib_main": "./lib/index.ts", @@ -37,23 +34,23 @@ "i18n": "matrix-gen-i18n", "prunei18n": "matrix-prune-i18n", "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && matrix-gen-i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", - "reskindex": "node scripts/reskindex.js -h header", "make-component": "node scripts/make-react-component.js", - "reskindex:watch": "node scripts/reskindex.js -h header -w", "rethemendex": "res/css/rethemendex.sh", "clean": "rimraf lib", "build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types", - "build:compile": "yarn reskindex && babel -d lib --verbose --extensions \".ts,.js,.tsx\" src", + "build:compile": "babel -d lib --verbose --extensions \".ts,.js,.tsx\" src", "build:types": "tsc --emitDeclarationOnly --jsx react", "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": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:build", "start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"", "lint": "yarn lint:types && yarn lint:js && yarn lint:style", - "lint:js": "eslint --max-warnings 0 src test", - "lint:js-fix": "eslint --fix src test", - "lint:types": "tsc --noEmit --jsx react", + "lint:js": "eslint --max-warnings 0 src test cypress", + "lint:js-fix": "eslint --fix src test cypress", + "lint:types": "tsc --noEmit --jsx react && tsc --noEmit -p cypress", "lint:style": "stylelint \"res/css/**/*.scss\"", "test": "jest", + "test:cypress": "cypress run", + "test:cypress:open": "cypress open", "test:e2e": "./test/end-to-end-tests/run.sh --app-url http://localhost:8080", "coverage": "yarn test --coverage" }, @@ -64,7 +61,6 @@ "@types/geojson": "^7946.0.8", "await-lock": "^2.1.0", "blurhash": "^1.1.3", - "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", "cheerio": "^1.0.0-rc.9", "classnames": "^2.2.6", @@ -93,8 +89,9 @@ "lodash": "^4.17.20", "maplibre-gl": "^1.15.2", "matrix-analytics-events": "github:matrix-org/matrix-analytics-events.git#daad3faed54f0b1f1e026a7498b4653e4d01cd90", + "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "^0.0.1-beta.7", - "matrix-js-sdk": "17.0.0", + "matrix-js-sdk": "17.1.0", "matrix-widget-api": "^0.1.0-beta.18", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", @@ -125,7 +122,6 @@ "@babel/eslint-plugin": "^7.12.10", "@babel/parser": "^7.12.11", "@babel/plugin-proposal-class-properties": "^7.12.1", - "@babel/plugin-proposal-decorators": "^7.12.12", "@babel/plugin-proposal-export-default-from": "^7.12.1", "@babel/plugin-proposal-numeric-separator": "^7.12.7", "@babel/plugin-proposal-object-rest-spread": "^7.12.1", @@ -138,7 +134,7 @@ "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz", "@peculiar/webcrypto": "^1.1.4", "@sentry/types": "^6.10.0", - "@sinonjs/fake-timers": "^7.0.2", + "@sinonjs/fake-timers": "^9.1.2", "@types/classnames": "^2.2.11", "@types/commonmark": "^0.27.4", "@types/counterpart": "^0.18.1", @@ -148,6 +144,7 @@ "@types/escape-html": "^1.0.1", "@types/file-saver": "^2.0.3", "@types/flux": "^3.1.9", + "@types/fs-extra": "^9.0.13", "@types/jest": "^26.0.20", "@types/lodash": "^4.14.168", "@types/modernizr": "^3.5.3", @@ -158,6 +155,7 @@ "@types/react": "17.0.14", "@types/react-beautiful-dnd": "^13.0.0", "@types/react-dom": "17.0.9", + "@types/react-test-renderer": "^17.0.1", "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "^2.3.1", "@types/zxcvbn": "^4.4.0", @@ -168,7 +166,7 @@ "babel-jest": "^26.6.3", "blob-polyfill": "^6.0.20211015", "chokidar": "^3.5.1", - "concurrently": "^5.3.0", + "cypress": "^9.5.4", "enzyme": "^3.11.0", "enzyme-to-json": "^3.6.2", "eslint": "8.9.0", @@ -178,6 +176,7 @@ "eslint-plugin-matrix-org": "^0.4.0", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", + "fs-extra": "^10.0.1", "glob": "^7.1.6", "jest": "^27.4.0", "jest-canvas-mock": "^2.3.0", diff --git a/res/css/_common.scss b/res/css/_common.scss index 4ead7381fc..5c6349c220 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -254,7 +254,7 @@ legend { } // These are magic constants which are excluded from tinting, to let themes -// (which only have CSS, unlike skins) tell the app what their non-tinted +// (which only have CSS) tell the app what their non-tinted // colourscheme is by inspecting the stylesheet DOM. // // They are not used for layout!! diff --git a/res/css/_components.scss b/res/css/_components.scss index 3f3038ccfd..8b5dc63a0e 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -4,14 +4,21 @@ @import "./_font-sizes.scss"; @import "./_font-weights.scss"; @import "./_spacing.scss"; +@import "./components/views/beacon/_BeaconStatus.scss"; +@import "./components/views/beacon/_BeaconViewDialog.scss"; @import "./components/views/beacon/_LeftPanelLiveShareWarning.scss"; +@import "./components/views/beacon/_LiveTimeRemaining.scss"; +@import "./components/views/beacon/_OwnBeaconStatus.scss"; @import "./components/views/beacon/_RoomLiveShareWarning.scss"; @import "./components/views/beacon/_StyledLiveBeaconIcon.scss"; @import "./components/views/location/_LiveDurationDropdown.scss"; @import "./components/views/location/_LocationShareMenu.scss"; @import "./components/views/location/_MapError.scss"; +@import "./components/views/location/_Marker.scss"; @import "./components/views/location/_ShareDialogButtons.scss"; @import "./components/views/location/_ShareType.scss"; +@import "./components/views/location/_ZoomButtons.scss"; +@import "./components/views/messages/_MBeaconBody.scss"; @import "./components/views/spaces/_QuickThemeSwitcher.scss"; @import "./structures/_AutoHideScrollbar.scss"; @import "./structures/_BackdropPanel.scss"; diff --git a/res/css/components/views/beacon/_BeaconStatus.scss b/res/css/components/views/beacon/_BeaconStatus.scss new file mode 100644 index 0000000000..8ac873604d --- /dev/null +++ b/res/css/components/views/beacon/_BeaconStatus.scss @@ -0,0 +1,61 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_BeaconStatus { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + + box-sizing: border-box; + padding: $spacing-8; + + color: var(--color); + font-size: $font-12px; +} + +.mx_BeaconStatus_Loading, +.mx_BeaconStatus_Stopped { + --color: $tertiary-content; +} + +.mx_BeaconStatus_Active, +.mx_BeaconStatus_Error { + --color: $primary-content; +} + +.mx_BeaconStatus_icon { + height: 32px; + width: 32px; + + flex: 0 0 32px; + margin-right: $spacing-8; +} + +.mx_BeaconStatus_description { + flex: 1; + display: flex; + flex-direction: column; + line-height: $font-14px; + + padding-right: $spacing-8; + + // TODO handle text-overflow +} + +.mx_BeaconStatus_expiryTime { + color: $secondary-content; +} diff --git a/res/css/components/views/beacon/_BeaconViewDialog.scss b/res/css/components/views/beacon/_BeaconViewDialog.scss new file mode 100644 index 0000000000..dc4d089bfe --- /dev/null +++ b/res/css/components/views/beacon/_BeaconViewDialog.scss @@ -0,0 +1,79 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_BeaconViewDialog_wrapper .mx_Dialog { + padding: 0px; + + /* Unset contain and position to allow the close button + to appear outside the dialog */ + contain: unset; + position: unset; +} + +.mx_BeaconViewDialog { + /* subtract 0.5px to prevent single-pixel margin due to rounding */ + width: calc(80vw - 0.5px); + height: calc(80vh - 0.5px); + overflow: hidden; + + .mx_Dialog_header { + margin: 0px; + padding: 0px; + position: unset; + + .mx_Dialog_title { + display: none; + } + + .mx_Dialog_cancelButton { + z-index: 4010; + position: absolute; + right: 5vw; + top: 5vh; + width: 20px; + height: 20px; + background-color: $dialog-close-external-color; + } + } +} + +.mx_BeaconViewDialog_map { + width: 80vw; + height: 80vh; + border-radius: 8px; +} + +.mx_BeaconViewDialog_mapFallback { + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + background: url('$(res)/img/location/map.svg'); + background-size: cover; +} + +.mx_BeaconViewDialog_mapFallbackIcon { + width: 65px; + margin-bottom: $spacing-16; + color: $quaternary-content; +} + +.mx_BeaconViewDialog_mapFallbackMessage { + color: $secondary-content; + margin-bottom: $spacing-16; +} diff --git a/res/css/components/views/beacon/_LiveTimeRemaining.scss b/res/css/components/views/beacon/_LiveTimeRemaining.scss new file mode 100644 index 0000000000..de13f7aab2 --- /dev/null +++ b/res/css/components/views/beacon/_LiveTimeRemaining.scss @@ -0,0 +1,20 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_LiveTimeRemaining { + color: $secondary-content; + font-size: $font-12px; +} diff --git a/res/css/components/views/beacon/_OwnBeaconStatus.scss b/res/css/components/views/beacon/_OwnBeaconStatus.scss new file mode 100644 index 0000000000..aa01b6269a --- /dev/null +++ b/res/css/components/views/beacon/_OwnBeaconStatus.scss @@ -0,0 +1,27 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_EventTile[data-layout="bubble"] .mx_OwnBeaconStatus_button { + // align to top to make room for timestamp + // in bubble view + align-self: start; +} + +.mx_OwnBeaconStatus_destructiveButton { + // override button link_inline styles + color: $alert !important; + font-weight: $font-semi-bold !important; +} diff --git a/res/css/components/views/beacon/_RoomLiveShareWarning.scss b/res/css/components/views/beacon/_RoomLiveShareWarning.scss index 7404f88aea..f82c7f4de4 100644 --- a/res/css/components/views/beacon/_RoomLiveShareWarning.scss +++ b/res/css/components/views/beacon/_RoomLiveShareWarning.scss @@ -39,12 +39,6 @@ limitations under the License. font-size: $font-15px; } -.mx_RoomLiveShareWarning_expiry { - color: $secondary-content; - font-size: $font-12px; - margin-right: $spacing-16; -} - .mx_RoomLiveShareWarning_spinner { margin-right: $spacing-16; } @@ -54,6 +48,10 @@ limitations under the License. margin-left: $spacing-16; } +.mx_RoomLiveShareWarning_stopButton { + margin-left: $spacing-16; +} + .mx_RoomLiveShareWarning_closeButtonIcon { height: $font-18px; padding: $spacing-4; diff --git a/res/css/components/views/beacon/_StyledLiveBeaconIcon.scss b/res/css/components/views/beacon/_StyledLiveBeaconIcon.scss index e31279c34b..9096c3c71f 100644 --- a/res/css/components/views/beacon/_StyledLiveBeaconIcon.scss +++ b/res/css/components/views/beacon/_StyledLiveBeaconIcon.scss @@ -33,3 +33,8 @@ limitations under the License. background-color: $alert; border-color: $alert; } + +.mx_StyledLiveBeaconIcon.mx_StyledLiveBeaconIcon_idle { + background-color: $quaternary-content; + border-color: $quaternary-content; +} diff --git a/res/css/components/views/location/_Marker.scss b/res/css/components/views/location/_Marker.scss new file mode 100644 index 0000000000..7a1baccf9f --- /dev/null +++ b/res/css/components/views/location/_Marker.scss @@ -0,0 +1,46 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_Marker_defaultColor { + color: $accent; +} + +.mx_Marker_border { + width: 42px; + height: 42px; + border-radius: 50%; + filter: drop-shadow(0px 3px 5px rgba(0, 0, 0, 0.2)); + background-color: currentColor; + + display: flex; + justify-content: center; + align-items: center; + + // caret down + &::before { + content: ''; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid currentColor; + position: absolute; + bottom: -4px; + } +} + +.mx_Marker_icon { + color: white; + height: 20px; +} diff --git a/res/css/components/views/location/_ZoomButtons.scss b/res/css/components/views/location/_ZoomButtons.scss new file mode 100644 index 0000000000..59d52477f9 --- /dev/null +++ b/res/css/components/views/location/_ZoomButtons.scss @@ -0,0 +1,45 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_ZoomButtons { + position: absolute; + bottom: $spacing-32; + right: $spacing-24; +} + +.mx_ZoomButtons_button { + @mixin ButtonResetDefault; + + margin-top: $spacing-8; + border-radius: 4px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + height: 24px; + width: 24px; + + background: $background; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.25); +} + +.mx_ZoomButtons_icon { + height: 10px; + width: 10px; + + color: $primary-content; +} diff --git a/res/css/components/views/messages/_MBeaconBody.scss b/res/css/components/views/messages/_MBeaconBody.scss new file mode 100644 index 0000000000..dc63d6676d --- /dev/null +++ b/res/css/components/views/messages/_MBeaconBody.scss @@ -0,0 +1,65 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_MBeaconBody { + position: relative; + height: 220px; + width: 325px; + + border-radius: $timeline-image-border-radius; + overflow: hidden; +} + +.mx_MBeaconBody_map { + height: 100%; + width: 100%; + z-index: 0; // keeps the entire map under the message action bar + + &:not(.mx_MBeaconBody_mapFallback) { + cursor: pointer; + } +} + +.mx_MBeaconBody_mapFallback { + box-sizing: border-box; + display: flex; + justify-content: center; + align-items: center; + + // pushes spinner/icon up + // to appear more centered with the footer + padding-bottom: 50px; + + background: url('$(res)/img/location/map.svg'); + background-size: cover; +} + +.mx_MBeaconBody_mapFallbackIcon { + width: 65px; + color: $quaternary-content; +} + +.mx_MBeaconBody_chin { + position: absolute; + bottom: 0; + width: 100%; + background-color: $overlay-background; +} + +.mx_EventTile[data-layout="bubble"] .mx_EventTile_line .mx_MBeaconBody { + max-width: 100%; + width: 450px; +} diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index 74d30bf59a..1cbc7cd735 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -251,7 +251,8 @@ $activeBorderColor: $primary-content; margin-top: auto; margin-bottom: auto; display: none; - position: relative; + position: absolute; + right: 4px; &::before { top: 3px; @@ -327,6 +328,16 @@ $activeBorderColor: $primary-content; } } + .mx_SpaceItem:not(.mx_SpaceItem_new) { + .mx_SpaceButton:hover, + .mx_SpaceButton:focus-within, + .mx_SpaceButton_hasMenuOpen { + &:not(.mx_SpaceButton_narrow):not(.mx_SpaceButton_invite) .mx_SpaceButton_name { + max-width: calc(100% - 56px); + } + } + } + /* root space buttons are bigger and not indented */ & > .mx_AutoHideScrollbar { flex: 1; diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index 5137aed95a..00c7cd05b4 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -108,26 +108,25 @@ limitations under the License. } .mx_Toast_title { + display: flex; + align-items: center; + column-gap: 8px; width: 100%; box-sizing: border-box; h2 { - grid-column: 1 / 3; - grid-row: 1; margin: 0; font-size: $font-15px; font-weight: 600; display: inline; width: auto; - vertical-align: middle; } - span { - padding-left: 8px; - float: right; + .mx_Toast_title_countIndicator { font-size: $font-12px; line-height: $font-22px; color: $secondary-content; + margin-inline-start: auto; // on the end side of the div } } @@ -137,17 +136,14 @@ limitations under the License. } .mx_Toast_buttons { - float: right; display: flex; + justify-content: flex-end; + column-gap: 5px; .mx_AccessibleButton { min-width: 96px; box-sizing: border-box; } - - .mx_AccessibleButton + .mx_AccessibleButton { - margin-left: 5px; - } } .mx_Toast_description { @@ -157,10 +153,6 @@ limitations under the License. margin: 4px 0 11px 0; font-size: $font-12px; - .mx_AccessibleButton_kind_link { - font-size: inherit; - } - a { text-decoration: none; } diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index 90b825c886..a88e9eddb9 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -147,11 +147,6 @@ limitations under the License. margin-top: 8px; } } - - .mx_AccessibleButton_kind_link { - font-weight: normal; - font-size: inherit; - } } } diff --git a/res/css/structures/_ViewSource.scss b/res/css/structures/_ViewSource.scss index e3d6135ef3..f1ada65786 100644 --- a/res/css/structures/_ViewSource.scss +++ b/res/css/structures/_ViewSource.scss @@ -34,8 +34,13 @@ limitations under the License. padding: 0.5em 1em 0.5em 1em; word-wrap: break-word; white-space: pre-wrap; + overflow-wrap: anywhere; } .mx_ViewSource_details { margin-top: 0.8em; } + +.mx_ViewSource_container { + max-width: calc(100% - 24px); +} diff --git a/res/css/structures/auth/_Login.scss b/res/css/structures/auth/_Login.scss index 0e6912e435..638917f1c2 100644 --- a/res/css/structures/auth/_Login.scss +++ b/res/css/structures/auth/_Login.scss @@ -88,13 +88,12 @@ limitations under the License. div.mx_AccessibleButton_kind_link.mx_Login_forgot { display: block; margin: 0 auto; - // style it as a link - font-size: inherit; &.mx_AccessibleButton_disabled { cursor: not-allowed; } } + .mx_Login_spinner { display: flex; justify-content: center; diff --git a/res/css/views/context_menus/_MessageContextMenu.scss b/res/css/views/context_menus/_MessageContextMenu.scss index e743619f8f..b92ce10d35 100644 --- a/res/css/views/context_menus/_MessageContextMenu.scss +++ b/res/css/views/context_menus/_MessageContextMenu.scss @@ -90,6 +90,22 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/pin.svg'); } + .mx_MessageContextMenu_iconCopy::before { + mask-image: url($copy-button-url); + } + + .mx_MessageContextMenu_iconEdit::before { + mask-image: url('$(res)/img/element-icons/room/message-bar/edit.svg'); + } + + .mx_MessageContextMenu_iconReply::before { + mask-image: url('$(res)/img/element-icons/room/message-bar/reply.svg'); + } + + .mx_MessageContextMenu_iconReact::before { + mask-image: url('$(res)/img/element-icons/room/message-bar/emoji.svg'); + } + .mx_MessageContextMenu_iconViewInRoom::before { mask-image: url('$(res)/img/element-icons/view-in-room.svg'); } diff --git a/res/css/views/dialogs/_FeedbackDialog.scss b/res/css/views/dialogs/_FeedbackDialog.scss index 588f65eb65..8d11b83fe1 100644 --- a/res/css/views/dialogs/_FeedbackDialog.scss +++ b/res/css/views/dialogs/_FeedbackDialog.scss @@ -58,10 +58,6 @@ limitations under the License. line-height: $font-15px; } - .mx_AccessibleButton_kind_link { - font-size: inherit; - } - a, .mx_AccessibleButton_kind_link { color: $accent; text-decoration: underline; diff --git a/res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss b/res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss index 3898e09756..83f9349514 100644 --- a/res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss +++ b/res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss @@ -21,9 +21,4 @@ limitations under the License. line-height: $font-20px; margin-bottom: 24px; } - - .mx_AccessibleButton_kind_link { - font-size: inherit; - line-height: inherit; - } } diff --git a/res/css/views/dialogs/_LocationViewDialog.scss b/res/css/views/dialogs/_LocationViewDialog.scss index e7cdaf8800..600c308265 100644 --- a/res/css/views/dialogs/_LocationViewDialog.scss +++ b/res/css/views/dialogs/_LocationViewDialog.scss @@ -48,49 +48,10 @@ limitations under the License. background-color: $dialog-close-external-color; } } - - .mx_MLocationBody { - position: absolute; - - .mx_MLocationBody_map { - width: 80vw; - height: 80vh; - } - - .mx_MLocationBody_zoomButtons { - position: absolute; - display: grid; - grid-template-columns: auto; - grid-row-gap: 8px; - - right: 24px; - bottom: 48px; - - .mx_AccessibleButton { - background-color: $background; - box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.25); - border-radius: 4px; - width: 24px; - height: 24px; - - .mx_MLocationBody_zoomButton { - background-color: $primary-content; - margin: 4px; - width: 16px; - height: 16px; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - } - - .mx_MLocationBody_plusButton { - mask-image: url('$(res)/img/element-icons/plus-button.svg'); - } - - .mx_MLocationBody_minusButton { - mask-image: url('$(res)/img/element-icons/minus-button.svg'); - } - } - } - } +} + +.mx_LocationViewDialog_map { + width: 80vw; + height: 80vh; + border-radius: 8px; } diff --git a/res/css/views/dialogs/_RoomSettingsDialogBridges.scss b/res/css/views/dialogs/_RoomSettingsDialogBridges.scss index f18b4917cf..07735ad027 100644 --- a/res/css/views/dialogs/_RoomSettingsDialogBridges.scss +++ b/res/css/views/dialogs/_RoomSettingsDialogBridges.scss @@ -22,84 +22,99 @@ limitations under the License. margin: 0; padding: 0; } -} -.mx_RoomSettingsDialog_BridgeList li { - list-style-type: none; - padding: 5px; - margin-bottom: 8px; - border-width: 1px 1px; - border-color: $primary-hairline-color; - border-style: solid; - border-radius: 5px; + li { + list-style-type: none; - .column-icon { - float: left; - padding-right: 10px; + &.mx_RoomSettingsDialog_BridgeList_listItem { + display: flex; + flex-wrap: wrap; + gap: $spacing-8; + padding: 5px; + margin-bottom: $spacing-8; - * { + // border-style around each bridge list item + border-width: 1px 1px; + border-color: $primary-hairline-color; + border-style: solid; border-radius: 5px; - border: 1px solid $input-darker-bg-color; - } - .noProtocolIcon { - width: 48px; - height: 48px; - background: $input-darker-bg-color; - border-radius: 5px; - } + .mx_RoomSettingsDialog_column_icon { + .mx_RoomSettingsDialog_protocolIcon, + .mx_RoomSettingsDialog_protocolIcon span, + .mx_RoomSettingsDialog_noProtocolIcon { + box-sizing: border-box; + border-radius: 5px; + border: 1px solid $input-darker-bg-color; + } - .protocol-icon { - float: left; - margin-right: 5px; - img { - border-radius: 5px; - border-width: 1px 1px; - border-color: $primary-hairline-color; + .mx_RoomSettingsDialog_noProtocolIcon, + .mx_RoomSettingsDialog_protocolIcon img { + border-radius: 5px; + } + + .mx_RoomSettingsDialog_noProtocolIcon { + width: 48px; + height: 48px; + background: $input-darker-bg-color; + } + + .mx_RoomSettingsDialog_protocolIcon { + img { + border-width: 1px 1px; + border-color: $primary-hairline-color; + } + + span { + /* Correct letter placement */ + left: auto; + } + } } - span { - /* Correct letter placement */ - left: auto; - } - } - } - .column-data { - display: inline-block; - width: 85%; + .mx_RoomSettingsDialog_column_data { + display: inline-block; + width: 85%; - > h3 { - margin-top: 0px; - margin-bottom: 0px; - font-size: 16pt; - color: $primary-content; - } + .mx_RoomSettingsDialog_column_data_details, + .mx_RoomSettingsDialog_column_data_metadata, + .mx_RoomSettingsDialog_column_data_metadata li, + .mx_RoomSettingsDialog_column_data_protocolName { + margin-bottom: 0; + } - > * { - margin-top: 4px; - margin-bottom: 0; - } + .mx_RoomSettingsDialog_column_data_details, + .mx_RoomSettingsDialog_column_data_metadata { + margin-top: $spacing-4; + } - .workspace-channel-details { - color: $primary-content; - font-weight: 600; + .mx_RoomSettingsDialog_column_data_metadata li { + margin-top: $spacing-8; + } - .channel { - margin-left: 5px; - } - } + .mx_RoomSettingsDialog_column_data_protocolName { + margin-top: 0; + font-size: 16pt; + color: $primary-content; + } - .metadata { - color: $muted-fg-color; - margin-bottom: 0; - overflow-y: visible; - text-overflow: ellipsis; - white-space: normal; - padding: 0; + .mx_RoomSettingsDialog_workspace_channel_details { + color: $primary-content; + font-weight: $font-semi-bold; - > li { - padding: 0; - border: 0; + .mx_RoomSettingsDialog_channel { + margin-inline-start: 5px; + } + } + + .mx_RoomSettingsDialog_metadata { + color: $muted-fg-color; + margin-bottom: 0; + overflow-y: visible; + text-overflow: ellipsis; + white-space: normal; + padding: 0; + } } } } diff --git a/res/css/views/dialogs/_SpaceSettingsDialog.scss b/res/css/views/dialogs/_SpaceSettingsDialog.scss index 31a544d166..ead082dba6 100644 --- a/res/css/views/dialogs/_SpaceSettingsDialog.scss +++ b/res/css/views/dialogs/_SpaceSettingsDialog.scss @@ -77,6 +77,7 @@ limitations under the License. .mx_AccessibleButton_hasKind { &.mx_AccessibleButton_kind_link { + font-size: $font-14px; margin: 7px 18px; &.mx_SettingsTab_showAdvanced { diff --git a/res/css/views/dialogs/_SpotlightDialog.scss b/res/css/views/dialogs/_SpotlightDialog.scss index 3041c86042..9f78d905b5 100644 --- a/res/css/views/dialogs/_SpotlightDialog.scss +++ b/res/css/views/dialogs/_SpotlightDialog.scss @@ -241,7 +241,6 @@ limitations under the License. .mx_SpotlightDialog_recentSearches > h4 > .mx_AccessibleButton_kind_link { padding: 0; float: right; - font-weight: normal; font-size: $font-12px; line-height: $font-15px; color: $secondary-content; diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index a87aab7af8..021e8ffc8c 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -39,6 +39,7 @@ limitations under the License. justify-content: center; font-size: $font-14px; border: none; // override default styles + word-break: keep-all; // prevent button text in Chinese/Japanese/Korean (CJK) from being collapsed &.mx_AccessibleButton_kind_primary_sm, &.mx_AccessibleButton_kind_danger_sm, @@ -130,14 +131,20 @@ limitations under the License. } } - &.mx_AccessibleButton_kind_link { + &.mx_AccessibleButton_kind_link, + &.mx_AccessibleButton_kind_link_inline { color: $accent; + font-size: inherit; + font-weight: normal; + line-height: inherit; + } + + &.mx_AccessibleButton_kind_link { padding: 0; } &.mx_AccessibleButton_kind_link_inline { - color: $accent; - font-size: inherit; + display: inline; padding: 0 2px; } diff --git a/res/css/views/elements/_CopyableText.scss b/res/css/views/elements/_CopyableText.scss index a08306b66a..ceafd42273 100644 --- a/res/css/views/elements/_CopyableText.scss +++ b/res/css/views/elements/_CopyableText.scss @@ -18,14 +18,17 @@ limitations under the License. .mx_CopyableText { display: flex; justify-content: space-between; - border-radius: 5px; - border: solid 1px $light-fg-color; - margin-bottom: 10px; - margin-top: 10px; - padding: 10px; width: max-content; max-width: 100%; + &.mx_CopyableText_border { + border-radius: 5px; + border: solid 1px $light-fg-color; + margin-bottom: 10px; + margin-top: 10px; + padding: 10px; + } + .mx_CopyableText_copyButton { flex-shrink: 0; width: 20px; diff --git a/res/css/views/elements/_TagComposer.scss b/res/css/views/elements/_TagComposer.scss index f5bdb8d2d5..7efe83281f 100644 --- a/res/css/views/elements/_TagComposer.scss +++ b/res/css/views/elements/_TagComposer.scss @@ -29,7 +29,9 @@ limitations under the License. margin-left: 16px; // distance from
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -62,6 +63,13 @@ function score(query, space) {
}
}
+function colonsTrimmed(str: string): string {
+ // Trim off leading and potentially trailing `:` to correctly match the emoji data as they exist in emojibase.
+ // Notes: The regex is pinned to the start and end of the string so that we can use the lazy-capturing `*?` matcher.
+ // It needs to be lazy so that the trailing `:` is not captured in the replacement group, if it exists.
+ return str.replace(/^:(.*?):?$/, "$1");
+}
+
export default class EmojiProvider extends AutocompleteProvider {
matcher: QueryMatcher;
nameMatcher: QueryMatcher;
@@ -108,8 +116,9 @@ export default class EmojiProvider extends AutocompleteProvider {
// then sort by score (Infinity if matchedString not in shortcode)
sorters.push(c => score(matchedString, c.emoji.shortcodes[0]));
// then sort by max score of all shortcodes, trim off the `:`
+ const trimmedMatch = colonsTrimmed(matchedString);
sorters.push(c => Math.min(
- ...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)),
+ ...c.emoji.shortcodes.map(s => score(trimmedMatch, s)),
));
// If the matchedString is not empty, sort by length of shortcode. Example:
// matchedString = ":bookmark"
diff --git a/src/call-types.ts b/src/call-types.ts
new file mode 100644
index 0000000000..c12491e102
--- /dev/null
+++ b/src/call-types.ts
@@ -0,0 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Event type for room account data and room creation content used to mark rooms as virtual rooms
+// (and store the ID of their native room)
+export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx
index 2231618994..a3c214eb45 100644
--- a/src/components/structures/ContextMenu.tsx
+++ b/src/components/structures/ContextMenu.tsx
@@ -22,7 +22,6 @@ import classNames from "classnames";
import FocusLock from "react-focus-lock";
import { Writeable } from "../../@types/common";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import UIStore from "../../stores/UIStore";
import { checkInputableElement, RovingTabIndexProvider } from "../../accessibility/RovingTabIndex";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
@@ -105,7 +104,6 @@ interface IState {
// Generic ContextMenu Portal wrapper
// all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1}
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
-@replaceableComponent("structures.ContextMenu")
export default class ContextMenu extends React.PureComponent {
private readonly initialFocus: HTMLElement;
@@ -431,7 +429,7 @@ export type AboveLeftOf = IPosition & {
// Placement method for to position context menu right-aligned and flowing to the left of elementRect,
// and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
export const aboveLeftOf = (
- elementRect: DOMRect,
+ elementRect: Pick,
chevronFace = ChevronFace.None,
vPadding = 0,
): AboveLeftOf => {
@@ -452,9 +450,37 @@ export const aboveLeftOf = (
return menuOptions;
};
+// Placement method for to position context menu right-aligned and flowing to the right of elementRect,
+// and either above or below: wherever there is more space (maybe this should be aboveOrBelowRightOf?)
+export const aboveRightOf = (
+ elementRect: Pick,
+ chevronFace = ChevronFace.None,
+ vPadding = 0,
+): AboveLeftOf => {
+ const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
+
+ const buttonLeft = elementRect.left + window.pageXOffset;
+ const buttonBottom = elementRect.bottom + window.pageYOffset;
+ const buttonTop = elementRect.top + window.pageYOffset;
+ // Align the left edge of the menu to the left edge of the button
+ menuOptions.left = buttonLeft;
+ // Align the menu vertically on whichever side of the button has more space available.
+ if (buttonBottom < UIStore.instance.windowHeight / 2) {
+ menuOptions.top = buttonBottom + vPadding;
+ } else {
+ menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
+ }
+
+ return menuOptions;
+};
+
// Placement method for to position context menu right-aligned and flowing to the left of elementRect
// and always above elementRect
-export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
+export const alwaysAboveLeftOf = (
+ elementRect: Pick,
+ chevronFace = ChevronFace.None,
+ vPadding = 0,
+) => {
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
const buttonRight = elementRect.right + window.pageXOffset;
@@ -474,7 +500,11 @@ export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFac
// Placement method for to position context menu right-aligned and flowing to the right of elementRect
// and always above elementRect
-export const alwaysAboveRightOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
+export const alwaysAboveRightOf = (
+ elementRect: Pick,
+ chevronFace = ChevronFace.None,
+ vPadding = 0,
+) => {
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
const buttonLeft = elementRect.left + window.pageXOffset;
diff --git a/src/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx
index 4b309a1838..2053140ba4 100644
--- a/src/components/structures/EmbeddedPage.tsx
+++ b/src/components/structures/EmbeddedPage.tsx
@@ -37,7 +37,7 @@ interface IProps {
// Whether to wrap the page in a scrollbar
scrollbar?: boolean;
// Map of keys to replace with values, e.g {$placeholder: "value"}
- replaceMap?: Map;
+ replaceMap?: Record;
}
interface IState {
@@ -57,8 +57,7 @@ export default class EmbeddedPage extends React.PureComponent {
};
}
- protected translate(s: string): string {
- // default implementation - skins may wish to extend this
+ private translate(s: string): string {
return sanitizeHtml(_t(s));
}
diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx
index 8466c26bac..d248c6556f 100644
--- a/src/components/structures/FilePanel.tsx
+++ b/src/components/structures/FilePanel.tsx
@@ -29,7 +29,6 @@ import EventIndexPeg from "../../indexing/EventIndexPeg";
import { _t } from '../../languageHandler';
import DesktopBuildsNotice, { WarningKind } from "../views/elements/DesktopBuildsNotice";
import BaseCard from "../views/right_panel/BaseCard";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import ResizeNotifier from '../../utils/ResizeNotifier';
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
@@ -51,7 +50,6 @@ interface IState {
/*
* Component which shows the filtered file using a TimelinePanel
*/
-@replaceableComponent("structures.FilePanel")
class FilePanel extends React.Component {
static contextType = RoomContext;
diff --git a/src/components/structures/GenericErrorPage.tsx b/src/components/structures/GenericErrorPage.tsx
index 25892f46df..f9a68753a0 100644
--- a/src/components/structures/GenericErrorPage.tsx
+++ b/src/components/structures/GenericErrorPage.tsx
@@ -16,14 +16,11 @@ limitations under the License.
import React from 'react';
-import { replaceableComponent } from "../../utils/replaceableComponent";
-
interface IProps {
title: React.ReactNode;
message: React.ReactNode;
}
-@replaceableComponent("structures.GenericErrorPage")
export default class GenericErrorPage extends React.PureComponent {
render() {
return
diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx
index 6ee31114ef..fdcb52a687 100644
--- a/src/components/structures/HomePage.tsx
+++ b/src/components/structures/HomePage.tsx
@@ -21,7 +21,6 @@ import AutoHideScrollbar from './AutoHideScrollbar';
import { getHomePageUrl } from "../../utils/pages";
import { _tDom } from "../../languageHandler";
import SdkConfig from "../../SdkConfig";
-import * as sdk from "../../index";
import dis from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions";
import BaseAvatar from "../views/avatars/BaseAvatar";
@@ -33,6 +32,7 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
import MiniAvatarUploader, { AVATAR_SIZE } from "../views/elements/MiniAvatarUploader";
import Analytics from "../../Analytics";
import PosthogTrackers from "../../PosthogTrackers";
+import EmbeddedPage from "./EmbeddedPage";
const onClickSendDm = () => {
Analytics.trackEvent('home_page', 'button', 'dm');
@@ -94,8 +94,6 @@ const HomePage: React.FC = ({ justRegistered = false }) => {
const pageUrl = getHomePageUrl(config);
if (pageUrl) {
- // FIXME: Using an import will result in wrench-element-tests failures
- const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
return ;
}
diff --git a/src/components/structures/HostSignupAction.tsx b/src/components/structures/HostSignupAction.tsx
index 5b2bafa03c..6ba37eb9aa 100644
--- a/src/components/structures/HostSignupAction.tsx
+++ b/src/components/structures/HostSignupAction.tsx
@@ -23,7 +23,6 @@ import {
import { _t } from "../../languageHandler";
import { HostSignupStore } from "../../stores/HostSignupStore";
import SdkConfig from "../../SdkConfig";
-import { replaceableComponent } from "../../utils/replaceableComponent";
interface IProps {
onClick?(): void;
@@ -31,7 +30,6 @@ interface IProps {
interface IState {}
-@replaceableComponent("structures.HostSignupAction")
export default class HostSignupAction extends React.PureComponent {
private openDialog = async () => {
this.props.onClick?.();
diff --git a/src/components/structures/IndicatorScrollbar.tsx b/src/components/structures/IndicatorScrollbar.tsx
index 307541cb0b..4b122345b3 100644
--- a/src/components/structures/IndicatorScrollbar.tsx
+++ b/src/components/structures/IndicatorScrollbar.tsx
@@ -17,7 +17,6 @@ limitations under the License.
import React, { ComponentProps, createRef } from "react";
import AutoHideScrollbar from "./AutoHideScrollbar";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
interface IProps extends Omit, "onWheel"> {
@@ -40,7 +39,6 @@ interface IState {
rightIndicatorOffset: string;
}
-@replaceableComponent("structures.IndicatorScrollbar")
export default class IndicatorScrollbar extends React.Component {
private autoHideScrollbar = createRef();
private scrollElement: HTMLDivElement;
diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx
index 01b55c1542..94c54b32e3 100644
--- a/src/components/structures/InteractiveAuth.tsx
+++ b/src/components/structures/InteractiveAuth.tsx
@@ -28,7 +28,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import getEntryComponentForLoginType, { IStageComponent } from '../views/auth/InteractiveAuthEntryComponents';
import Spinner from "../views/elements/Spinner";
-import { replaceableComponent } from "../../utils/replaceableComponent";
export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
@@ -89,7 +88,6 @@ interface IState {
submitButtonEnabled: boolean;
}
-@replaceableComponent("structures.InteractiveAuthComponent")
export default class InteractiveAuthComponent extends React.Component {
private readonly authLogic: InteractiveAuth;
private readonly intervalId: number = null;
diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx
index db6d4be1d8..9b4e98e66c 100644
--- a/src/components/structures/LeftPanel.tsx
+++ b/src/components/structures/LeftPanel.tsx
@@ -27,7 +27,6 @@ import { Action } from "../../dispatcher/actions";
import RoomSearch from "./RoomSearch";
import ResizeNotifier from "../../utils/ResizeNotifier";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { MetaSpace, SpaceKey, UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
@@ -61,7 +60,6 @@ interface IState {
activeSpace: SpaceKey;
}
-@replaceableComponent("structures.LeftPanel")
export default class LeftPanel extends React.Component {
private listContainerRef = createRef();
private roomSearchRef = createRef();
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index ff8616e29f..de60ca71fa 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -52,7 +52,6 @@ import HostSignupContainer from '../views/host_signup/HostSignupContainer';
import { getKeyBindingsManager } from '../../KeyBindingsManager';
import { IOpts } from "../../createRoom";
import SpacePanel from "../views/spaces/SpacePanel";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
import { OwnProfileStore } from '../../stores/OwnProfileStore';
@@ -63,7 +62,7 @@ import ToastContainer from './ToastContainer';
import UserView from "./UserView";
import BackdropPanel from "./BackdropPanel";
import { mediaFromMxc } from "../../customisations/Media";
-import { UserTab } from "../views/dialogs/UserSettingsDialog";
+import { UserTab } from "../views/dialogs/UserTab";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import RightPanelStore from '../../stores/right-panel/RightPanelStore';
import { TimelineRenderingType } from "../../contexts/RoomContext";
@@ -127,7 +126,6 @@ interface IState {
*
* Components mounted below us can access the matrix client via the react context.
*/
-@replaceableComponent("structures.LoggedInView")
class LoggedInView extends React.Component {
static displayName = 'LoggedInView';
diff --git a/src/components/structures/MainSplit.tsx b/src/components/structures/MainSplit.tsx
index 7cf6964c1f..a90758b442 100644
--- a/src/components/structures/MainSplit.tsx
+++ b/src/components/structures/MainSplit.tsx
@@ -19,7 +19,6 @@ import React from 'react';
import { NumberSize, Resizable } from 're-resizable';
import { Direction } from "re-resizable/lib/resizer";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import ResizeNotifier from "../../utils/ResizeNotifier";
interface IProps {
@@ -28,7 +27,6 @@ interface IProps {
panel?: JSX.Element;
}
-@replaceableComponent("structures.MainSplit")
export default class MainSplit extends React.Component {
private onResizeStart = (): void => {
this.props.resizeNotifier.startResizing();
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 969486c536..624909db31 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -33,7 +33,7 @@ import { throttle } from "lodash";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { RoomType } from "matrix-js-sdk/src/@types/event";
-// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
+// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by various components
import 'focus-visible';
// what-input helps improve keyboard accessibility
import 'what-input';
@@ -84,19 +84,18 @@ import {
UPDATE_STATUS_INDICATOR,
} from "../../stores/notifications/RoomNotificationStateStore";
import { SettingLevel } from "../../settings/SettingLevel";
-import { leaveRoomBehaviour } from "../../utils/membership";
import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore";
import { UIFeature } from "../../settings/UIFeature";
import DialPadModal from "../views/voip/DialPadModal";
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
import { shouldUseLoginForWelcome } from "../../utils/pages";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import RoomListStore from "../../stores/room-list/RoomListStore";
import { RoomUpdateCause } from "../../stores/room-list/models";
import SecurityCustomisations from "../../customisations/Security";
import Spinner from "../views/elements/Spinner";
import QuestionDialog from "../views/dialogs/QuestionDialog";
-import UserSettingsDialog, { UserTab } from '../views/dialogs/UserSettingsDialog';
+import UserSettingsDialog from '../views/dialogs/UserSettingsDialog';
+import { UserTab } from "../views/dialogs/UserTab";
import CreateRoomDialog from '../views/dialogs/CreateRoomDialog';
import RoomDirectory from './RoomDirectory';
import KeySignatureUploadFailedDialog from "../views/dialogs/KeySignatureUploadFailedDialog";
@@ -131,6 +130,7 @@ import { ViewStartChatOrReusePayload } from '../../dispatcher/payloads/ViewStart
import { IConfigOptions } from "../../IConfigOptions";
import { SnakedObject } from "../../utils/SnakedObject";
import InfoDialog from '../views/dialogs/InfoDialog';
+import { leaveRoomBehaviour } from "../../utils/leave-behaviour";
// legacy export
export { default as Views } from "../../Views";
@@ -208,7 +208,6 @@ interface IState {
forceTimeline?: boolean; // see props
}
-@replaceableComponent("structures.MatrixChat")
export default class MatrixChat extends React.PureComponent {
static displayName = "MatrixChat";
@@ -2097,12 +2096,3 @@ export default class MatrixChat extends React.PureComponent {
;
}
}
-
-export function isLoggedIn(): boolean {
- // JRS: Maybe we should move the step that writes this to the window out of
- // `element-web` and into this file? Better yet, we should probably create a
- // store to hold this state.
- // See also https://github.com/vector-im/element-web/issues/15034.
- const app = window.matrixChat;
- return app && (app as MatrixChat).state.view === Views.LOGGED_IN;
-}
diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx
index cc6ad9ff5d..ff9bc82652 100644
--- a/src/components/structures/MessagePanel.tsx
+++ b/src/components/structures/MessagePanel.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { createRef, KeyboardEvent, ReactNode, SyntheticEvent, TransitionEvent } from 'react';
+import React, { createRef, KeyboardEvent, ReactNode, TransitionEvent } from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import { Room } from 'matrix-js-sdk/src/models/room';
@@ -31,13 +31,12 @@ import SettingsStore from '../../settings/SettingsStore';
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import { Layout } from "../../settings/enums/Layout";
import { _t } from "../../languageHandler";
-import EventTile, { UnwrappedEventTile, haveTileForEvent, IReadReceiptProps } from "../views/rooms/EventTile";
+import EventTile, { UnwrappedEventTile, IReadReceiptProps } from "../views/rooms/EventTile";
import { hasText } from "../../TextForEvent";
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro";
import HistoryTile from "../views/rooms/HistoryTile";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import defaultDispatcher from '../../dispatcher/dispatcher';
import CallEventGrouper from "./CallEventGrouper";
import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
@@ -51,8 +50,9 @@ import Spinner from "../views/elements/Spinner";
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
import EditorStateTransfer from "../../utils/EditorStateTransfer";
import { Action } from '../../dispatcher/actions';
-import { getEventDisplayInfo } from "../../utils/EventUtils";
+import { getEventDisplayInfo } from "../../utils/EventRenderingUtils";
import { IReadReceiptInfo } from "../views/rooms/ReadReceiptMarker";
+import { haveRendererForEvent } from "../../events/EventTileFactory";
import { editorRoomKey } from "../../Editing";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
@@ -97,7 +97,7 @@ export function shouldFormContinuation(
timelineRenderingType !== TimelineRenderingType.Thread) return false;
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
- if (!haveTileForEvent(prevEvent, showHiddenEvents)) return false;
+ if (!haveRendererForEvent(prevEvent, showHiddenEvents)) return false;
return true;
}
@@ -170,9 +170,6 @@ interface IProps {
// callback which is called when the panel is scrolled.
onScroll?(event: Event): void;
- // callback which is called when the user interacts with the room timeline
- onUserScroll(event: SyntheticEvent): void;
-
// callback which is called when more content is needed.
onFillRequest?(backwards: boolean): Promise;
@@ -200,7 +197,6 @@ interface IReadReceiptForUser {
/* (almost) stateless UI component which builds the event tiles in the room timeline.
*/
-@replaceableComponent("structures.MessagePanel")
export default class MessagePanel extends React.Component {
static contextType = RoomContext;
public context!: React.ContextType;
@@ -246,7 +242,7 @@ export default class MessagePanel extends React.Component {
// displayed event in the current render cycle.
private readReceiptsByUserId: Record = {};
- private readonly showHiddenEventsInTimeline: boolean;
+ private readonly _showHiddenEvents: boolean;
private readonly threadsEnabled: boolean;
private isMounted = false;
@@ -274,7 +270,7 @@ export default class MessagePanel extends React.Component {
// Cache these settings on mount since Settings is expensive to query,
// and we check this in a hot code path. This is also cached in our
// RoomContext, however we still need a fallback for roomless MessagePanels.
- this.showHiddenEventsInTimeline = SettingsStore.getValue("showHiddenEventsInTimeline");
+ this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline");
this.threadsEnabled = SettingsStore.getValue("feature_thread");
this.showTypingNotificationsWatcherRef =
@@ -469,7 +465,7 @@ export default class MessagePanel extends React.Component {
};
public get showHiddenEvents(): boolean {
- return this.context?.showHiddenEventsInTimeline ?? this.showHiddenEventsInTimeline;
+ return this.context?.showHiddenEvents ?? this._showHiddenEvents;
}
// TODO: Implement granular (per-room) hide options
@@ -492,7 +488,7 @@ export default class MessagePanel extends React.Component {
return true;
}
- if (!haveTileForEvent(mxEv, this.showHiddenEvents)) {
+ if (!haveRendererForEvent(mxEv, this.showHiddenEvents)) {
return false; // no tile = no show
}
@@ -752,7 +748,7 @@ export default class MessagePanel extends React.Component {
const willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEv.getDate() || new Date());
lastInSection = willWantDateSeparator ||
mxEv.getSender() !== nextEv.getSender() ||
- getEventDisplayInfo(nextEv).isInfoMessage ||
+ getEventDisplayInfo(nextEv, this.showHiddenEvents).isInfoMessage ||
!shouldFormContinuation(
mxEv, nextEv, this.showHiddenEvents, this.threadsEnabled, this.context.timelineRenderingType,
);
@@ -1031,7 +1027,6 @@ export default class MessagePanel extends React.Component {
ref={this.scrollPanel}
className={classes}
onScroll={this.props.onScroll}
- onUserScroll={this.props.onUserScroll}
onFillRequest={this.props.onFillRequest}
onUnfillRequest={this.props.onUnfillRequest}
style={style}
diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx
index 7872c2596c..10cb1ad427 100644
--- a/src/components/structures/NonUrgentToastContainer.tsx
+++ b/src/components/structures/NonUrgentToastContainer.tsx
@@ -19,7 +19,6 @@ import * as React from "react";
import { ComponentClass } from "../../@types/common";
import NonUrgentToastStore from "../../stores/NonUrgentToastStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
-import { replaceableComponent } from "../../utils/replaceableComponent";
interface IProps {
}
@@ -28,7 +27,6 @@ interface IState {
toasts: ComponentClass[];
}
-@replaceableComponent("structures.NonUrgentToastContainer")
export default class NonUrgentToastContainer extends React.PureComponent {
public constructor(props, context) {
super(props, context);
diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx
index 6af271bcc2..e5f094773b 100644
--- a/src/components/structures/NotificationPanel.tsx
+++ b/src/components/structures/NotificationPanel.tsx
@@ -20,7 +20,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../languageHandler';
import { MatrixClientPeg } from "../../MatrixClientPeg";
import BaseCard from "../views/right_panel/BaseCard";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
import { Layout } from "../../settings/enums/Layout";
@@ -38,7 +37,6 @@ interface IState {
/*
* Component which shows the global notification list using a TimelinePanel
*/
-@replaceableComponent("structures.NotificationPanel")
export default class NotificationPanel extends React.PureComponent {
static contextType = RoomContext;
diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx
index 3bf0ed26b9..5c2eaaf9a4 100644
--- a/src/components/structures/RightPanel.tsx
+++ b/src/components/structures/RightPanel.tsx
@@ -28,7 +28,6 @@ import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
import WidgetCard from "../views/right_panel/WidgetCard";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import SettingsStore from "../../settings/SettingsStore";
import MemberList from "../views/rooms/MemberList";
import UserInfo from "../views/right_panel/UserInfo";
@@ -60,7 +59,6 @@ interface IState {
cardState?: IRightPanelCardState;
}
-@replaceableComponent("structures.RightPanel")
export default class RightPanel extends React.Component {
static contextType = MatrixClientContext;
public context!: React.ContextType;
@@ -233,6 +231,7 @@ export default class RightPanel extends React.Component {
mxEvent={cardState.threadHeadEvent}
initialEvent={cardState.initialEvent}
isInitialEventHighlighted={cardState.isInitialEventHighlighted}
+ initialEventScrollIntoView={cardState.initialEventScrollIntoView}
permalinkCreator={this.props.permalinkCreator}
e2eStatus={this.props.e2eStatus}
/>;
diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx
index 5cb90f1086..5577bc29e7 100644
--- a/src/components/structures/RoomDirectory.tsx
+++ b/src/components/structures/RoomDirectory.tsx
@@ -24,18 +24,14 @@ import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import dis from "../../dispatcher/dispatcher";
import Modal from "../../Modal";
-import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
import { _t } from '../../languageHandler';
import SdkConfig from '../../SdkConfig';
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
import Analytics from '../../Analytics';
import NetworkDropdown, { ALL_ROOMS, Protocols } from "../views/directory/NetworkDropdown";
import SettingsStore from "../../settings/SettingsStore";
-import { replaceableComponent } from "../../utils/replaceableComponent";
-import { mediaFromMxc } from "../../customisations/Media";
import { IDialogProps } from "../views/dialogs/IDialogProps";
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
-import BaseAvatar from "../views/avatars/BaseAvatar";
import ErrorDialog from "../views/dialogs/ErrorDialog";
import QuestionDialog from "../views/dialogs/QuestionDialog";
import BaseDialog from "../views/dialogs/BaseDialog";
@@ -46,9 +42,7 @@ import { getDisplayAliasForAliasSet } from "../../Rooms";
import { Action } from "../../dispatcher/actions";
import PosthogTrackers from "../../PosthogTrackers";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
-
-const MAX_NAME_LENGTH = 80;
-const MAX_TOPIC_LENGTH = 800;
+import { PublicRoomTile } from "../views/rooms/PublicRoomTile";
const LAST_SERVER_KEY = "mx_last_room_directory_server";
const LAST_INSTANCE_KEY = "mx_last_room_directory_instance";
@@ -71,7 +65,6 @@ interface IState {
filterString: string;
}
-@replaceableComponent("structures.RoomDirectory")
export default class RoomDirectory extends React.Component {
private unmounted = false;
private nextBatch: string = null;
@@ -251,7 +244,7 @@ export default class RoomDirectory extends React.Component {
* HS admins to do this through the RoomSettings interface, but
* this needs SPEC-417.
*/
- private removeFromDirectory(room: IPublicRoomsChunkRoom) {
+ private removeFromDirectory = (room: IPublicRoomsChunkRoom) => {
const alias = getDisplayAliasForRoom(room);
const name = room.name || alias || _t('Unnamed room');
@@ -291,14 +284,6 @@ export default class RoomDirectory extends React.Component {
});
},
});
- }
-
- private onRoomClicked = (room: IPublicRoomsChunkRoom, ev: React.MouseEvent) => {
- // If room was shift-clicked, remove it from the room directory
- if (ev.shiftKey) {
- ev.preventDefault();
- this.removeFromDirectory(room);
- }
};
private onOptionChange = (server: string, instanceId?: string) => {
@@ -406,21 +391,6 @@ export default class RoomDirectory extends React.Component {
}
};
- private onPreviewClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
- this.showRoom(room, null, false, true);
- ev.stopPropagation();
- };
-
- private onViewClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
- this.showRoom(room);
- ev.stopPropagation();
- };
-
- private onJoinClick = (ev: ButtonEvent, room: IPublicRoomsChunkRoom) => {
- this.showRoom(room, null, true);
- ev.stopPropagation();
- };
-
private onCreateRoomClick = (ev: ButtonEvent) => {
this.onFinished();
dis.dispatch({
@@ -435,7 +405,7 @@ export default class RoomDirectory extends React.Component {
this.showRoom(null, alias, autoJoin);
}
- private showRoom(room: IPublicRoomsChunkRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) {
+ private showRoom = (room: IPublicRoomsChunkRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) => {
this.onFinished();
const payload: ViewRoomPayload = {
action: Action.ViewRoom,
@@ -479,112 +449,7 @@ export default class RoomDirectory extends React.Component {
payload.room_id = room.room_id;
}
dis.dispatch(payload);
- }
-
- private createRoomCells(room: IPublicRoomsChunkRoom) {
- const client = MatrixClientPeg.get();
- const clientRoom = client.getRoom(room.room_id);
- const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
- const isGuest = client.isGuest();
- let previewButton;
- let joinOrViewButton;
-
- // Element Web currently does not allow guests to join rooms, so we
- // instead show them preview buttons for all rooms. If the room is not
- // world readable, a modal will appear asking you to register first. If
- // it is readable, the preview appears as normal.
- if (!hasJoinedRoom && (room.world_readable || isGuest)) {
- previewButton = (
- this.onPreviewClick(ev, room)}>
- { _t("Preview") }
-
- );
- }
- if (hasJoinedRoom) {
- joinOrViewButton = (
- this.onViewClick(ev, room)}>
- { _t("View") }
-
- );
- } else if (!isGuest) {
- joinOrViewButton = (
- this.onJoinClick(ev, room)}>
- { _t("Join") }
-
- );
- }
-
- let name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room');
- if (name.length > MAX_NAME_LENGTH) {
- name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
- }
-
- let topic = room.topic || '';
- // Additional truncation based on line numbers is done via CSS,
- // but to ensure that the DOM is not polluted with a huge string
- // we give it a hard limit before rendering.
- if (topic.length > MAX_TOPIC_LENGTH) {
- topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
- }
- topic = linkifyAndSanitizeHtml(topic);
- let avatarUrl = null;
- if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
-
- // We use onMouseDown instead of onClick, so that we can avoid text getting selected
- return
- this.onRoomClicked(room, ev)}
- className="mx_RoomDirectory_roomAvatar"
- >
-
-
- this.onRoomClicked(room, ev)}
- className="mx_RoomDirectory_roomDescription"
- >
-
- { name }
-
-
-
- { getDisplayAliasForRoom(room) }
-
-
- this.onRoomClicked(room, ev)}
- className="mx_RoomDirectory_roomMemberCount"
- >
- { room.num_joined_members }
-
- this.onRoomClicked(room, ev)}
- className="mx_RoomDirectory_preview"
- >
- { previewButton }
-
- this.onRoomClicked(room, ev)}
- className="mx_RoomDirectory_join"
- >
- { joinOrViewButton }
-
- ;
- }
-
+ };
private stringLooksLikeId(s: string, fieldType: IFieldType) {
let pat = /^#[^\s]+:[^\s]/;
if (fieldType && fieldType.regexp) {
@@ -622,7 +487,14 @@ export default class RoomDirectory extends React.Component {
content = ;
} else {
const cells = (this.state.publicRooms || [])
- .reduce((cells, room) => cells.concat(this.createRoomCells(room)), []);
+ .map(room =>
+ ,
+ );
// we still show the scrollpanel, at least for now, because
// otherwise we don't fetch more because we don't get a fill
// request from the scrollpanel because there isn't one
diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx
index 5591e382ea..9421292764 100644
--- a/src/components/structures/RoomSearch.tsx
+++ b/src/components/structures/RoomSearch.tsx
@@ -26,7 +26,6 @@ import { Action } from "../../dispatcher/actions";
import RoomListStore from "../../stores/room-list/RoomListStore";
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import { isMac, Key } from "../../Keyboard";
@@ -50,7 +49,6 @@ interface IState {
spotlightBetaEnabled: boolean;
}
-@replaceableComponent("structures.RoomSearch")
export default class RoomSearch extends React.PureComponent {
private readonly dispatcherRef: string;
private readonly betaRef: string;
diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx
index 514c3a507c..94b9905bec 100644
--- a/src/components/structures/RoomStatusBar.tsx
+++ b/src/components/structures/RoomStatusBar.tsx
@@ -24,7 +24,6 @@ import Resend from '../../Resend';
import dis from '../../dispatcher/dispatcher';
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
import { Action } from "../../dispatcher/actions";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import NotificationBadge from "../views/rooms/NotificationBadge";
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
import AccessibleButton from "../views/elements/AccessibleButton";
@@ -82,7 +81,6 @@ interface IState {
isResending: boolean;
}
-@replaceableComponent("structures.RoomStatusBar")
export default class RoomStatusBar extends React.PureComponent {
private unmounted = false;
public static contextType = MatrixClientContext;
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 29f6445574..f53e75db0b 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -23,7 +23,7 @@ limitations under the License.
import React, { createRef } from 'react';
import classNames from 'classnames';
import { IRecommendedVersion, NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
-import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
+import { IThreadBundledRelationship, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
import { EventSubscription } from "fbemitter";
import { ISearchResults } from 'matrix-js-sdk/src/@types/search';
import { logger } from "matrix-js-sdk/src/logger";
@@ -35,6 +35,7 @@ import { throttle } from "lodash";
import { MatrixError } from 'matrix-js-sdk/src/http-api';
import { ClientEvent } from "matrix-js-sdk/src/client";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
+import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';
import shouldHideEvent from '../../shouldHideEvent';
import { _t } from '../../languageHandler';
@@ -48,14 +49,13 @@ import * as Rooms from '../../Rooms';
import eventSearch, { searchPagination } from '../../Searching';
import MainSplit from './MainSplit';
import RightPanel from './RightPanel';
-import RoomViewStore from '../../stores/RoomViewStore';
+import { RoomViewStore } from '../../stores/RoomViewStore';
import RoomScrollStateStore, { ScrollState } from '../../stores/RoomScrollStateStore';
import WidgetEchoStore from '../../stores/WidgetEchoStore';
import SettingsStore from "../../settings/SettingsStore";
import { Layout } from "../../settings/enums/Layout";
import AccessibleButton from "../views/elements/AccessibleButton";
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
-import { haveTileForEvent } from "../views/rooms/EventTile";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import MatrixClientContext, { MatrixClientProps, withMatrixClientHOC } from "../../contexts/MatrixClientContext";
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
@@ -86,7 +86,6 @@ import { getKeyBindingsManager } from '../../KeyBindingsManager';
import { objectHasDiff } from "../../utils/objects";
import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import EditorStateTransfer from "../../utils/EditorStateTransfer";
import ErrorDialog from '../views/dialogs/ErrorDialog';
import SearchResultTile from '../views/rooms/SearchResultTile';
@@ -109,6 +108,7 @@ import { DoAfterSyncPreparedPayload } from '../../dispatcher/payloads/DoAfterSyn
import FileDropTarget from './FileDropTarget';
import Measured from '../views/elements/Measured';
import { FocusComposerPayload } from '../../dispatcher/payloads/FocusComposerPayload';
+import { haveRendererForEvent } from "../../events/EventTileFactory";
const DEBUG = false;
let debuglog = function(msg: string) {};
@@ -156,6 +156,8 @@ export interface IRoomState {
initialEventPixelOffset?: number;
// Whether to highlight the event scrolled to
isInitialEventHighlighted?: boolean;
+ // Whether to scroll the event into view
+ initialEventScrollIntoView?: boolean;
replyToEvent?: MatrixEvent;
numUnreadMessages: number;
searchTerm?: string;
@@ -197,7 +199,7 @@ export interface IRoomState {
showTwelveHourTimestamps: boolean;
readMarkerInViewThresholdMs: number;
readMarkerOutOfViewThresholdMs: number;
- showHiddenEventsInTimeline: boolean;
+ showHiddenEvents: boolean;
showReadReceipts: boolean;
showRedactions: boolean;
showJoinLeaves: boolean;
@@ -220,7 +222,6 @@ export interface IRoomState {
narrow: boolean;
}
-@replaceableComponent("structures.RoomView")
export class RoomView extends React.Component {
private readonly dispatcherRef: string;
private readonly roomStoreToken: EventSubscription;
@@ -270,7 +271,7 @@ export class RoomView extends React.Component {
showTwelveHourTimestamps: SettingsStore.getValue("showTwelveHourTimestamps"),
readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
- showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline"),
+ showHiddenEvents: SettingsStore.getValue("showHiddenEventsInTimeline"),
showReadReceipts: true,
showRedactions: true,
showJoinLeaves: true,
@@ -298,7 +299,7 @@ export class RoomView extends React.Component {
context.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
context.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
// Start listening for RoomViewStore updates
- this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
+ this.roomStoreToken = RoomViewStore.instance.addListener(this.onRoomViewStoreUpdate);
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
@@ -327,7 +328,7 @@ export class RoomView extends React.Component {
this.setState({ readMarkerOutOfViewThresholdMs: value as number }),
),
SettingsStore.watchSetting("showHiddenEventsInTimeline", null, (...[,,, value]) =>
- this.setState({ showHiddenEventsInTimeline: value as boolean }),
+ this.setState({ showHiddenEvents: value as boolean }),
),
];
}
@@ -389,7 +390,7 @@ export class RoomView extends React.Component {
return;
}
- if (!initial && this.state.roomId !== RoomViewStore.getRoomId()) {
+ if (!initial && this.state.roomId !== RoomViewStore.instance.getRoomId()) {
// RoomView explicitly does not support changing what room
// is being viewed: instead it should just be re-mounted when
// switching rooms. Therefore, if the room ID changes, we
@@ -404,28 +405,29 @@ export class RoomView extends React.Component {
return;
}
- const roomId = RoomViewStore.getRoomId();
+ const roomId = RoomViewStore.instance.getRoomId();
- const newState: Pick = {
+ // This convoluted type signature ensures we get IntelliSense *and* correct typing
+ const newState: Partial & Pick = {
roomId,
- roomAlias: RoomViewStore.getRoomAlias(),
- roomLoading: RoomViewStore.isRoomLoading(),
- roomLoadError: RoomViewStore.getRoomLoadError(),
- joining: RoomViewStore.isJoining(),
- replyToEvent: RoomViewStore.getQuotingEvent(),
+ roomAlias: RoomViewStore.instance.getRoomAlias(),
+ roomLoading: RoomViewStore.instance.isRoomLoading(),
+ roomLoadError: RoomViewStore.instance.getRoomLoadError(),
+ joining: RoomViewStore.instance.isJoining(),
+ replyToEvent: RoomViewStore.instance.getQuotingEvent(),
// we should only peek once we have a ready client
- shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
+ shouldPeek: this.state.matrixClientIsReady && RoomViewStore.instance.shouldPeek(),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
showRedactions: SettingsStore.getValue("showRedactions", roomId),
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
- wasContextSwitch: RoomViewStore.getWasContextSwitch(),
+ wasContextSwitch: RoomViewStore.instance.getWasContextSwitch(),
initialEventId: null, // default to clearing this, will get set later in the method if needed
showRightPanel: RightPanelStore.instance.isOpenForRoom(roomId),
};
- const initialEventId = RoomViewStore.getInitialEventId();
+ const initialEventId = RoomViewStore.instance.getInitialEventId();
if (initialEventId) {
const room = this.context.getRoom(roomId);
let initialEvent = room?.findEventById(initialEventId);
@@ -445,22 +447,29 @@ export class RoomView extends React.Component {
);
}
+ // If we have an initial event, we want to reset the event pixel offset to ensure it ends up
+ // visible
+ newState.initialEventPixelOffset = null;
+
const thread = initialEvent?.getThread();
if (thread && !initialEvent?.isThreadRoot) {
showThread({
rootEvent: thread.rootEvent,
initialEvent,
- highlighted: RoomViewStore.isInitialEventHighlighted(),
+ highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
+ scroll_into_view: RoomViewStore.instance.initialEventScrollIntoView(),
});
} else {
newState.initialEventId = initialEventId;
- newState.isInitialEventHighlighted = RoomViewStore.isInitialEventHighlighted();
+ newState.isInitialEventHighlighted = RoomViewStore.instance.isInitialEventHighlighted();
+ newState.initialEventScrollIntoView = RoomViewStore.instance.initialEventScrollIntoView();
if (thread && initialEvent?.isThreadRoot) {
showThread({
rootEvent: thread.rootEvent,
initialEvent,
- highlighted: RoomViewStore.isInitialEventHighlighted(),
+ highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
+ scroll_into_view: RoomViewStore.instance.initialEventScrollIntoView(),
});
}
}
@@ -760,19 +769,6 @@ export class RoomView extends React.Component {
}
}
- private onUserScroll = () => {
- if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
- dis.dispatch({
- action: Action.ViewRoom,
- room_id: this.state.room.roomId,
- event_id: this.state.initialEventId,
- highlighted: false,
- replyingToEvent: this.state.replyToEvent,
- metricsTrigger: undefined, // room doesn't change
- });
- }
- };
-
private onRightPanelStoreUpdate = () => {
this.setState({
showRightPanel: RightPanelStore.instance.isOpenForRoom(this.state.roomId),
@@ -1303,6 +1299,22 @@ export class RoomView extends React.Component {
this.updateTopUnreadMessagesBar();
};
+ private resetJumpToEvent = (eventId?: string) => {
+ if (this.state.initialEventId && this.state.initialEventScrollIntoView &&
+ this.state.initialEventId === eventId) {
+ debuglog("Removing scroll_into_view flag from initial event");
+ dis.dispatch({
+ action: Action.ViewRoom,
+ room_id: this.state.room.roomId,
+ event_id: this.state.initialEventId,
+ highlighted: this.state.isInitialEventHighlighted,
+ scroll_into_view: false,
+ replyingToEvent: this.state.replyToEvent,
+ metricsTrigger: undefined, // room doesn't change
+ });
+ }
+ };
+
private injectSticker(url: string, info: object, text: string, threadId: string | null) {
if (this.context.isGuest()) {
dis.dispatch({ action: 'require_registration' });
@@ -1347,7 +1359,7 @@ export class RoomView extends React.Component {
this.handleSearchResult(searchPromise);
};
- private handleSearchResult(searchPromise: Promise): Promise {
+ private handleSearchResult(searchPromise: Promise): Promise {
// keep a record of the current search id, so that if the search terms
// change before we get a response, we can ignore the results.
const localSearchId = this.searchId;
@@ -1356,7 +1368,7 @@ export class RoomView extends React.Component {
searchInProgress: true,
});
- return searchPromise.then((results) => {
+ return searchPromise.then(async (results) => {
debuglog("search complete");
if (this.unmounted ||
this.state.timelineRenderingType !== TimelineRenderingType.Search ||
@@ -1383,6 +1395,20 @@ export class RoomView extends React.Component {
return b.length - a.length;
});
+ if (this.context.supportsExperimentalThreads()) {
+ // Process all thread roots returned in this batch of search results
+ // XXX: This won't work for results coming from Seshat which won't include the bundled relationship
+ for (const result of results.results) {
+ for (const event of result.context.getTimeline()) {
+ const bundledRelationship = event
+ .getServerAggregatedRelation(THREAD_RELATION_TYPE.name);
+ if (!bundledRelationship || event.getThread()) continue;
+ const room = this.context.getRoom(event.getRoomId());
+ event.setThread(room.findThreadForEvent(event) ?? room.createThread(event, [], true));
+ }
+ }
+ }
+
this.setState({
searchHighlights: highlights,
searchResults: results,
@@ -1454,7 +1480,7 @@ export class RoomView extends React.Component {
continue;
}
- if (!haveTileForEvent(mxEv, this.state.showHiddenEventsInTimeline)) {
+ if (!haveRendererForEvent(mxEv, this.state.showHiddenEvents)) {
// XXX: can this ever happen? It will make the result count
// not match the displayed count.
continue;
@@ -2053,9 +2079,10 @@ export class RoomView extends React.Component {
hidden={hideMessagePanel}
highlightedEventId={highlightedEventId}
eventId={this.state.initialEventId}
+ eventScrollIntoView={this.state.initialEventScrollIntoView}
eventPixelOffset={this.state.initialEventPixelOffset}
onScroll={this.onMessageListScroll}
- onUserScroll={this.onUserScroll}
+ onEventScrolledIntoView={this.resetJumpToEvent}
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
showUrlPreview={this.state.showUrlPreview}
className={this.messagePanelClassNames}
diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx
index d650cfc962..359c10509d 100644
--- a/src/components/structures/ScrollPanel.tsx
+++ b/src/components/structures/ScrollPanel.tsx
@@ -14,12 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { createRef, CSSProperties, ReactNode, SyntheticEvent, KeyboardEvent } from "react";
+import React, { createRef, CSSProperties, ReactNode, KeyboardEvent } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import Timer from '../../utils/Timer';
import AutoHideScrollbar from "./AutoHideScrollbar";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import ResizeNotifier from "../../utils/ResizeNotifier";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
@@ -110,10 +109,6 @@ interface IProps {
/* onScroll: a callback which is called whenever any scroll happens.
*/
onScroll?(event: Event): void;
-
- /* onUserScroll: callback which is called when the user interacts with the room timeline
- */
- onUserScroll?(event: SyntheticEvent): void;
}
/* This component implements an intelligent scrolling list.
@@ -170,7 +165,6 @@ interface IPreventShrinkingState {
offsetNode: HTMLElement;
}
-@replaceableComponent("structures.ScrollPanel")
export default class ScrollPanel extends React.Component {
static defaultProps = {
stickyBottom: true,
@@ -595,29 +589,21 @@ export default class ScrollPanel extends React.Component {
* @param {object} ev the keyboard event
*/
public handleScrollKey = (ev: KeyboardEvent) => {
- let isScrolling = false;
const roomAction = getKeyBindingsManager().getRoomAction(ev);
switch (roomAction) {
case KeyBindingAction.ScrollUp:
this.scrollRelative(-1);
- isScrolling = true;
break;
case KeyBindingAction.ScrollDown:
this.scrollRelative(1);
- isScrolling = true;
break;
case KeyBindingAction.JumpToFirstMessage:
this.scrollToTop();
- isScrolling = true;
break;
case KeyBindingAction.JumpToLatestMessage:
this.scrollToBottom();
- isScrolling = true;
break;
}
- if (isScrolling && this.props.onUserScroll) {
- this.props.onUserScroll(ev);
- }
};
/* Scroll the panel to bring the DOM node with the scroll token
@@ -967,7 +953,6 @@ export default class ScrollPanel extends React.Component {
diff --git a/src/components/structures/SearchBox.tsx b/src/components/structures/SearchBox.tsx
index 5789e5adf1..bcf3dcc875 100644
--- a/src/components/structures/SearchBox.tsx
+++ b/src/components/structures/SearchBox.tsx
@@ -20,7 +20,6 @@ import { throttle } from 'lodash';
import classNames from 'classnames';
import AccessibleButton from '../../components/views/elements/AccessibleButton';
-import { replaceableComponent } from "../../utils/replaceableComponent";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
@@ -43,7 +42,6 @@ interface IState {
blurred: boolean;
}
-@replaceableComponent("structures.SearchBox")
export default class SearchBox extends React.Component {
private search = createRef();
diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx
index 625a252365..756cacab1f 100644
--- a/src/components/structures/SpaceHierarchy.tsx
+++ b/src/components/structures/SpaceHierarchy.tsx
@@ -60,7 +60,7 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
import { useTypedEventEmitterState } from "../../hooks/useEventEmitter";
import { IOOBData } from "../../stores/ThreepidInviteStore";
import { awaitRoomDownSync } from "../../utils/RoomUpgrade";
-import RoomViewStore from "../../stores/RoomViewStore";
+import { RoomViewStore } from "../../stores/RoomViewStore";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { JoinRoomReadyPayload } from "../../dispatcher/payloads/JoinRoomReadyPayload";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
@@ -371,7 +371,7 @@ export const joinRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st
metricsTrigger: "SpaceHierarchy",
});
}, err => {
- RoomViewStore.showJoinRoomError(err, roomId);
+ RoomViewStore.instance.showJoinRoomError(err, roomId);
});
return prom;
diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx
index ea116b7d8e..91e64946ad 100644
--- a/src/components/structures/TabbedView.tsx
+++ b/src/components/structures/TabbedView.tsx
@@ -22,7 +22,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../languageHandler';
import AutoHideScrollbar from './AutoHideScrollbar';
-import { replaceableComponent } from "../../utils/replaceableComponent";
import AccessibleButton from "../views/elements/AccessibleButton";
import { PosthogScreenTracker, ScreenName } from "../../PosthogTrackers";
@@ -64,7 +63,6 @@ interface IState {
activeTabIndex: number;
}
-@replaceableComponent("structures.TabbedView")
export default class TabbedView extends React.Component {
constructor(props: IProps) {
super(props);
diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx
index bc2c53d203..3009bedae3 100644
--- a/src/components/structures/ThreadPanel.tsx
+++ b/src/components/structures/ThreadPanel.tsx
@@ -37,7 +37,7 @@ import SdkConfig from '../../SdkConfig';
import Modal from '../../Modal';
import BetaFeedbackDialog from '../views/dialogs/BetaFeedbackDialog';
import { Action } from '../../dispatcher/actions';
-import { UserTab } from '../views/dialogs/UserSettingsDialog';
+import { UserTab } from '../views/dialogs/UserTab';
import dis from '../../dispatcher/dispatcher';
interface IProps {
@@ -163,7 +163,9 @@ const EmptyThread: React.FC = ({ hasThreads, filterOption, sh
body = <>
{ _t("Threads help keep your conversations on-topic and easy to track.") }
- { _t('Tip: Use "Reply in thread" when hovering over a message.', {}, {
+ { _t('Tip: Use “%(replyInThread)s” when hovering over a message.', {
+ replyInThread: _t("Reply in thread"),
+ }, {
b: sub => { sub },
}) }
@@ -250,7 +252,7 @@ const ThreadPanel: React.FC = ({
{
static contextType = RoomContext;
public context!: React.ContextType;
@@ -112,7 +111,7 @@ export default class ThreadView extends React.Component {
room.removeListener(ThreadEvent.New, this.onNewThread);
SettingsStore.unwatchSetting(this.layoutWatcherRef);
- const hasRoomChanged = RoomViewStore.getRoomId() !== roomId;
+ const hasRoomChanged = RoomViewStore.instance.getRoomId() !== roomId;
if (this.props.isInitialEventHighlighted && !hasRoomChanged) {
dis.dispatch({
action: Action.ViewRoom,
@@ -217,13 +216,15 @@ export default class ThreadView extends React.Component {
}
};
- private resetHighlightedEvent = (): void => {
- if (this.props.initialEvent && this.props.isInitialEventHighlighted) {
+ private resetJumpToEvent = (event?: string): void => {
+ if (this.props.initialEvent && this.props.initialEventScrollIntoView &&
+ event === this.props.initialEvent?.getId()) {
dis.dispatch({
action: Action.ViewRoom,
room_id: this.props.room.roomId,
event_id: this.props.initialEvent?.getId(),
- highlighted: false,
+ highlighted: this.props.isInitialEventHighlighted,
+ scroll_into_view: false,
replyingToEvent: this.state.replyToEvent,
metricsTrigger: undefined, // room doesn't change
});
@@ -374,7 +375,8 @@ export default class ThreadView extends React.Component {
editState={this.state.editState}
eventId={this.props.initialEvent?.getId()}
highlightedEventId={highlightedEventId}
- onUserScroll={this.resetHighlightedEvent}
+ eventScrollIntoView={this.props.initialEventScrollIntoView}
+ onEventScrolledIntoView={this.resetJumpToEvent}
onPaginationRequest={this.onPaginationRequest}
/>
}
diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx
index 96b892245b..bc4ea5bdcb 100644
--- a/src/components/structures/TimelinePanel.tsx
+++ b/src/components/structures/TimelinePanel.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { createRef, ReactNode, SyntheticEvent } from 'react';
+import React, { createRef, ReactNode } from 'react';
import ReactDOM from "react-dom";
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
@@ -40,8 +40,6 @@ import dis from "../../dispatcher/dispatcher";
import { Action } from '../../dispatcher/actions';
import Timer from '../../utils/Timer';
import shouldHideEvent from '../../shouldHideEvent';
-import { haveTileForEvent } from "../views/rooms/EventTile";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import { arrayFastClone } from "../../utils/arrays";
import MessagePanel from "./MessagePanel";
import { IScrollState } from "./ScrollPanel";
@@ -55,6 +53,7 @@ import CallEventGrouper, { buildCallEventGroupers } from "./CallEventGrouper";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
+import { haveRendererForEvent } from "../../events/EventTileFactory";
const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20;
@@ -92,6 +91,9 @@ interface IProps {
// id of an event to jump to. If not given, will go to the end of the live timeline.
eventId?: string;
+ // whether we should scroll the event into view
+ eventScrollIntoView?: boolean;
+
// where to position the event given by eventId, in pixels from the bottom of the viewport.
// If not given, will try to put the event half way down the viewport.
eventPixelOffset?: number;
@@ -125,8 +127,7 @@ interface IProps {
// callback which is called when the panel is scrolled.
onScroll?(event: Event): void;
- // callback which is called when the user interacts with the room timeline
- onUserScroll?(event: SyntheticEvent): void;
+ onEventScrolledIntoView?(eventId?: string): void;
// callback which is called when the read-up-to mark is updated.
onReadMarkerUpdated?(): void;
@@ -211,7 +212,6 @@ interface IEventIndexOpts {
*
* Also responsible for handling and sending read receipts.
*/
-@replaceableComponent("structures.TimelinePanel")
class TimelinePanel extends React.Component {
static contextType = RoomContext;
@@ -329,9 +329,11 @@ class TimelinePanel extends React.Component {
const differentEventId = newProps.eventId != this.props.eventId;
const differentHighlightedEventId = newProps.highlightedEventId != this.props.highlightedEventId;
- if (differentEventId || differentHighlightedEventId) {
- logger.log("TimelinePanel switching to eventId " + newProps.eventId +
- " (was " + this.props.eventId + ")");
+ const differentAvoidJump = newProps.eventScrollIntoView && !this.props.eventScrollIntoView;
+ if (differentEventId || differentHighlightedEventId || differentAvoidJump) {
+ logger.log("TimelinePanel switching to " +
+ "eventId " + newProps.eventId + " (was " + this.props.eventId + "), " +
+ "scrollIntoView: " + newProps.eventScrollIntoView + " (was " + this.props.eventScrollIntoView + ")");
return this.initTimeline(newProps);
}
}
@@ -1125,7 +1127,41 @@ class TimelinePanel extends React.Component {
offsetBase = 0.5;
}
- return this.loadTimeline(initialEvent, pixelOffset, offsetBase);
+ return this.loadTimeline(initialEvent, pixelOffset, offsetBase, props.eventScrollIntoView);
+ }
+
+ private scrollIntoView(eventId?: string, pixelOffset?: number, offsetBase?: number): void {
+ const doScroll = () => {
+ if (eventId) {
+ debuglog("TimelinePanel scrolling to eventId " + eventId +
+ " at position " + (offsetBase * 100) + "% + " + pixelOffset);
+ this.messagePanel.current.scrollToEvent(
+ eventId,
+ pixelOffset,
+ offsetBase,
+ );
+ } else {
+ debuglog("TimelinePanel scrolling to bottom");
+ this.messagePanel.current.scrollToBottom();
+ }
+ };
+
+ debuglog("TimelinePanel scheduling scroll to event");
+ this.props.onEventScrolledIntoView?.(eventId);
+ // Ensure the correct scroll position pre render, if the messages have already been loaded to DOM,
+ // to avoid it jumping around
+ doScroll();
+
+ // Ensure the correct scroll position post render for correct behaviour.
+ //
+ // requestAnimationFrame runs our code immediately after the DOM update but before the next repaint.
+ //
+ // If the messages have just been loaded for the first time, this ensures we'll repeat setting the
+ // correct scroll position after React has re-rendered the TimelinePanel and MessagePanel and
+ // updated the DOM.
+ window.requestAnimationFrame(() => {
+ doScroll();
+ });
}
/**
@@ -1141,8 +1177,10 @@ class TimelinePanel extends React.Component {
* @param {number?} offsetBase the reference point for the pixelOffset. 0
* means the top of the container, 1 means the bottom, and fractional
* values mean somewhere in the middle. If omitted, it defaults to 0.
+ *
+ * @param {boolean?} scrollIntoView whether to scroll the event into view.
*/
- private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number): void {
+ private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number, scrollIntoView = true): void {
this.timelineWindow = new TimelineWindow(
MatrixClientPeg.get(), this.props.timelineSet,
{ windowLimit: this.props.timelineCap });
@@ -1177,11 +1215,9 @@ class TimelinePanel extends React.Component {
"messagePanel didn't load");
return;
}
- if (eventId) {
- this.messagePanel.current.scrollToEvent(eventId, pixelOffset,
- offsetBase);
- } else {
- this.messagePanel.current.scrollToBottom();
+
+ if (scrollIntoView) {
+ this.scrollIntoView(eventId, pixelOffset, offsetBase);
}
if (this.props.sendReadReceiptOnLoad) {
@@ -1475,7 +1511,7 @@ class TimelinePanel extends React.Component {
const shouldIgnore = !!ev.status || // local echo
(ignoreOwn && ev.getSender() === myUserId); // own message
- const isWithoutTile = !haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline) ||
+ const isWithoutTile = !haveRendererForEvent(ev, this.context?.showHiddenEvents) ||
shouldHideEvent(ev, this.context);
if (isWithoutTile || !node) {
@@ -1632,7 +1668,6 @@ class TimelinePanel extends React.Component {
ourUserId={MatrixClientPeg.get().credentials.userId}
stickyBottom={stickyBottom}
onScroll={this.onMessageListScroll}
- onUserScroll={this.props.onUserScroll}
onFillRequest={this.onMessageListFillRequest}
onUnfillRequest={this.onMessageListUnfillRequest}
isTwelveHour={this.context?.showTwelveHourTimestamps ?? this.state.isTwelveHour}
diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx
index 0b71fa9dc9..4cd1e654af 100644
--- a/src/components/structures/ToastContainer.tsx
+++ b/src/components/structures/ToastContainer.tsx
@@ -18,14 +18,12 @@ import * as React from "react";
import classNames from "classnames";
import ToastStore, { IToast } from "../../stores/ToastStore";
-import { replaceableComponent } from "../../utils/replaceableComponent";
interface IState {
toasts: IToast[];
countSeen: number;
}
-@replaceableComponent("structures.ToastContainer")
export default class ToastContainer extends React.Component<{}, IState> {
constructor(props, context) {
super(props, context);
@@ -81,7 +79,7 @@ export default class ToastContainer extends React.Component<{}, IState> {
titleElement = (
{ title }
- { countIndicator }
+ { countIndicator }
);
}
diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx
index eb4ef51681..e3dc77a466 100644
--- a/src/components/structures/UploadBar.tsx
+++ b/src/components/structures/UploadBar.tsx
@@ -27,7 +27,6 @@ import { Action } from "../../dispatcher/actions";
import ProgressBar from "../views/elements/ProgressBar";
import AccessibleButton from "../views/elements/AccessibleButton";
import { IUpload } from "../../models/IUpload";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import MatrixClientContext from "../../contexts/MatrixClientContext";
interface IProps {
@@ -40,7 +39,6 @@ interface IState {
uploadsHere: IUpload[];
}
-@replaceableComponent("structures.UploadBar")
export default class UploadBar extends React.Component {
static contextType = MatrixClientContext;
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index e245a73a9f..27cc08f0bc 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -25,7 +25,7 @@ import { ActionPayload } from "../../dispatcher/payloads";
import { Action } from "../../dispatcher/actions";
import { _t } from "../../languageHandler";
import { ChevronFace, ContextMenuButton } from "./ContextMenu";
-import { UserTab } from "../views/dialogs/UserSettingsDialog";
+import { UserTab } from "../views/dialogs/UserTab";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
import Modal from "../../Modal";
@@ -53,7 +53,6 @@ import { UIFeature } from "../../settings/UIFeature";
import HostSignupAction from "./HostSignupAction";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";
import UserIdentifierCustomisations from "../../customisations/UserIdentifier";
@@ -143,7 +142,6 @@ const below = (rect: PartialDOMRect) => {
};
};
-@replaceableComponent("structures.UserMenu")
export default class UserMenu extends React.Component {
private dispatcherRef: string;
private themeWatcherRef: string;
diff --git a/src/components/structures/UserView.tsx b/src/components/structures/UserView.tsx
index 657b9ab6ff..9ba9d808c9 100644
--- a/src/components/structures/UserView.tsx
+++ b/src/components/structures/UserView.tsx
@@ -23,7 +23,6 @@ import { MatrixClientPeg } from "../../MatrixClientPeg";
import Modal from '../../Modal';
import { _t } from '../../languageHandler';
import HomePage from "./HomePage";
-import { replaceableComponent } from "../../utils/replaceableComponent";
import ErrorDialog from "../views/dialogs/ErrorDialog";
import MainSplit from "./MainSplit";
import RightPanel from "./RightPanel";
@@ -41,7 +40,6 @@ interface IState {
member?: RoomMember;
}
-@replaceableComponent("structures.UserView")
export default class UserView extends React.Component {
constructor(props: IProps) {
super(props);
diff --git a/src/components/structures/ViewSource.tsx b/src/components/structures/ViewSource.tsx
index bd2f3cdb29..c8628a7f96 100644
--- a/src/components/structures/ViewSource.tsx
+++ b/src/components/structures/ViewSource.tsx
@@ -24,12 +24,12 @@ import { _t } from "../../languageHandler";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { canEditContent } from "../../utils/EventUtils";
import { MatrixClientPeg } from '../../MatrixClientPeg';
-import { replaceableComponent } from "../../utils/replaceableComponent";
import { IDialogProps } from "../views/dialogs/IDialogProps";
import BaseDialog from "../views/dialogs/BaseDialog";
import { DevtoolsContext } from "../views/dialogs/devtools/BaseTool";
import { StateEventEditor } from "../views/dialogs/devtools/RoomState";
import { stringify, TimelineEventEditor } from "../views/dialogs/devtools/Event";
+import CopyableText from "../views/elements/CopyableText";
interface IProps extends IDialogProps {
mxEvent: MatrixEvent; // the MatrixEvent associated with the context menu
@@ -39,7 +39,6 @@ interface IState {
isEditing: boolean;
}
-@replaceableComponent("structures.ViewSource")
export default class ViewSource extends React.Component {
constructor(props: IProps) {
super(props);
@@ -65,29 +64,58 @@ export default class ViewSource extends React.Component {
// @ts-ignore
const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private
const originalEventSource = mxEvent.event;
-
+ const copyOriginalFunc = (): string => {
+ return stringify(originalEventSource);
+ };
if (isEncrypted) {
+ const copyDecryptedFunc = (): string => {
+ return stringify(decryptedEventSource);
+ };
return (
<>
- { _t("Decrypted event source") }
+
+ { _t("Decrypted event source") }
+
- { stringify(decryptedEventSource) }
+
+
+
+ { stringify(decryptedEventSource) }
+
+
+
- { _t("Original event source") }
+
+ { _t("Original event source") }
+
- { stringify(originalEventSource) }
+
+
+
+ { stringify(originalEventSource) }
+
+
+
>
);
} else {
return (
<>
- { _t("Original event source") }
- { stringify(originalEventSource) }
+
+ { _t("Original event source") }
+
+
+
+
+ { stringify(originalEventSource) }
+
+
+
>
);
}
@@ -139,8 +167,16 @@ export default class ViewSource extends React.Component {
return (
- { _t("Room ID: %(roomId)s", { roomId }) }
- { _t("Event ID: %(eventId)s", { eventId }) }
+
+ roomId} border={false}>
+ { _t("Room ID: %(roomId)s", { roomId }) }
+
+
+
+ eventId} border={false}>
+ { _t("Event ID: %(eventId)s", { eventId }) }
+
+
{ isEditing ? this.editSourceContent() : this.viewSourceContent() }
diff --git a/src/components/structures/auth/CompleteSecurity.tsx b/src/components/structures/auth/CompleteSecurity.tsx
index 7043878a72..c187e2d789 100644
--- a/src/components/structures/auth/CompleteSecurity.tsx
+++ b/src/components/structures/auth/CompleteSecurity.tsx
@@ -19,7 +19,6 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
import SetupEncryptionBody from "./SetupEncryptionBody";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleButton from '../../views/elements/AccessibleButton';
import CompleteSecurityBody from "../../views/auth/CompleteSecurityBody";
import AuthPage from "../../views/auth/AuthPage";
@@ -33,7 +32,6 @@ interface IState {
lostKeys: boolean;
}
-@replaceableComponent("structures.auth.CompleteSecurity")
export default class CompleteSecurity extends React.Component {
constructor(props: IProps) {
super(props);
diff --git a/src/components/structures/auth/E2eSetup.tsx b/src/components/structures/auth/E2eSetup.tsx
index 96d6a0dc0b..ebd88b882c 100644
--- a/src/components/structures/auth/E2eSetup.tsx
+++ b/src/components/structures/auth/E2eSetup.tsx
@@ -19,7 +19,6 @@ import React from 'react';
import AuthPage from '../../views/auth/AuthPage';
import CompleteSecurityBody from '../../views/auth/CompleteSecurityBody';
import CreateCrossSigningDialog from '../../views/dialogs/security/CreateCrossSigningDialog';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
onFinished: () => void;
@@ -27,7 +26,6 @@ interface IProps {
tokenLogin?: boolean;
}
-@replaceableComponent("structures.auth.E2eSetup")
export default class E2eSetup extends React.Component {
render() {
return (
diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx
index 14b201afc6..911ee5d1b9 100644
--- a/src/components/structures/auth/ForgotPassword.tsx
+++ b/src/components/structures/auth/ForgotPassword.tsx
@@ -28,7 +28,6 @@ import AuthPage from "../../views/auth/AuthPage";
import ServerPicker from "../../views/elements/ServerPicker";
import EmailField from "../../views/auth/EmailField";
import PassphraseField from '../../views/auth/PassphraseField';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm';
import InlineSpinner from '../../views/elements/InlineSpinner';
import Spinner from "../../views/elements/Spinner";
@@ -81,7 +80,6 @@ enum ForgotPasswordField {
PasswordConfirm = 'field_password_confirm',
}
-@replaceableComponent("structures.auth.ForgotPassword")
export default class ForgotPassword extends React.Component {
private reset: PasswordReset;
@@ -224,8 +222,10 @@ export default class ForgotPassword extends React.Component {
}
private onInputChanged = (stateKey: string, ev: React.FormEvent) => {
+ let value = ev.currentTarget.value;
+ if (stateKey === "email") value = value.trim();
this.setState({
- [stateKey]: ev.currentTarget.value,
+ [stateKey]: value,
} as any);
};
diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index c5e8a05665..0b99066414 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -34,7 +34,6 @@ import InlineSpinner from "../../views/elements/InlineSpinner";
import Spinner from "../../views/elements/Spinner";
import SSOButtons from "../../views/elements/SSOButtons";
import ServerPicker from "../../views/elements/ServerPicker";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import AuthBody from "../../views/auth/AuthBody";
import AuthHeader from "../../views/auth/AuthHeader";
import AccessibleButton from '../../views/elements/AccessibleButton';
@@ -103,7 +102,6 @@ interface IState {
/*
* A wire component which glues together login UI components and Login logic
*/
-@replaceableComponent("structures.auth.LoginComponent")
export default class LoginComponent extends React.PureComponent {
private unmounted = false;
private loginLogic: Login;
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index 828ca8d79d..ecb4691e1b 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -30,7 +30,6 @@ import Login, { ISSOFlow } from "../../../Login";
import dis from "../../../dispatcher/dispatcher";
import SSOButtons from "../../views/elements/SSOButtons";
import ServerPicker from '../../views/elements/ServerPicker';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import RegistrationForm from '../../views/auth/RegistrationForm';
import AccessibleButton from '../../views/elements/AccessibleButton';
import AuthBody from "../../views/auth/AuthBody";
@@ -110,7 +109,6 @@ interface IState {
ssoFlow?: ISSOFlow;
}
-@replaceableComponent("structures.auth.Registration")
export default class Registration extends React.Component {
loginLogic: Login;
diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx
index 8783ae3f58..2553f8fbaa 100644
--- a/src/components/structures/auth/SetupEncryptionBody.tsx
+++ b/src/components/structures/auth/SetupEncryptionBody.tsx
@@ -25,7 +25,6 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import EncryptionPanel from "../../views/right_panel/EncryptionPanel";
import AccessibleButton from '../../views/elements/AccessibleButton';
import Spinner from '../../views/elements/Spinner';
@@ -49,7 +48,6 @@ interface IState {
lostKeys: boolean;
}
-@replaceableComponent("structures.auth.SetupEncryptionBody")
export default class SetupEncryptionBody extends React.Component {
constructor(props) {
super(props);
diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx
index f24230806a..7a366d7122 100644
--- a/src/components/structures/auth/SoftLogout.tsx
+++ b/src/components/structures/auth/SoftLogout.tsx
@@ -27,7 +27,6 @@ import { ISSOFlow, LoginFlow, sendLoginRequest } from "../../../Login";
import AuthPage from "../../views/auth/AuthPage";
import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../BasePlatform";
import SSOButtons from "../../views/elements/SSOButtons";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import ConfirmWipeDeviceDialog from '../../views/dialogs/ConfirmWipeDeviceDialog';
import Field from '../../views/elements/Field';
import AccessibleButton from '../../views/elements/AccessibleButton';
@@ -70,7 +69,6 @@ interface IState {
flows: LoginFlow[];
}
-@replaceableComponent("structures.auth.SoftLogout")
export default class SoftLogout extends React.Component {
public constructor(props: IProps) {
super(props);
diff --git a/src/components/structures/static-page-vars.ts b/src/components/structures/static-page-vars.ts
new file mode 100644
index 0000000000..801eeb155a
--- /dev/null
+++ b/src/components/structures/static-page-vars.ts
@@ -0,0 +1,26 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// We're importing via require specifically so the svg becomes a URI rather than a DOM element.
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const matrixSvg = require('../../../res/img/matrix.svg').default;
+
+/**
+ * Intended to replace $matrixLogo in the welcome page.
+ */
+export const MATRIX_LOGO_HTML = `
+
+`;
diff --git a/src/components/views/audio_messages/AudioPlayer.tsx b/src/components/views/audio_messages/AudioPlayer.tsx
index 5f0ccdf450..c681b366cf 100644
--- a/src/components/views/audio_messages/AudioPlayer.tsx
+++ b/src/components/views/audio_messages/AudioPlayer.tsx
@@ -17,7 +17,6 @@ limitations under the License.
import React, { ReactNode } from "react";
import PlayPauseButton from "./PlayPauseButton";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { formatBytes } from "../../../utils/FormattingUtils";
import DurationClock from "./DurationClock";
import { _t } from "../../../languageHandler";
@@ -25,7 +24,6 @@ import SeekBar from "./SeekBar";
import PlaybackClock from "./PlaybackClock";
import AudioPlayerBase from "./AudioPlayerBase";
-@replaceableComponent("views.audio_messages.AudioPlayer")
export default class AudioPlayer extends AudioPlayerBase {
protected renderFileSize(): string {
const bytes = this.props.playback.sizeBytes;
diff --git a/src/components/views/audio_messages/AudioPlayerBase.tsx b/src/components/views/audio_messages/AudioPlayerBase.tsx
index f364350859..9b3a7e4cef 100644
--- a/src/components/views/audio_messages/AudioPlayerBase.tsx
+++ b/src/components/views/audio_messages/AudioPlayerBase.tsx
@@ -19,7 +19,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { Playback, PlaybackState } from "../../../audio/Playback";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { _t } from "../../../languageHandler";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
@@ -39,7 +38,6 @@ interface IState {
error?: boolean;
}
-@replaceableComponent("views.audio_messages.AudioPlayerBase")
export default abstract class AudioPlayerBase extends React.PureComponent {
protected seekRef: RefObject = createRef();
protected playPauseRef: RefObject = createRef();
diff --git a/src/components/views/audio_messages/Clock.tsx b/src/components/views/audio_messages/Clock.tsx
index 13ffaa7d76..f978d8837c 100644
--- a/src/components/views/audio_messages/Clock.tsx
+++ b/src/components/views/audio_messages/Clock.tsx
@@ -17,7 +17,6 @@ limitations under the License.
import React, { HTMLProps } from "react";
import { formatSeconds } from "../../../DateUtils";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
export interface IProps extends Pick, "aria-live"> {
seconds: number;
@@ -27,7 +26,6 @@ export interface IProps extends Pick, "aria-live"> {
* Simply converts seconds into minutes and seconds. Note that hours will not be
* displayed, making it possible to see "82:29".
*/
-@replaceableComponent("views.audio_messages.Clock")
export default class Clock extends React.Component {
public constructor(props) {
super(props);
diff --git a/src/components/views/audio_messages/DurationClock.tsx b/src/components/views/audio_messages/DurationClock.tsx
index e0c180123a..47dc8e8375 100644
--- a/src/components/views/audio_messages/DurationClock.tsx
+++ b/src/components/views/audio_messages/DurationClock.tsx
@@ -16,7 +16,6 @@ limitations under the License.
import React from "react";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock";
import { Playback } from "../../../audio/Playback";
@@ -31,7 +30,6 @@ interface IState {
/**
* A clock which shows a clip's maximum duration.
*/
-@replaceableComponent("views.audio_messages.DurationClock")
export default class DurationClock extends React.PureComponent {
public constructor(props) {
super(props);
diff --git a/src/components/views/audio_messages/LiveRecordingClock.tsx b/src/components/views/audio_messages/LiveRecordingClock.tsx
index aa88d999be..34e4c559fe 100644
--- a/src/components/views/audio_messages/LiveRecordingClock.tsx
+++ b/src/components/views/audio_messages/LiveRecordingClock.tsx
@@ -17,7 +17,6 @@ limitations under the License.
import React from "react";
import { IRecordingUpdate, VoiceRecording } from "../../../audio/VoiceRecording";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock";
import { MarkedExecution } from "../../../utils/MarkedExecution";
@@ -32,7 +31,6 @@ interface IState {
/**
* A clock for a live recording.
*/
-@replaceableComponent("views.audio_messages.LiveRecordingClock")
export default class LiveRecordingClock extends React.PureComponent {
private seconds = 0;
private scheduledUpdate = new MarkedExecution(
diff --git a/src/components/views/audio_messages/LiveRecordingWaveform.tsx b/src/components/views/audio_messages/LiveRecordingWaveform.tsx
index 13ba7b9f96..c9c122c98a 100644
--- a/src/components/views/audio_messages/LiveRecordingWaveform.tsx
+++ b/src/components/views/audio_messages/LiveRecordingWaveform.tsx
@@ -17,7 +17,6 @@ limitations under the License.
import React from "react";
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { arrayFastResample, arraySeed } from "../../../utils/arrays";
import Waveform from "./Waveform";
import { MarkedExecution } from "../../../utils/MarkedExecution";
@@ -33,7 +32,6 @@ interface IState {
/**
* A waveform which shows the waveform of a live recording
*/
-@replaceableComponent("views.audio_messages.LiveRecordingWaveform")
export default class LiveRecordingWaveform extends React.PureComponent {
public static defaultProps = {
progress: 1,
diff --git a/src/components/views/audio_messages/PlayPauseButton.tsx b/src/components/views/audio_messages/PlayPauseButton.tsx
index 5562a33ab5..1a2da04a38 100644
--- a/src/components/views/audio_messages/PlayPauseButton.tsx
+++ b/src/components/views/audio_messages/PlayPauseButton.tsx
@@ -17,7 +17,6 @@ limitations under the License.
import React, { ReactNode } from "react";
import classNames from "classnames";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { _t } from "../../../languageHandler";
import { Playback, PlaybackState } from "../../../audio/Playback";
@@ -35,7 +34,6 @@ interface IProps extends Omit {
public constructor(props) {
super(props);
diff --git a/src/components/views/audio_messages/PlaybackClock.tsx b/src/components/views/audio_messages/PlaybackClock.tsx
index c792d1c483..3f05ad0b89 100644
--- a/src/components/views/audio_messages/PlaybackClock.tsx
+++ b/src/components/views/audio_messages/PlaybackClock.tsx
@@ -16,7 +16,6 @@ limitations under the License.
import React from "react";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock";
import { Playback, PlaybackState } from "../../../audio/Playback";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
@@ -39,7 +38,6 @@ interface IState {
/**
* A clock for a playback of a recording.
*/
-@replaceableComponent("views.audio_messages.PlaybackClock")
export default class PlaybackClock extends React.PureComponent {
public constructor(props) {
super(props);
diff --git a/src/components/views/audio_messages/PlaybackWaveform.tsx b/src/components/views/audio_messages/PlaybackWaveform.tsx
index 70bf0ebeef..712044ca78 100644
--- a/src/components/views/audio_messages/PlaybackWaveform.tsx
+++ b/src/components/views/audio_messages/PlaybackWaveform.tsx
@@ -16,7 +16,6 @@ limitations under the License.
import React from "react";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { arraySeed, arrayTrimFill } from "../../../utils/arrays";
import Waveform from "./Waveform";
import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../audio/Playback";
@@ -34,7 +33,6 @@ interface IState {
/**
* A waveform which shows the waveform of a previously recorded recording
*/
-@replaceableComponent("views.audio_messages.PlaybackWaveform")
export default class PlaybackWaveform extends React.PureComponent {
public constructor(props) {
super(props);
diff --git a/src/components/views/audio_messages/RecordingPlayback.tsx b/src/components/views/audio_messages/RecordingPlayback.tsx
index 700231faac..f9e8405958 100644
--- a/src/components/views/audio_messages/RecordingPlayback.tsx
+++ b/src/components/views/audio_messages/RecordingPlayback.tsx
@@ -18,7 +18,6 @@ import React, { ReactNode } from "react";
import PlayPauseButton from "./PlayPauseButton";
import PlaybackClock from "./PlaybackClock";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import AudioPlayerBase, { IProps as IAudioPlayerBaseProps } from "./AudioPlayerBase";
import SeekBar from "./SeekBar";
import PlaybackWaveform from "./PlaybackWaveform";
@@ -30,7 +29,6 @@ interface IProps extends IAudioPlayerBaseProps {
withWaveform?: boolean;
}
-@replaceableComponent("views.audio_messages.RecordingPlayback")
export default class RecordingPlayback extends AudioPlayerBase {
// This component is rendered in two ways: the composer and timeline. They have different
// rendering properties (specifically the difference of a waveform or not).
diff --git a/src/components/views/audio_messages/SeekBar.tsx b/src/components/views/audio_messages/SeekBar.tsx
index 817f3e0452..d86d11c95e 100644
--- a/src/components/views/audio_messages/SeekBar.tsx
+++ b/src/components/views/audio_messages/SeekBar.tsx
@@ -17,7 +17,6 @@ limitations under the License.
import React, { ChangeEvent, CSSProperties, ReactNode } from "react";
import { Playback, PlaybackState } from "../../../audio/Playback";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MarkedExecution } from "../../../utils/MarkedExecution";
import { percentageOf } from "../../../utils/numbers";
@@ -43,7 +42,6 @@ interface ISeekCSS extends CSSProperties {
const ARROW_SKIP_SECONDS = 5; // arbitrary
-@replaceableComponent("views.audio_messages.SeekBar")
export default class SeekBar extends React.PureComponent {
// We use an animation frame request to avoid overly spamming prop updates, even if we aren't
// really using anything demanding on the CSS front.
diff --git a/src/components/views/audio_messages/Waveform.tsx b/src/components/views/audio_messages/Waveform.tsx
index dedd95c7e1..deccc23ac6 100644
--- a/src/components/views/audio_messages/Waveform.tsx
+++ b/src/components/views/audio_messages/Waveform.tsx
@@ -17,8 +17,6 @@ limitations under the License.
import React, { CSSProperties } from "react";
import classNames from "classnames";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
-
interface WaveformCSSProperties extends CSSProperties {
'--barHeight': number;
}
@@ -39,7 +37,6 @@ interface IState {
* For CSS purposes, a mx_Waveform_bar_100pct class is added when the bar should be
* "filled", as a demonstration of the progress property.
*/
-@replaceableComponent("views.audio_messages.Waveform")
export default class Waveform extends React.PureComponent {
public static defaultProps = {
progress: 1,
diff --git a/src/components/views/auth/AuthBody.tsx b/src/components/views/auth/AuthBody.tsx
index 22bb919141..4532ceeaf4 100644
--- a/src/components/views/auth/AuthBody.tsx
+++ b/src/components/views/auth/AuthBody.tsx
@@ -16,9 +16,6 @@ limitations under the License.
import React from 'react';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
-
-@replaceableComponent("views.auth.AuthBody")
export default class AuthBody extends React.PureComponent {
public render(): React.ReactNode {
return
diff --git a/src/components/views/auth/AuthFooter.tsx b/src/components/views/auth/AuthFooter.tsx
index d936e346e6..46e389cb46 100644
--- a/src/components/views/auth/AuthFooter.tsx
+++ b/src/components/views/auth/AuthFooter.tsx
@@ -19,9 +19,7 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
-@replaceableComponent("views.auth.AuthFooter")
export default class AuthFooter extends React.Component {
public render(): React.ReactNode {
return (
diff --git a/src/components/views/auth/AuthHeader.tsx b/src/components/views/auth/AuthHeader.tsx
index 27768c9e6e..90e9e888af 100644
--- a/src/components/views/auth/AuthHeader.tsx
+++ b/src/components/views/auth/AuthHeader.tsx
@@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import AuthHeaderLogo from "./AuthHeaderLogo";
import LanguageSelector from "./LanguageSelector";
@@ -25,7 +24,6 @@ interface IProps {
disableLanguageSelector?: boolean;
}
-@replaceableComponent("views.auth.AuthHeader")
export default class AuthHeader extends React.Component {
public render(): React.ReactNode {
return (
diff --git a/src/components/views/auth/AuthHeaderLogo.tsx b/src/components/views/auth/AuthHeaderLogo.tsx
index 29e5737238..72a2df7b83 100644
--- a/src/components/views/auth/AuthHeaderLogo.tsx
+++ b/src/components/views/auth/AuthHeaderLogo.tsx
@@ -16,9 +16,6 @@ limitations under the License.
import React from 'react';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
-
-@replaceableComponent("views.auth.AuthHeaderLogo")
export default class AuthHeaderLogo extends React.PureComponent {
public render(): React.ReactNode {
return
diff --git a/src/components/views/auth/AuthPage.tsx b/src/components/views/auth/AuthPage.tsx
index d13c8dd6ae..344e890ebb 100644
--- a/src/components/views/auth/AuthPage.tsx
+++ b/src/components/views/auth/AuthPage.tsx
@@ -18,10 +18,8 @@ limitations under the License.
import React from 'react';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import AuthFooter from "./AuthFooter";
-@replaceableComponent("views.auth.AuthPage")
export default class AuthPage extends React.PureComponent {
public render(): React.ReactNode {
return (
diff --git a/src/components/views/auth/CaptchaForm.tsx b/src/components/views/auth/CaptchaForm.tsx
index 887f174c00..7f718bae6d 100644
--- a/src/components/views/auth/CaptchaForm.tsx
+++ b/src/components/views/auth/CaptchaForm.tsx
@@ -18,7 +18,6 @@ import React, { createRef } from 'react';
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../../languageHandler';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
const DIV_ID = 'mx_recaptcha';
@@ -35,7 +34,6 @@ interface ICaptchaFormState {
/**
* A pure UI component which displays a captcha form.
*/
-@replaceableComponent("views.auth.CaptchaForm")
export default class CaptchaForm extends React.Component {
static defaultProps = {
onCaptchaResponse: () => {},
diff --git a/src/components/views/auth/CompleteSecurityBody.tsx b/src/components/views/auth/CompleteSecurityBody.tsx
index 349016add5..ecc6eace7d 100644
--- a/src/components/views/auth/CompleteSecurityBody.tsx
+++ b/src/components/views/auth/CompleteSecurityBody.tsx
@@ -16,9 +16,6 @@ limitations under the License.
import React from 'react';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
-
-@replaceableComponent("views.auth.CompleteSecurityBody")
export default class CompleteSecurityBody extends React.PureComponent {
public render(): React.ReactNode {
return
diff --git a/src/components/views/auth/CountryDropdown.tsx b/src/components/views/auth/CountryDropdown.tsx
index d4a3987ccb..0318c4b95d 100644
--- a/src/components/views/auth/CountryDropdown.tsx
+++ b/src/components/views/auth/CountryDropdown.tsx
@@ -19,7 +19,6 @@ import React from 'react';
import { COUNTRIES, getEmojiFlag, PhoneNumberCountryDefinition } from '../../../phonenumber';
import SdkConfig from "../../../SdkConfig";
import { _t } from "../../../languageHandler";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import Dropdown from "../elements/Dropdown";
const COUNTRIES_BY_ISO2 = {};
@@ -53,7 +52,6 @@ interface IState {
defaultCountry: PhoneNumberCountryDefinition;
}
-@replaceableComponent("views.auth.CountryDropdown")
export default class CountryDropdown extends React.Component {
constructor(props: IProps) {
super(props);
diff --git a/src/components/views/auth/EmailField.tsx b/src/components/views/auth/EmailField.tsx
index f274f1fdfa..2aa6204cfa 100644
--- a/src/components/views/auth/EmailField.tsx
+++ b/src/components/views/auth/EmailField.tsx
@@ -16,7 +16,6 @@ limitations under the License.
import React, { PureComponent, RefCallback, RefObject } from "react";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import Field, { IInputProps } from "../elements/Field";
import { _t, _td } from "../../../languageHandler";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
@@ -39,7 +38,6 @@ interface IProps extends Omit {
onValidate?(result: IValidationResult): void;
}
-@replaceableComponent("views.auth.EmailField")
class EmailField extends PureComponent {
static defaultProps = {
label: _td("Email"),
diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx
index e4b9a7422e..11a28d1e05 100644
--- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx
+++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx
@@ -24,7 +24,6 @@ import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import Spinner from "../elements/Spinner";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { LocalisedPolicy, Policies } from '../../../Terms';
import Field from '../elements/Field';
import CaptchaForm from "./CaptchaForm";
@@ -93,7 +92,6 @@ interface IPasswordAuthEntryState {
password: string;
}
-@replaceableComponent("views.auth.PasswordAuthEntry")
export class PasswordAuthEntry extends React.Component {
static LOGIN_TYPE = AuthType.Password;
@@ -191,7 +189,6 @@ interface IRecaptchaAuthEntryProps extends IAuthEntryProps {
}
/* eslint-enable camelcase */
-@replaceableComponent("views.auth.RecaptchaAuthEntry")
export class RecaptchaAuthEntry extends React.Component {
static LOGIN_TYPE = AuthType.Recaptcha;
@@ -262,7 +259,6 @@ interface ITermsAuthEntryState {
errorText?: string;
}
-@replaceableComponent("views.auth.TermsAuthEntry")
export class TermsAuthEntry extends React.Component {
static LOGIN_TYPE = AuthType.Terms;
@@ -409,7 +405,6 @@ interface IEmailIdentityAuthEntryProps extends IAuthEntryProps {
};
}
-@replaceableComponent("views.auth.EmailIdentityAuthEntry")
export class EmailIdentityAuthEntry extends React.Component {
static LOGIN_TYPE = AuthType.Email;
@@ -472,7 +467,6 @@ interface IMsisdnAuthEntryState {
errorText: string;
}
-@replaceableComponent("views.auth.MsisdnAuthEntry")
export class MsisdnAuthEntry extends React.Component {
static LOGIN_TYPE = AuthType.Msisdn;
@@ -623,7 +617,6 @@ interface ISSOAuthEntryState {
attemptFailed: boolean;
}
-@replaceableComponent("views.auth.SSOAuthEntry")
export class SSOAuthEntry extends React.Component {
static LOGIN_TYPE = AuthType.Sso;
static UNSTABLE_LOGIN_TYPE = AuthType.SsoUnstable;
@@ -743,7 +736,6 @@ export class SSOAuthEntry extends React.Component {
private popupWindow: Window;
private fallbackButton = createRef();
diff --git a/src/components/views/auth/PassphraseConfirmField.tsx b/src/components/views/auth/PassphraseConfirmField.tsx
index 3a2b6f89b7..e30e25a00e 100644
--- a/src/components/views/auth/PassphraseConfirmField.tsx
+++ b/src/components/views/auth/PassphraseConfirmField.tsx
@@ -16,7 +16,6 @@ limitations under the License.
import React, { PureComponent, RefCallback, RefObject } from "react";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import Field, { IInputProps } from "../elements/Field";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
import { _t, _td } from "../../../languageHandler";
@@ -35,7 +34,6 @@ interface IProps extends Omit {
onValidate?(result: IValidationResult);
}
-@replaceableComponent("views.auth.EmailField")
class PassphraseConfirmField extends PureComponent {
static defaultProps = {
label: _td("Confirm password"),
diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx
index 5a9aa0b85f..94353669ff 100644
--- a/src/components/views/auth/PassphraseField.tsx
+++ b/src/components/views/auth/PassphraseField.tsx
@@ -22,7 +22,6 @@ import SdkConfig from "../../../SdkConfig";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
import { _t, _td } from "../../../languageHandler";
import Field, { IInputProps } from "../elements/Field";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends Omit {
autoFocus?: boolean;
@@ -41,7 +40,6 @@ interface IProps extends Omit {
onValidate?(result: IValidationResult);
}
-@replaceableComponent("views.auth.PassphraseField")
class PassphraseField extends PureComponent {
static defaultProps = {
label: _td("Password"),
diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx
index 9142358674..db22393112 100644
--- a/src/components/views/auth/PasswordLogin.tsx
+++ b/src/components/views/auth/PasswordLogin.tsx
@@ -24,7 +24,6 @@ import AccessibleButton from "../elements/AccessibleButton";
import withValidation, { IValidationResult } from "../elements/Validation";
import Field from "../elements/Field";
import CountryDropdown from "./CountryDropdown";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import EmailField from "./EmailField";
// For validating phone numbers without country codes
@@ -66,7 +65,6 @@ enum LoginField {
* A pure UI component which displays a username/password form.
* The email/username/phone fields are fully-controlled, the password field is not.
*/
-@replaceableComponent("views.auth.PasswordLogin")
export default class PasswordLogin extends React.PureComponent {
static defaultProps = {
onUsernameChanged: function() {},
diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx
index 2665e1789f..b57ddf5009 100644
--- a/src/components/views/auth/RegistrationForm.tsx
+++ b/src/components/views/auth/RegistrationForm.tsx
@@ -31,7 +31,6 @@ import EmailField from "./EmailField";
import PassphraseField from "./PassphraseField";
import Field from '../elements/Field';
import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import CountryDropdown from "./CountryDropdown";
import PassphraseConfirmField from "./PassphraseConfirmField";
@@ -92,7 +91,6 @@ interface IState {
/*
* A pure UI component which displays a registration form.
*/
-@replaceableComponent("views.auth.RegistrationForm")
export default class RegistrationForm extends React.PureComponent {
static defaultProps = {
onValidationChange: logger.error,
diff --git a/src/components/views/auth/Welcome.tsx b/src/components/views/auth/Welcome.tsx
index 2ac974610b..5f59dbdf52 100644
--- a/src/components/views/auth/Welcome.tsx
+++ b/src/components/views/auth/Welcome.tsx
@@ -17,14 +17,14 @@ limitations under the License.
import React from 'react';
import classNames from "classnames";
-import * as sdk from "../../../index";
import SdkConfig from '../../../SdkConfig';
import AuthPage from "./AuthPage";
import { _td } from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import LanguageSelector from "./LanguageSelector";
+import EmbeddedPage from "../../structures/EmbeddedPage";
+import { MATRIX_LOGO_HTML } from "../../structures/static-page-vars";
// translatable strings for Welcome pages
_td("Sign in with SSO");
@@ -33,12 +33,8 @@ interface IProps {
}
-@replaceableComponent("views.auth.Welcome")
export default class Welcome extends React.PureComponent {
public render(): React.ReactNode {
- // FIXME: Using an import will result in wrench-element-tests failures
- const EmbeddedPage = sdk.getComponent("structures.EmbeddedPage");
-
const pagesConfig = SdkConfig.getObject("embedded_pages");
let pageUrl = null;
if (pagesConfig) {
@@ -59,6 +55,8 @@ export default class Welcome extends React.PureComponent {
replaceMap={{
"$riot:ssoUrl": "#/start_sso",
"$riot:casUrl": "#/start_cas",
+ "$matrixLogo": MATRIX_LOGO_HTML,
+ "[matrix]": MATRIX_LOGO_HTML,
}}
/>
diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx
index dffd3cfbb4..06ee20cc08 100644
--- a/src/components/views/avatars/DecoratedRoomAvatar.tsx
+++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx
@@ -32,7 +32,6 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler";
import TextWithTooltip from "../elements/TextWithTooltip";
import DMRoomMap from "../../../utils/DMRoomMap";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IOOBData } from "../../../stores/ThreepidInviteStore";
import TooltipTarget from "../elements/TooltipTarget";
@@ -78,7 +77,6 @@ function tooltipText(variant: Icon) {
}
}
-@replaceableComponent("views.avatars.DecoratedRoomAvatar")
export default class DecoratedRoomAvatar extends React.PureComponent {
private _dmUser: User;
private isUnmounted = false;
diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx
index e00b9b4b85..c5dd1bfd3c 100644
--- a/src/components/views/avatars/MemberAvatar.tsx
+++ b/src/components/views/avatars/MemberAvatar.tsx
@@ -23,9 +23,8 @@ import { logger } from "matrix-js-sdk/src/logger";
import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import BaseAvatar from "./BaseAvatar";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
-import { CardContext } from '../right_panel/BaseCard';
+import { CardContext } from '../right_panel/context';
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
import SettingsStore from "../../../settings/SettingsStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@@ -52,7 +51,6 @@ interface IState {
imageUrl?: string;
}
-@replaceableComponent("views.avatars.MemberAvatar")
export default class MemberAvatar extends React.PureComponent {
public static defaultProps = {
width: 40,
diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx
index 9625b64138..f50232fb22 100644
--- a/src/components/views/avatars/RoomAvatar.tsx
+++ b/src/components/views/avatars/RoomAvatar.tsx
@@ -28,7 +28,6 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import * as Avatar from '../../../Avatar';
import DMRoomMap from "../../../utils/DMRoomMap";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import { IOOBData } from '../../../stores/ThreepidInviteStore';
@@ -52,7 +51,6 @@ interface IState {
urls: string[];
}
-@replaceableComponent("views.avatars.RoomAvatar")
export default class RoomAvatar extends React.Component {
public static defaultProps = {
width: 36,
diff --git a/src/components/views/beacon/BeaconMarker.tsx b/src/components/views/beacon/BeaconMarker.tsx
new file mode 100644
index 0000000000..f7f284b88e
--- /dev/null
+++ b/src/components/views/beacon/BeaconMarker.tsx
@@ -0,0 +1,65 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { useContext } from 'react';
+import maplibregl from 'maplibre-gl';
+import {
+ Beacon,
+ BeaconEvent,
+} from 'matrix-js-sdk/src/matrix';
+import { LocationAssetType } from 'matrix-js-sdk/src/@types/location';
+
+import MatrixClientContext from '../../../contexts/MatrixClientContext';
+import { useEventEmitterState } from '../../../hooks/useEventEmitter';
+import SmartMarker from '../location/SmartMarker';
+
+interface Props {
+ map: maplibregl.Map;
+ beacon: Beacon;
+}
+
+/**
+ * Updates a map SmartMarker with latest location from given beacon
+ */
+const BeaconMarker: React.FC = ({ map, beacon }) => {
+ const latestLocationState = useEventEmitterState(
+ beacon,
+ BeaconEvent.LocationUpdate,
+ () => beacon.latestLocationState,
+ );
+ const matrixClient = useContext(MatrixClientContext);
+ const room = matrixClient.getRoom(beacon.roomId);
+
+ if (!latestLocationState || !beacon.isLive) {
+ return null;
+ }
+
+ const geoUri = latestLocationState?.uri;
+
+ const markerRoomMember = beacon.beaconInfo.assetType === LocationAssetType.Self ?
+ room.getMember(beacon.beaconInfoOwner) :
+ undefined;
+
+ return ;
+};
+
+export default BeaconMarker;
diff --git a/src/components/views/beacon/BeaconStatus.tsx b/src/components/views/beacon/BeaconStatus.tsx
new file mode 100644
index 0000000000..c9d7bd3762
--- /dev/null
+++ b/src/components/views/beacon/BeaconStatus.tsx
@@ -0,0 +1,84 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { HTMLProps } from 'react';
+import classNames from 'classnames';
+import { Beacon } from 'matrix-js-sdk/src/matrix';
+
+import StyledLiveBeaconIcon from './StyledLiveBeaconIcon';
+import { _t } from '../../../languageHandler';
+import LiveTimeRemaining from './LiveTimeRemaining';
+import { BeaconDisplayStatus } from './displayStatus';
+import { getBeaconExpiryTimestamp } from '../../../utils/beacon';
+import { formatTime } from '../../../DateUtils';
+
+interface Props {
+ displayStatus: BeaconDisplayStatus;
+ displayLiveTimeRemaining?: boolean;
+ beacon?: Beacon;
+ label?: string;
+}
+
+const BeaconExpiryTime: React.FC<{ beacon: Beacon }> = ({ beacon }) => {
+ const expiryTime = formatTime(new Date(getBeaconExpiryTimestamp(beacon)));
+ return { _t('Live until %(expiryTime)s', { expiryTime }) };
+};
+
+const BeaconStatus: React.FC> =
+ ({
+ beacon,
+ displayStatus,
+ displayLiveTimeRemaining,
+ label,
+ className,
+ children,
+ ...rest
+ }) => {
+ const isIdle = displayStatus === BeaconDisplayStatus.Loading ||
+ displayStatus === BeaconDisplayStatus.Stopped;
+
+ return
+
+
+
+ { displayStatus === BeaconDisplayStatus.Loading && { _t('Loading live location...') } }
+ { displayStatus === BeaconDisplayStatus.Stopped && { _t('Live location ended') } }
+
+ { displayStatus === BeaconDisplayStatus.Error && { _t('Live location error') } }
+
+ { displayStatus === BeaconDisplayStatus.Active && beacon && <>
+ <>
+ { label }
+ { displayLiveTimeRemaining ?
+ :
+
+ }
+ >
+ >
+ }
+
+ { children }
+ ;
+ };
+
+export default BeaconStatus;
diff --git a/src/components/views/beacon/BeaconViewDialog.tsx b/src/components/views/beacon/BeaconViewDialog.tsx
new file mode 100644
index 0000000000..12f26a0a54
--- /dev/null
+++ b/src/components/views/beacon/BeaconViewDialog.tsx
@@ -0,0 +1,116 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+import { MatrixClient } from 'matrix-js-sdk/src/client';
+import {
+ Beacon,
+ Room,
+} from 'matrix-js-sdk/src/matrix';
+import maplibregl from 'maplibre-gl';
+
+import { useLiveBeacons } from '../../../utils/beacon/useLiveBeacons';
+import MatrixClientContext from '../../../contexts/MatrixClientContext';
+import BaseDialog from "../dialogs/BaseDialog";
+import { IDialogProps } from "../dialogs/IDialogProps";
+import Map from '../location/Map';
+import ZoomButtons from '../location/ZoomButtons';
+import BeaconMarker from './BeaconMarker';
+import { Bounds, getBeaconBounds } from '../../../utils/beacon/bounds';
+import { getGeoUri } from '../../../utils/beacon';
+import { Icon as LocationIcon } from '../../../../res/img/element-icons/location.svg';
+import { _t } from '../../../languageHandler';
+import AccessibleButton from '../elements/AccessibleButton';
+
+interface IProps extends IDialogProps {
+ roomId: Room['roomId'];
+ matrixClient: MatrixClient;
+ // open the map centered on this beacon's location
+ focusBeacon?: Beacon;
+}
+
+const getBoundsCenter = (bounds: Bounds): string | undefined => {
+ if (!bounds) {
+ return;
+ }
+ return getGeoUri({
+ latitude: (bounds.north + bounds.south) / 2,
+ longitude: (bounds.east + bounds.west) / 2,
+ timestamp: Date.now(),
+ });
+};
+
+/**
+ * Dialog to view live beacons maximised
+ */
+const BeaconViewDialog: React.FC = ({
+ focusBeacon,
+ roomId,
+ matrixClient,
+ onFinished,
+}) => {
+ const liveBeacons = useLiveBeacons(roomId, matrixClient);
+
+ const bounds = getBeaconBounds(liveBeacons);
+ const centerGeoUri = focusBeacon?.latestLocationState?.uri || getBoundsCenter(bounds);
+
+ return (
+
+
+ { !!bounds ? :
+
+
+ { _t('No live locations') }
+
+ { _t('Close') }
+
+
+ }
+
+
+ );
+};
+
+export default BeaconViewDialog;
diff --git a/src/components/views/beacon/LiveTimeRemaining.tsx b/src/components/views/beacon/LiveTimeRemaining.tsx
new file mode 100644
index 0000000000..1f25706a13
--- /dev/null
+++ b/src/components/views/beacon/LiveTimeRemaining.tsx
@@ -0,0 +1,75 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { useCallback, useEffect, useState } from 'react';
+import { BeaconEvent, Beacon } from 'matrix-js-sdk/src/matrix';
+
+import { formatDuration } from '../../../DateUtils';
+import { useEventEmitterState } from '../../../hooks/useEventEmitter';
+import { useInterval } from '../../../hooks/useTimeout';
+import { _t } from '../../../languageHandler';
+import { getBeaconMsUntilExpiry } from '../../../utils/beacon';
+
+const MINUTE_MS = 60000;
+const HOUR_MS = MINUTE_MS * 60;
+const getUpdateInterval = (ms: number) => {
+ // every 10 mins when more than an hour
+ if (ms > HOUR_MS) {
+ return MINUTE_MS * 10;
+ }
+ // every minute when more than a minute
+ if (ms > MINUTE_MS) {
+ return MINUTE_MS;
+ }
+ // otherwise every second
+ return 1000;
+};
+const useMsRemaining = (beacon: Beacon): number => {
+ const beaconInfo = useEventEmitterState(
+ beacon,
+ BeaconEvent.Update,
+ () => beacon.beaconInfo,
+ );
+
+ const [msRemaining, setMsRemaining] = useState(() => getBeaconMsUntilExpiry(beaconInfo));
+
+ useEffect(() => {
+ setMsRemaining(getBeaconMsUntilExpiry(beaconInfo));
+ }, [beaconInfo]);
+
+ const updateMsRemaining = useCallback(() => {
+ const ms = getBeaconMsUntilExpiry(beaconInfo);
+ setMsRemaining(ms);
+ }, [beaconInfo]);
+
+ useInterval(updateMsRemaining, getUpdateInterval(msRemaining));
+
+ return msRemaining;
+};
+
+const LiveTimeRemaining: React.FC<{ beacon: Beacon }> = ({ beacon }) => {
+ const msRemaining = useMsRemaining(beacon);
+
+ const timeRemaining = formatDuration(msRemaining);
+ const liveTimeRemaining = _t(`%(timeRemaining)s left`, { timeRemaining });
+
+ return { liveTimeRemaining };
+};
+
+export default LiveTimeRemaining;
diff --git a/src/components/views/beacon/OwnBeaconStatus.tsx b/src/components/views/beacon/OwnBeaconStatus.tsx
new file mode 100644
index 0000000000..204e296829
--- /dev/null
+++ b/src/components/views/beacon/OwnBeaconStatus.tsx
@@ -0,0 +1,89 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { Beacon } from 'matrix-js-sdk/src/matrix';
+import React, { HTMLProps } from 'react';
+
+import { _t } from '../../../languageHandler';
+import { useOwnLiveBeacons } from '../../../utils/beacon';
+import BeaconStatus from './BeaconStatus';
+import { BeaconDisplayStatus } from './displayStatus';
+import AccessibleButton from '../elements/AccessibleButton';
+
+interface Props {
+ displayStatus: BeaconDisplayStatus;
+ beacon?: Beacon;
+}
+
+/**
+ * Wraps BeaconStatus with more capabilities
+ * for errors and actions available for users own live beacons
+ */
+const OwnBeaconStatus: React.FC> = ({
+ beacon, displayStatus, className, ...rest
+}) => {
+ const {
+ hasWireError,
+ hasStopSharingError,
+ stoppingInProgress,
+ onStopSharing,
+ onResetWireError,
+ } = useOwnLiveBeacons([beacon?.identifier]);
+
+ // combine display status with errors that only occur for user's own beacons
+ const ownDisplayStatus = hasWireError || hasStopSharingError ?
+ BeaconDisplayStatus.Error :
+ displayStatus;
+
+ return
+ { ownDisplayStatus === BeaconDisplayStatus.Active &&
+ { _t('Stop') }
+
+ }
+ { hasWireError &&
+ { _t('Retry') }
+
+ }
+ { hasStopSharingError &&
+ { _t('Retry') }
+ }
+ ;
+};
+
+export default OwnBeaconStatus;
diff --git a/src/components/views/beacon/RoomLiveShareWarning.tsx b/src/components/views/beacon/RoomLiveShareWarning.tsx
index d51c22f644..01c749a26a 100644
--- a/src/components/views/beacon/RoomLiveShareWarning.tsx
+++ b/src/components/views/beacon/RoomLiveShareWarning.tsx
@@ -14,126 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { useCallback, useEffect, useState } from 'react';
+import React from 'react';
import classNames from 'classnames';
-import { Room, Beacon } from 'matrix-js-sdk/src/matrix';
+import { Room } from 'matrix-js-sdk/src/matrix';
-import { formatDuration } from '../../../DateUtils';
import { _t } from '../../../languageHandler';
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
-import { useInterval } from '../../../hooks/useTimeout';
import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../stores/OwnBeaconStore';
-import { getBeaconMsUntilExpiry, sortBeaconsByLatestExpiry } from '../../../utils/beacon';
+import { useOwnLiveBeacons } from '../../../utils/beacon';
import AccessibleButton from '../elements/AccessibleButton';
import Spinner from '../elements/Spinner';
import StyledLiveBeaconIcon from './StyledLiveBeaconIcon';
import { Icon as CloseIcon } from '../../../../res/img/image-view/close.svg';
-
-const MINUTE_MS = 60000;
-const HOUR_MS = MINUTE_MS * 60;
-
-const getUpdateInterval = (ms: number) => {
- // every 10 mins when more than an hour
- if (ms > HOUR_MS) {
- return MINUTE_MS * 10;
- }
- // every minute when more than a minute
- if (ms > MINUTE_MS) {
- return MINUTE_MS;
- }
- // otherwise every second
- return 1000;
-};
-const useMsRemaining = (beacon: Beacon): number => {
- const [msRemaining, setMsRemaining] = useState(() => getBeaconMsUntilExpiry(beacon));
-
- useEffect(() => {
- setMsRemaining(getBeaconMsUntilExpiry(beacon));
- }, [beacon]);
-
- const updateMsRemaining = useCallback(() => {
- const ms = getBeaconMsUntilExpiry(beacon);
- setMsRemaining(ms);
- }, [beacon]);
-
- useInterval(updateMsRemaining, getUpdateInterval(msRemaining));
-
- return msRemaining;
-};
-
-/**
- * It's technically possible to have multiple live beacons in one room
- * Select the latest expiry to display,
- * and kill all beacons on stop sharing
- */
-type LiveBeaconsState = {
- beacon?: Beacon;
- onStopSharing?: () => void;
- onResetWireError?: () => void;
- stoppingInProgress?: boolean;
- hasStopSharingError?: boolean;
- hasWireError?: boolean;
-};
-const useLiveBeacons = (liveBeaconIds: string[], roomId: string): LiveBeaconsState => {
- const [stoppingInProgress, setStoppingInProgress] = useState(false);
- const [error, setError] = useState();
-
- const hasWireError = useEventEmitterState(
- OwnBeaconStore.instance,
- OwnBeaconStoreEvent.WireError,
- () =>
- OwnBeaconStore.instance.hasWireErrors(roomId),
- );
-
- // reset stopping in progress on change in live ids
- useEffect(() => {
- setStoppingInProgress(false);
- setError(undefined);
- }, [liveBeaconIds]);
-
- // select the beacon with latest expiry to display expiry time
- const beacon = liveBeaconIds.map(beaconId => OwnBeaconStore.instance.getBeaconById(beaconId))
- .sort(sortBeaconsByLatestExpiry)
- .shift();
-
- const onStopSharing = async () => {
- setStoppingInProgress(true);
- try {
- await Promise.all(liveBeaconIds.map(beaconId => OwnBeaconStore.instance.stopBeacon(beaconId)));
- } catch (error) {
- // only clear loading in case of error
- // to avoid flash of not-loading state
- // after beacons have been stopped but we wait for sync
- setError(error);
- setStoppingInProgress(false);
- }
- };
-
- const onResetWireError = () => {
- liveBeaconIds.map(beaconId => OwnBeaconStore.instance.resetWireError(beaconId));
- };
-
- return {
- onStopSharing,
- onResetWireError,
- beacon,
- stoppingInProgress,
- hasWireError,
- hasStopSharingError: !!error,
- };
-};
-
-const LiveTimeRemaining: React.FC<{ beacon: Beacon }> = ({ beacon }) => {
- const msRemaining = useMsRemaining(beacon);
-
- const timeRemaining = formatDuration(msRemaining);
- const liveTimeRemaining = _t(`%(timeRemaining)s left`, { timeRemaining });
-
- return { liveTimeRemaining };
-};
+import LiveTimeRemaining from './LiveTimeRemaining';
const getLabel = (hasWireError: boolean, hasStopSharingError: boolean): string => {
if (hasWireError) {
@@ -157,7 +50,7 @@ const RoomLiveShareWarningInner: React.FC = ({ l
stoppingInProgress,
hasStopSharingError,
hasWireError,
- } = useLiveBeacons(liveBeaconIds, roomId);
+ } = useOwnLiveBeacons(liveBeaconIds);
if (!beacon) {
return null;
@@ -188,6 +81,7 @@ const RoomLiveShareWarningInner: React.FC = ({ l
{ !stoppingInProgress && !hasError && }
{
// use error styling when true
withError?: boolean;
+ isIdle?: boolean;
}
-const StyledLiveBeaconIcon: React.FC = ({ className, withError, ...props }) =>
+const StyledLiveBeaconIcon: React.FC = ({ className, withError, isIdle, ...props }) =>
;
export default StyledLiveBeaconIcon;
diff --git a/src/components/views/beacon/displayStatus.ts b/src/components/views/beacon/displayStatus.ts
new file mode 100644
index 0000000000..ee65991070
--- /dev/null
+++ b/src/components/views/beacon/displayStatus.ts
@@ -0,0 +1,42 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { BeaconLocationState } from "matrix-js-sdk/src/content-helpers";
+
+export enum BeaconDisplayStatus {
+ Loading = 'Loading',
+ Error = 'Error',
+ Stopped = 'Stopped',
+ Active = 'Active',
+}
+export const getBeaconDisplayStatus = (
+ isLive: boolean,
+ latestLocationState?: BeaconLocationState,
+ error?: Error): BeaconDisplayStatus => {
+ if (error) {
+ return BeaconDisplayStatus.Error;
+ }
+ if (!isLive) {
+ return BeaconDisplayStatus.Stopped;
+ }
+
+ if (!latestLocationState) {
+ return BeaconDisplayStatus.Loading;
+ }
+ if (latestLocationState) {
+ return BeaconDisplayStatus.Active;
+ }
+};
diff --git a/src/components/views/context_menus/CallContextMenu.tsx b/src/components/views/context_menus/CallContextMenu.tsx
index ec835d1e31..8c9e07dfcc 100644
--- a/src/components/views/context_menus/CallContextMenu.tsx
+++ b/src/components/views/context_menus/CallContextMenu.tsx
@@ -21,13 +21,11 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import { _t } from '../../../languageHandler';
import ContextMenu, { IProps as IContextMenuProps, MenuItem } from '../../structures/ContextMenu';
import CallHandler from '../../../CallHandler';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends IContextMenuProps {
call: MatrixCall;
}
-@replaceableComponent("views.context_menus.CallContextMenu")
export default class CallContextMenu extends React.Component {
static propTypes = {
// js-sdk User object. Not required because it might not exist.
diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx
index 70a5aae2e8..f6530452d0 100644
--- a/src/components/views/context_menus/DialpadContextMenu.tsx
+++ b/src/components/views/context_menus/DialpadContextMenu.tsx
@@ -22,7 +22,6 @@ import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import ContextMenu, { IProps as IContextMenuProps } from '../../structures/ContextMenu';
import Field from "../elements/Field";
import DialPad from '../voip/DialPad';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends IContextMenuProps {
call: MatrixCall;
@@ -32,7 +31,6 @@ interface IState {
value: string;
}
-@replaceableComponent("views.context_menus.DialpadContextMenu")
export default class DialpadContextMenu extends React.Component {
private numberEntryFieldRef: React.RefObject = createRef();
diff --git a/src/components/views/context_menus/GenericElementContextMenu.tsx b/src/components/views/context_menus/GenericElementContextMenu.tsx
index d5ebfca26e..7b53dc0ed9 100644
--- a/src/components/views/context_menus/GenericElementContextMenu.tsx
+++ b/src/components/views/context_menus/GenericElementContextMenu.tsx
@@ -16,8 +16,6 @@ limitations under the License.
import React from 'react';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
-
interface IProps {
element: React.ReactNode;
// Function to be called when the parent window is resized
@@ -30,7 +28,6 @@ interface IProps {
* This component can be used to display generic HTML content in a contextual
* menu.
*/
-@replaceableComponent("views.context_menus.GenericElementContextMenu")
export default class GenericElementContextMenu extends React.Component {
constructor(props: IProps) {
super(props);
diff --git a/src/components/views/context_menus/GenericTextContextMenu.tsx b/src/components/views/context_menus/GenericTextContextMenu.tsx
index d25c708622..0f6cc708a1 100644
--- a/src/components/views/context_menus/GenericTextContextMenu.tsx
+++ b/src/components/views/context_menus/GenericTextContextMenu.tsx
@@ -16,13 +16,10 @@ limitations under the License.
import React from 'react';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
-
interface IProps {
message: string;
}
-@replaceableComponent("views.context_menus.GenericTextContextMenu")
export default class GenericTextContextMenu extends React.Component {
public render(): JSX.Element {
return
diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx
index e0b1b7c9a8..55b893b784 100644
--- a/src/components/views/context_menus/MessageContextMenu.tsx
+++ b/src/components/views/context_menus/MessageContextMenu.tsx
@@ -1,6 +1,7 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
+Copyright 2021 - 2022 Šimon Brandner
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,12 +16,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { ReactElement } from 'react';
+import React, { createRef } from 'react';
import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
import { Relations } from 'matrix-js-sdk/src/models/relations';
import { RoomMemberEvent } from "matrix-js-sdk/src/models/room-member";
-import { M_LOCATION } from 'matrix-js-sdk/src/@types/location';
import { M_POLL_START } from "matrix-events-sdk";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
@@ -30,79 +30,81 @@ import Modal from '../../../Modal';
import Resend from '../../../Resend';
import SettingsStore from '../../../settings/SettingsStore';
import { isUrlPermitted } from '../../../HtmlUtils';
-import { isContentActionable } from '../../../utils/EventUtils';
+import { canEditContent, canForward, editEvent, isContentActionable, isLocationEvent } from '../../../utils/EventUtils';
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
-import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
-import ForwardDialog from "../dialogs/ForwardDialog";
+import { ReadPinsEventId } from "../right_panel/types";
import { Action } from "../../../dispatcher/actions";
-import ReportEventDialog from '../dialogs/ReportEventDialog';
+import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
+import { ButtonEvent } from '../elements/AccessibleButton';
+import { copyPlaintext, getSelectedText } from '../../../utils/strings';
+import ContextMenu, { toRightOf } from '../../structures/ContextMenu';
+import ReactionPicker from '../emojipicker/ReactionPicker';
import ViewSource from '../../structures/ViewSource';
import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
import ShareDialog from '../dialogs/ShareDialog';
-import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
-import { ChevronFace, IPosition } from '../../structures/ContextMenu';
+import { IPosition, ChevronFace } from '../../structures/ContextMenu';
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import EndPollDialog from '../dialogs/EndPollDialog';
import { isPollEnded } from '../messages/MPollBody';
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
+import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile";
+import { OpenForwardDialogPayload } from "../../../dispatcher/payloads/OpenForwardDialogPayload";
+import { OpenReportEventDialogPayload } from "../../../dispatcher/payloads/OpenReportEventDialogPayload";
import { createMapSiteLink } from '../../../utils/location';
-export function canCancel(status: EventStatus): boolean {
- return status === EventStatus.QUEUED || status === EventStatus.NOT_SENT || status === EventStatus.ENCRYPTING;
-}
-
-export interface IEventTileOps {
- isWidgetHidden(): boolean;
- unhideWidget(): void;
-}
-
-export interface IOperableEventTile {
- getEventTileOps(): IEventTileOps;
-}
-
interface IProps extends IPosition {
chevronFace: ChevronFace;
/* the MatrixEvent associated with the context menu */
mxEvent: MatrixEvent;
- /* an optional EventTileOps implementation that can be used to unhide preview widgets */
+ // An optional EventTileOps implementation that can be used to unhide preview widgets
eventTileOps?: IEventTileOps;
+ // Callback called when the menu is dismissed
permalinkCreator?: RoomPermalinkCreator;
/* an optional function to be called when the user clicks collapse thread, if not provided hide button */
collapseReplyChain?(): void;
/* callback called when the menu is dismissed */
onFinished(): void;
- /* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
+ // If the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding)
onCloseDialog?(): void;
- getRelationsForEvent?: (
- eventId: string,
- relationType: string,
- eventType: string
- ) => Relations;
+ // True if the menu is being used as a right click menu
+ rightClick?: boolean;
+ // The Relations model from the JS SDK for reactions to `mxEvent`
+ reactions?: Relations;
+ // A permalink to the event
+ showPermalink?: boolean;
+
+ getRelationsForEvent?: GetRelationsForEvent;
}
interface IState {
canRedact: boolean;
canPin: boolean;
+ reactionPickerDisplayed: boolean;
}
-@replaceableComponent("views.context_menus.MessageContextMenu")
export default class MessageContextMenu extends React.Component {
static contextType = RoomContext;
public context!: React.ContextType;
- state = {
- canRedact: false,
- canPin: false,
- };
+ private reactButtonRef = createRef(); // XXX Ref to a functional component
- componentDidMount() {
+ constructor(props: IProps) {
+ super(props);
+
+ this.state = {
+ canRedact: false,
+ canPin: false,
+ reactionPickerDisplayed: false,
+ };
+ }
+
+ public componentDidMount() {
MatrixClientPeg.get().on(RoomMemberEvent.PowerLevel, this.checkPermissions);
this.checkPermissions();
}
- componentWillUnmount() {
+ public componentWillUnmount(): void {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener(RoomMemberEvent.PowerLevel, this.checkPermissions);
@@ -155,9 +157,10 @@ export default class MessageContextMenu extends React.Component
};
private onReportEventClick = (): void => {
- Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
- mxEvent: this.props.mxEvent,
- }, 'mx_Dialog_reportEvent');
+ dis.dispatch({
+ action: Action.OpenReportEventDialog,
+ event: this.props.mxEvent,
+ });
this.closeMenu();
};
@@ -178,8 +181,8 @@ export default class MessageContextMenu extends React.Component
};
private onForwardClick = (): void => {
- Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
- matrixClient: MatrixClientPeg.get(),
+ dis.dispatch({
+ action: Action.OpenForwardDialog,
event: this.props.mxEvent,
permalinkCreator: this.props.permalinkCreator,
});
@@ -234,11 +237,45 @@ export default class MessageContextMenu extends React.Component
this.closeMenu();
};
+ private onCopyPermalinkClick = (e: ButtonEvent): void => {
+ e.preventDefault(); // So that we don't open the permalink
+ copyPlaintext(this.getPermalink());
+ this.closeMenu();
+ };
+
private onCollapseReplyChainClick = (): void => {
this.props.collapseReplyChain();
this.closeMenu();
};
+ private onCopyClick = (): void => {
+ copyPlaintext(getSelectedText());
+ this.closeMenu();
+ };
+
+ private onEditClick = (): void => {
+ editEvent(this.props.mxEvent, this.context.timelineRenderingType, this.props.getRelationsForEvent);
+ this.closeMenu();
+ };
+
+ private onReplyClick = (): void => {
+ dis.dispatch({
+ action: 'reply_to_event',
+ event: this.props.mxEvent,
+ context: this.context.timelineRenderingType,
+ });
+ this.closeMenu();
+ };
+
+ private onReactClick = (): void => {
+ this.setState({ reactionPickerDisplayed: true });
+ };
+
+ private onCloseReactionPicker = (): void => {
+ this.setState({ reactionPickerDisplayed: false });
+ this.closeMenu();
+ };
+
private onEndPollClick = (): void => {
const matrixClient = MatrixClientPeg.get();
Modal.createTrackedDialog('End Poll', '', EndPollDialog, {
@@ -259,11 +296,16 @@ export default class MessageContextMenu extends React.Component
});
}
+ private getPermalink(): string {
+ if (!this.props.permalinkCreator) return;
+ return this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
+ }
+
private getUnsentReactions(): MatrixEvent[] {
return this.getReactions(e => e.status === EventStatus.NOT_SENT);
}
- private viewInRoom = () => {
+ private viewInRoom = (): void => {
dis.dispatch({
action: Action.ViewRoom,
event_id: this.props.mxEvent.getId(),
@@ -274,39 +316,35 @@ export default class MessageContextMenu extends React.Component
this.closeMenu();
};
- render() {
+ public render(): JSX.Element {
const cli = MatrixClientPeg.get();
const me = cli.getUserId();
- const mxEvent = this.props.mxEvent;
+ const { mxEvent, rightClick, showPermalink, eventTileOps, reactions, collapseReplyChain } = this.props;
const eventStatus = mxEvent.status;
const unsentReactionsCount = this.getUnsentReactions().length;
-
- let openInMapSiteButton: JSX.Element;
- let endPollButton: JSX.Element;
- let resendReactionsButton: JSX.Element;
- let redactButton: JSX.Element;
- let forwardButton: JSX.Element;
- let pinButton: JSX.Element;
- let unhidePreviewButton: JSX.Element;
- let externalURLButton: JSX.Element;
- let quoteButton: JSX.Element;
- let collapseReplyChain: JSX.Element;
- let redactItemList: JSX.Element;
-
+ const contentActionable = isContentActionable(mxEvent);
+ const permalink = this.getPermalink();
// status is SENT before remote-echo, null after
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
- if (!mxEvent.isRedacted()) {
- if (unsentReactionsCount !== 0) {
- resendReactionsButton = (
-
- );
- }
+ const { timelineRenderingType, canReact, canSendMessages } = this.context;
+ const isThread = (
+ timelineRenderingType === TimelineRenderingType.Thread ||
+ timelineRenderingType === TimelineRenderingType.ThreadsList
+ );
+ const isThreadRootEvent = isThread && mxEvent?.getThread()?.rootEvent === mxEvent;
+
+ let resendReactionsButton: JSX.Element;
+ if (!mxEvent.isRedacted() && unsentReactionsCount !== 0) {
+ resendReactionsButton = (
+
+ );
}
+ let redactButton: JSX.Element;
if (isSent && this.state.canRedact) {
redactButton = (
);
}
+ let openInMapSiteButton: JSX.Element;
if (this.canOpenInMapSite(mxEvent)) {
const mapSiteLink = createMapSiteLink(mxEvent);
openInMapSiteButton = (
@@ -336,26 +375,26 @@ export default class MessageContextMenu extends React.Component
);
}
- if (isContentActionable(mxEvent)) {
- if (canForward(mxEvent)) {
- forwardButton = (
-
- );
- }
+ let forwardButton: JSX.Element;
+ if (contentActionable && canForward(mxEvent)) {
+ forwardButton = (
+
+ );
+ }
- if (this.state.canPin) {
- pinButton = (
-
- );
- }
+ let pinButton: JSX.Element;
+ if (contentActionable && this.state.canPin) {
+ pinButton = (
+
+ );
}
let viewSourceButton: JSX.Element;
@@ -369,40 +408,42 @@ export default class MessageContextMenu extends React.Component
);
}
- if (this.props.eventTileOps) {
- if (this.props.eventTileOps.isWidgetHidden()) {
- unhidePreviewButton = (
-
- );
- }
+ let unhidePreviewButton: JSX.Element;
+ if (eventTileOps?.isWidgetHidden()) {
+ unhidePreviewButton = (
+
+ );
}
- let permalink: string | null = null;
- let permalinkButton: ReactElement | null = null;
- if (this.props.permalinkCreator) {
- permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
- }
- permalinkButton = (
-
- );
+ onClick={showPermalink ? this.onCopyPermalinkClick : this.onPermalinkClick}
+ label={showPermalink ? _t('Copy link') : _t('Share')}
+ element="a"
+ {
+ // XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
+ ...{
+ href: permalink,
+ target: "_blank",
+ rel: "noreferrer noopener",
+ }
+ }
+ />
+ );
+ }
+
+ let endPollButton: JSX.Element;
if (this.canEndPoll(mxEvent)) {
endPollButton = (
);
}
- if (this.props.eventTileOps) { // this event is rendered using TextualBody
+ let quoteButton: JSX.Element;
+ if (eventTileOps) { // this event is rendered using TextualBody
quoteButton = (
}
// Bridges can provide a 'external_url' to link back to the source.
- if (typeof (mxEvent.getContent().external_url) === "string" &&
+ let externalURLButton: JSX.Element;
+ if (
+ typeof (mxEvent.getContent().external_url) === "string" &&
isUrlPermitted(mxEvent.getContent().external_url)
) {
externalURLButton = (
@@ -445,8 +489,9 @@ export default class MessageContextMenu extends React.Component
);
}
- if (this.props.collapseReplyChain) {
- collapseReplyChain = (
+ let collapseReplyChainButton: JSX.Element;
+ if (collapseReplyChain) {
+ collapseReplyChainButton = (
);
}
- const { timelineRenderingType } = this.context;
- const isThread = (
- timelineRenderingType === TimelineRenderingType.Thread ||
- timelineRenderingType === TimelineRenderingType.ThreadsList
- );
- const isThreadRootEvent = isThread && this.props.mxEvent.isThreadRoot;
+ let copyButton: JSX.Element;
+ if (rightClick && getSelectedText()) {
+ copyButton = (
+
+ );
+ }
- const commonItemsList = (
-
- { isThreadRootEvent &&
+ );
+ }
+
+ let replyButton: JSX.Element;
+ if (rightClick && contentActionable && canSendMessages) {
+ replyButton = (
+
+ );
+ }
+
+ let reactButton;
+ if (rightClick && contentActionable && canReact) {
+ reactButton = (
+
+ );
+ }
+
+ let viewInRoomButton: JSX.Element;
+ if (isThreadRootEvent) {
+ viewInRoomButton = (
+ }
+ />
+ );
+ }
+
+ let nativeItemsList: JSX.Element;
+ if (copyButton) {
+ nativeItemsList = (
+
+ { copyButton }
+
+ );
+ }
+
+ let quickItemsList: JSX.Element;
+ if (editButton || replyButton || reactButton) {
+ quickItemsList = (
+
+ { reactButton }
+ { replyButton }
+ { editButton }
+
+ );
+ }
+
+ const commonItemsList = (
+
+ { viewInRoomButton }
{ openInMapSiteButton }
{ endPollButton }
{ quoteButton }
@@ -491,10 +602,11 @@ export default class MessageContextMenu extends React.Component
{ unhidePreviewButton }
{ viewSourceButton }
{ resendReactionsButton }
- { collapseReplyChain }
+ { collapseReplyChainButton }
);
+ let redactItemList: JSX.Element;
if (redactButton) {
redactItemList = (
@@ -502,33 +614,40 @@ export default class MessageContextMenu extends React.Component
);
}
+
+ let reactionPicker: JSX.Element;
+ if (this.state.reactionPickerDisplayed) {
+ const buttonRect = (this.reactButtonRef.current as HTMLElement)?.getBoundingClientRect();
+ reactionPicker = (
+
+
+
+ );
+ }
+
return (
-
- { commonItemsList }
- { redactItemList }
-
+
+
+ { nativeItemsList }
+ { quickItemsList }
+ { commonItemsList }
+ { redactItemList }
+
+ { reactionPicker }
+
);
}
}
-function canForward(event: MatrixEvent): boolean {
- return !(
- isLocationEvent(event) ||
- M_POLL_START.matches(event.getType())
- );
-}
-
-function isLocationEvent(event: MatrixEvent): boolean {
- const eventType = event.getType();
- return (
- M_LOCATION.matches(eventType) ||
- (
- eventType === EventType.RoomMessage &&
- M_LOCATION.matches(event.getContent().msgtype)
- )
- );
-}
diff --git a/src/components/views/context_menus/RoomContextMenu.tsx b/src/components/views/context_menus/RoomContextMenu.tsx
index 8a0f5a04c1..e8aa73c779 100644
--- a/src/components/views/context_menus/RoomContextMenu.tsx
+++ b/src/components/views/context_menus/RoomContextMenu.tsx
@@ -37,7 +37,7 @@ import Modal from "../../../Modal";
import ExportDialog from "../dialogs/ExportDialog";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import { usePinnedEvents } from "../right_panel/PinnedMessagesCard";
-import RoomViewStore from "../../../stores/RoomViewStore";
+import { RoomViewStore } from "../../../stores/RoomViewStore";
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
import { ROOM_NOTIFICATIONS_TAB } from "../dialogs/RoomSettingsDialog";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
@@ -328,7 +328,7 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
};
const ensureViewingRoom = (ev: ButtonEvent) => {
- if (RoomViewStore.getRoomId() === room.roomId) return;
+ if (RoomViewStore.instance.getRoomId() === room.roomId) return;
dis.dispatch({
action: Action.ViewRoom,
room_id: room.roomId,
@@ -373,7 +373,7 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
ev.stopPropagation();
Modal.createDialog(DevtoolsDialog, {
- roomId: RoomViewStore.getRoomId(),
+ roomId: RoomViewStore.instance.getRoomId(),
}, "mx_DevtoolsDialog_wrapper");
onFinished();
}}
diff --git a/src/components/views/context_menus/SpaceContextMenu.tsx b/src/components/views/context_menus/SpaceContextMenu.tsx
index aab270e2e8..d9286c618b 100644
--- a/src/components/views/context_menus/SpaceContextMenu.tsx
+++ b/src/components/views/context_menus/SpaceContextMenu.tsx
@@ -22,7 +22,6 @@ import { IProps as IContextMenuProps } from "../../structures/ContextMenu";
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
import { _t } from "../../../languageHandler";
import {
- leaveSpace,
shouldShowSpaceSettings,
showCreateNewRoom,
showCreateNewSubspace,
@@ -30,6 +29,7 @@ import {
showSpacePreferences,
showSpaceSettings,
} from "../../../utils/space";
+import { leaveSpace } from "../../../utils/leave-behaviour";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { ButtonEvent } from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.tsx b/src/components/views/dialogs/AskInviteAnywayDialog.tsx
index 1815a43d57..bd691a1ed4 100644
--- a/src/components/views/dialogs/AskInviteAnywayDialog.tsx
+++ b/src/components/views/dialogs/AskInviteAnywayDialog.tsx
@@ -19,7 +19,6 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
interface IProps {
@@ -32,7 +31,6 @@ interface IProps {
onFinished: (success: boolean) => void;
}
-@replaceableComponent("views.dialogs.AskInviteAnywayDialog")
export default class AskInviteAnywayDialog extends React.Component {
private onInviteClicked = (): void => {
this.props.onInviteAnyways();
diff --git a/src/components/views/dialogs/BaseDialog.tsx b/src/components/views/dialogs/BaseDialog.tsx
index 519e38601c..5eb0c6ac82 100644
--- a/src/components/views/dialogs/BaseDialog.tsx
+++ b/src/components/views/dialogs/BaseDialog.tsx
@@ -25,7 +25,6 @@ import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import Heading from '../typography/Heading';
import { IDialogProps } from "./IDialogProps";
import { PosthogScreenTracker, ScreenName } from "../../../PosthogTrackers";
@@ -81,7 +80,6 @@ interface IProps extends IDialogProps {
* Includes a div for the title, and a keypress handler which cancels the
* dialog on escape.
*/
-@replaceableComponent("views.dialogs.BaseDialog")
export default class BaseDialog extends React.Component {
private matrixClient: MatrixClient;
diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx
index b0afec4e91..bc6a05a0be 100644
--- a/src/components/views/dialogs/BetaFeedbackDialog.tsx
+++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx
@@ -22,7 +22,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
-import { UserTab } from "./UserSettingsDialog";
+import { UserTab } from "./UserTab";
import GenericFeatureFeedbackDialog from "./GenericFeatureFeedbackDialog";
// XXX: Keep this around for re-use in future Betas
diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx
index 2fa3f3b33f..eae2673521 100644
--- a/src/components/views/dialogs/BugReportDialog.tsx
+++ b/src/components/views/dialogs/BugReportDialog.tsx
@@ -24,7 +24,6 @@ import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import sendBugReport, { downloadBugReport } from '../../../rageshake/submit-rageshake';
import AccessibleButton from "../elements/AccessibleButton";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import QuestionDialog from "./QuestionDialog";
import BaseDialog from "./BaseDialog";
import Field from '../elements/Field';
@@ -50,7 +49,6 @@ interface IState {
downloadProgress: string;
}
-@replaceableComponent("views.dialogs.BugReportDialog")
export default class BugReportDialog extends React.Component {
private unmounted: boolean;
diff --git a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx
index fbf603c44d..f56f4f6194 100644
--- a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx
+++ b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx
@@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import ConfirmRedactDialog from './ConfirmRedactDialog';
import ErrorDialog from './ErrorDialog';
import BaseDialog from "./BaseDialog";
@@ -45,7 +44,6 @@ interface IState {
*
* To avoid this, we keep the dialog open as long as /redact is in progress.
*/
-@replaceableComponent("views.dialogs.ConfirmAndWaitRedactDialog")
export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
constructor(props) {
super(props);
diff --git a/src/components/views/dialogs/ConfirmRedactDialog.tsx b/src/components/views/dialogs/ConfirmRedactDialog.tsx
index 6839c0f7bd..9a9f3627d2 100644
--- a/src/components/views/dialogs/ConfirmRedactDialog.tsx
+++ b/src/components/views/dialogs/ConfirmRedactDialog.tsx
@@ -20,7 +20,6 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import ErrorDialog from './ErrorDialog';
import TextInputDialog from "./TextInputDialog";
@@ -31,7 +30,6 @@ interface IProps {
/*
* A dialog for confirming a redaction.
*/
-@replaceableComponent("views.dialogs.ConfirmRedactDialog")
export default class ConfirmRedactDialog extends React.Component {
render() {
return (
diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.tsx b/src/components/views/dialogs/ConfirmUserActionDialog.tsx
index c10ead0b72..8a41c8f2de 100644
--- a/src/components/views/dialogs/ConfirmUserActionDialog.tsx
+++ b/src/components/views/dialogs/ConfirmUserActionDialog.tsx
@@ -19,7 +19,6 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import classNames from "classnames";
import { _t } from '../../../languageHandler';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import MemberAvatar from '../avatars/MemberAvatar';
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
@@ -55,7 +54,6 @@ interface IState {
* to make it obvious what is going to happen.
* Also tweaks the style for 'dangerous' actions (albeit only with colour)
*/
-@replaceableComponent("views.dialogs.ConfirmUserActionDialog")
export default class ConfirmUserActionDialog extends React.Component {
static defaultProps = {
danger: false,
diff --git a/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx
index 33e26ff7a7..a61b47c04f 100644
--- a/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx
+++ b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx
@@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import { _t } from "../../../languageHandler";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
@@ -25,7 +24,6 @@ interface IProps {
onFinished: (success: boolean) => void;
}
-@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog")
export default class ConfirmWipeDeviceDialog extends React.Component {
private onConfirm = (): void => {
this.props.onFinished(true);
diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx
index ffb28719a7..7d1028bba7 100644
--- a/src/components/views/dialogs/CreateRoomDialog.tsx
+++ b/src/components/views/dialogs/CreateRoomDialog.tsx
@@ -24,8 +24,7 @@ import SdkConfig from '../../../SdkConfig';
import withValidation, { IFieldState } from '../elements/Validation';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
-import { IOpts, privateShouldBeEncrypted } from "../../../createRoom";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { IOpts } from "../../../createRoom";
import Field from "../elements/Field";
import RoomAliasField from "../elements/RoomAliasField";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
@@ -35,6 +34,7 @@ import SpaceStore from "../../../stores/spaces/SpaceStore";
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
+import { privateShouldBeEncrypted } from "../../../utils/rooms";
interface IProps {
type?: RoomType;
@@ -58,7 +58,6 @@ interface IState {
canChangeEncryption: boolean;
}
-@replaceableComponent("views.dialogs.CreateRoomDialog")
export default class CreateRoomDialog extends React.Component {
private readonly supportsRestricted: boolean;
private nameField = createRef();
@@ -104,7 +103,7 @@ export default class CreateRoomDialog extends React.Component {
createOpts.preset = Preset.PublicChat;
opts.guestAccess = false;
const { alias } = this.state;
- createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1);
+ createOpts.room_alias_name = alias.substring(1, alias.indexOf(":"));
} else {
// If we cannot change encryption we pass `true` for safety, the server should automatically do this for us.
opts.encryption = this.state.canChangeEncryption ? this.state.isEncrypted : true;
diff --git a/src/components/views/dialogs/DeactivateAccountDialog.tsx b/src/components/views/dialogs/DeactivateAccountDialog.tsx
index aece59ef84..a505d5e647 100644
--- a/src/components/views/dialogs/DeactivateAccountDialog.tsx
+++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx
@@ -21,13 +21,13 @@ import { logger } from "matrix-js-sdk/src/logger";
import Analytics from '../../../Analytics';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
-import * as Lifecycle from '../../../Lifecycle';
import { _t } from '../../../languageHandler';
import InteractiveAuth, { ERROR_USER_CANCELLED } from "../../structures/InteractiveAuth";
import { DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
import StyledCheckbox from "../elements/StyledCheckbox";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
+import defaultDispatcher from "../../../dispatcher/dispatcher";
+import { Action } from "../../../dispatcher/actions";
interface IProps {
onFinished: (success: boolean) => void;
@@ -46,7 +46,6 @@ interface IState {
continueKind: string;
}
-@replaceableComponent("views.dialogs.DeactivateAccountDialog")
export default class DeactivateAccountDialog extends React.Component {
constructor(props) {
super(props);
@@ -124,7 +123,7 @@ export default class DeactivateAccountDialog extends React.Component {
// Deactivation worked - logout & close this dialog
Analytics.trackEvent('Account', 'Deactivate Account');
- Lifecycle.onLoggedOut();
+ defaultDispatcher.fire(Action.TriggerLogout);
this.props.onFinished(true);
}).catch(e => {
logger.error(e);
diff --git a/src/components/views/dialogs/ErrorDialog.tsx b/src/components/views/dialogs/ErrorDialog.tsx
index e19d93f918..286124c6bf 100644
--- a/src/components/views/dialogs/ErrorDialog.tsx
+++ b/src/components/views/dialogs/ErrorDialog.tsx
@@ -28,7 +28,6 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
interface IProps {
@@ -44,7 +43,6 @@ interface IState {
onFinished: (success: boolean) => void;
}
-@replaceableComponent("views.dialogs.ErrorDialog")
export default class ErrorDialog extends React.Component {
public static defaultProps = {
focus: true,
diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index d3f38fa9d1..0a063df9c7 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -43,10 +43,10 @@ import QueryMatcher from "../../../autocomplete/QueryMatcher";
import TruncatedList from "../elements/TruncatedList";
import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar";
-import { roomContextDetailsText } from "../../../Rooms";
import { Action } from "../../../dispatcher/actions";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { ButtonEvent } from "../elements/AccessibleButton";
+import { roomContextDetailsText } from "../../../utils/i18n-helpers";
const AVATAR_SIZE = 30;
diff --git a/src/components/views/dialogs/HostSignupDialog.tsx b/src/components/views/dialogs/HostSignupDialog.tsx
index 5f82ba8086..91c29c68d1 100644
--- a/src/components/views/dialogs/HostSignupDialog.tsx
+++ b/src/components/views/dialogs/HostSignupDialog.tsx
@@ -32,7 +32,6 @@ import {
IPostmessageResponseData,
PostmessageAction,
} from "./HostSignupDialogTypes";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IConfigOptions } from "../../../IConfigOptions";
import { SnakedObject } from "../../../utils/SnakedObject";
@@ -46,7 +45,6 @@ interface IState {
minimized: boolean;
}
-@replaceableComponent("views.dialogs.HostSignupDialog")
export default class HostSignupDialog extends React.PureComponent {
private iframeRef: React.RefObject = React.createRef();
private readonly config: SnakedObject;
diff --git a/src/components/views/dialogs/IncomingSasDialog.tsx b/src/components/views/dialogs/IncomingSasDialog.tsx
index d856412c67..2b15749c8d 100644
--- a/src/components/views/dialogs/IncomingSasDialog.tsx
+++ b/src/components/views/dialogs/IncomingSasDialog.tsx
@@ -21,7 +21,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import VerificationComplete from "../verification/VerificationComplete";
import VerificationCancelled from "../verification/VerificationCancelled";
@@ -54,7 +53,6 @@ interface IState {
sas: IGeneratedSas;
}
-@replaceableComponent("views.dialogs.IncomingSasDialog")
export default class IncomingSasDialog extends React.Component {
private showSasEvent: ISasEvent;
diff --git a/src/components/views/dialogs/InfoDialog.tsx b/src/components/views/dialogs/InfoDialog.tsx
index ddcea275a9..dd9c985f8e 100644
--- a/src/components/views/dialogs/InfoDialog.tsx
+++ b/src/components/views/dialogs/InfoDialog.tsx
@@ -19,8 +19,9 @@ import React, { ReactNode, KeyboardEvent } from 'react';
import classNames from "classnames";
import { _t } from '../../../languageHandler';
-import * as sdk from '../../../index';
import { IDialogProps } from "./IDialogProps";
+import BaseDialog from "./BaseDialog";
+import DialogButtons from "../elements/DialogButtons";
interface IProps extends IDialogProps {
title?: string;
@@ -44,9 +45,6 @@ export default class InfoDialog extends React.Component {
};
render() {
- // FIXME: Using a regular import will break the app
- const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
- const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
{
private onAcknowledgeClick = (): void => {
this.props.onFinished();
diff --git a/src/components/views/dialogs/IntegrationsImpossibleDialog.tsx b/src/components/views/dialogs/IntegrationsImpossibleDialog.tsx
index 0e7db93c53..73c7fbc629 100644
--- a/src/components/views/dialogs/IntegrationsImpossibleDialog.tsx
+++ b/src/components/views/dialogs/IntegrationsImpossibleDialog.tsx
@@ -18,14 +18,12 @@ import React from 'react';
import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
interface IProps extends IDialogProps {}
-@replaceableComponent("views.dialogs.IntegrationsImpossibleDialog")
export default class IntegrationsImpossibleDialog extends React.Component {
private onAcknowledgeClick = (): void => {
this.props.onFinished();
diff --git a/src/components/views/dialogs/InteractiveAuthDialog.tsx b/src/components/views/dialogs/InteractiveAuthDialog.tsx
index c6a7d414bf..46bad5fd0e 100644
--- a/src/components/views/dialogs/InteractiveAuthDialog.tsx
+++ b/src/components/views/dialogs/InteractiveAuthDialog.tsx
@@ -24,7 +24,6 @@ import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import InteractiveAuth, { ERROR_USER_CANCELLED } from "../../structures/InteractiveAuth";
import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import { IDialogProps } from "./IDialogProps";
@@ -83,7 +82,6 @@ interface IState {
uiaStagePhase: number | string;
}
-@replaceableComponent("views.dialogs.InteractiveAuthDialog")
export default class InteractiveAuthDialog extends React.Component {
constructor(props: IProps) {
super(props);
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index 36b2bd54e2..978c176ab0 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -19,7 +19,6 @@ import classNames from 'classnames';
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
-import { IInvite3PID } from "matrix-js-sdk/src/@types/requests";
import { logger } from "matrix-js-sdk/src/logger";
import { _t, _td } from "../../../languageHandler";
@@ -30,15 +29,8 @@ import SdkConfig from "../../../SdkConfig";
import * as Email from "../../../email";
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from "../../../utils/IdentityServerUtils";
import { abbreviateUrl } from "../../../utils/UrlUtils";
-import dis from "../../../dispatcher/dispatcher";
import IdentityAuthClient from "../../../IdentityAuthClient";
-import Modal from "../../../Modal";
import { humanizeTime } from "../../../utils/humanize";
-import createRoom, {
- canEncryptToAllUsers,
- findDMForUser,
- privateShouldBeEncrypted,
-} from "../../../createRoom";
import {
IInviteResult,
inviteMultipleToRoom,
@@ -49,9 +41,7 @@ import { DefaultTagID } from "../../../stores/room-list/models";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
-import { getAddressType } from "../../../UserAddress";
import BaseAvatar from '../avatars/BaseAvatar';
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import { compare, selectText } from '../../../utils/strings';
@@ -66,9 +56,12 @@ import CallHandler from "../../../CallHandler";
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
import CopyableText from "../elements/CopyableText";
import { ScreenName } from '../../../PosthogTrackers';
-import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
+import { DirectoryMember, IDMUserTileProps, Member, startDm, ThreepidMember } from "../../../utils/direct-messages";
+import { AnyInviteKind, KIND_CALL_TRANSFER, KIND_DM, KIND_INVITE } from './InviteDialogTypes';
+import Modal from '../../../Modal';
+import dis from "../../../dispatcher/dispatcher";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */
@@ -79,13 +72,6 @@ interface IRecentUser {
lastActive: number;
}
-export const KIND_DM = "dm";
-export const KIND_INVITE = "invite";
-// NB. This dialog needs the 'mx_InviteDialog_transferWrapper' wrapper class to have the correct
-// padding on the bottom (because all modals have 24px padding on all sides), so this needs to
-// be passed when creating the modal
-export const KIND_CALL_TRANSFER = "call_transfer";
-
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
@@ -94,90 +80,6 @@ enum TabId {
DialPad = 'dialpad',
}
-// This is the interface that is expected by various components in the Invite Dialog and RoomInvite.
-// It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support
-// for 3PIDs/email addresses.
-export abstract class Member {
- /**
- * The display name of this Member. For users this should be their profile's display
- * name or user ID if none set. For 3PIDs this should be the 3PID address (email).
- */
- public abstract get name(): string;
-
- /**
- * The ID of this Member. For users this should be their user ID. For 3PIDs this should
- * be the 3PID address (email).
- */
- public abstract get userId(): string;
-
- /**
- * Gets the MXC URL of this Member's avatar. For users this should be their profile's
- * avatar MXC URL or null if none set. For 3PIDs this should always be null.
- */
- public abstract getMxcAvatarUrl(): string;
-}
-
-class DirectoryMember extends Member {
- private readonly _userId: string;
- private readonly displayName?: string;
- private readonly avatarUrl?: string;
-
- // eslint-disable-next-line camelcase
- constructor(userDirResult: { user_id: string, display_name?: string, avatar_url?: string }) {
- super();
- this._userId = userDirResult.user_id;
- this.displayName = userDirResult.display_name;
- this.avatarUrl = userDirResult.avatar_url;
- }
-
- // These next class members are for the Member interface
- get name(): string {
- return this.displayName || this._userId;
- }
-
- get userId(): string {
- return this._userId;
- }
-
- getMxcAvatarUrl(): string {
- return this.avatarUrl;
- }
-}
-
-class ThreepidMember extends Member {
- private readonly id: string;
-
- constructor(id: string) {
- super();
- this.id = id;
- }
-
- // This is a getter that would be falsey on all other implementations. Until we have
- // better type support in the react-sdk we can use this trick to determine the kind
- // of 3PID we're dealing with, if any.
- get isEmail(): boolean {
- return this.id.includes('@');
- }
-
- // These next class members are for the Member interface
- get name(): string {
- return this.id;
- }
-
- get userId(): string {
- return this.id;
- }
-
- getMxcAvatarUrl(): string {
- return null;
- }
-}
-
-interface IDMUserTileProps {
- member: Member;
- onRemove(member: Member): void;
-}
-
class DMUserTile extends React.PureComponent {
private onRemove = (e) => {
// Stop the browser from highlighting text
@@ -355,7 +257,7 @@ interface IInviteDialogProps {
// The kind of invite being performed. Assumed to be KIND_DM if
// not provided.
- kind: string;
+ kind: AnyInviteKind;
// The room ID this dialog is for. Only required for KIND_INVITE.
roomId: string;
@@ -387,7 +289,6 @@ interface IInviteDialogState {
errorText: string;
}
-@replaceableComponent("views.dialogs.InviteDialog")
export default class InviteDialog extends React.PureComponent {
static defaultProps = {
kind: KIND_DM,
@@ -663,72 +564,10 @@ export default class InviteDialog extends React.PureComponent {
this.setState({ busy: true });
- const client = MatrixClientPeg.get();
- const targets = this.convertFilter();
- const targetIds = targets.map(t => t.userId);
-
- // Check if there is already a DM with these people and reuse it if possible.
- let existingRoom: Room;
- if (targetIds.length === 1) {
- existingRoom = findDMForUser(client, targetIds[0]);
- } else {
- existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
- }
- if (existingRoom) {
- dis.dispatch({
- action: Action.ViewRoom,
- room_id: existingRoom.roomId,
- should_peek: false,
- joining: false,
- metricsTrigger: "MessageUser",
- });
- this.props.onFinished(true);
- return;
- }
-
- const createRoomOptions = { inlineErrors: true } as any; // XXX: Type out `createRoomOptions`
-
- if (privateShouldBeEncrypted()) {
- // Check whether all users have uploaded device keys before.
- // If so, enable encryption in the new room.
- const has3PidMembers = targets.some(t => t instanceof ThreepidMember);
- if (!has3PidMembers) {
- const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
- if (allHaveDeviceKeys) {
- createRoomOptions.encryption = true;
- }
- }
- }
-
- // Check if it's a traditional DM and create the room if required.
- // TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
try {
- const isSelf = targetIds.length === 1 && targetIds[0] === client.getUserId();
- if (targetIds.length === 1 && !isSelf) {
- createRoomOptions.dmUserId = targetIds[0];
- }
-
- if (targetIds.length > 1) {
- createRoomOptions.createOpts = targetIds.reduce(
- (roomOptions, address) => {
- const type = getAddressType(address);
- if (type === 'email') {
- const invite: IInvite3PID = {
- id_server: client.getIdentityServerUrl(true),
- medium: 'email',
- address,
- };
- roomOptions.invite_3pid.push(invite);
- } else if (type === 'mx-user-id') {
- roomOptions.invite.push(address);
- }
- return roomOptions;
- },
- { invite: [], invite_3pid: [] },
- );
- }
-
- await createRoom(createRoomOptions);
+ const cli = MatrixClientPeg.get();
+ const targets = this.convertFilter();
+ await startDm(cli, targets);
this.props.onFinished(true);
} catch (err) {
logger.error(err);
@@ -736,6 +575,8 @@ export default class InviteDialog extends React.PureComponent {
static defaultProps = {
onFinished: function() {},
diff --git a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx
index 6b94eb9c7a..ef5a40a8b0 100644
--- a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx
+++ b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx
@@ -24,7 +24,6 @@ import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import QuestionDialog from "./QuestionDialog";
import { IDialogProps } from "./IDialogProps";
@@ -33,7 +32,6 @@ interface IProps extends IDialogProps {
device: DeviceInfo;
}
-@replaceableComponent("views.dialogs.ManualDeviceKeyVerificationDialog")
export default class ManualDeviceKeyVerificationDialog extends React.Component {
private onLegacyFinished = (confirm: boolean): void => {
if (confirm) {
diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.tsx b/src/components/views/dialogs/MessageEditHistoryDialog.tsx
index 2a9e02cc04..2dedfb5293 100644
--- a/src/components/views/dialogs/MessageEditHistoryDialog.tsx
+++ b/src/components/views/dialogs/MessageEditHistoryDialog.tsx
@@ -25,7 +25,6 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from '../../../languageHandler';
import { wantsDateSeparator } from '../../../DateUtils';
import SettingsStore from '../../../settings/SettingsStore';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import ScrollPanel from "../../structures/ScrollPanel";
import Spinner from "../elements/Spinner";
@@ -48,7 +47,6 @@ interface IState {
isTwelveHour: boolean;
}
-@replaceableComponent("views.dialogs.MessageEditHistoryDialog")
export default class MessageEditHistoryDialog extends React.PureComponent {
constructor(props: IProps) {
super(props);
diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx
index 95e873cada..84564c22cf 100644
--- a/src/components/views/dialogs/ModalWidgetDialog.tsx
+++ b/src/components/views/dialogs/ModalWidgetDialog.tsx
@@ -39,7 +39,6 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
import { arrayFastClone } from "../../../utils/arrays";
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ELEMENT_CLIENT_ID } from "../../../identifiers";
import SettingsStore from "../../../settings/SettingsStore";
@@ -57,7 +56,6 @@ interface IState {
const MAX_BUTTONS = 3;
-@replaceableComponent("views.dialogs.ModalWidgetDialog")
export default class ModalWidgetDialog extends React.PureComponent {
private readonly widget: Widget;
private readonly possibleButtons: ModalButtonID[];
diff --git a/src/components/views/dialogs/QuestionDialog.tsx b/src/components/views/dialogs/QuestionDialog.tsx
index aaddff34ed..a0389f75e3 100644
--- a/src/components/views/dialogs/QuestionDialog.tsx
+++ b/src/components/views/dialogs/QuestionDialog.tsx
@@ -18,9 +18,10 @@ limitations under the License.
import React from 'react';
import classNames from "classnames";
-import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { IDialogProps } from "./IDialogProps";
+import BaseDialog from "./BaseDialog";
+import DialogButtons from "../elements/DialogButtons";
interface IProps extends IDialogProps {
title?: string;
@@ -58,10 +59,6 @@ export default class QuestionDialog extends React.Component {
};
public render(): JSX.Element {
- // Converting these to imports breaks wrench tests
- const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
- const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
-
let primaryButtonClass = "";
if (this.props.danger) {
primaryButtonClass = "danger";
diff --git a/src/components/views/dialogs/ReportEventDialog.tsx b/src/components/views/dialogs/ReportEventDialog.tsx
index af09e55ee6..7a2d51889f 100644
--- a/src/components/views/dialogs/ReportEventDialog.tsx
+++ b/src/components/views/dialogs/ReportEventDialog.tsx
@@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../../languageHandler';
import { ensureDMExists } from "../../../createRoom";
@@ -23,7 +24,6 @@ import { IDialogProps } from "./IDialogProps";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import SdkConfig from '../../../SdkConfig';
import Markdown from '../../../Markdown';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import SettingsStore from "../../../settings/SettingsStore";
import StyledRadioButton from "../elements/StyledRadioButton";
import BaseDialog from "./BaseDialog";
@@ -89,7 +89,6 @@ type Moderation = {
* a well-formed state event `m.room.moderation.moderated_by`
* /`org.matrix.msc3215.room.moderation.moderated_by`?
*/
-@replaceableComponent("views.dialogs.ReportEventDialog")
export default class ReportEventDialog extends React.Component {
// If the room supports moderation, the moderation information.
private moderation?: Moderation;
@@ -215,7 +214,7 @@ export default class ReportEventDialog extends React.Component {
try {
const client = MatrixClientPeg.get();
const ev = this.props.mxEvent;
- if (this.moderation && this.state.nature != NonStandardValue.Admin) {
+ if (this.moderation && this.state.nature !== NonStandardValue.Admin) {
const nature: Nature = this.state.nature;
// Report to moderators through to the dedicated bot,
@@ -235,6 +234,7 @@ export default class ReportEventDialog extends React.Component {
}
this.props.onFinished(true);
} catch (e) {
+ logger.error(e);
this.setState({
busy: false,
err: e.message,
diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx
index 3806e9288c..ce8d24cd3f 100644
--- a/src/components/views/dialogs/RoomSettingsDialog.tsx
+++ b/src/components/views/dialogs/RoomSettingsDialog.tsx
@@ -30,7 +30,6 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import { Action } from '../../../dispatcher/actions';
@@ -51,7 +50,6 @@ interface IState {
roomName: string;
}
-@replaceableComponent("views.dialogs.RoomSettingsDialog")
export default class RoomSettingsDialog extends React.Component {
private dispatcherRef: string;
diff --git a/src/components/views/dialogs/RoomUpgradeDialog.tsx b/src/components/views/dialogs/RoomUpgradeDialog.tsx
index bcca0e3829..8ff2181d75 100644
--- a/src/components/views/dialogs/RoomUpgradeDialog.tsx
+++ b/src/components/views/dialogs/RoomUpgradeDialog.tsx
@@ -19,7 +19,6 @@ import { Room } from "matrix-js-sdk/src/models/room";
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { upgradeRoom } from "../../../utils/RoomUpgrade";
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
@@ -35,7 +34,6 @@ interface IState {
busy: boolean;
}
-@replaceableComponent("views.dialogs.RoomUpgradeDialog")
export default class RoomUpgradeDialog extends React.Component {
private targetVersion: string;
diff --git a/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx b/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx
index 0da8fb97d3..a34674ff63 100644
--- a/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx
+++ b/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx
@@ -23,7 +23,6 @@ import SdkConfig from "../../../SdkConfig";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IDialogProps } from "./IDialogProps";
import BugReportDialog from './BugReportDialog';
import BaseDialog from "./BaseDialog";
@@ -50,7 +49,6 @@ interface IState {
total?: number;
}
-@replaceableComponent("views.dialogs.RoomUpgradeWarningDialog")
export default class RoomUpgradeWarningDialog extends React.Component {
private readonly isPrivate: boolean;
private readonly currentVersion: string;
diff --git a/src/components/views/dialogs/ServerOfflineDialog.tsx b/src/components/views/dialogs/ServerOfflineDialog.tsx
index 6cf07f6bd6..de068d9a79 100644
--- a/src/components/views/dialogs/ServerOfflineDialog.tsx
+++ b/src/components/views/dialogs/ServerOfflineDialog.tsx
@@ -29,12 +29,10 @@ import AccessibleButton from "../elements/AccessibleButton";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { IDialogProps } from "./IDialogProps";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends IDialogProps {
}
-@replaceableComponent("views.dialogs.ServerOfflineDialog")
export default class ServerOfflineDialog extends React.PureComponent {
public componentDidMount() {
EchoStore.instance.on(UPDATE_EVENT, this.onEchosUpdated);
diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx
index b1433978dd..0761bb6b9d 100644
--- a/src/components/views/dialogs/ServerPickerDialog.tsx
+++ b/src/components/views/dialogs/ServerPickerDialog.tsx
@@ -27,7 +27,6 @@ import Field from "../elements/Field";
import StyledRadioButton from "../elements/StyledRadioButton";
import TextWithTooltip from "../elements/TextWithTooltip";
import withValidation, { IFieldState } from "../elements/Validation";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
title?: string;
@@ -40,7 +39,6 @@ interface IState {
otherHomeserver: string;
}
-@replaceableComponent("views.dialogs.ServerPickerDialog")
export default class ServerPickerDialog extends React.PureComponent {
private readonly defaultServer: ValidatedServerConfig;
private readonly fieldRef = createRef();
diff --git a/src/components/views/dialogs/SeshatResetDialog.tsx b/src/components/views/dialogs/SeshatResetDialog.tsx
index 1c048dd9fa..369106188b 100644
--- a/src/components/views/dialogs/SeshatResetDialog.tsx
+++ b/src/components/views/dialogs/SeshatResetDialog.tsx
@@ -17,12 +17,10 @@ limitations under the License.
import React from 'react';
import { _t } from "../../../languageHandler";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import { IDialogProps } from "./IDialogProps";
-@replaceableComponent("views.dialogs.SeshatResetDialog")
export default class SeshatResetDialog extends React.PureComponent {
render() {
return (
diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.tsx b/src/components/views/dialogs/SessionRestoreErrorDialog.tsx
index ad0c8e939a..72b35671ba 100644
--- a/src/components/views/dialogs/SessionRestoreErrorDialog.tsx
+++ b/src/components/views/dialogs/SessionRestoreErrorDialog.tsx
@@ -21,7 +21,6 @@ import React from 'react';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import QuestionDialog from "./QuestionDialog";
import BugReportDialog from "./BugReportDialog";
import BaseDialog from "./BaseDialog";
@@ -32,7 +31,6 @@ interface IProps extends IDialogProps {
error: Error;
}
-@replaceableComponent("views.dialogs.SessionRestoreErrorDialog")
export default class SessionRestoreErrorDialog extends React.Component {
private sendBugReport = (): void => {
Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {
diff --git a/src/components/views/dialogs/SetEmailDialog.tsx b/src/components/views/dialogs/SetEmailDialog.tsx
index 6e5f0cf050..773f56db02 100644
--- a/src/components/views/dialogs/SetEmailDialog.tsx
+++ b/src/components/views/dialogs/SetEmailDialog.tsx
@@ -22,7 +22,6 @@ import * as Email from '../../../email';
import AddThreepid from '../../../AddThreepid';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from "../elements/Spinner";
import ErrorDialog from "./ErrorDialog";
import QuestionDialog from "./QuestionDialog";
@@ -44,7 +43,6 @@ interface IState {
*
* On success, `onFinished(true)` is called.
*/
-@replaceableComponent("views.dialogs.SetEmailDialog")
export default class SetEmailDialog extends React.Component {
private addThreepid: AddThreepid;
diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx
index a411c18571..cb06d09d80 100644
--- a/src/components/views/dialogs/ShareDialog.tsx
+++ b/src/components/views/dialogs/ShareDialog.tsx
@@ -29,7 +29,6 @@ import StyledCheckbox from '../elements/StyledCheckbox';
import { IDialogProps } from "./IDialogProps";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import CopyableText from "../elements/CopyableText";
@@ -71,7 +70,6 @@ interface IState {
permalinkCreator: RoomPermalinkCreator;
}
-@replaceableComponent("views.dialogs.ShareDialog")
export default class ShareDialog extends React.PureComponent {
protected closeCopiedTooltip: () => void;
diff --git a/src/components/views/dialogs/SpacePreferencesDialog.tsx b/src/components/views/dialogs/SpacePreferencesDialog.tsx
index d8a1afa342..1d35aa30ef 100644
--- a/src/components/views/dialogs/SpacePreferencesDialog.tsx
+++ b/src/components/views/dialogs/SpacePreferencesDialog.tsx
@@ -26,10 +26,7 @@ import { useSettingValue } from "../../../hooks/useSettings";
import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel";
import RoomName from "../elements/RoomName";
-
-export enum SpacePreferenceTab {
- Appearance = "SPACE_PREFERENCE_APPEARANCE_TAB",
-}
+import { SpacePreferenceTab } from "../../../dispatcher/payloads/OpenSpacePreferencesPayload";
interface IProps extends IDialogProps {
space: Room;
diff --git a/src/components/views/dialogs/SpotlightDialog.tsx b/src/components/views/dialogs/SpotlightDialog.tsx
index 7568e45f0f..f579e26287 100644
--- a/src/components/views/dialogs/SpotlightDialog.tsx
+++ b/src/components/views/dialogs/SpotlightDialog.tsx
@@ -53,19 +53,18 @@ import DMRoomMap from "../../../utils/DMRoomMap";
import { mediaFromMxc } from "../../../customisations/Media";
import BaseAvatar from "../avatars/BaseAvatar";
import Spinner from "../elements/Spinner";
-import { roomContextDetailsText, spaceContextDetailsText } from "../../../Rooms";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import { Action } from "../../../dispatcher/actions";
import Modal from "../../../Modal";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
-import RoomViewStore from "../../../stores/RoomViewStore";
+import { RoomViewStore } from "../../../stores/RoomViewStore";
import { showStartChatInviteDialog } from "../../../RoomInvite";
import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel";
import NotificationBadge from "../rooms/NotificationBadge";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { BetaPill } from "../beta/BetaCard";
-import { UserTab } from "./UserSettingsDialog";
+import { UserTab } from "./UserTab";
import BetaFeedbackDialog from "./BetaFeedbackDialog";
import SdkConfig from "../../../SdkConfig";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
@@ -74,6 +73,7 @@ import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { PosthogAnalytics } from "../../../PosthogAnalytics";
import { getCachedRoomIDForAlias } from "../../../RoomAliasCache";
+import { roomContextDetailsText, spaceContextDetailsText } from "../../../utils/i18n-helpers";
const MAX_RECENT_SEARCHES = 10;
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
@@ -559,7 +559,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", onFinished }) =>
{ _t("Recently viewed") }
{ BreadcrumbsStore.instance.rooms
- .filter(r => r.roomId !== RoomViewStore.getRoomId())
+ .filter(r => r.roomId !== RoomViewStore.instance.getRoomId())
.map(room => (
{
private sendBugReport = (ev: React.MouseEvent): void => {
ev.preventDefault();
diff --git a/src/components/views/dialogs/TabbedIntegrationManagerDialog.tsx b/src/components/views/dialogs/TabbedIntegrationManagerDialog.tsx
index 7ab425a5f6..5a5d6e3822 100644
--- a/src/components/views/dialogs/TabbedIntegrationManagerDialog.tsx
+++ b/src/components/views/dialogs/TabbedIntegrationManagerDialog.tsx
@@ -22,7 +22,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import { dialogTermsInteractionCallback, TermsNotSignedError } from "../../../Terms";
import * as ScalarMessaging from "../../../ScalarMessaging";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IntegrationManagerInstance } from "../../../integrations/IntegrationManagerInstance";
import ScalarAuthClient from "../../../ScalarAuthClient";
import AccessibleButton from "../elements/AccessibleButton";
@@ -55,7 +54,6 @@ interface IState {
currentScalarClient: ScalarAuthClient;
}
-@replaceableComponent("views.dialogs.TabbedIntegrationManagerDialog")
export default class TabbedIntegrationManagerDialog extends React.Component {
constructor(props: IProps) {
super(props);
diff --git a/src/components/views/dialogs/TermsDialog.tsx b/src/components/views/dialogs/TermsDialog.tsx
index f2f713a75f..aa3825ab3d 100644
--- a/src/components/views/dialogs/TermsDialog.tsx
+++ b/src/components/views/dialogs/TermsDialog.tsx
@@ -19,7 +19,6 @@ import React from 'react';
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
import { _t, pickBestLanguage } from '../../../languageHandler';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "./BaseDialog";
@@ -66,7 +65,6 @@ interface IState {
agreedUrls: any;
}
-@replaceableComponent("views.dialogs.TermsDialog")
export default class TermsDialog extends React.PureComponent {
constructor(props) {
super(props);
diff --git a/src/components/views/dialogs/TextInputDialog.tsx b/src/components/views/dialogs/TextInputDialog.tsx
index 2667c131b9..7a13fd3d45 100644
--- a/src/components/views/dialogs/TextInputDialog.tsx
+++ b/src/components/views/dialogs/TextInputDialog.tsx
@@ -18,7 +18,6 @@ import React, { ChangeEvent, createRef } from 'react';
import Field from "../elements/Field";
import { _t, _td } from '../../../languageHandler';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IFieldState, IValidationResult } from "../elements/Validation";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
@@ -43,7 +42,6 @@ interface IState {
valid: boolean;
}
-@replaceableComponent("views.dialogs.TextInputDialog")
export default class TextInputDialog extends React.Component {
private field = createRef();
diff --git a/src/components/views/dialogs/UploadConfirmDialog.tsx b/src/components/views/dialogs/UploadConfirmDialog.tsx
index 0b6c4d0add..c705da1ac3 100644
--- a/src/components/views/dialogs/UploadConfirmDialog.tsx
+++ b/src/components/views/dialogs/UploadConfirmDialog.tsx
@@ -19,7 +19,6 @@ import React from 'react';
import filesize from "filesize";
import { _t } from '../../../languageHandler';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { getBlobSafeMimeType } from '../../../utils/blobs';
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
@@ -31,7 +30,6 @@ interface IProps {
onFinished: (uploadConfirmed: boolean, uploadAll?: boolean) => void;
}
-@replaceableComponent("views.dialogs.UploadConfirmDialog")
export default class UploadConfirmDialog extends React.Component {
private readonly objectUrl: string;
private readonly mimeType: string;
diff --git a/src/components/views/dialogs/UploadFailureDialog.tsx b/src/components/views/dialogs/UploadFailureDialog.tsx
index 8226a82170..8a25671ea0 100644
--- a/src/components/views/dialogs/UploadFailureDialog.tsx
+++ b/src/components/views/dialogs/UploadFailureDialog.tsx
@@ -19,7 +19,6 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import ContentMessages from '../../../ContentMessages';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import { IDialogProps } from "./IDialogProps";
@@ -35,7 +34,6 @@ interface IProps extends IDialogProps {
* them. This is named fairly generically but the only thing we check right now is
* the size of the file.
*/
-@replaceableComponent("views.dialogs.UploadFailureDialog")
export default class UploadFailureDialog extends React.Component {
private onCancelClick = (): void => {
this.props.onFinished(false);
diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx
index ab25f689f2..8f5e1a6108 100644
--- a/src/components/views/dialogs/UserSettingsDialog.tsx
+++ b/src/components/views/dialogs/UserSettingsDialog.tsx
@@ -31,25 +31,11 @@ import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab";
import SdkConfig from "../../../SdkConfig";
import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
import { UIFeature } from "../../../settings/UIFeature";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import { IDialogProps } from "./IDialogProps";
import SidebarUserSettingsTab from "../settings/tabs/user/SidebarUserSettingsTab";
import KeyboardUserSettingsTab from "../settings/tabs/user/KeyboardUserSettingsTab";
-
-export enum UserTab {
- General = "USER_GENERAL_TAB",
- Appearance = "USER_APPEARANCE_TAB",
- Notifications = "USER_NOTIFICATIONS_TAB",
- Preferences = "USER_PREFERENCES_TAB",
- Keyboard = "USER_KEYBOARD_TAB",
- Sidebar = "USER_SIDEBAR_TAB",
- Voice = "USER_VOICE_TAB",
- Security = "USER_SECURITY_TAB",
- Labs = "USER_LABS_TAB",
- Mjolnir = "USER_MJOLNIR_TAB",
- Help = "USER_HELP_TAB",
-}
+import { UserTab } from "./UserTab";
interface IProps extends IDialogProps {
initialTabId?: UserTab;
@@ -59,7 +45,6 @@ interface IState {
mjolnirEnabled: boolean;
}
-@replaceableComponent("views.dialogs.UserSettingsDialog")
export default class UserSettingsDialog extends React.Component {
private mjolnirWatcher: string;
diff --git a/src/components/views/dialogs/UserTab.ts b/src/components/views/dialogs/UserTab.ts
new file mode 100644
index 0000000000..b5b2782c0d
--- /dev/null
+++ b/src/components/views/dialogs/UserTab.ts
@@ -0,0 +1,29 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+export enum UserTab {
+ General = "USER_GENERAL_TAB",
+ Appearance = "USER_APPEARANCE_TAB",
+ Notifications = "USER_NOTIFICATIONS_TAB",
+ Preferences = "USER_PREFERENCES_TAB",
+ Keyboard = "USER_KEYBOARD_TAB",
+ Sidebar = "USER_SIDEBAR_TAB",
+ Voice = "USER_VOICE_TAB",
+ Security = "USER_SECURITY_TAB",
+ Labs = "USER_LABS_TAB",
+ Mjolnir = "USER_MJOLNIR_TAB",
+ Help = "USER_HELP_TAB",
+}
diff --git a/src/components/views/dialogs/VerificationRequestDialog.tsx b/src/components/views/dialogs/VerificationRequestDialog.tsx
index f73cda91f4..8830ee29f7 100644
--- a/src/components/views/dialogs/VerificationRequestDialog.tsx
+++ b/src/components/views/dialogs/VerificationRequestDialog.tsx
@@ -20,7 +20,6 @@ import { User } from 'matrix-js-sdk/src/models/user';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import EncryptionPanel from "../right_panel/EncryptionPanel";
@@ -35,7 +34,6 @@ interface IState {
verificationRequest: VerificationRequest;
}
-@replaceableComponent("views.dialogs.VerificationRequestDialog")
export default class VerificationRequestDialog extends React.Component {
constructor(props) {
super(props);
diff --git a/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx b/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx
index b794592b2f..17be7d4150 100644
--- a/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx
+++ b/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx
@@ -32,7 +32,6 @@ import StyledCheckbox from "../elements/StyledCheckbox";
import DialogButtons from "../elements/DialogButtons";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import { CapabilityText } from "../../../widgets/CapabilityText";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends IDialogProps {
requestedCapabilities: Set;
@@ -50,7 +49,6 @@ interface IState {
rememberSelection: boolean;
}
-@replaceableComponent("views.dialogs.WidgetCapabilitiesPromptDialog")
export default class WidgetCapabilitiesPromptDialog extends React.PureComponent {
private eventPermissionsMap = new Map();
diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx
index 4faf4e5237..68c2991ed8 100644
--- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx
+++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx
@@ -22,7 +22,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import { OIDCState, WidgetPermissionStore } from "../../../stores/widgets/WidgetPermissionStore";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
@@ -37,7 +36,6 @@ interface IState {
rememberSelection: boolean;
}
-@replaceableComponent("views.dialogs.WidgetOpenIDPermissionsDialog")
export default class WidgetOpenIDPermissionsDialog extends React.PureComponent {
constructor(props: IProps) {
super(props);
diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx
index 090eac783e..ac7eea8e2b 100644
--- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx
+++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx
@@ -20,7 +20,6 @@ import React, { ChangeEvent, FormEvent } from 'react';
import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api";
import { logger } from "matrix-js-sdk/src/logger";
-import * as sdk from '../../../../index';
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import Field from '../../elements/Field';
import AccessibleButton from '../../elements/AccessibleButton';
@@ -28,6 +27,10 @@ import { _t } from '../../../../languageHandler';
import { IDialogProps } from "../IDialogProps";
import { accessSecretStorage } from "../../../../SecurityManager";
import Modal from "../../../../Modal";
+import InteractiveAuthDialog from "../InteractiveAuthDialog";
+import DialogButtons from "../../elements/DialogButtons";
+import BaseDialog from "../BaseDialog";
+import { chromeFileInputFix } from "../../../../utils/BrowserWorkarounds";
// Maximum acceptable size of a key file. It's 59 characters including the spaces we encode,
// so this should be plenty and allow for people putting extra whitespace in the file because
@@ -172,7 +175,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent) => {
+ private onPassPhraseNext = async (ev: FormEvent | React.MouseEvent) => {
ev.preventDefault();
if (this.state.passPhrase.length <= 0) return;
@@ -187,7 +190,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent) => {
+ private onRecoveryKeyNext = async (ev: FormEvent | React.MouseEvent) => {
ev.preventDefault();
if (!this.state.recoveryKeyValid) return;
@@ -230,8 +233,6 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
- // XXX: Making this an import breaks the app.
- const InteractiveAuthDialog = sdk.getComponent("views.dialogs.InteractiveAuthDialog");
const { finished } = Modal.createTrackedDialog(
'Cross-signing keys dialog', '', InteractiveAuthDialog,
{
@@ -273,10 +274,6 @@ export default class AccessSecretStorageDialog extends React.PureComponent
;
} else if (hasPassphrase && !this.state.forceRecoveryKey) {
- const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Security Phrase");
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_securePhraseTitle'];
@@ -408,6 +404,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent
diff --git a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx
index 3496d00fea..e5d2aee926 100644
--- a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx
+++ b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx
@@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import { _t } from "../../../../languageHandler";
-import { replaceableComponent } from "../../../../utils/replaceableComponent";
import BaseDialog from "../BaseDialog";
import DialogButtons from "../../elements/DialogButtons";
@@ -25,7 +24,6 @@ interface IProps {
onFinished: (success: boolean) => void;
}
-@replaceableComponent("views.dialogs.security.ConfirmDestroyCrossSigningDialog")
export default class ConfirmDestroyCrossSigningDialog extends React.Component {
private onConfirm = (): void => {
this.props.onFinished(true);
diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
index f60f8a9c19..ca5b1db9fb 100644
--- a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
+++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
@@ -27,7 +27,6 @@ import DialogButtons from '../../elements/DialogButtons';
import BaseDialog from '../BaseDialog';
import Spinner from '../../elements/Spinner';
import InteractiveAuthDialog from '../InteractiveAuthDialog';
-import { replaceableComponent } from "../../../../utils/replaceableComponent";
interface IProps {
accountPassword?: string;
@@ -46,7 +45,6 @@ interface IState {
* cases, only a spinner is shown, but for more complex auth like SSO, the user
* may need to complete some steps to proceed.
*/
-@replaceableComponent("views.dialogs.security.CreateCrossSigningDialog")
export default class CreateCrossSigningDialog extends React.PureComponent {
constructor(props: IProps) {
super(props);
diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx
index 3db78f6877..719c31c48f 100644
--- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx
+++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx
@@ -24,8 +24,11 @@ import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../SecurityManager';
-import * as sdk from '../../../../index';
import { IDialogProps } from "../IDialogProps";
+import Spinner from '../../elements/Spinner';
+import DialogButtons from "../../elements/DialogButtons";
+import AccessibleButton from "../../elements/AccessibleButton";
+import BaseDialog from "../BaseDialog";
enum RestoreType {
Passphrase = "passphrase",
@@ -298,12 +301,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
private store: SetupEncryptionStore;
diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx
index bc96bfe708..3db6d0dfb0 100644
--- a/src/components/views/elements/AccessibleButton.tsx
+++ b/src/components/views/elements/AccessibleButton.tsx
@@ -53,6 +53,7 @@ interface IProps extends React.InputHTMLAttributes {
tabIndex?: number;
disabled?: boolean;
className?: string;
+ triggerOnMouseDown?: boolean;
onClick(e?: ButtonEvent): void | Promise;
}
@@ -78,13 +79,18 @@ export default function AccessibleButton({
className,
onKeyDown,
onKeyUp,
+ triggerOnMouseDown,
...restProps
}: IProps) {
const newProps: IAccessibleButtonProps = restProps;
if (disabled) {
newProps["aria-disabled"] = true;
} else {
- newProps.onClick = onClick;
+ if (triggerOnMouseDown) {
+ newProps.onMouseDown = onClick;
+ } else {
+ newProps.onClick = onClick;
+ }
// We need to consume enter onKeyDown and space onKeyUp
// otherwise we are risking also activating other keyboard focusable elements
// that might receive focus as a result of the AccessibleButtonClick action
diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx
index be3bbcfdb9..f0be8ba12d 100644
--- a/src/components/views/elements/AccessibleTooltipButton.tsx
+++ b/src/components/views/elements/AccessibleTooltipButton.tsx
@@ -19,12 +19,11 @@ import React, { SyntheticEvent } from 'react';
import AccessibleButton from "./AccessibleButton";
import Tooltip, { Alignment } from './Tooltip';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps extends React.ComponentProps {
title: string;
tooltip?: React.ReactNode;
- label?: React.ReactNode;
+ label?: string;
tooltipClassName?: string;
forceHide?: boolean;
yOffset?: number;
@@ -36,7 +35,6 @@ interface IState {
hover: boolean;
}
-@replaceableComponent("views.elements.AccessibleTooltipButton")
export default class AccessibleTooltipButton extends React.PureComponent {
constructor(props: IProps) {
super(props);
diff --git a/src/components/views/elements/ActionButton.tsx b/src/components/views/elements/ActionButton.tsx
index 9b1484918f..9d8038f99e 100644
--- a/src/components/views/elements/ActionButton.tsx
+++ b/src/components/views/elements/ActionButton.tsx
@@ -19,7 +19,6 @@ import React from 'react';
import AccessibleButton from './AccessibleButton';
import dis from '../../../dispatcher/dispatcher';
import Analytics from '../../../Analytics';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import Tooltip from './Tooltip';
interface IProps {
@@ -37,7 +36,6 @@ interface IState {
showTooltip: boolean;
}
-@replaceableComponent("views.elements.ActionButton")
export default class ActionButton extends React.Component {
static defaultProps: Partial = {
size: "25",
diff --git a/src/components/views/elements/AddressSelector.tsx b/src/components/views/elements/AddressSelector.tsx
index f1203fbee5..a084e69098 100644
--- a/src/components/views/elements/AddressSelector.tsx
+++ b/src/components/views/elements/AddressSelector.tsx
@@ -18,7 +18,6 @@ limitations under the License.
import React, { createRef } from 'react';
import classNames from 'classnames';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IUserAddress } from '../../../UserAddress';
import AddressTile from './AddressTile';
@@ -41,7 +40,6 @@ interface IState {
hover: boolean;
}
-@replaceableComponent("views.elements.AddressSelector")
export default class AddressSelector extends React.Component {
private scrollElement = createRef();
private addressListElement = createRef();
diff --git a/src/components/views/elements/AddressTile.tsx b/src/components/views/elements/AddressTile.tsx
index d8738e69f3..8aafa0381b 100644
--- a/src/components/views/elements/AddressTile.tsx
+++ b/src/components/views/elements/AddressTile.tsx
@@ -19,7 +19,6 @@ import React from 'react';
import classNames from 'classnames';
import { _t } from '../../../languageHandler';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import { IUserAddress } from '../../../UserAddress';
import BaseAvatar from '../avatars/BaseAvatar';
@@ -33,7 +32,6 @@ interface IProps {
showAddress?: boolean;
}
-@replaceableComponent("views.elements.AddressTile")
export default class AddressTile extends React.Component {
static defaultProps: Partial = {
canDismiss: false,
diff --git a/src/components/views/elements/AppPermission.tsx b/src/components/views/elements/AppPermission.tsx
index 11bbd6204d..fd53981bca 100644
--- a/src/components/views/elements/AppPermission.tsx
+++ b/src/components/views/elements/AppPermission.tsx
@@ -24,7 +24,6 @@ import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import WidgetUtils from "../../../utils/WidgetUtils";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import MemberAvatar from '../avatars/MemberAvatar';
import BaseAvatar from '../avatars/BaseAvatar';
import AccessibleButton from './AccessibleButton';
@@ -44,7 +43,6 @@ interface IState {
widgetDomain: string;
}
-@replaceableComponent("views.elements.AppPermission")
export default class AppPermission extends React.Component {
static defaultProps: Partial = {
onPermissionGranted: () => {},
diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx
index 0a33b23908..e9e8c9474a 100644
--- a/src/components/views/elements/AppTile.tsx
+++ b/src/components/views/elements/AppTile.tsx
@@ -39,17 +39,17 @@ import { StopGapWidget } from "../../../stores/widgets/StopGapWidget";
import { ElementWidgetActions } from "../../../stores/widgets/ElementWidgetActions";
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
import WidgetAvatar from "../avatars/WidgetAvatar";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import CallHandler from '../../../CallHandler';
import { IApp } from "../../../stores/WidgetStore";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { OwnProfileStore } from '../../../stores/OwnProfileStore';
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
-import RoomViewStore from '../../../stores/RoomViewStore';
+import { RoomViewStore } from '../../../stores/RoomViewStore';
import WidgetUtils from '../../../utils/WidgetUtils';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { ActionPayload } from "../../../dispatcher/payloads";
import { Action } from '../../../dispatcher/actions';
+import { ElementWidgetCapabilities } from '../../../stores/widgets/ElementWidgetCapabilities';
interface IProps {
app: IApp;
@@ -101,7 +101,6 @@ interface IState {
requiresClient: boolean;
}
-@replaceableComponent("views.elements.AppTile")
export default class AppTile extends React.Component {
public static contextType = MatrixClientContext;
context: ContextType;
@@ -197,7 +196,7 @@ export default class AppTile extends React.Component {
);
if (isActiveWidget) {
// We just left the room that the active widget was from.
- if (this.props.room && RoomViewStore.getRoomId() !== this.props.room.roomId) {
+ if (this.props.room && RoomViewStore.instance.getRoomId() !== this.props.room.roomId) {
// If we are not actively looking at the room then destroy this widget entirely.
this.endWidgetActions();
} else if (WidgetType.JITSI.matches(this.props.app.type)) {
@@ -432,7 +431,7 @@ export default class AppTile extends React.Component {
private onWidgetCapabilitiesNotified = (): void => {
this.setState({
- requiresClient: this.sgWidget.widgetApi.hasCapability(MatrixCapabilities.RequiresClient),
+ requiresClient: this.sgWidget.widgetApi.hasCapability(ElementWidgetCapabilities.RequiresClient),
});
};
diff --git a/src/components/views/elements/CopyableText.tsx b/src/components/views/elements/CopyableText.tsx
index d1632af382..4ed3a7a58c 100644
--- a/src/components/views/elements/CopyableText.tsx
+++ b/src/components/views/elements/CopyableText.tsx
@@ -16,6 +16,7 @@ limitations under the License.
*/
import React, { useState } from "react";
+import classNames from "classnames";
import { _t } from "../../../languageHandler";
import { copyPlaintext } from "../../../utils/strings";
@@ -25,9 +26,10 @@ import AccessibleTooltipButton from "./AccessibleTooltipButton";
interface IProps {
children: React.ReactNode;
getTextToCopy: () => string;
+ border?: boolean;
}
-const CopyableText: React.FC = ({ children, getTextToCopy }) => {
+const CopyableText: React.FC = ({ children, getTextToCopy, border=true }) => {
const [tooltip, setTooltip] = useState(undefined);
const onCopyClickInternal = async (e: ButtonEvent) => {
@@ -42,7 +44,11 @@ const CopyableText: React.FC = ({ children, getTextToCopy }) => {
}
};
- return
+ const className = classNames("mx_CopyableText", {
+ mx_CopyableText_border: border,
+ });
+
+ return
{ children }
void;
+ // onClick handler for the primary button. Note that the returned promise, if
+ // returning a promise, is not used.
+ onPrimaryButtonClick?: (ev: React.MouseEvent) => (void | Promise);
// should there be a cancel button? default: true
hasCancel?: boolean;
@@ -61,7 +61,6 @@ interface IProps {
/**
* Basic container for buttons in modal dialogs.
*/
-@replaceableComponent("views.elements.DialogButtons")
export default class DialogButtons extends React.Component {
public static defaultProps: Partial = {
hasCancel: true,
diff --git a/src/components/views/elements/DirectorySearchBox.tsx b/src/components/views/elements/DirectorySearchBox.tsx
index 015065f45e..d4f20817e1 100644
--- a/src/components/views/elements/DirectorySearchBox.tsx
+++ b/src/components/views/elements/DirectorySearchBox.tsx
@@ -17,7 +17,6 @@ limitations under the License.
import React, { ChangeEvent, createRef } from 'react';
import { _t } from '../../../languageHandler';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleButton from "./AccessibleButton";
interface IProps {
@@ -34,7 +33,6 @@ interface IState {
value: string;
}
-@replaceableComponent("views.elements.DirectorySearchBox")
export default class DirectorySearchBox extends React.Component {
private input = createRef();
diff --git a/src/components/views/elements/Draggable.tsx b/src/components/views/elements/Draggable.tsx
index e9e24a57e4..a6eb8323f3 100644
--- a/src/components/views/elements/Draggable.tsx
+++ b/src/components/views/elements/Draggable.tsx
@@ -16,8 +16,6 @@ limitations under the License.
import React from 'react';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
-
interface IProps {
className: string;
dragFunc: (currentLocation: ILocationState, event: MouseEvent) => ILocationState;
@@ -35,7 +33,6 @@ export interface ILocationState {
currentY: number;
}
-@replaceableComponent("views.elements.Draggable")
export default class Draggable extends React.Component {
constructor(props: IProps) {
super(props);
diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx
index 9f071cafa9..4f572875a5 100644
--- a/src/components/views/elements/Dropdown.tsx
+++ b/src/components/views/elements/Dropdown.tsx
@@ -20,7 +20,6 @@ import classnames from 'classnames';
import AccessibleButton, { ButtonEvent } from './AccessibleButton';
import { _t } from '../../../languageHandler';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
@@ -107,7 +106,6 @@ interface IState {
* but somewhat simpler as react-select is 79KB of minified
* javascript.
*/
-@replaceableComponent("views.elements.Dropdown")
export default class Dropdown extends React.Component {
private readonly buttonRef = createRef();
private dropdownRootElement: HTMLDivElement = null;
diff --git a/src/components/views/elements/EditableItemList.tsx b/src/components/views/elements/EditableItemList.tsx
index a6d1d3f06f..d33990b570 100644
--- a/src/components/views/elements/EditableItemList.tsx
+++ b/src/components/views/elements/EditableItemList.tsx
@@ -19,7 +19,6 @@ import React from "react";
import { _t } from '../../../languageHandler';
import Field from "./Field";
import AccessibleButton from "./AccessibleButton";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IItemProps {
index?: number;
@@ -107,7 +106,6 @@ interface IProps {
onNewItemChanged?(item: string): void;
}
-@replaceableComponent("views.elements.EditableItemList")
export default class EditableItemList extends React.PureComponent {
protected onItemAdded = (e) => {
e.stopPropagation();
diff --git a/src/components/views/elements/EditableText.tsx b/src/components/views/elements/EditableText.tsx
index 8a9e4ed91a..344db02c3d 100644
--- a/src/components/views/elements/EditableText.tsx
+++ b/src/components/views/elements/EditableText.tsx
@@ -19,7 +19,6 @@ import React, { createRef } from 'react';
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
enum Phases {
Display = "display",
@@ -45,7 +44,6 @@ interface IState {
phase: Phases;
}
-@replaceableComponent("views.elements.EditableText")
export default class EditableText extends React.Component {
// we track value as an JS object field rather than in React state
// as React doesn't play nice with contentEditable.
diff --git a/src/components/views/elements/EditableTextContainer.tsx b/src/components/views/elements/EditableTextContainer.tsx
index ede1e8a655..d452cf10f0 100644
--- a/src/components/views/elements/EditableTextContainer.tsx
+++ b/src/components/views/elements/EditableTextContainer.tsx
@@ -16,7 +16,6 @@ limitations under the License.
import React from 'react';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from "./Spinner";
import EditableText from "./EditableText";
@@ -56,7 +55,6 @@ interface IState {
* similarly asynchronous way. If this is not provided, the initial value is
* taken from the 'initialValue' property.
*/
-@replaceableComponent("views.elements.EditableTextContainer")
export default class EditableTextContainer extends React.Component {
private unmounted = false;
public static defaultProps: Partial = {
diff --git a/src/components/views/elements/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx
index 5115d130cd..c405d0c9a6 100644
--- a/src/components/views/elements/EffectsOverlay.tsx
+++ b/src/components/views/elements/EffectsOverlay.tsx
@@ -55,7 +55,7 @@ const EffectsOverlay: FunctionComponent = ({ roomWidth }) => {
const onAction = (payload: { action: string }) => {
const actionPrefix = 'effects.';
if (payload.action.indexOf(actionPrefix) === 0) {
- const effect = payload.action.substr(actionPrefix.length);
+ const effect = payload.action.slice(actionPrefix.length);
lazyLoadEffectModule(effect).then((module) => module?.start(canvasRef.current));
}
};
diff --git a/src/components/views/elements/ErrorBoundary.tsx b/src/components/views/elements/ErrorBoundary.tsx
index 45fb9eafd4..91e56b82be 100644
--- a/src/components/views/elements/ErrorBoundary.tsx
+++ b/src/components/views/elements/ErrorBoundary.tsx
@@ -22,7 +22,6 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg';
import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import BugReportDialog from '../dialogs/BugReportDialog';
import AccessibleButton from './AccessibleButton';
@@ -34,7 +33,6 @@ interface IState {
* This error boundary component can be used to wrap large content areas and
* catch exceptions during rendering in the component tree below them.
*/
-@replaceableComponent("views.elements.ErrorBoundary")
export default class ErrorBoundary extends React.PureComponent<{}, IState> {
constructor(props) {
super(props);
diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx
index 748c3b353b..1fc99c774a 100644
--- a/src/components/views/elements/EventListSummary.tsx
+++ b/src/components/views/elements/EventListSummary.tsx
@@ -25,7 +25,6 @@ import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import { isValid3pidInvite } from "../../../RoomInvite";
import GenericEventListSummary from "./GenericEventListSummary";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
import { jsxJoin } from '../../../utils/ReactUtils';
import { Layout } from '../../../settings/enums/Layout';
@@ -79,7 +78,6 @@ enum TransitionType {
const SEP = ",";
-@replaceableComponent("views.elements.EventListSummary")
export default class EventListSummary extends React.Component {
static contextType = RoomContext;
public context!: React.ContextType;
diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
index 069a743d7f..86f8eeaf20 100644
--- a/src/components/views/elements/EventTilePreview.tsx
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -22,7 +22,6 @@ import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import * as Avatar from '../../../Avatar';
import EventTile from '../rooms/EventTile';
import { Layout } from "../../../settings/enums/Layout";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from './Spinner';
interface IProps {
@@ -63,7 +62,6 @@ interface IState {
const AVATAR_SIZE = 32;
-@replaceableComponent("views.elements.EventTilePreview")
export default class EventTilePreview extends React.Component {
constructor(props: IProps) {
super(props);
diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx
index 578ecb3333..3942a9eb1d 100644
--- a/src/components/views/elements/Field.tsx
+++ b/src/components/views/elements/Field.tsx
@@ -18,9 +18,9 @@ import React, { InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttribute
import classNames from 'classnames';
import { debounce } from "lodash";
-import * as sdk from '../../../index';
import { IFieldState, IValidationResult } from "./Validation";
import { ComponentClass } from "../../../@types/common";
+import Tooltip from "./Tooltip";
// Invoke validation from user input (when typing, etc.) at most once every N ms.
const VALIDATION_THROTTLE_MS = 200;
@@ -295,8 +295,6 @@ export default class Field extends React.PureComponent {
);
// Handle displaying feedback on validity
- // FIXME: Using an import will result in test failures
- const Tooltip = sdk.getComponent("elements.Tooltip");
let fieldTooltip;
if (tooltipContent || this.state.feedback) {
fieldTooltip = {
constructor(props: IProps) {
super(props);
diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx
index df9b128885..fd24bc745a 100644
--- a/src/components/views/elements/ImageView.tsx
+++ b/src/components/views/elements/ImageView.tsx
@@ -31,7 +31,6 @@ import SettingsStore from "../../../settings/SettingsStore";
import { formatFullDate } from "../../../DateUtils";
import dis from '../../../dispatcher/dispatcher';
import { Action } from '../../../dispatcher/actions';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { normalizeWheelEvent } from "../../../utils/Mouse";
import { IDialogProps } from '../dialogs/IDialogProps';
@@ -90,7 +89,6 @@ interface IState {
contextMenuDisplayed: boolean;
}
-@replaceableComponent("views.elements.ImageView")
export default class ImageView extends React.Component {
constructor(props) {
super(props);
diff --git a/src/components/views/elements/InfoTooltip.tsx b/src/components/views/elements/InfoTooltip.tsx
index f77a07bd21..d34a6f9bb0 100644
--- a/src/components/views/elements/InfoTooltip.tsx
+++ b/src/components/views/elements/InfoTooltip.tsx
@@ -20,7 +20,6 @@ import classNames from 'classnames';
import { Alignment } from './Tooltip';
import { _t } from "../../../languageHandler";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import TooltipTarget from './TooltipTarget';
export enum InfoTooltipKind {
@@ -35,7 +34,6 @@ interface ITooltipProps {
kind?: InfoTooltipKind;
}
-@replaceableComponent("views.elements.InfoTooltip")
export default class InfoTooltip extends React.PureComponent {
constructor(props: ITooltipProps) {
super(props);
diff --git a/src/components/views/elements/InlineSpinner.tsx b/src/components/views/elements/InlineSpinner.tsx
index 3a404430db..385e317cda 100644
--- a/src/components/views/elements/InlineSpinner.tsx
+++ b/src/components/views/elements/InlineSpinner.tsx
@@ -17,7 +17,6 @@ limitations under the License.
import React from "react";
import { _t } from "../../../languageHandler";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
w?: number;
@@ -25,7 +24,6 @@ interface IProps {
children?: React.ReactNode;
}
-@replaceableComponent("views.elements.InlineSpinner")
export default class InlineSpinner extends React.PureComponent {
static defaultProps = {
w: 16,
diff --git a/src/components/views/elements/InviteReason.tsx b/src/components/views/elements/InviteReason.tsx
index 13a62d463c..d3f7e34031 100644
--- a/src/components/views/elements/InviteReason.tsx
+++ b/src/components/views/elements/InviteReason.tsx
@@ -19,7 +19,6 @@ import React from "react";
import { sanitizedHtmlNode } from "../../../HtmlUtils";
import { _t } from "../../../languageHandler";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
reason: string;
@@ -30,7 +29,6 @@ interface IState {
hidden: boolean;
}
-@replaceableComponent("views.elements.InviteReason")
export default class InviteReason extends React.PureComponent {
constructor(props) {
super(props);
diff --git a/src/components/views/elements/LabelledToggleSwitch.tsx b/src/components/views/elements/LabelledToggleSwitch.tsx
index 24647df502..952c92ac42 100644
--- a/src/components/views/elements/LabelledToggleSwitch.tsx
+++ b/src/components/views/elements/LabelledToggleSwitch.tsx
@@ -17,7 +17,6 @@ limitations under the License.
import React from "react";
import ToggleSwitch from "./ToggleSwitch";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
// The value for the toggle switch
@@ -35,7 +34,6 @@ interface IProps {
onChange(checked: boolean): void;
}
-@replaceableComponent("views.elements.LabelledToggleSwitch")
export default class LabelledToggleSwitch extends React.PureComponent {
render() {
// This is a minimal version of a SettingsFlag
diff --git a/src/components/views/elements/LanguageDropdown.tsx b/src/components/views/elements/LanguageDropdown.tsx
index c6c52ee4e8..7d19dbfce1 100644
--- a/src/components/views/elements/LanguageDropdown.tsx
+++ b/src/components/views/elements/LanguageDropdown.tsx
@@ -20,7 +20,6 @@ import React from 'react';
import * as languageHandler from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from "./Spinner";
import Dropdown from "./Dropdown";
@@ -42,7 +41,6 @@ interface IState {
langs: string[];
}
-@replaceableComponent("views.elements.LanguageDropdown")
export default class LanguageDropdown extends React.Component {
constructor(props: IProps) {
super(props);
diff --git a/src/components/views/elements/LazyRenderList.tsx b/src/components/views/elements/LazyRenderList.tsx
index 6a1920bec9..d573a38104 100644
--- a/src/components/views/elements/LazyRenderList.tsx
+++ b/src/components/views/elements/LazyRenderList.tsx
@@ -16,8 +16,6 @@ limitations under the License.
import React from "react";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
-
class ItemRange {
constructor(
public topCount: number,
@@ -84,7 +82,6 @@ interface IState {
renderRange: ItemRange;
}
-@replaceableComponent("views.elements.LazyRenderList")
export default class LazyRenderList extends React.Component, IState> {
public static defaultProps: Partial> = {
overflowItems: 20,
diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx
index a590427d23..43e66db09c 100644
--- a/src/components/views/elements/MiniAvatarUploader.tsx
+++ b/src/components/views/elements/MiniAvatarUploader.tsx
@@ -25,6 +25,7 @@ import { useTimeout } from "../../../hooks/useTimeout";
import Analytics from "../../../Analytics";
import { TranslatedString } from '../../../languageHandler';
import RoomContext from "../../../contexts/RoomContext";
+import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
export const AVATAR_SIZE = 52;
@@ -62,6 +63,7 @@ const MiniAvatarUploader: React.FC = ({ hasAvatar, hasAvatarLabel, noAva
type="file"
ref={uploadRef}
className="mx_MiniAvatarUploader_input"
+ onClick={chromeFileInputFix}
onChange={async (ev) => {
if (!ev.target.files?.length) return;
setBusy(true);
diff --git a/src/components/views/elements/PersistedElement.tsx b/src/components/views/elements/PersistedElement.tsx
index 97a197d2bf..cd8239a1f1 100644
--- a/src/components/views/elements/PersistedElement.tsx
+++ b/src/components/views/elements/PersistedElement.tsx
@@ -22,7 +22,6 @@ import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import dis from '../../../dispatcher/dispatcher';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ActionPayload } from "../../../dispatcher/payloads";
export const getPersistKey = (appId: string) => 'widget_' + appId;
@@ -70,7 +69,6 @@ interface IProps {
* children are made visible and are positioned into a div that is given the same
* bounding rect as the parent of PE.
*/
-@replaceableComponent("views.elements.PersistedElement")
export default class PersistedElement extends React.Component {
private resizeObserver: ResizeObserver;
private dispatcherRef: string;
diff --git a/src/components/views/elements/PersistentApp.tsx b/src/components/views/elements/PersistentApp.tsx
index fd78237bfb..5851c1c614 100644
--- a/src/components/views/elements/PersistentApp.tsx
+++ b/src/components/views/elements/PersistentApp.tsx
@@ -19,7 +19,6 @@ import React, { ContextType } from 'react';
import { Room } from "matrix-js-sdk/src/models/room";
import WidgetUtils from '../../../utils/WidgetUtils';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import AppTile from "./AppTile";
import { IApp } from '../../../stores/WidgetStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
@@ -30,7 +29,6 @@ interface IProps {
pointerEvents?: string;
}
-@replaceableComponent("views.elements.PersistentApp")
export default class PersistentApp extends React.Component {
public static contextType = MatrixClientContext;
context: ContextType;
diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js
index 610135ef99..7d5a9973c7 100644
--- a/src/components/views/elements/Pill.js
+++ b/src/components/views/elements/Pill.js
@@ -20,16 +20,15 @@ import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import PropTypes from 'prop-types';
import { logger } from "matrix-js-sdk/src/logger";
-import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { getPrimaryPermalinkEntity, parsePermalink } from "../../../utils/permalinks/Permalinks";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { Action } from "../../../dispatcher/actions";
import Tooltip from './Tooltip';
-import { replaceableComponent } from "../../../utils/replaceableComponent";
+import RoomAvatar from "../avatars/RoomAvatar";
+import MemberAvatar from "../avatars/MemberAvatar";
-@replaceableComponent("views.elements.Pill")
class Pill extends React.Component {
static roomNotifPos(text) {
return text.indexOf("@room");
@@ -183,9 +182,6 @@ class Pill extends React.Component {
};
render() {
- const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
- const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
-
const resource = this.state.resourceId;
let avatar = null;
diff --git a/src/components/views/elements/PowerSelector.tsx b/src/components/views/elements/PowerSelector.tsx
index 2cf028e225..3a9e87d158 100644
--- a/src/components/views/elements/PowerSelector.tsx
+++ b/src/components/views/elements/PowerSelector.tsx
@@ -19,7 +19,6 @@ import React from 'react';
import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler';
import Field from "./Field";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
@@ -55,7 +54,6 @@ interface IState {
customLevel?: number;
}
-@replaceableComponent("views.elements.PowerSelector")
export default class PowerSelector extends React.Component {
public static defaultProps: Partial = {
maxValue: Infinity,
diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx
index 6b8d7314ea..11498c5a55 100644
--- a/src/components/views/elements/ReplyChain.tsx
+++ b/src/components/views/elements/ReplyChain.tsx
@@ -29,7 +29,6 @@ import SettingsStore from "../../../settings/SettingsStore";
import { Layout } from "../../../settings/enums/Layout";
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
import { Action } from "../../../dispatcher/actions";
-import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from './Spinner';
import ReplyTile from "../rooms/ReplyTile";
import Pill from './Pill';
@@ -76,7 +75,6 @@ interface IState {
// 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
// be low as each event being loaded (after the first) is triggered by an explicit user action.
-@replaceableComponent("views.elements.ReplyChain")
export default class ReplyChain extends React.Component