Merge branch 'develop' into text-for-event-perf

This commit is contained in:
Robin Townsend 2021-07-11 11:35:12 -04:00
commit b147bcd207
238 changed files with 3504 additions and 2904 deletions

View file

@ -1,16 +0,0 @@
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
src/Markdown.js
src/NodeAnimator.js
src/components/structures/RoomDirectory.js
src/components/views/rooms/MemberList.js
src/ratelimitedfunc.js
src/utils/DMRoomMap.js
src/utils/MultiInviter.js
test/components/structures/MessagePanel-test.js
test/components/views/dialogs/InteractiveAuthDialog-test.js
test/mock-clock.js
src/component-index.js
test/end-to-end-tests/node_modules/
test/end-to-end-tests/element/
test/end-to-end-tests/synapse/

View file

@ -24,6 +24,18 @@ module.exports = {
// It's disabled here, but we should using it sparingly. // It's disabled here, but we should using it sparingly.
"react/jsx-no-bind": "off", "react/jsx-no-bind": "off",
"react/jsx-key": ["error"], "react/jsx-key": ["error"],
"no-restricted-properties": [
"error",
...buildRestrictedPropertiesOptions(
["window.innerHeight", "window.innerWidth", "window.visualViewport"],
"Use UIStore to access window dimensions instead.",
),
...buildRestrictedPropertiesOptions(
["*.mxcUrlToHttp", "*.getHttpUriForMxc"],
"Use Media helper instead to centralise access for customisation.",
),
],
}, },
overrides: [{ overrides: [{
files: [ files: [
@ -49,21 +61,16 @@ module.exports = {
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
// We'd rather not do this but we do // We'd rather not do this but we do
"@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-comment": "off",
"no-restricted-properties": [
"error",
...buildRestrictedPropertiesOptions(
["window.innerHeight", "window.innerWidth", "window.visualViewport"],
"Use UIStore to access window dimensions instead",
),
],
}, },
}], }],
}; };
function buildRestrictedPropertiesOptions(properties, message) { function buildRestrictedPropertiesOptions(properties, message) {
return properties.map(prop => { return properties.map(prop => {
const [object, property] = prop.split("."); let [object, property] = prop.split(".");
if (object === "*") {
object = undefined;
}
return { return {
object, object,
property, property,

View file

@ -1,3 +1,174 @@
Changes in [3.25.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.25.0) (2021-07-05)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.25.0-rc.1...v3.25.0)
* Remove reminescent references to the tinter
[\#6316](https://github.com/matrix-org/matrix-react-sdk/pull/6316)
* Update to released version of js-sdk
Changes in [3.25.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.25.0-rc.1) (2021-06-29)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.24.0...v3.25.0-rc.1)
* Update to js-sdk v12.0.1-rc.1
* Translations update from Weblate
[\#6286](https://github.com/matrix-org/matrix-react-sdk/pull/6286)
* Fix back button on user info card after clicking a permalink
[\#6277](https://github.com/matrix-org/matrix-react-sdk/pull/6277)
* Group ACLs with MELS
[\#6280](https://github.com/matrix-org/matrix-react-sdk/pull/6280)
* Fix editState not getting passed through
[\#6282](https://github.com/matrix-org/matrix-react-sdk/pull/6282)
* Migrate message context menu to IconizedContextMenu
[\#5671](https://github.com/matrix-org/matrix-react-sdk/pull/5671)
* Improve audio recording performance
[\#6240](https://github.com/matrix-org/matrix-react-sdk/pull/6240)
* Fix multiple timeline panels handling composer and edit events
[\#6278](https://github.com/matrix-org/matrix-react-sdk/pull/6278)
* Let m.notice messages mark a room as unread
[\#6281](https://github.com/matrix-org/matrix-react-sdk/pull/6281)
* Removes the override on the Bubble Container
[\#5953](https://github.com/matrix-org/matrix-react-sdk/pull/5953)
* Fix IRC layout regressions
[\#6193](https://github.com/matrix-org/matrix-react-sdk/pull/6193)
* Fix trashcan.svg by exporting it with its viewbox
[\#6248](https://github.com/matrix-org/matrix-react-sdk/pull/6248)
* Fix tiny scrollbar dot on chrome/electron in Forward Dialog
[\#6276](https://github.com/matrix-org/matrix-react-sdk/pull/6276)
* Upgrade puppeteer to use newer version of Chrome
[\#6268](https://github.com/matrix-org/matrix-react-sdk/pull/6268)
* Make toast dismiss button less prominent
[\#6275](https://github.com/matrix-org/matrix-react-sdk/pull/6275)
* Encrypt the voice message file if needed
[\#6269](https://github.com/matrix-org/matrix-react-sdk/pull/6269)
* Fix hyper-precise presence
[\#6270](https://github.com/matrix-org/matrix-react-sdk/pull/6270)
* Fix issues around private spaces, including previewable
[\#6265](https://github.com/matrix-org/matrix-react-sdk/pull/6265)
* Make _pinned messages_ in `m.room.pinned_events` event clickable
[\#6257](https://github.com/matrix-org/matrix-react-sdk/pull/6257)
* Fix space avatar management layout being broken
[\#6266](https://github.com/matrix-org/matrix-react-sdk/pull/6266)
* Convert EntityTile, MemberTile and PresenceLabel to TS
[\#6251](https://github.com/matrix-org/matrix-react-sdk/pull/6251)
* Fix UserInfo not working when rendered without a room
[\#6260](https://github.com/matrix-org/matrix-react-sdk/pull/6260)
* Update membership reason handling, including leave reason displaying
[\#6253](https://github.com/matrix-org/matrix-react-sdk/pull/6253)
* Consolidate types with js-sdk changes
[\#6220](https://github.com/matrix-org/matrix-react-sdk/pull/6220)
* Fix edit history modal
[\#6258](https://github.com/matrix-org/matrix-react-sdk/pull/6258)
* Convert MemberList to TS
[\#6249](https://github.com/matrix-org/matrix-react-sdk/pull/6249)
* Fix two PRs duplicating the css attribute
[\#6259](https://github.com/matrix-org/matrix-react-sdk/pull/6259)
* Improve invite error messages in InviteDialog for room invites
[\#6201](https://github.com/matrix-org/matrix-react-sdk/pull/6201)
* Fix invite dialog being cut off when it has limited results
[\#6256](https://github.com/matrix-org/matrix-react-sdk/pull/6256)
* Fix pinning event in a room which hasn't had events pinned in before
[\#6255](https://github.com/matrix-org/matrix-react-sdk/pull/6255)
* Allow modal widget buttons to be disabled when the modal opens
[\#6178](https://github.com/matrix-org/matrix-react-sdk/pull/6178)
* Decrease e2e shield fill mask size so that it doesn't overlap
[\#6250](https://github.com/matrix-org/matrix-react-sdk/pull/6250)
* Dial Pad UI bug fixes
[\#5786](https://github.com/matrix-org/matrix-react-sdk/pull/5786)
* Simple handling of mid-call output changes
[\#6247](https://github.com/matrix-org/matrix-react-sdk/pull/6247)
* Improve ForwardDialog performance by using TruncatedList
[\#6228](https://github.com/matrix-org/matrix-react-sdk/pull/6228)
* Fix dependency and lockfile mismatch
[\#6246](https://github.com/matrix-org/matrix-react-sdk/pull/6246)
* Improve room directory click behaviour
[\#6234](https://github.com/matrix-org/matrix-react-sdk/pull/6234)
* Fix keyboard accessibility of the space panel
[\#6239](https://github.com/matrix-org/matrix-react-sdk/pull/6239)
* Add ways to manage addresses for Spaces
[\#6151](https://github.com/matrix-org/matrix-react-sdk/pull/6151)
* Hide communities invites and the community autocompleter when Spaces on
[\#6244](https://github.com/matrix-org/matrix-react-sdk/pull/6244)
* Convert bunch of files to TS
[\#6241](https://github.com/matrix-org/matrix-react-sdk/pull/6241)
* Open local addresses section by default when there are no existing local
addresses
[\#6179](https://github.com/matrix-org/matrix-react-sdk/pull/6179)
* Allow reordering of the space panel via Drag and Drop
[\#6137](https://github.com/matrix-org/matrix-react-sdk/pull/6137)
* Replace drag and drop mechanism in communities with something simpler
[\#6134](https://github.com/matrix-org/matrix-react-sdk/pull/6134)
* EventTilePreview fixes
[\#6000](https://github.com/matrix-org/matrix-react-sdk/pull/6000)
* Upgrade @types/react and @types/react-dom
[\#6233](https://github.com/matrix-org/matrix-react-sdk/pull/6233)
* Fix type error in the SpaceStore
[\#6242](https://github.com/matrix-org/matrix-react-sdk/pull/6242)
* Add experimental options to the Spaces beta
[\#6199](https://github.com/matrix-org/matrix-react-sdk/pull/6199)
* Consolidate types with js-sdk changes
[\#6215](https://github.com/matrix-org/matrix-react-sdk/pull/6215)
* Fix branch matching for Buildkite
[\#6236](https://github.com/matrix-org/matrix-react-sdk/pull/6236)
* Migrate SearchBar to TypeScript
[\#6230](https://github.com/matrix-org/matrix-react-sdk/pull/6230)
* Add support to keyboard shortcuts dialog for [digits]
[\#6088](https://github.com/matrix-org/matrix-react-sdk/pull/6088)
* Fix modal opening race condition
[\#6238](https://github.com/matrix-org/matrix-react-sdk/pull/6238)
* Deprecate FormButton in favour of AccessibleButton
[\#6229](https://github.com/matrix-org/matrix-react-sdk/pull/6229)
* Add PR template
[\#6216](https://github.com/matrix-org/matrix-react-sdk/pull/6216)
* Prefer canonical aliases while autocompleting rooms
[\#6222](https://github.com/matrix-org/matrix-react-sdk/pull/6222)
* Fix quote button
[\#6232](https://github.com/matrix-org/matrix-react-sdk/pull/6232)
* Restore branch matching support for GitHub Actions e2e tests
[\#6224](https://github.com/matrix-org/matrix-react-sdk/pull/6224)
* Fix View Source accessing renamed private field on MatrixEvent
[\#6225](https://github.com/matrix-org/matrix-react-sdk/pull/6225)
* Fix ConfirmUserActionDialog returning an input field rather than text
[\#6219](https://github.com/matrix-org/matrix-react-sdk/pull/6219)
* Revert "Partially restore immutable event objects at the rendering layer"
[\#6221](https://github.com/matrix-org/matrix-react-sdk/pull/6221)
* Add jq to e2e tests Dockerfile
[\#6218](https://github.com/matrix-org/matrix-react-sdk/pull/6218)
* Partially restore immutable event objects at the rendering layer
[\#6196](https://github.com/matrix-org/matrix-react-sdk/pull/6196)
* Update MSC number references for voice messages
[\#6197](https://github.com/matrix-org/matrix-react-sdk/pull/6197)
* Fix phase enum usage in JS modules as well
[\#6214](https://github.com/matrix-org/matrix-react-sdk/pull/6214)
* Migrate some dialogs to TypeScript
[\#6185](https://github.com/matrix-org/matrix-react-sdk/pull/6185)
* Typescript fixes due to MatrixEvent being TSified
[\#6208](https://github.com/matrix-org/matrix-react-sdk/pull/6208)
* Allow click-to-ping, quote & emoji picker for edit composer too
[\#5858](https://github.com/matrix-org/matrix-react-sdk/pull/5858)
* Add call silencing
[\#6082](https://github.com/matrix-org/matrix-react-sdk/pull/6082)
* Fix types in SlashCommands
[\#6207](https://github.com/matrix-org/matrix-react-sdk/pull/6207)
* Benchmark multiple common user scenario
[\#6190](https://github.com/matrix-org/matrix-react-sdk/pull/6190)
* Fix forward dialog message preview display names
[\#6204](https://github.com/matrix-org/matrix-react-sdk/pull/6204)
* Remove stray bullet point in reply preview
[\#6206](https://github.com/matrix-org/matrix-react-sdk/pull/6206)
* Stop requesting null next replies from the server
[\#6203](https://github.com/matrix-org/matrix-react-sdk/pull/6203)
* Fix soft crash caused by a broken shouldComponentUpdate
[\#6202](https://github.com/matrix-org/matrix-react-sdk/pull/6202)
* Keep composer reply when scrolling away from a highlighted event
[\#6200](https://github.com/matrix-org/matrix-react-sdk/pull/6200)
* Cache virtual/native room mappings when they're created
[\#6194](https://github.com/matrix-org/matrix-react-sdk/pull/6194)
* Disable comment-on-alert
[\#6191](https://github.com/matrix-org/matrix-react-sdk/pull/6191)
* Bump postcss from 7.0.35 to 7.0.36
[\#6195](https://github.com/matrix-org/matrix-react-sdk/pull/6195)
Changes in [3.24.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.24.0) (2021-06-21) Changes in [3.24.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.24.0) (2021-06-21)
===================================================================================================== =====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.24.0-rc.1...v3.24.0) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.24.0-rc.1...v3.24.0)

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "3.24.0", "version": "3.25.0",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -45,7 +45,7 @@
"start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn start:build\" \"yarn reskindex:watch\"", "start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn start:build\" \"yarn reskindex:watch\"",
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"", "start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"lint": "yarn lint:types && yarn lint:js && yarn lint:style", "lint": "yarn lint:types && yarn lint:js && yarn lint:style",
"lint:js": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test", "lint:js": "eslint --max-warnings 0 src test",
"lint:types": "tsc --noEmit --jsx react", "lint:types": "tsc --noEmit --jsx react",
"lint:style": "stylelint 'res/css/**/*.scss'", "lint:style": "stylelint 'res/css/**/*.scss'",
"test": "jest", "test": "jest",
@ -55,6 +55,7 @@
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"await-lock": "^2.1.0", "await-lock": "^2.1.0",
"blurhash": "^1.1.3",
"browser-encrypt-attachment": "^0.3.0", "browser-encrypt-attachment": "^0.3.0",
"browser-request": "^0.3.3", "browser-request": "^0.3.3",
"cheerio": "^1.0.0-rc.9", "cheerio": "^1.0.0-rc.9",
@ -78,7 +79,7 @@
"katex": "^0.12.0", "katex": "^0.12.0",
"linkifyjs": "^2.1.9", "linkifyjs": "^2.1.9",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"matrix-js-sdk": "12.0.0", "matrix-js-sdk": "12.0.1",
"matrix-widget-api": "^0.1.0-beta.15", "matrix-widget-api": "^0.1.0-beta.15",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"opus-recorder": "^8.0.3", "opus-recorder": "^8.0.3",
@ -90,6 +91,7 @@
"re-resizable": "^6.9.0", "re-resizable": "^6.9.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-blurhash": "^0.1.3",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-focus-lock": "^2.5.0", "react-focus-lock": "^2.5.0",
"react-transition-group": "^4.4.1", "react-transition-group": "^4.4.1",
@ -122,6 +124,7 @@
"@peculiar/webcrypto": "^1.1.4", "@peculiar/webcrypto": "^1.1.4",
"@sinonjs/fake-timers": "^7.0.2", "@sinonjs/fake-timers": "^7.0.2",
"@types/classnames": "^2.2.11", "@types/classnames": "^2.2.11",
"@types/commonmark": "^0.27.4",
"@types/counterpart": "^0.18.1", "@types/counterpart": "^0.18.1",
"@types/diff-match-patch": "^1.0.32", "@types/diff-match-patch": "^1.0.32",
"@types/flux": "^3.1.9", "@types/flux": "^3.1.9",

View file

@ -37,6 +37,11 @@
@import "./structures/_ViewSource.scss"; @import "./structures/_ViewSource.scss";
@import "./structures/auth/_CompleteSecurity.scss"; @import "./structures/auth/_CompleteSecurity.scss";
@import "./structures/auth/_Login.scss"; @import "./structures/auth/_Login.scss";
@import "./views/audio_messages/_AudioPlayer.scss";
@import "./views/audio_messages/_PlayPauseButton.scss";
@import "./views/audio_messages/_PlaybackContainer.scss";
@import "./views/audio_messages/_SeekBar.scss";
@import "./views/audio_messages/_Waveform.scss";
@import "./views/auth/_AuthBody.scss"; @import "./views/auth/_AuthBody.scss";
@import "./views/auth/_AuthButtons.scss"; @import "./views/auth/_AuthButtons.scss";
@import "./views/auth/_AuthFooter.scss"; @import "./views/auth/_AuthFooter.scss";
@ -52,7 +57,6 @@
@import "./views/avatars/_BaseAvatar.scss"; @import "./views/avatars/_BaseAvatar.scss";
@import "./views/avatars/_DecoratedRoomAvatar.scss"; @import "./views/avatars/_DecoratedRoomAvatar.scss";
@import "./views/avatars/_MemberStatusMessageAvatar.scss"; @import "./views/avatars/_MemberStatusMessageAvatar.scss";
@import "./views/avatars/_PulsedAvatar.scss";
@import "./views/avatars/_WidgetAvatar.scss"; @import "./views/avatars/_WidgetAvatar.scss";
@import "./views/beta/_BetaCard.scss"; @import "./views/beta/_BetaCard.scss";
@import "./views/context_menus/_CallContextMenu.scss"; @import "./views/context_menus/_CallContextMenu.scss";
@ -165,6 +169,7 @@
@import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MTextBody.scss";
@import "./views/messages/_MVideoBody.scss"; @import "./views/messages/_MVideoBody.scss";
@import "./views/messages/_MVoiceMessageBody.scss"; @import "./views/messages/_MVoiceMessageBody.scss";
@import "./views/messages/_MediaBody.scss";
@import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageActionBar.scss";
@import "./views/messages/_MessageTimestamp.scss"; @import "./views/messages/_MessageTimestamp.scss";
@import "./views/messages/_MjolnirBody.scss"; @import "./views/messages/_MjolnirBody.scss";
@ -196,6 +201,7 @@
@import "./views/rooms/_GroupLayout.scss"; @import "./views/rooms/_GroupLayout.scss";
@import "./views/rooms/_IRCLayout.scss"; @import "./views/rooms/_IRCLayout.scss";
@import "./views/rooms/_JumpToBottomButton.scss"; @import "./views/rooms/_JumpToBottomButton.scss";
@import "./views/rooms/_LinkPreviewGroup.scss";
@import "./views/rooms/_LinkPreviewWidget.scss"; @import "./views/rooms/_LinkPreviewWidget.scss";
@import "./views/rooms/_MemberInfo.scss"; @import "./views/rooms/_MemberInfo.scss";
@import "./views/rooms/_MemberList.scss"; @import "./views/rooms/_MemberList.scss";
@ -253,12 +259,10 @@
@import "./views/toasts/_AnalyticsToast.scss"; @import "./views/toasts/_AnalyticsToast.scss";
@import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/toasts/_NonUrgentEchoFailureToast.scss";
@import "./views/verification/_VerificationShowSas.scss"; @import "./views/verification/_VerificationShowSas.scss";
@import "./views/voice_messages/_PlayPauseButton.scss";
@import "./views/voice_messages/_PlaybackContainer.scss";
@import "./views/voice_messages/_Waveform.scss";
@import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallContainer.scss";
@import "./views/voip/_CallView.scss"; @import "./views/voip/_CallView.scss";
@import "./views/voip/_CallViewForRoom.scss"; @import "./views/voip/_CallViewForRoom.scss";
@import "./views/voip/_CallPreview.scss";
@import "./views/voip/_DialPad.scss"; @import "./views/voip/_DialPad.scss";
@import "./views/voip/_DialPadContextMenu.scss"; @import "./views/voip/_DialPadContextMenu.scss";
@import "./views/voip/_DialPadModal.scss"; @import "./views/voip/_DialPadModal.scss";

View file

@ -323,7 +323,7 @@ limitations under the License.
} }
.mx_GroupView_featuredThing .mx_BaseAvatar { .mx_GroupView_featuredThing .mx_BaseAvatar {
/* To prevent misalignment with mx_TintableSvg (in addButton) */ /* To prevent misalignment with img (in addButton) */
vertical-align: initial; vertical-align: initial;
} }

View file

@ -121,23 +121,51 @@ $pulse-color: $pinned-unread-color;
box-shadow: 0 0 0 0 rgba($pulse-color, 1); box-shadow: 0 0 0 0 rgba($pulse-color, 1);
animation: mx_RightPanel_indicator_pulse 2s infinite; animation: mx_RightPanel_indicator_pulse 2s infinite;
animation-iteration-count: 1; animation-iteration-count: 1;
&::after {
content: "";
position: absolute;
width: inherit;
height: inherit;
top: 0;
left: 0;
transform: scale(1);
transform-origin: center center;
animation-name: mx_RightPanel_indicator_pulse_shadow;
animation-duration: inherit;
animation-iteration-count: inherit;
border-radius: 50%;
background: rgba($pulse-color, 1);
}
} }
} }
@keyframes mx_RightPanel_indicator_pulse { @keyframes mx_RightPanel_indicator_pulse {
0% { 0% {
transform: scale(0.95); transform: scale(0.95);
box-shadow: 0 0 0 0 rgba($pulse-color, 0.7);
} }
70% { 70% {
transform: scale(1); transform: scale(1);
box-shadow: 0 0 0 10px rgba($pulse-color, 0);
} }
100% { 100% {
transform: scale(0.95); transform: scale(0.95);
box-shadow: 0 0 0 0 rgba($pulse-color, 0); }
}
@keyframes mx_RightPanel_indicator_pulse_shadow {
0% {
opacity: 0.7;
}
70% {
transform: scale(2.2);
opacity: 0;
}
100% {
opacity: 0;
} }
} }

View file

@ -57,14 +57,15 @@ limitations under the License.
@keyframes mx_RoomView_fileDropTarget_image_animation { @keyframes mx_RoomView_fileDropTarget_image_animation {
from { from {
width: 0px; transform: scaleX(0);
} }
to { to {
width: 32px; transform: scaleX(1);
} }
} }
.mx_RoomView_fileDropTarget_image { .mx_RoomView_fileDropTarget_image {
width: 32px;
animation: mx_RoomView_fileDropTarget_image_animation; animation: mx_RoomView_fileDropTarget_image_animation;
animation-duration: 0.5s; animation-duration: 0.5s;
margin-bottom: 16px; margin-bottom: 16px;

View file

@ -0,0 +1,68 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_AudioPlayer_container {
padding: 16px 12px 12px 12px;
max-width: 267px; // use max to make the control fit in the files/pinned panels
.mx_AudioPlayer_primaryContainer {
display: flex;
.mx_PlayPauseButton {
margin-right: 8px;
}
.mx_AudioPlayer_mediaInfo {
flex: 1;
overflow: hidden; // makes the ellipsis on the file name work
& > * {
display: block;
}
.mx_AudioPlayer_mediaName {
color: $primary-fg-color;
font-size: $font-15px;
line-height: $font-15px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
padding-bottom: 4px; // mimics the line-height differences in the Figma
}
.mx_AudioPlayer_byline {
font-size: $font-12px;
line-height: $font-12px;
}
}
}
.mx_AudioPlayer_seek {
display: flex;
align-items: center;
.mx_SeekBar {
flex: 1;
}
.mx_Clock {
width: $font-42px; // we're not using a monospace font, so fake it
min-width: $font-42px; // for flexbox
padding-left: 4px; // isolate from seek bar
text-align: right;
}
}
}

View file

@ -18,6 +18,8 @@ limitations under the License.
position: relative; position: relative;
width: 32px; width: 32px;
height: 32px; height: 32px;
min-width: 32px; // for when the button is used in a flexbox
min-height: 32px; // for when the button is used in a flexbox
border-radius: 32px; border-radius: 32px;
background-color: $voice-playback-button-bg-color; background-color: $voice-playback-button-bg-color;

View file

@ -22,17 +22,11 @@ limitations under the License.
// 7px top and bottom for visual design. 12px left & right, but the waveform (right) // 7px top and bottom for visual design. 12px left & right, but the waveform (right)
// has a 1px padding on it that we want to account for. // has a 1px padding on it that we want to account for.
padding: 7px 12px 7px 11px; padding: 7px 12px 7px 11px;
background-color: $voice-record-waveform-bg-color;
border-radius: 12px;
// Cheat at alignment a bit // Cheat at alignment a bit
display: flex; display: flex;
align-items: center; align-items: center;
color: $voice-record-waveform-fg-color;
font-size: $font-14px;
line-height: $font-24px;
contain: content; contain: content;
.mx_Waveform { .mx_Waveform {
@ -45,7 +39,7 @@ limitations under the License.
&.mx_Waveform_bar_100pct { &.mx_Waveform_bar_100pct {
// Small animation to remove the mechanical feel of progress // Small animation to remove the mechanical feel of progress
transition: background-color 250ms ease; transition: background-color 250ms ease;
background-color: $voice-record-waveform-fg-color; background-color: $message-body-panel-fg-color;
} }
} }
} }

View file

@ -0,0 +1,103 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// CSS inspiration from:
// * https://www.w3schools.com/howto/howto_js_rangeslider.asp
// * https://stackoverflow.com/a/28283806
// * https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/
.mx_SeekBar {
// Dev note: we deliberately do not have the -ms-track (and friends) selectors because we don't
// need to support IE.
appearance: none; // default style override
width: 100%;
height: 1px;
background: $quaternary-fg-color;
outline: none; // remove blue selection border
position: relative; // for before+after pseudo elements later on
cursor: pointer;
&::-webkit-slider-thumb {
appearance: none; // default style override
// Dev note: This needs to be duplicated with the -moz-range-thumb selector
// because otherwise Edge (webkit) will fail to see the styles and just refuse
// to apply them.
width: 8px;
height: 8px;
border-radius: 8px;
background-color: $tertiary-fg-color;
cursor: pointer;
}
&::-moz-range-thumb {
width: 8px;
height: 8px;
border-radius: 8px;
background-color: $tertiary-fg-color;
cursor: pointer;
// Firefox adds a border on the thumb
border: none;
}
// This is for webkit support, but we can't limit the functionality of it to just webkit
// browsers. Firefox responds to webkit-prefixed values now, which means we can't use media
// or support queries to selectively apply the rule. An upside is that this CSS doesn't work
// in firefox, so it's just wasted CPU/GPU time.
&::before { // ::before to ensure it ends up under the thumb
content: '';
background-color: $tertiary-fg-color;
// Absolute positioning to ensure it overlaps with the existing bar
position: absolute;
top: 0;
left: 0;
// Sizing to match the bar
width: 100%;
height: 1px;
// And finally dynamic width without overly hurting the rendering engine.
transform-origin: 0 100%;
transform: scaleX(var(--fillTo));
}
// This is firefox's built-in support for the above, with 100% less hacks.
&::-moz-range-progress {
background-color: $tertiary-fg-color;
height: 1px;
}
&:disabled {
opacity: 0.5;
}
// Increase clickable area for the slider (approximately same size as browser default)
// We do it this way to keep the same padding and margins of the element, avoiding margin math.
// Source: https://front-back.com/expand-clickable-areas-for-a-better-touch-experience/
&::after {
content: '';
position: absolute;
top: -6px;
bottom: -6px;
left: 0;
right: 0;
}
}

View file

@ -110,24 +110,52 @@ $dot-size: 12px;
width: $dot-size; width: $dot-size;
transform: scale(1); transform: scale(1);
background: rgba($pulse-color, 1); background: rgba($pulse-color, 1);
box-shadow: 0 0 0 0 rgba($pulse-color, 1);
animation: mx_Beta_bluePulse 2s infinite; animation: mx_Beta_bluePulse 2s infinite;
animation-iteration-count: 20; animation-iteration-count: 20;
position: relative;
&::after {
content: "";
position: absolute;
width: inherit;
height: inherit;
top: 0;
left: 0;
transform: scale(1);
transform-origin: center center;
animation-name: mx_Beta_bluePulse_shadow;
animation-duration: inherit;
animation-iteration-count: inherit;
border-radius: 50%;
background: rgba($pulse-color, 1);
}
} }
@keyframes mx_Beta_bluePulse { @keyframes mx_Beta_bluePulse {
0% { 0% {
transform: scale(0.95); transform: scale(0.95);
box-shadow: 0 0 0 0 rgba($pulse-color, 0.7);
} }
70% { 70% {
transform: scale(1); transform: scale(1);
box-shadow: 0 0 0 10px rgba($pulse-color, 0);
} }
100% { 100% {
transform: scale(0.95); transform: scale(0.95);
box-shadow: 0 0 0 0 rgba($pulse-color, 0); }
}
@keyframes mx_Beta_bluePulse_shadow {
0% {
opacity: 0.7;
}
70% {
transform: scale(2.2);
opacity: 0;
}
100% {
opacity: 0;
} }
} }

View file

@ -28,6 +28,7 @@ limitations under the License.
left: 0; left: 0;
top: 2px; // alignment top: 2px; // alignment
background-image: url("$(res)/img/element-icons/warning-badge.svg"); background-image: url("$(res)/img/element-icons/warning-badge.svg");
background-size: contain;
} }
.mx_AccessSecretStorageDialog_reset_link { .mx_AccessSecretStorageDialog_reset_link {

View file

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
$timelineImageBorderRadius: 4px;
.mx_MImageBody { .mx_MImageBody {
display: block; display: block;
margin-right: 34px; margin-right: 34px;
@ -25,7 +27,11 @@ limitations under the License.
height: 100%; height: 100%;
left: 0; left: 0;
top: 0; top: 0;
border-radius: 4px; border-radius: $timelineImageBorderRadius;
> canvas {
border-radius: $timelineImageBorderRadius;
}
} }
.mx_MImageBody_thumbnail_container { .mx_MImageBody_thumbnail_container {
@ -43,7 +49,7 @@ limitations under the License.
top: 50%; top: 50%;
} }
// Inner img and TintableSvg should be centered around 0, 0 // Inner img should be centered around 0, 0
.mx_MImageBody_thumbnail_spinner > * { .mx_MImageBody_thumbnail_spinner > * {
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }

View file

@ -0,0 +1,28 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// A "media body" is any file upload looking thing, apart from images and videos (they
// have unique styles).
.mx_MediaBody {
background-color: $message-body-panel-bg-color;
border-radius: 12px;
color: $message-body-panel-fg-color;
font-size: $font-14px;
line-height: $font-24px;
}

View file

@ -48,6 +48,7 @@ limitations under the License.
.mx_cryptoEvent_buttons { .mx_cryptoEvent_buttons {
align-items: center; align-items: center;
display: flex; display: flex;
gap: 5px;
} }
.mx_cryptoEvent_state { .mx_cryptoEvent_state {

View file

@ -477,8 +477,7 @@ $hover-select-border: 4px;
pre, code { pre, code {
font-family: $monospace-font-family !important; font-family: $monospace-font-family !important;
// deliberate constants as we're behind an invert filter background-color: $header-panel-bg-color;
color: #333;
} }
pre { pre {
@ -488,11 +487,6 @@ $hover-select-border: 4px;
overflow-x: overlay; overflow-x: overlay;
overflow-y: visible; overflow-y: visible;
} }
code {
// deliberate constants as we're behind an invert filter
background-color: #f8f8f8;
}
} }
.mx_EventTile_lineNumbers { .mx_EventTile_lineNumbers {

View file

@ -0,0 +1,38 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_LinkPreviewGroup {
.mx_LinkPreviewGroup_hide {
cursor: pointer;
width: 18px;
height: 18px;
img {
flex: 0 0 40px;
visibility: hidden;
}
}
&:hover .mx_LinkPreviewGroup_hide img,
.mx_LinkPreviewGroup_hide.focus-visible:focus img {
visibility: visible;
}
> .mx_AccessibleButton {
color: $accent-color;
text-align: center;
}
}

View file

@ -33,38 +33,29 @@ limitations under the License.
.mx_LinkPreviewWidget_caption { .mx_LinkPreviewWidget_caption {
margin-left: 15px; margin-left: 15px;
flex: 1 1 auto; flex: 1 1 auto;
overflow-x: hidden; // cause it to wrap rather than clip
} }
.mx_LinkPreviewWidget_title { .mx_LinkPreviewWidget_title {
display: inline;
font-weight: bold; font-weight: bold;
white-space: normal; white-space: normal;
} display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
.mx_LinkPreviewWidget_siteName { .mx_LinkPreviewWidget_siteName {
display: inline; font-weight: normal;
}
} }
.mx_LinkPreviewWidget_description { .mx_LinkPreviewWidget_description {
margin-top: 8px; margin-top: 8px;
white-space: normal; white-space: normal;
word-wrap: break-word; word-wrap: break-word;
} display: -webkit-box;
-webkit-line-clamp: 3;
.mx_LinkPreviewWidget_cancel { -webkit-box-orient: vertical;
cursor: pointer;
width: 18px;
height: 18px;
img {
flex: 0 0 40px;
visibility: hidden;
}
}
.mx_LinkPreviewWidget:hover .mx_LinkPreviewWidget_cancel img,
.mx_LinkPreviewWidget_cancel.focus-visible:focus img {
visibility: visible;
} }
.mx_MatrixChat_useCompactLayout { .mx_MatrixChat_useCompactLayout {

View file

@ -30,8 +30,8 @@ limitations under the License.
pointer-events: initial; // restore pointer events so the user can leave/interact pointer-events: initial; // restore pointer events so the user can leave/interact
cursor: pointer; cursor: pointer;
.mx_CallView_video { .mx_VideoFeed_remote.mx_VideoFeed_voice {
width: 350px; min-height: 150px;
} }
.mx_VideoFeed_local { .mx_VideoFeed_local {

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,17 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_PulsedAvatar { .mx_CallPreview {
@keyframes shadow-pulse { position: fixed;
0% { left: 0;
box-shadow: 0 0 0 0px rgba($accent-color, 0.2); top: 0;
}
100% {
box-shadow: 0 0 0 6px rgba($accent-color, 0);
}
}
img {
animation: shadow-pulse 1s infinite;
}
} }

View file

@ -39,7 +39,6 @@ limitations under the License.
.mx_CallView_pip { .mx_CallView_pip {
width: 320px; width: 320px;
padding-bottom: 8px; padding-bottom: 8px;
margin-top: 10px;
background-color: $voipcall-plinth-color; background-color: $voipcall-plinth-color;
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20); box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20);
border-radius: 8px; border-radius: 8px;

View file

@ -15,8 +15,6 @@ limitations under the License.
*/ */
.mx_VideoFeed_voice { .mx_VideoFeed_voice {
// We don't want to collide with the call controls that have 52px of height
padding-bottom: 52px;
background-color: $inverted-bg-color; background-color: $inverted-bg-color;
} }

View file

@ -119,8 +119,6 @@ $voipcall-plinth-color: #394049;
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$dialpad-button-bg-color: #6F7882; $dialpad-button-bg-color: #6F7882;
;
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
$roomlist-filter-active-bg-color: $bg-color; $roomlist-filter-active-bg-color: $bg-color;
@ -215,8 +213,6 @@ $message-body-panel-icon-fg-color: #21262C; // "Separator"
$message-body-panel-icon-bg-color: $tertiary-fg-color; $message-body-panel-icon-bg-color: $tertiary-fg-color;
$voice-record-stop-border-color: $quaternary-fg-color; $voice-record-stop-border-color: $quaternary-fg-color;
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; $voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
$voice-record-icon-color: $quaternary-fg-color; $voice-record-icon-color: $quaternary-fg-color;
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color; $voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
@ -276,24 +272,7 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
} }
// markdown overrides: // markdown overrides:
.mx_EventTile_content .markdown-body pre:hover {
border-color: #808080 !important; // inverted due to rules below
scrollbar-color: rgba(0, 0, 0, 0.2) transparent; // copied from light theme due to inversion below
// the code above works only in Firefox, this is for other browsers
// see https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2); // copied from light theme due to inversion below
}
}
.mx_EventTile_content .markdown-body { .mx_EventTile_content .markdown-body {
pre, code {
filter: invert(1);
}
pre code {
filter: none;
}
table { table {
tr { tr {
background-color: #000000; background-color: #000000;
@ -303,18 +282,9 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
background-color: #080808; background-color: #080808;
} }
} }
blockquote {
color: #919191;
}
} }
// diff highlight colors // highlight.js overrides
// intentionally swapped to avoid inversion .hljs-tag {
.hljs-addition { color: inherit; // Without this they'd be weirdly blue which doesn't match the theme
background: #fdd;
}
.hljs-deletion {
background: #dfd;
} }

View file

@ -9,3 +9,4 @@
@import "_dark.scss"; @import "_dark.scss";
@import "../../light/css/_mods.scss"; @import "../../light/css/_mods.scss";
@import "../../../../res/css/_components.scss"; @import "../../../../res/css/_components.scss";
@import url("highlight.js/styles/atom-one-dark.css");

View file

@ -20,6 +20,9 @@ $tertiary-fg-color: $primary-fg-color;
$primary-bg-color: $bg-color; $primary-bg-color: $bg-color;
$muted-fg-color: $header-panel-text-primary-color; $muted-fg-color: $header-panel-text-primary-color;
// Legacy theme backports
$quaternary-fg-color: #6F7882;
// used for dialog box text // used for dialog box text
$light-fg-color: $header-panel-text-secondary-color; $light-fg-color: $header-panel-text-secondary-color;
@ -115,7 +118,7 @@ $voipcall-plinth-color: #394049;
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$dialpad-button-bg-color: #6F7882; $dialpad-button-bg-color: #6F7882;
;
$roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons
$roomlist-filter-active-bg-color: $roomlist-button-bg-color; $roomlist-filter-active-bg-color: $roomlist-button-bg-color;
@ -209,8 +212,6 @@ $message-body-panel-icon-bg-color: $secondary-fg-color;
// See non-legacy dark for variable information // See non-legacy dark for variable information
$voice-record-stop-border-color: #6F7882; $voice-record-stop-border-color: #6F7882;
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
$voice-record-waveform-incomplete-fg-color: #6F7882; $voice-record-waveform-incomplete-fg-color: #6F7882;
$voice-record-icon-color: #6F7882; $voice-record-icon-color: #6F7882;
$voice-playback-button-bg-color: $tertiary-fg-color; $voice-playback-button-bg-color: $tertiary-fg-color;
@ -248,7 +249,7 @@ $composer-shadow-color: tranparent;
@define-mixin mx_DialogButton_secondary { @define-mixin mx_DialogButton_secondary {
// flip colours for the secondary ones // flip colours for the secondary ones
font-weight: 600; font-weight: 600;
border: 1px solid $accent-color ! important; border: 1px solid $accent-color !important;
color: $accent-color; color: $accent-color;
background-color: $button-secondary-bg-color; background-color: $button-secondary-bg-color;
} }
@ -266,18 +267,7 @@ $composer-shadow-color: tranparent;
} }
// markdown overrides: // markdown overrides:
.mx_EventTile_content .markdown-body pre:hover {
border-color: #808080 !important; // inverted due to rules below
}
.mx_EventTile_content .markdown-body { .mx_EventTile_content .markdown-body {
pre, code {
filter: invert(1);
}
pre code {
filter: none;
}
table { table {
tr { tr {
background-color: #000000; background-color: #000000;
@ -289,12 +279,7 @@ $composer-shadow-color: tranparent;
} }
} }
// diff highlight colors // highlight.js overrides:
// intentionally swapped to avoid inversion .hljs-tag {
.hljs-addition { color: inherit; // Without this they'd be weirdly blue which doesn't match the theme
background: #fdd;
}
.hljs-deletion {
background: #dfd;
} }

View file

@ -4,3 +4,4 @@
@import "../../legacy-light/css/_legacy-light.scss"; @import "../../legacy-light/css/_legacy-light.scss";
@import "_legacy-dark.scss"; @import "_legacy-dark.scss";
@import "../../../../res/css/_components.scss"; @import "../../../../res/css/_components.scss";
@import url("highlight.js/styles/atom-one-dark.css");

View file

@ -28,6 +28,9 @@ $tertiary-fg-color: $primary-fg-color;
$primary-bg-color: #ffffff; $primary-bg-color: #ffffff;
$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text $muted-fg-color: #61708b; // Commonly used in headings and relevant alt text
// Legacy theme backports
$quaternary-fg-color: #C1C6CD;
// used for dialog box text // used for dialog box text
$light-fg-color: #747474; $light-fg-color: #747474;
@ -334,8 +337,6 @@ $message-body-panel-icon-bg-color: $primary-bg-color;
$voice-record-stop-symbol-color: #ff4b55; $voice-record-stop-symbol-color: #ff4b55;
$voice-record-live-circle-color: #ff4b55; $voice-record-live-circle-color: #ff4b55;
$voice-record-stop-border-color: #E3E8F0; $voice-record-stop-border-color: #E3E8F0;
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
$voice-record-waveform-incomplete-fg-color: #C1C6CD; $voice-record-waveform-incomplete-fg-color: #C1C6CD;
$voice-record-icon-color: $tertiary-fg-color; $voice-record-icon-color: $tertiary-fg-color;
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color; $voice-playback-button-bg-color: $message-body-panel-icon-bg-color;

View file

@ -3,3 +3,4 @@
@import "_fonts.scss"; @import "_fonts.scss";
@import "_legacy-light.scss"; @import "_legacy-light.scss";
@import "../../../../res/css/_components.scss"; @import "../../../../res/css/_components.scss";
@import url("highlight.js/styles/atom-one-light.css");

View file

@ -335,8 +335,6 @@ $voice-record-stop-symbol-color: #ff4b55;
$voice-record-live-circle-color: #ff4b55; $voice-record-live-circle-color: #ff4b55;
$voice-record-stop-border-color: #E3E8F0; // "Separator" $voice-record-stop-border-color: #E3E8F0; // "Separator"
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; $voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
$voice-record-icon-color: $tertiary-fg-color; $voice-record-icon-color: $tertiary-fg-color;
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color; $voice-playback-button-bg-color: $message-body-panel-icon-bg-color;

View file

@ -4,3 +4,4 @@
@import "_light.scss"; @import "_light.scss";
@import "_mods.scss"; @import "_mods.scss";
@import "../../../../res/css/_components.scss"; @import "../../../../res/css/_components.scss";
@import url("highlight.js/styles/atom-one-light.css");

View file

@ -6,8 +6,8 @@ scripts/fetchdep.sh matrix-org matrix-js-sdk
pushd matrix-js-sdk pushd matrix-js-sdk
yarn link yarn link
yarn install $@ yarn install --pure-lockfile $@
popd popd
yarn link matrix-js-sdk yarn link matrix-js-sdk
yarn install $@ yarn install --pure-lockfile $@

View file

@ -13,13 +13,13 @@
scripts/fetchdep.sh matrix-org matrix-js-sdk scripts/fetchdep.sh matrix-org matrix-js-sdk
pushd matrix-js-sdk pushd matrix-js-sdk
yarn link yarn link
yarn install yarn install --pure-lockfile
popd popd
# Now set up the react-sdk # Now set up the react-sdk
yarn link matrix-js-sdk yarn link matrix-js-sdk
yarn link yarn link
yarn install yarn install --pure-lockfile
yarn reskindex yarn reskindex
# Finally, set up element-web # Finally, set up element-web
@ -27,6 +27,6 @@ scripts/fetchdep.sh vector-im element-web
pushd element-web pushd element-web
yarn link matrix-js-sdk yarn link matrix-js-sdk
yarn link matrix-react-sdk yarn link matrix-react-sdk
yarn install yarn install --pure-lockfile
yarn build:res yarn build:res
popd popd

View file

@ -1,23 +0,0 @@
#!/bin/sh
#
# generates .eslintignore.errorfiles to list the files which have errors in,
# so that they can be ignored in future automated linting.
out=.eslintignore.errorfiles
cd `dirname $0`/..
echo "generating $out"
{
cat <<EOF
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
EOF
./node_modules/.bin/eslint -f json src test |
jq -r '.[] | select((.errorCount + .warningCount) > 0) | .filePath' |
sed -e 's/.*matrix-react-sdk\///';
} > "$out"
# also append rules from eslintignore file
cat .eslintignore >> $out

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
import * as ModernizrStatic from "modernizr"; import "@types/modernizr";
import ContentMessages from "../ContentMessages"; import ContentMessages from "../ContentMessages";
import { IMatrixClientPeg } from "../MatrixClientPeg"; import { IMatrixClientPeg } from "../MatrixClientPeg";
@ -46,10 +46,10 @@ import { VoiceRecordingStore } from "../stores/VoiceRecordingStore";
import PerformanceMonitor from "../performance"; import PerformanceMonitor from "../performance";
import UIStore from "../stores/UIStore"; import UIStore from "../stores/UIStore";
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore"; import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
declare global { declare global {
interface Window { interface Window {
Modernizr: ModernizrStatic;
matrixChat: ReturnType<Renderer>; matrixChat: ReturnType<Renderer>;
mxMatrixClientPeg: IMatrixClientPeg; mxMatrixClientPeg: IMatrixClientPeg;
Olm: { Olm: {
@ -87,6 +87,7 @@ declare global {
mxPerformanceEntryNames: any; mxPerformanceEntryNames: any;
mxUIStore: UIStore; mxUIStore: UIStore;
mxSetupEncryptionStore?: SetupEncryptionStore; mxSetupEncryptionStore?: SetupEncryptionStore;
mxRoomScrollStateStore?: RoomScrollStateStore;
} }
interface Document { interface Document {

View file

@ -390,6 +390,7 @@ export class Analytics {
{ expl: _td('Your device resolution'), value: resolution }, { expl: _td('Your device resolution'), value: resolution },
]; ];
// FIXME: Using an import will result in test failures
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, { Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, {
title: _t('Analytics'), title: _t('Analytics'),

View file

@ -77,6 +77,7 @@ export default class AsyncWrapper extends React.Component<IProps, IState> {
const Component = this.state.component; const Component = this.state.component;
return <Component {...this.props} />; return <Component {...this.props} />;
} else if (this.state.error) { } else if (this.state.error) {
// FIXME: Using an import will result in test failures
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <BaseDialog onFinished={this.props.onFinished} title={_t("Error")}> return <BaseDialog onFinished={this.props.onFinished} title={_t("Error")}>

View file

@ -124,9 +124,9 @@ interface ThirdpartyLookupResponseFields {
} }
interface ThirdpartyLookupResponse { interface ThirdpartyLookupResponse {
userid: string, userid: string;
protocol: string, protocol: string;
fields: ThirdpartyLookupResponseFields, fields: ThirdpartyLookupResponseFields;
} }
// Unlike 'CallType' in js-sdk, this one includes screen sharing // Unlike 'CallType' in js-sdk, this one includes screen sharing

View file

@ -17,9 +17,10 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import dis from './dispatcher/dispatcher'; import { encode } from "blurhash";
import { MatrixClientPeg } from './MatrixClientPeg';
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import dis from './dispatcher/dispatcher';
import * as sdk from './index'; import * as sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import Modal from './Modal'; import Modal from './Modal';
@ -47,6 +48,8 @@ const MAX_HEIGHT = 600;
// 5669 px (x-axis) , 5669 px (y-axis) , per metre // 5669 px (x-axis) , 5669 px (y-axis) , per metre
const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01]; const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448
export class UploadCanceledError extends Error {} export class UploadCanceledError extends Error {}
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement; type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
@ -77,6 +80,7 @@ interface IThumbnail {
}; };
w: number; w: number;
h: number; h: number;
[BLURHASH_FIELD]: string;
}; };
thumbnail: Blob; thumbnail: Blob;
} }
@ -124,7 +128,17 @@ function createThumbnail(
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = targetWidth; canvas.width = targetWidth;
canvas.height = targetHeight; canvas.height = targetHeight;
canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight); const context = canvas.getContext("2d");
context.drawImage(element, 0, 0, targetWidth, targetHeight);
const imageData = context.getImageData(0, 0, targetWidth, targetHeight);
const blurhash = encode(
imageData.data,
imageData.width,
imageData.height,
// use 4 components on the longer dimension, if square then both
imageData.width >= imageData.height ? 4 : 3,
imageData.height >= imageData.width ? 4 : 3,
);
canvas.toBlob(function(thumbnail) { canvas.toBlob(function(thumbnail) {
resolve({ resolve({
info: { info: {
@ -136,8 +150,9 @@ function createThumbnail(
}, },
w: inputWidth, w: inputWidth,
h: inputHeight, h: inputHeight,
[BLURHASH_FIELD]: blurhash,
}, },
thumbnail: thumbnail, thumbnail,
}); });
}, mimeType); }, mimeType);
}); });
@ -220,7 +235,8 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
} }
/** /**
* Load a file into a newly created video element. * Load a file into a newly created video element and pull some strings
* in an attempt to guarantee the first frame will be showing.
* *
* @param {File} videoFile The file to load in an video element. * @param {File} videoFile The file to load in an video element.
* @return {Promise} A promise that resolves with the video image element. * @return {Promise} A promise that resolves with the video image element.
@ -229,20 +245,25 @@ function loadVideoElement(videoFile): Promise<HTMLVideoElement> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Load the file into an html element // Load the file into an html element
const video = document.createElement("video"); const video = document.createElement("video");
video.preload = "metadata";
video.playsInline = true;
video.muted = true;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = function(ev) { reader.onload = function(ev) {
video.src = ev.target.result as string;
// Once ready, returns its size
// Wait until we have enough data to thumbnail the first frame. // Wait until we have enough data to thumbnail the first frame.
video.onloadeddata = function() { video.onloadeddata = async function() {
resolve(video); resolve(video);
video.pause();
}; };
video.onerror = function(e) { video.onerror = function(e) {
reject(e); reject(e);
}; };
video.src = ev.target.result as string;
video.load();
video.play();
}; };
reader.onerror = function(e) { reader.onerror = function(e) {
reject(e); reject(e);
@ -347,7 +368,7 @@ export function uploadFile(
}); });
(prom as IAbortablePromise<any>).abort = () => { (prom as IAbortablePromise<any>).abort = () => {
canceled = true; canceled = true;
if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise); if (uploadPromise) matrixClient.cancelUpload(uploadPromise);
}; };
return prom; return prom;
} else { } else {
@ -357,11 +378,11 @@ export function uploadFile(
const promise1 = basePromise.then(function(url) { const promise1 = basePromise.then(function(url) {
if (canceled) throw new UploadCanceledError(); if (canceled) throw new UploadCanceledError();
// If the attachment isn't encrypted then include the URL directly. // If the attachment isn't encrypted then include the URL directly.
return { "url": url }; return { url };
}); });
(promise1 as any).abort = () => { (promise1 as any).abort = () => {
canceled = true; canceled = true;
MatrixClientPeg.get().cancelUpload(basePromise); matrixClient.cancelUpload(basePromise);
}; };
return promise1; return promise1;
} }
@ -373,7 +394,7 @@ export default class ContentMessages {
sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) { sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) {
const startTime = CountlyAnalytics.getTimestamp(); const startTime = CountlyAnalytics.getTimestamp();
const prom = MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => { const prom = matrixClient.sendStickerMessage(roomId, url, info, text).catch((e) => {
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e); console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
throw e; throw e;
}); });
@ -397,6 +418,7 @@ export default class ContentMessages {
const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
if (isQuoting) { if (isQuoting) {
// FIXME: Using an import will result in Element crashing
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, { const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, {
title: _t('Replying With Files'), title: _t('Replying With Files'),
@ -415,7 +437,7 @@ export default class ContentMessages {
if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner'); const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
await this.ensureMediaConfigFetched(); await this.ensureMediaConfigFetched(matrixClient);
modal.close(); modal.close();
} }
@ -431,6 +453,7 @@ export default class ContentMessages {
} }
if (tooBigFiles.length > 0) { if (tooBigFiles.length > 0) {
// FIXME: Using an import will result in Element crashing
const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog"); const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog");
const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Failure', '', UploadFailureDialog, { const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Failure', '', UploadFailureDialog, {
badFiles: tooBigFiles, badFiles: tooBigFiles,
@ -441,7 +464,6 @@ export default class ContentMessages {
if (!shouldContinue) return; if (!shouldContinue) return;
} }
const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog");
let uploadAll = false; let uploadAll = false;
// Promise to complete before sending next file into room, used for synchronisation of file-sending // Promise to complete before sending next file into room, used for synchronisation of file-sending
// to match the order the files were specified in // to match the order the files were specified in
@ -449,6 +471,8 @@ export default class ContentMessages {
for (let i = 0; i < okFiles.length; ++i) { for (let i = 0; i < okFiles.length; ++i) {
const file = okFiles[i]; const file = okFiles[i];
if (!uploadAll) { if (!uploadAll) {
// FIXME: Using an import will result in Element crashing
const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog");
const { finished } = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation', const { finished } = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation',
'', UploadConfirmDialog, { '', UploadConfirmDialog, {
file, file,
@ -470,7 +494,7 @@ export default class ContentMessages {
return this.inprogress.filter(u => !u.canceled); return this.inprogress.filter(u => !u.canceled);
} }
cancelUpload(promise: Promise<any>) { cancelUpload(promise: Promise<any>, matrixClient: MatrixClient) {
let upload: IUpload; let upload: IUpload;
for (let i = 0; i < this.inprogress.length; ++i) { for (let i = 0; i < this.inprogress.length; ++i) {
if (this.inprogress[i].promise === promise) { if (this.inprogress[i].promise === promise) {
@ -480,7 +504,7 @@ export default class ContentMessages {
} }
if (upload) { if (upload) {
upload.canceled = true; upload.canceled = true;
MatrixClientPeg.get().cancelUpload(upload.promise); matrixClient.cancelUpload(upload.promise);
dis.dispatch<UploadCanceledPayload>({ action: Action.UploadCanceled, upload }); dis.dispatch<UploadCanceledPayload>({ action: Action.UploadCanceled, upload });
} }
} }
@ -545,7 +569,7 @@ export default class ContentMessages {
dis.dispatch<UploadStartedPayload>({ action: Action.UploadStarted, upload }); dis.dispatch<UploadStartedPayload>({ action: Action.UploadStarted, upload });
// Focus the composer view // Focus the composer view
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
function onProgress(ev) { function onProgress(ev) {
upload.total = ev.total; upload.total = ev.total;
@ -584,6 +608,7 @@ export default class ContentMessages {
{ fileName: upload.fileName }, { fileName: upload.fileName },
); );
} }
// FIXME: Using an import will result in Element crashing
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, { Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
title: _t('Upload Failed'), title: _t('Upload Failed'),
@ -621,11 +646,11 @@ export default class ContentMessages {
return true; return true;
} }
private ensureMediaConfigFetched() { private ensureMediaConfigFetched(matrixClient: MatrixClient) {
if (this.mediaConfig !== null) return; if (this.mediaConfig !== null) return;
console.log("[Media Config] Fetching"); console.log("[Media Config] Fetching");
return MatrixClientPeg.get().getMediaConfig().then((config) => { return matrixClient.getMediaConfig().then((config) => {
console.log("[Media Config] Fetched config:", config); console.log("[Media Config] Fetched config:", config);
return config; return config;
}).catch(() => { }).catch(() => {

View file

@ -15,12 +15,13 @@ limitations under the License.
*/ */
import { randomString } from "matrix-js-sdk/src/randomstring"; import { randomString } from "matrix-js-sdk/src/randomstring";
import { IContent } from "matrix-js-sdk/src/models/event";
import { sleep } from "matrix-js-sdk/src/utils";
import { getCurrentLanguage } from './languageHandler'; import { getCurrentLanguage } from './languageHandler';
import PlatformPeg from './PlatformPeg'; import PlatformPeg from './PlatformPeg';
import SdkConfig from './SdkConfig'; import SdkConfig from './SdkConfig';
import { MatrixClientPeg } from "./MatrixClientPeg"; import { MatrixClientPeg } from "./MatrixClientPeg";
import { sleep } from "./utils/promise";
import RoomViewStore from "./stores/RoomViewStore"; import RoomViewStore from "./stores/RoomViewStore";
import { Action } from "./dispatcher/actions"; import { Action } from "./dispatcher/actions";
@ -255,7 +256,7 @@ interface ICreateRoomEvent extends IEvent {
num_users: number; num_users: number;
is_encrypted: boolean; is_encrypted: boolean;
is_public: boolean; is_public: boolean;
} };
} }
interface IJoinRoomEvent extends IEvent { interface IJoinRoomEvent extends IEvent {
@ -868,7 +869,7 @@ export default class CountlyAnalytics {
roomId: string, roomId: string,
isEdit: boolean, isEdit: boolean,
isReply: boolean, isReply: boolean,
content: {format?: string, msgtype: string}, content: IContent,
) { ) {
if (this.disabled) return; if (this.disabled) return;
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();

View file

@ -160,7 +160,8 @@ export default class DeviceListener {
// which result in account data changes affecting checks below. // which result in account data changes affecting checks below.
if ( if (
ev.getType().startsWith('m.secret_storage.') || ev.getType().startsWith('m.secret_storage.') ||
ev.getType().startsWith('m.cross_signing.') ev.getType().startsWith('m.cross_signing.') ||
ev.getType() === 'm.megolm_backup.v1'
) { ) {
this._recheck(); this._recheck();
} }

View file

@ -358,11 +358,11 @@ interface IOpts {
stripReplyFallback?: boolean; stripReplyFallback?: boolean;
returnString?: boolean; returnString?: boolean;
forComposerQuote?: boolean; forComposerQuote?: boolean;
ref?: React.Ref<any>; ref?: React.Ref<HTMLSpanElement>;
} }
export interface IOptsReturnNode extends IOpts { export interface IOptsReturnNode extends IOpts {
returnString: false; returnString: false | undefined;
} }
export interface IOptsReturnString extends IOpts { export interface IOptsReturnString extends IOpts {
@ -403,9 +403,14 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
try { try {
if (highlights && highlights.length > 0) { if (highlights && highlights.length > 0) {
const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink); const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
const safeHighlights = highlights.map(function(highlight) { const safeHighlights = highlights
return sanitizeHtml(highlight, sanitizeParams); // sanitizeHtml can hang if an unclosed HTML tag is thrown at it
}); // A search for `<foo` will make the browser crash
// an alternative would be to escape HTML special characters
// but that would bring no additional benefit as the highlighter
// does not work with those special chars
.filter((highlight: string): boolean => !highlight.includes("<"))
.map((highlight: string): string => sanitizeHtml(highlight, sanitizeParams));
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure.
sanitizeParams.textFilter = function(safeText) { sanitizeParams.textFilter = function(safeText) {
return highlighter.applyHighlights(safeText, safeHighlights).join(''); return highlighter.applyHighlights(safeText, safeHighlights).join('');

View file

@ -33,7 +33,6 @@ import Presence from './Presence';
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import DMRoomMap from './utils/DMRoomMap'; import DMRoomMap from './utils/DMRoomMap';
import Modal from './Modal'; import Modal from './Modal';
import * as sdk from './index';
import ActiveWidgetStore from './stores/ActiveWidgetStore'; import ActiveWidgetStore from './stores/ActiveWidgetStore';
import PlatformPeg from "./PlatformPeg"; import PlatformPeg from "./PlatformPeg";
import { sendLoginRequest } from "./Login"; import { sendLoginRequest } from "./Login";
@ -52,6 +51,10 @@ import CallHandler from './CallHandler';
import LifecycleCustomisations from "./customisations/Lifecycle"; import LifecycleCustomisations from "./customisations/Lifecycle";
import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import { _t } from "./languageHandler"; import { _t } from "./languageHandler";
import LazyLoadingResyncDialog from "./components/views/dialogs/LazyLoadingResyncDialog";
import LazyLoadingDisabledDialog from "./components/views/dialogs/LazyLoadingDisabledDialog";
import SessionRestoreErrorDialog from "./components/views/dialogs/SessionRestoreErrorDialog";
import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog";
const HOMESERVER_URL_KEY = "mx_hs_url"; const HOMESERVER_URL_KEY = "mx_hs_url";
const ID_SERVER_URL_KEY = "mx_is_url"; const ID_SERVER_URL_KEY = "mx_is_url";
@ -238,8 +241,6 @@ export function handleInvalidStoreError(e: InvalidStoreError): Promise<void> {
return Promise.resolve().then(() => { return Promise.resolve().then(() => {
const lazyLoadEnabled = e.value; const lazyLoadEnabled = e.value;
if (lazyLoadEnabled) { if (lazyLoadEnabled) {
const LazyLoadingResyncDialog =
sdk.getComponent("views.dialogs.LazyLoadingResyncDialog");
return new Promise((resolve) => { return new Promise((resolve) => {
Modal.createDialog(LazyLoadingResyncDialog, { Modal.createDialog(LazyLoadingResyncDialog, {
onFinished: resolve, onFinished: resolve,
@ -250,8 +251,6 @@ export function handleInvalidStoreError(e: InvalidStoreError): Promise<void> {
// between LL/non-LL version on same host. // between LL/non-LL version on same host.
// as disabling LL when previously enabled // as disabling LL when previously enabled
// is a strong indicator of this (/develop & /app) // is a strong indicator of this (/develop & /app)
const LazyLoadingDisabledDialog =
sdk.getComponent("views.dialogs.LazyLoadingDisabledDialog");
return new Promise((resolve) => { return new Promise((resolve) => {
Modal.createDialog(LazyLoadingDisabledDialog, { Modal.createDialog(LazyLoadingDisabledDialog, {
onFinished: resolve, onFinished: resolve,
@ -451,9 +450,6 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }):
async function handleLoadSessionFailure(e: Error): Promise<boolean> { async function handleLoadSessionFailure(e: Error): Promise<boolean> {
console.error("Unable to load session", e); console.error("Unable to load session", e);
const SessionRestoreErrorDialog =
sdk.getComponent('views.dialogs.SessionRestoreErrorDialog');
const modal = Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { const modal = Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, {
error: e.message, error: e.message,
}); });
@ -612,7 +608,6 @@ async function doSetLoggedIn(
} }
function showStorageEvictedDialog(): Promise<boolean> { function showStorageEvictedDialog(): Promise<boolean> {
const StorageEvictedDialog = sdk.getComponent('views.dialogs.StorageEvictedDialog');
return new Promise(resolve => { return new Promise(resolve => {
Modal.createTrackedDialog('Storage evicted', '', StorageEvictedDialog, { Modal.createTrackedDialog('Storage evicted', '', StorageEvictedDialog, {
onFinished: resolve, onFinished: resolve,

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,16 +16,24 @@ limitations under the License.
*/ */
import * as commonmark from 'commonmark'; import * as commonmark from 'commonmark';
import {escape} from "lodash"; import { escape } from "lodash";
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
// These types of node are definitely text // These types of node are definitely text
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document']; const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
function is_allowed_html_tag(node) { // As far as @types/commonmark is concerned, these are not public, so add them
interface CommonmarkHtmlRendererInternal extends commonmark.HtmlRenderer {
paragraph: (node: commonmark.Node, entering: boolean) => void;
link: (node: commonmark.Node, entering: boolean) => void;
html_inline: (node: commonmark.Node) => void; // eslint-disable-line camelcase
html_block: (node: commonmark.Node) => void; // eslint-disable-line camelcase
}
function isAllowedHtmlTag(node: commonmark.Node): boolean {
if (node.literal != null && if (node.literal != null &&
node.literal.match('^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$') != null) { node.literal.match('^<((div|span) data-mx-maths="[^"]*"|/(div|span))>$') != null) {
return true; return true;
} }
@ -39,21 +48,12 @@ function is_allowed_html_tag(node) {
return false; return false;
} }
function html_if_tag_allowed(node) {
if (is_allowed_html_tag(node)) {
this.lit(node.literal);
return;
} else {
this.lit(escape(node.literal));
}
}
/* /*
* Returns true if the parse output containing the node * Returns true if the parse output containing the node
* comprises multiple block level elements (ie. lines), * comprises multiple block level elements (ie. lines),
* or false if it is only a single line. * or false if it is only a single line.
*/ */
function is_multi_line(node) { function isMultiLine(node: commonmark.Node): boolean {
let par = node; let par = node;
while (par.parent) { while (par.parent) {
par = par.parent; par = par.parent;
@ -67,6 +67,9 @@ function is_multi_line(node) {
* it's plain text. * it's plain text.
*/ */
export default class Markdown { export default class Markdown {
private input: string;
private parsed: commonmark.Node;
constructor(input) { constructor(input) {
this.input = input; this.input = input;
@ -74,7 +77,7 @@ export default class Markdown {
this.parsed = parser.parse(this.input); this.parsed = parser.parse(this.input);
} }
isPlainText() { isPlainText(): boolean {
const walker = this.parsed.walker(); const walker = this.parsed.walker();
let ev; let ev;
@ -87,7 +90,7 @@ export default class Markdown {
// if it's an allowed html tag, we need to render it and therefore // if it's an allowed html tag, we need to render it and therefore
// we will need to use HTML. If it's not allowed, it's not HTML since // we will need to use HTML. If it's not allowed, it's not HTML since
// we'll just be treating it as text. // we'll just be treating it as text.
if (is_allowed_html_tag(node)) { if (isAllowedHtmlTag(node)) {
return false; return false;
} }
} else { } else {
@ -97,7 +100,7 @@ export default class Markdown {
return true; return true;
} }
toHTML({ externalLinks = false } = {}) { toHTML({ externalLinks = false } = {}): string {
const renderer = new commonmark.HtmlRenderer({ const renderer = new commonmark.HtmlRenderer({
safe: false, safe: false,
@ -107,7 +110,7 @@ export default class Markdown {
// block quote ends up all on one line // block quote ends up all on one line
// (https://github.com/vector-im/element-web/issues/3154) // (https://github.com/vector-im/element-web/issues/3154)
softbreak: '<br />', softbreak: '<br />',
}); }) as CommonmarkHtmlRendererInternal;
// Trying to strip out the wrapping <p/> causes a lot more complication // Trying to strip out the wrapping <p/> causes a lot more complication
// than it's worth, i think. For instance, this code will go and strip // than it's worth, i think. For instance, this code will go and strip
@ -118,16 +121,16 @@ export default class Markdown {
// //
// Let's try sending with <p/>s anyway for now, though. // Let's try sending with <p/>s anyway for now, though.
const real_paragraph = renderer.paragraph; const realParagraph = renderer.paragraph;
renderer.paragraph = function(node, entering) { renderer.paragraph = function(node: commonmark.Node, entering: boolean) {
// If there is only one top level node, just return the // If there is only one top level node, just return the
// bare text: it's a single line of text and so should be // bare text: it's a single line of text and so should be
// 'inline', rather than unnecessarily wrapped in its own // 'inline', rather than unnecessarily wrapped in its own
// p tag. If, however, we have multiple nodes, each gets // p tag. If, however, we have multiple nodes, each gets
// its own p tag to keep them as separate paragraphs. // its own p tag to keep them as separate paragraphs.
if (is_multi_line(node)) { if (isMultiLine(node)) {
real_paragraph.call(this, node, entering); realParagraph.call(this, node, entering);
} }
}; };
@ -150,19 +153,26 @@ export default class Markdown {
} }
}; };
renderer.html_inline = html_if_tag_allowed; renderer.html_inline = function(node: commonmark.Node) {
if (isAllowedHtmlTag(node)) {
this.lit(node.literal);
return;
} else {
this.lit(escape(node.literal));
}
};
renderer.html_block = function(node) { renderer.html_block = function(node: commonmark.Node) {
/* /*
// as with `paragraph`, we only insert line breaks // as with `paragraph`, we only insert line breaks
// if there are multiple lines in the markdown. // if there are multiple lines in the markdown.
const isMultiLine = is_multi_line(node); const isMultiLine = is_multi_line(node);
if (isMultiLine) this.cr(); if (isMultiLine) this.cr();
*/ */
html_if_tag_allowed.call(this, node); renderer.html_inline(node);
/* /*
if (isMultiLine) this.cr(); if (isMultiLine) this.cr();
*/ */
}; };
return renderer.render(this.parsed); return renderer.render(this.parsed);
@ -177,23 +187,22 @@ export default class Markdown {
* N.B. this does **NOT** render arbitrary MD to plain text - only MD * N.B. this does **NOT** render arbitrary MD to plain text - only MD
* which has no formatting. Otherwise it emits HTML(!). * which has no formatting. Otherwise it emits HTML(!).
*/ */
toPlaintext() { toPlaintext(): string {
const renderer = new commonmark.HtmlRenderer({safe: false}); const renderer = new commonmark.HtmlRenderer({ safe: false }) as CommonmarkHtmlRendererInternal;
const real_paragraph = renderer.paragraph;
renderer.paragraph = function(node, entering) { renderer.paragraph = function(node: commonmark.Node, entering: boolean) {
// as with toHTML, only append lines to paragraphs if there are // as with toHTML, only append lines to paragraphs if there are
// multiple paragraphs // multiple paragraphs
if (is_multi_line(node)) { if (isMultiLine(node)) {
if (!entering && node.next) { if (!entering && node.next) {
this.lit('\n\n'); this.lit('\n\n');
} }
} }
}; };
renderer.html_block = function(node) { renderer.html_block = function(node: commonmark.Node) {
this.lit(node.literal); this.lit(node.literal);
if (is_multi_line(node) && node.next) this.lit('\n\n'); if (isMultiLine(node) && node.next) this.lit('\n\n');
}; };
return renderer.render(this.parsed); return renderer.render(this.parsed);

View file

@ -219,6 +219,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
} catch (e) { } catch (e) {
if (e && e.name === 'InvalidCryptoStoreError') { if (e && e.name === 'InvalidCryptoStoreError') {
// The js-sdk found a crypto DB too new for it to use // The js-sdk found a crypto DB too new for it to use
// FIXME: Using an import will result in test failures
const CryptoStoreTooNewDialog = const CryptoStoreTooNewDialog =
sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog"); sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog");
Modal.createDialog(CryptoStoreTooNewDialog); Modal.createDialog(CryptoStoreTooNewDialog);

View file

@ -20,12 +20,15 @@ import { SettingLevel } from "./settings/SettingLevel";
import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix"; import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix";
import EventEmitter from 'events'; import EventEmitter from 'events';
interface IMediaDevices { // XXX: MediaDeviceKind is a union type, so we make our own enum
audioOutput: Array<MediaDeviceInfo>; export enum MediaDeviceKindEnum {
audioInput: Array<MediaDeviceInfo>; AudioOutput = "audiooutput",
videoInput: Array<MediaDeviceInfo>; AudioInput = "audioinput",
VideoInput = "videoinput",
} }
export type IMediaDevices = Record<MediaDeviceKindEnum, Array<MediaDeviceInfo>>;
export enum MediaDeviceHandlerEvent { export enum MediaDeviceHandlerEvent {
AudioOutputChanged = "audio_output_changed", AudioOutputChanged = "audio_output_changed",
} }
@ -51,20 +54,14 @@ export default class MediaDeviceHandler extends EventEmitter {
try { try {
const devices = await navigator.mediaDevices.enumerateDevices(); const devices = await navigator.mediaDevices.enumerateDevices();
const output = {
[MediaDeviceKindEnum.AudioOutput]: [],
[MediaDeviceKindEnum.AudioInput]: [],
[MediaDeviceKindEnum.VideoInput]: [],
};
const audioOutput = []; devices.forEach((device) => output[device.kind].push(device));
const audioInput = []; return output;
const videoInput = [];
devices.forEach((device) => {
switch (device.kind) {
case 'audiooutput': audioOutput.push(device); break;
case 'audioinput': audioInput.push(device); break;
case 'videoinput': videoInput.push(device); break;
}
});
return { audioOutput, audioInput, videoInput };
} catch (error) { } catch (error) {
console.warn('Unable to refresh WebRTC Devices: ', error); console.warn('Unable to refresh WebRTC Devices: ', error);
} }
@ -106,6 +103,14 @@ export default class MediaDeviceHandler extends EventEmitter {
setMatrixCallVideoInput(deviceId); setMatrixCallVideoInput(deviceId);
} }
public setDevice(deviceId: string, kind: MediaDeviceKindEnum): void {
switch (kind) {
case MediaDeviceKindEnum.AudioOutput: this.setAudioOutput(deviceId); break;
case MediaDeviceKindEnum.AudioInput: this.setAudioInput(deviceId); break;
case MediaDeviceKindEnum.VideoInput: this.setVideoInput(deviceId); break;
}
}
public static getAudioOutput(): string { public static getAudioOutput(): string {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput"); return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
} }

View file

@ -18,10 +18,10 @@ limitations under the License.
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import { defer } from "matrix-js-sdk/src/utils";
import Analytics from './Analytics'; import Analytics from './Analytics';
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import { defer } from './utils/promise';
import AsyncWrapper from './AsyncWrapper'; import AsyncWrapper from './AsyncWrapper';
const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container";

View file

@ -27,7 +27,6 @@ import * as TextForEvent from './TextForEvent';
import Analytics from './Analytics'; import Analytics from './Analytics';
import * as Avatar from './Avatar'; import * as Avatar from './Avatar';
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import * as sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import Modal from './Modal'; import Modal from './Modal';
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
@ -37,6 +36,7 @@ import { isPushNotifyDisabled } from "./settings/controllers/NotificationControl
import RoomViewStore from "./stores/RoomViewStore"; import RoomViewStore from "./stores/RoomViewStore";
import UserActivity from "./UserActivity"; import UserActivity from "./UserActivity";
import { mediaFromMxc } from "./customisations/Media"; import { mediaFromMxc } from "./customisations/Media";
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
/* /*
* Dispatches: * Dispatches:
@ -240,7 +240,6 @@ export const Notifier = {
? _t('%(brand)s does not have permission to send you notifications - ' + ? _t('%(brand)s does not have permission to send you notifications - ' +
'please check your browser settings', { brand }) 'please check your browser settings', { brand })
: _t('%(brand)s was not given permission to send notifications - please try again', { brand }); : _t('%(brand)s was not given permission to send notifications - please try again', { brand });
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('Unable to enable Notifications', result, ErrorDialog, { Modal.createTrackedDialog('Unable to enable Notifications', result, ErrorDialog, {
title: _t('Unable to enable Notifications'), title: _t('Unable to enable Notifications'),
description, description,

View file

@ -22,13 +22,13 @@ import { User } from "matrix-js-sdk/src/models/user";
import { MatrixClientPeg } from './MatrixClientPeg'; import { MatrixClientPeg } from './MatrixClientPeg';
import MultiInviter, { CompletionStates } from './utils/MultiInviter'; import MultiInviter, { CompletionStates } from './utils/MultiInviter';
import Modal from './Modal'; import Modal from './Modal';
import * as sdk from './';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import InviteDialog, { KIND_DM, KIND_INVITE, Member } from "./components/views/dialogs/InviteDialog"; import InviteDialog, { KIND_DM, KIND_INVITE, Member } from "./components/views/dialogs/InviteDialog";
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog"; import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
import { CommunityPrototypeStore } from "./stores/CommunityPrototypeStore"; import { CommunityPrototypeStore } from "./stores/CommunityPrototypeStore";
import BaseAvatar from "./components/views/avatars/BaseAvatar"; import BaseAvatar from "./components/views/avatars/BaseAvatar";
import { mediaFromMxc } from "./customisations/Media"; import { mediaFromMxc } from "./customisations/Media";
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
export interface IInviteResult { export interface IInviteResult {
states: CompletionStates; states: CompletionStates;
@ -51,7 +51,6 @@ export function inviteMultipleToRoom(roomId: string, addresses: string[]): Promi
export function showStartChatInviteDialog(initialText = ""): void { export function showStartChatInviteDialog(initialText = ""): void {
// This dialog handles the room creation internally - we don't need to worry about it. // This dialog handles the room creation internally - we don't need to worry about it.
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
Modal.createTrackedDialog( Modal.createTrackedDialog(
'Start DM', '', InviteDialog, { kind: KIND_DM, initialText }, 'Start DM', '', InviteDialog, { kind: KIND_DM, initialText },
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
@ -111,7 +110,6 @@ export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise<vo
showAnyInviteErrors(result.states, room, result.inviter); showAnyInviteErrors(result.states, room, result.inviter);
}).catch((err) => { }).catch((err) => {
console.error(err.stack); console.error(err.stack);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
title: _t("Failed to invite"), title: _t("Failed to invite"),
description: ((err && err.message) ? err.message : _t("Operation failed")), description: ((err && err.message) ? err.message : _t("Operation failed")),
@ -131,7 +129,6 @@ export function showAnyInviteErrors(
// Just get the first message because there was a fatal problem on the first // Just get the first message because there was a fatal problem on the first
// user. This usually means that no other users were attempted, making it // user. This usually means that no other users were attempted, making it
// pointless for us to list who failed exactly. // pointless for us to list who failed exactly.
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite users to the room', '', ErrorDialog, { Modal.createTrackedDialog('Failed to invite users to the room', '', ErrorDialog, {
title: _t("Failed to invite users to the room:", { roomName: room.name }), title: _t("Failed to invite users to the room:", { roomName: room.name }),
description: inviter.getErrorText(failedUsers[0]), description: inviter.getErrorText(failedUsers[0]),
@ -178,7 +175,6 @@ export function showAnyInviteErrors(
</div> </div>
</div>; </div>;
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog("Some invites could not be sent", "", ErrorDialog, { Modal.createTrackedDialog("Some invites could not be sent", "", ErrorDialog, {
title: _t("Some invites couldn't be sent"), title: _t("Some invites couldn't be sent"),
description, description,

View file

@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ICryptoCallbacks, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix'; import { ICryptoCallbacks } from 'matrix-js-sdk/src/matrix';
import { ISecretStorageKeyInfo } from 'matrix-js-sdk/src/crypto/api';
import { MatrixClient } from 'matrix-js-sdk/src/client'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import Modal from './Modal'; import Modal from './Modal';
import * as sdk from './index'; import * as sdk from './index';
@ -42,8 +43,8 @@ let secretStorageBeingAccessed = false;
let nonInteractive = false; let nonInteractive = false;
let dehydrationCache: { let dehydrationCache: {
key?: Uint8Array, key?: Uint8Array;
keyInfo?: ISecretStorageKeyInfo, keyInfo?: ISecretStorageKeyInfo;
} = {}; } = {};
function isCachingAllowed(): boolean { function isCachingAllowed(): boolean {
@ -354,6 +355,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
throw new Error("Secret storage creation canceled"); throw new Error("Secret storage creation canceled");
} }
} else { } else {
// FIXME: Using an import will result in test failures
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
await cli.bootstrapCrossSigning({ await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest) => { authUploadDeviceSigningKeys: async (makeRequest) => {

View file

@ -23,7 +23,6 @@ import { User } from "matrix-js-sdk/src/models/user";
import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers'; import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers';
import { MatrixClientPeg } from './MatrixClientPeg'; import { MatrixClientPeg } from './MatrixClientPeg';
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import * as sdk from './index';
import { _t, _td } from './languageHandler'; import { _t, _td } from './languageHandler';
import Modal from './Modal'; import Modal from './Modal';
import MultiInviter from './utils/MultiInviter'; import MultiInviter from './utils/MultiInviter';
@ -50,6 +49,12 @@ import { UIFeature } from "./settings/UIFeature";
import { CHAT_EFFECTS } from "./effects"; import { CHAT_EFFECTS } from "./effects";
import CallHandler from "./CallHandler"; import CallHandler from "./CallHandler";
import { guessAndSetDMRoom } from "./Rooms"; import { guessAndSetDMRoom } from "./Rooms";
import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog';
import ErrorDialog from './components/views/dialogs/ErrorDialog';
import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog';
import RoomUpgradeWarningDialog from "./components/views/dialogs/RoomUpgradeWarningDialog";
import InfoDialog from "./components/views/dialogs/InfoDialog";
import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog";
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
interface HTMLInputEvent extends Event { interface HTMLInputEvent extends Event {
@ -63,7 +68,6 @@ const singleMxcUpload = async (): Promise<any> => {
fileSelector.onchange = (ev: HTMLInputEvent) => { fileSelector.onchange = (ev: HTMLInputEvent) => {
const file = ev.target.files[0]; const file = ev.target.files[0];
const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog");
Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, { Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, {
file, file,
onFinished: (shouldContinue) => { onFinished: (shouldContinue) => {
@ -246,7 +250,6 @@ export const Commands = [
args: '<query>', args: '<query>',
description: _td('Searches DuckDuckGo for results'), description: _td('Searches DuckDuckGo for results'),
runFn: function() { runFn: function() {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
// TODO Don't explain this away, actually show a search UI here. // TODO Don't explain this away, actually show a search UI here.
Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, { Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, {
title: _t('/ddg is not a command'), title: _t('/ddg is not a command'),
@ -269,8 +272,6 @@ export const Commands = [
return reject(_t("You do not have the required permissions to use this command.")); return reject(_t("You do not have the required permissions to use this command."));
} }
const RoomUpgradeWarningDialog = sdk.getComponent("dialogs.RoomUpgradeWarningDialog");
const { finished } = Modal.createTrackedDialog('Slash Commands', 'upgrade room confirmation', const { finished } = Modal.createTrackedDialog('Slash Commands', 'upgrade room confirmation',
RoomUpgradeWarningDialog, { roomId: roomId, targetVersion: args }, /*className=*/null, RoomUpgradeWarningDialog, { roomId: roomId, targetVersion: args }, /*className=*/null,
/*isPriority=*/false, /*isStatic=*/true); /*isPriority=*/false, /*isStatic=*/true);
@ -314,7 +315,6 @@ export const Commands = [
if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn); if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn);
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, { Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, {
title: _t('Error upgrading room'), title: _t('Error upgrading room'),
description: _t( description: _t(
@ -434,7 +434,6 @@ export const Commands = [
const topic = topicEvents && topicEvents.getContent().topic; const topic = topicEvents && topicEvents.getContent().topic;
const topicHtml = topic ? linkifyAndSanitizeHtml(topic) : _t('This room has no topic.'); const topicHtml = topic ? linkifyAndSanitizeHtml(topic) : _t('This room has no topic.');
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
Modal.createTrackedDialog('Slash Commands', 'Topic', InfoDialog, { Modal.createTrackedDialog('Slash Commands', 'Topic', InfoDialog, {
title: room.name, title: room.name,
description: <div dangerouslySetInnerHTML={{ __html: topicHtml }} />, description: <div dangerouslySetInnerHTML={{ __html: topicHtml }} />,
@ -737,7 +736,6 @@ export const Commands = [
ignoredUsers.push(userId); // de-duped internally in the js-sdk ignoredUsers.push(userId); // de-duped internally in the js-sdk
return success( return success(
cli.setIgnoredUsers(ignoredUsers).then(() => { cli.setIgnoredUsers(ignoredUsers).then(() => {
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
Modal.createTrackedDialog('Slash Commands', 'User ignored', InfoDialog, { Modal.createTrackedDialog('Slash Commands', 'User ignored', InfoDialog, {
title: _t('Ignored user'), title: _t('Ignored user'),
description: <div> description: <div>
@ -768,7 +766,6 @@ export const Commands = [
if (index !== -1) ignoredUsers.splice(index, 1); if (index !== -1) ignoredUsers.splice(index, 1);
return success( return success(
cli.setIgnoredUsers(ignoredUsers).then(() => { cli.setIgnoredUsers(ignoredUsers).then(() => {
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
Modal.createTrackedDialog('Slash Commands', 'User unignored', InfoDialog, { Modal.createTrackedDialog('Slash Commands', 'User unignored', InfoDialog, {
title: _t('Unignored user'), title: _t('Unignored user'),
description: <div> description: <div>
@ -838,7 +835,6 @@ export const Commands = [
command: 'devtools', command: 'devtools',
description: _td('Opens the Developer Tools dialog'), description: _td('Opens the Developer Tools dialog'),
runFn: function(roomId) { runFn: function(roomId) {
const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
Modal.createDialog(DevtoolsDialog, { roomId }); Modal.createDialog(DevtoolsDialog, { roomId });
return success(); return success();
}, },
@ -943,7 +939,6 @@ export const Commands = [
await cli.setDeviceVerified(userId, deviceId, true); await cli.setDeviceVerified(userId, deviceId, true);
// Tell the user we verified everything // Tell the user we verified everything
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, { Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, {
title: _t('Verified key'), title: _t('Verified key'),
description: <div> description: <div>
@ -1000,8 +995,6 @@ export const Commands = [
command: "help", command: "help",
description: _td("Displays list of commands with usages and descriptions"), description: _td("Displays list of commands with usages and descriptions"),
runFn: function() { runFn: function() {
const SlashCommandHelpDialog = sdk.getComponent('dialogs.SlashCommandHelpDialog');
Modal.createTrackedDialog('Slash Commands', 'Help', SlashCommandHelpDialog); Modal.createTrackedDialog('Slash Commands', 'Help', SlashCommandHelpDialog);
return success(); return success();
}, },
@ -1181,7 +1174,7 @@ export const Commands = [
]; ];
// build a map from names and aliases to the Command objects. // build a map from names and aliases to the Command objects.
export const CommandMap = new Map(); export const CommandMap = new Map<string, Command>();
Commands.forEach(cmd => { Commands.forEach(cmd => {
CommandMap.set(cmd.command, cmd); CommandMap.set(cmd.command, cmd);
cmd.aliases.forEach(alias => { cmd.aliases.forEach(alias => {
@ -1189,15 +1182,15 @@ Commands.forEach(cmd => {
}); });
}); });
export function parseCommandString(input: string) { export function parseCommandString(input: string): { cmd?: string, args?: string } {
// trim any trailing whitespace, as it can confuse the parser for // trim any trailing whitespace, as it can confuse the parser for
// IRC-style commands // IRC-style commands
input = input.replace(/\s+$/, ''); input = input.replace(/\s+$/, '');
if (input[0] !== '/') return {}; // not a command if (input[0] !== '/') return {}; // not a command
const bits = input.match(/^(\S+?)(?:[ \n]+((.|\n)*))?$/); const bits = input.match(/^(\S+?)(?:[ \n]+((.|\n)*))?$/);
let cmd; let cmd: string;
let args; let args: string;
if (bits) { if (bits) {
cmd = bits[1].substring(1).toLowerCase(); cmd = bits[1].substring(1).toLowerCase();
args = bits[2]; args = bits[2];
@ -1208,6 +1201,11 @@ export function parseCommandString(input: string) {
return { cmd, args }; return { cmd, args };
} }
interface ICmd {
cmd?: Command;
args?: string;
}
/** /**
* Process the given text for /commands and return a bound method to perform them. * Process the given text for /commands and return a bound method to perform them.
* @param {string} roomId The room in which the command was performed. * @param {string} roomId The room in which the command was performed.
@ -1216,7 +1214,7 @@ export function parseCommandString(input: string) {
* processing the command, or 'promise' if a request was sent out. * processing the command, or 'promise' if a request was sent out.
* Returns null if the input didn't match a command. * Returns null if the input didn't match a command.
*/ */
export function getCommand(input: string) { export function getCommand(input: string): ICmd {
const { cmd, args } = parseCommandString(input); const { cmd, args } = parseCommandString(input);
if (CommandMap.has(cmd) && CommandMap.get(cmd).isEnabled()) { if (CommandMap.has(cmd) && CommandMap.get(cmd).isEnabled()) {

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import classNames from 'classnames'; import classNames from 'classnames';
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
import { MatrixClientPeg } from './MatrixClientPeg'; import { MatrixClientPeg } from './MatrixClientPeg';
import * as sdk from '.'; import * as sdk from '.';
@ -32,7 +33,7 @@ export class Service {
* @param {string} baseUrl The Base URL of the service (ie. before '/_matrix') * @param {string} baseUrl The Base URL of the service (ie. before '/_matrix')
* @param {string} accessToken The user's access token for the service * @param {string} accessToken The user's access token for the service
*/ */
constructor(public serviceType: string, public baseUrl: string, public accessToken: string) { constructor(public serviceType: SERVICE_TYPES, public baseUrl: string, public accessToken: string) {
} }
} }
@ -48,13 +49,13 @@ export interface Policy {
} }
export type Policies = { export type Policies = {
[policy: string]: Policy, [policy: string]: Policy;
}; };
export type TermsInteractionCallback = ( export type TermsInteractionCallback = (
policiesAndServicePairs: { policiesAndServicePairs: {
service: Service, service: Service;
policies: Policies, policies: Policies;
}[], }[],
agreedUrls: string[], agreedUrls: string[],
extraClassNames?: string, extraClassNames?: string,
@ -180,14 +181,15 @@ export async function startTermsFlow(
export function dialogTermsInteractionCallback( export function dialogTermsInteractionCallback(
policiesAndServicePairs: { policiesAndServicePairs: {
service: Service, service: Service;
policies: { [policy: string]: Policy }, policies: { [policy: string]: Policy };
}[], }[],
agreedUrls: string[], agreedUrls: string[],
extraClassNames?: string, extraClassNames?: string,
): Promise<string[]> { ): Promise<string[]> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
console.log("Terms that need agreement", policiesAndServicePairs); console.log("Terms that need agreement", policiesAndServicePairs);
// FIXME: Using an import will result in test failures
const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog"); const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog");
Modal.createTrackedDialog('Terms of Service', '', TermsDialog, { Modal.createTrackedDialog('Terms of Service', '', TermsDialog, {

View file

@ -17,10 +17,10 @@ limitations under the License.
import * as React from "react"; import * as React from "react";
import classNames from "classnames"; import classNames from "classnames";
import * as sdk from "../index";
import Modal from "../Modal"; import Modal from "../Modal";
import { _t, _td } from "../languageHandler"; import { _t, _td } from "../languageHandler";
import { isMac, Key } from "../Keyboard"; import { isMac, Key } from "../Keyboard";
import InfoDialog from "../components/views/dialogs/InfoDialog";
// TS: once languageHandler is TS we can probably inline this into the enum // TS: once languageHandler is TS we can probably inline this into the enum
_td("Navigation"); _td("Navigation");
@ -375,7 +375,6 @@ export const toggleDialog = () => {
</div>; </div>;
}); });
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
activeModal = Modal.createTrackedDialog("Keyboard Shortcuts", "", InfoDialog, { activeModal = Modal.createTrackedDialog("Keyboard Shortcuts", "", InfoDialog, {
className: "mx_KeyboardShortcutsDialog", className: "mx_KeyboardShortcutsDialog",
title: _t("Keyboard Shortcuts"), title: _t("Keyboard Shortcuts"),

View file

@ -19,13 +19,13 @@ import { asyncAction } from './actionCreators';
import Modal from '../Modal'; import Modal from '../Modal';
import * as Rooms from '../Rooms'; import * as Rooms from '../Rooms';
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
import * as sdk from '../index';
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { AsyncActionPayload } from "../dispatcher/payloads"; import { AsyncActionPayload } from "../dispatcher/payloads";
import RoomListStore from "../stores/room-list/RoomListStore"; import RoomListStore from "../stores/room-list/RoomListStore";
import { SortAlgorithm } from "../stores/room-list/algorithms/models"; import { SortAlgorithm } from "../stores/room-list/algorithms/models";
import { DefaultTagID } from "../stores/room-list/models"; import { DefaultTagID } from "../stores/room-list/models";
import ErrorDialog from '../components/views/dialogs/ErrorDialog';
export default class RoomListActions { export default class RoomListActions {
/** /**
@ -88,7 +88,6 @@ export default class RoomListActions {
return Rooms.guessAndSetDMRoom( return Rooms.guessAndSetDMRoom(
room, newTag === DefaultTagID.DM, room, newTag === DefaultTagID.DM,
).catch((err) => { ).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to set direct chat tag " + err); console.error("Failed to set direct chat tag " + err);
Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, { Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, {
title: _t('Failed to set direct chat tag'), title: _t('Failed to set direct chat tag'),
@ -109,7 +108,6 @@ export default class RoomListActions {
const promiseToDelete = matrixClient.deleteRoomTag( const promiseToDelete = matrixClient.deleteRoomTag(
roomId, oldTag, roomId, oldTag,
).catch(function(err) { ).catch(function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to remove tag " + oldTag + " from room: " + err); console.error("Failed to remove tag " + oldTag + " from room: " + err);
Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, {
title: _t('Failed to remove tag %(tagName)s from room', { tagName: oldTag }), title: _t('Failed to remove tag %(tagName)s from room', { tagName: oldTag }),
@ -129,7 +127,6 @@ export default class RoomListActions {
metaData = metaData || {}; metaData = metaData || {};
const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) { const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to add tag " + newTag + " to room: " + err); console.error("Failed to add tag " + newTag + " to room: " + err);
Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, {
title: _t('Failed to add tag %(tagName)s to room', { tagName: newTag }), title: _t('Failed to add tag %(tagName)s to room', { tagName: newTag }),

View file

@ -15,7 +15,6 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import * as sdk from '../../../../index';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import SdkConfig from '../../../../SdkConfig'; import SdkConfig from '../../../../SdkConfig';
import SettingsStore from "../../../../settings/SettingsStore"; import SettingsStore from "../../../../settings/SettingsStore";
@ -24,6 +23,9 @@ import Modal from '../../../../Modal';
import { formatBytes, formatCountLong } from "../../../../utils/FormattingUtils"; import { formatBytes, formatCountLong } from "../../../../utils/FormattingUtils";
import EventIndexPeg from "../../../../indexing/EventIndexPeg"; import EventIndexPeg from "../../../../indexing/EventIndexPeg";
import { SettingLevel } from "../../../../settings/SettingLevel"; import { SettingLevel } from "../../../../settings/SettingLevel";
import Field from '../../../../components/views/elements/Field';
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
interface IProps { interface IProps {
onFinished: (confirmed: boolean) => void; onFinished: (confirmed: boolean) => void;
@ -145,7 +147,6 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
render() { render() {
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
const Field = sdk.getComponent('views.elements.Field');
let crawlerState; let crawlerState;
if (this.state.currentRoom === null) { if (this.state.currentRoom === null) {
@ -176,15 +177,12 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
<Field <Field
label={_t('Message downloading sleep time(ms)')} label={_t('Message downloading sleep time(ms)')}
type='number' type='number'
value={this.state.crawlerSleepTime} value={this.state.crawlerSleepTime.toString()}
onChange={this.onCrawlerSleepTimeChange} /> onChange={this.onCrawlerSleepTimeChange} />
</div> </div>
</div> </div>
); );
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return ( return (
<BaseDialog className='mx_ManageEventIndexDialog' <BaseDialog className='mx_ManageEventIndexDialog'
onFinished={this.props.onFinished} onFinished={this.props.onFinished}

View file

@ -22,12 +22,12 @@ import AutocompleteProvider from './AutocompleteProvider';
import { MatrixClientPeg } from '../MatrixClientPeg'; import { MatrixClientPeg } from '../MatrixClientPeg';
import QueryMatcher from './QueryMatcher'; import QueryMatcher from './QueryMatcher';
import { PillCompletion } from './Components'; import { PillCompletion } from './Components';
import * as sdk from '../index';
import { sortBy } from "lodash"; import { sortBy } from "lodash";
import { makeGroupPermalink } from "../utils/permalinks/Permalinks"; import { makeGroupPermalink } from "../utils/permalinks/Permalinks";
import { ICompletion, ISelectionRange } from "./Autocompleter"; import { ICompletion, ISelectionRange } from "./Autocompleter";
import FlairStore from "../stores/FlairStore"; import FlairStore from "../stores/FlairStore";
import { mediaFromMxc } from "../customisations/Media"; import { mediaFromMxc } from "../customisations/Media";
import BaseAvatar from '../components/views/avatars/BaseAvatar';
const COMMUNITY_REGEX = /\B\+\S*/g; const COMMUNITY_REGEX = /\B\+\S*/g;
@ -56,8 +56,6 @@ export default class CommunityProvider extends AutocompleteProvider {
force = false, force = false,
limit = -1, limit = -1,
): Promise<ICompletion[]> { ): Promise<ICompletion[]> {
const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar');
// Disable autocompletions when composing commands because of various issues // Disable autocompletions when composing commands because of various issues
// (see https://github.com/vector-im/element-web/issues/4762) // (see https://github.com/vector-im/element-web/issues/4762)
if (/^(\/join|\/leave)/.test(query)) { if (/^(\/join|\/leave)/.test(query)) {

View file

@ -21,8 +21,8 @@ import AutocompleteProvider from './AutocompleteProvider';
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
import { MatrixClientPeg } from '../MatrixClientPeg'; import { MatrixClientPeg } from '../MatrixClientPeg';
import { PillCompletion } from './Components'; import { PillCompletion } from './Components';
import * as sdk from '../index';
import { ICompletion, ISelectionRange } from "./Autocompleter"; import { ICompletion, ISelectionRange } from "./Autocompleter";
import RoomAvatar from '../components/views/avatars/RoomAvatar';
const AT_ROOM_REGEX = /@\S*/g; const AT_ROOM_REGEX = /@\S*/g;
@ -40,8 +40,6 @@ export default class NotifProvider extends AutocompleteProvider {
force = false, force = false,
limit = -1, limit = -1,
): Promise<ICompletion[]> { ): Promise<ICompletion[]> {
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (!this.room.currentState.mayTriggerNotifOfType('room', client.credentials.userId)) return []; if (!this.room.currentState.mayTriggerNotifOfType('room', client.credentials.userId)) return [];

View file

@ -21,7 +21,6 @@ import React from 'react';
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import { PillCompletion } from './Components'; import { PillCompletion } from './Components';
import * as sdk from '../index';
import QueryMatcher from './QueryMatcher'; import QueryMatcher from './QueryMatcher';
import { sortBy } from 'lodash'; import { sortBy } from 'lodash';
import { MatrixClientPeg } from '../MatrixClientPeg'; import { MatrixClientPeg } from '../MatrixClientPeg';
@ -33,6 +32,7 @@ import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
import { makeUserPermalink } from "../utils/permalinks/Permalinks"; import { makeUserPermalink } from "../utils/permalinks/Permalinks";
import { ICompletion, ISelectionRange } from "./Autocompleter"; import { ICompletion, ISelectionRange } from "./Autocompleter";
import MemberAvatar from '../components/views/avatars/MemberAvatar';
const USER_REGEX = /\B@\S*/g; const USER_REGEX = /\B@\S*/g;
@ -108,8 +108,6 @@ export default class UserProvider extends AutocompleteProvider {
force = false, force = false,
limit = -1, limit = -1,
): Promise<ICompletion[]> { ): Promise<ICompletion[]> {
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
// lazy-load user list into matcher // lazy-load user list into matcher
if (!this.users) this._makeUsers(); if (!this.users) this._makeUsers();

View file

@ -21,8 +21,8 @@ interface IProps extends Omit<HTMLAttributes<HTMLDivElement>, "onScroll"> {
className?: string; className?: string;
onScroll?: (event: Event) => void; onScroll?: (event: Event) => void;
onWheel?: (event: WheelEvent) => void; onWheel?: (event: WheelEvent) => void;
style?: React.CSSProperties style?: React.CSSProperties;
tabIndex?: number, tabIndex?: number;
wrappedRef?: (ref: HTMLDivElement) => void; wrappedRef?: (ref: HTMLDivElement) => void;
} }

View file

@ -19,11 +19,11 @@ import React from 'react';
import { Filter } from 'matrix-js-sdk/src/filter'; import { Filter } from 'matrix-js-sdk/src/filter';
import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set"; import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set";
import { Direction } from "matrix-js-sdk/src/models/event-timeline";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from 'matrix-js-sdk/src/models/room'; import { Room } from 'matrix-js-sdk/src/models/room';
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window'; import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
import * as sdk from '../../index';
import { MatrixClientPeg } from '../../MatrixClientPeg'; import { MatrixClientPeg } from '../../MatrixClientPeg';
import EventIndexPeg from "../../indexing/EventIndexPeg"; import EventIndexPeg from "../../indexing/EventIndexPeg";
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
@ -33,11 +33,14 @@ import DesktopBuildsNotice, { WarningKind } from "../views/elements/DesktopBuild
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import ResizeNotifier from '../../utils/ResizeNotifier'; import ResizeNotifier from '../../utils/ResizeNotifier';
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
import { TileShape } from '../views/rooms/EventTile';
interface IProps { interface IProps {
roomId: string; roomId: string;
onClose: () => void; onClose: () => void;
resizeNotifier: ResizeNotifier resizeNotifier: ResizeNotifier;
} }
interface IState { interface IState {
@ -129,7 +132,7 @@ class FilePanel extends React.Component<IProps, IState> {
} }
} }
public async fetchFileEventsServer(room: Room): Promise<void> { public async fetchFileEventsServer(room: Room): Promise<EventTimelineSet> {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const filter = new Filter(client.credentials.userId); const filter = new Filter(client.credentials.userId);
@ -153,7 +156,11 @@ class FilePanel extends React.Component<IProps, IState> {
return timelineSet; return timelineSet;
} }
private onPaginationRequest = (timelineWindow: TimelineWindow, direction: string, limit: number): void => { private onPaginationRequest = (
timelineWindow: TimelineWindow,
direction: Direction,
limit: number,
): Promise<boolean> => {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const eventIndex = EventIndexPeg.get(); const eventIndex = EventIndexPeg.get();
const roomId = this.props.roomId; const roomId = this.props.roomId;
@ -232,8 +239,6 @@ class FilePanel extends React.Component<IProps, IState> {
} }
// wrap a TimelinePanel with the jump-to-event bits turned off. // wrap a TimelinePanel with the jump-to-event bits turned off.
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
const Loader = sdk.getComponent("elements.Spinner");
const emptyState = (<div className="mx_RightPanel_empty mx_FilePanel_empty"> const emptyState = (<div className="mx_RightPanel_empty mx_FilePanel_empty">
<h2>{_t('No files visible in this room')}</h2> <h2>{_t('No files visible in this room')}</h2>
@ -259,7 +264,7 @@ class FilePanel extends React.Component<IProps, IState> {
timelineSet={this.state.timelineSet} timelineSet={this.state.timelineSet}
showUrlPreview = {false} showUrlPreview = {false}
onPaginationRequest={this.onPaginationRequest} onPaginationRequest={this.onPaginationRequest}
tileShape="file_grid" tileShape={TileShape.FileGrid}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
empty={emptyState} empty={emptyState}
/> />
@ -272,7 +277,7 @@ class FilePanel extends React.Component<IProps, IState> {
onClose={this.props.onClose} onClose={this.props.onClose}
previousPhase={RightPanelPhases.RoomSummary} previousPhase={RightPanelPhases.RoomSummary}
> >
<Loader /> <Spinner />
</BaseCard> </BaseCard>
); );
} }

View file

@ -36,7 +36,7 @@ import FlairStore from '../../stores/FlairStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
import { makeGroupPermalink, makeUserPermalink } from "../../utils/permalinks/Permalinks"; import { makeGroupPermalink, makeUserPermalink } from "../../utils/permalinks/Permalinks";
import { Group } from "matrix-js-sdk/src/models/group"; import { Group } from "matrix-js-sdk/src/models/group";
import { sleep } from "../../utils/promise"; import { sleep } from "matrix-js-sdk/src/utils";
import RightPanelStore from "../../stores/RightPanelStore"; import RightPanelStore from "../../stores/RightPanelStore";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import { mediaFromMxc } from "../../customisations/Media"; import { mediaFromMxc } from "../../customisations/Media";

View file

@ -96,6 +96,7 @@ const HomePage: React.FC<IProps> = ({ justRegistered = false }) => {
const pageUrl = getHomePageUrl(config); const pageUrl = getHomePageUrl(config);
if (pageUrl) { if (pageUrl) {
// FIXME: Using an import will result in wrench-element-tests failures
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage'); const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
return <EmbeddedPage className="mx_HomePage" url={pageUrl} scrollbar={true} />; return <EmbeddedPage className="mx_HomePage" url={pageUrl} scrollbar={true} />;
} }

View file

@ -24,7 +24,6 @@ import { Key } from '../../Keyboard';
import PageTypes from '../../PageTypes'; import PageTypes from '../../PageTypes';
import MediaDeviceHandler from '../../MediaDeviceHandler'; import MediaDeviceHandler from '../../MediaDeviceHandler';
import { fixupColorFonts } from '../../utils/FontManager'; import { fixupColorFonts } from '../../utils/FontManager';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import { IMatrixClientCreds } from '../../MatrixClientPeg'; import { IMatrixClientCreds } from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
@ -48,7 +47,7 @@ import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPay
import RoomListStore from "../../stores/room-list/RoomListStore"; import RoomListStore from "../../stores/room-list/RoomListStore";
import NonUrgentToastContainer from "./NonUrgentToastContainer"; import NonUrgentToastContainer from "./NonUrgentToastContainer";
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload"; import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
import { IThreepidInvite } from "../../stores/ThreepidInviteStore"; import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
import Modal from "../../Modal"; import Modal from "../../Modal";
import { ICollapseConfig } from "../../resizer/distributors/collapse"; import { ICollapseConfig } from "../../resizer/distributors/collapse";
import HostSignupContainer from '../views/host_signup/HostSignupContainer'; import HostSignupContainer from '../views/host_signup/HostSignupContainer';
@ -59,6 +58,11 @@ import { replaceableComponent } from "../../utils/replaceableComponent";
import CallHandler, { CallHandlerEvent } from '../../CallHandler'; import CallHandler, { CallHandlerEvent } from '../../CallHandler';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall'; import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
import RoomView from './RoomView';
import ToastContainer from './ToastContainer';
import MyGroups from "./MyGroups";
import UserView from "./UserView";
import GroupView from "./GroupView";
// We need to fetch each pinned message individually (if we don't already have it) // We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity. // so each pinned message may trigger a request. Limit the number per room for sanity.
@ -78,17 +82,17 @@ interface IProps {
hideToSRUsers: boolean; hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
page_type: string; page_type?: string;
autoJoin: boolean; autoJoin?: boolean;
threepidInvite?: IThreepidInvite; threepidInvite?: IThreepidInvite;
roomOobData?: object; roomOobData?: IOOBData;
currentRoomId: string; currentRoomId: string;
collapseLhs: boolean; collapseLhs: boolean;
config: { config: {
piwik: { piwik: {
policyUrl: string; policyUrl: string;
}, };
[key: string]: any, [key: string]: any;
}; };
currentUserId?: string; currentUserId?: string;
currentGroupId?: string; currentGroupId?: string;
@ -394,7 +398,7 @@ class LoggedInView extends React.Component<IProps, IState> {
// refocusing during a paste event will make the // refocusing during a paste event will make the
// paste end up in the newly focused element, // paste end up in the newly focused element,
// so dispatch synchronously before paste happens // so dispatch synchronously before paste happens
dis.fire(Action.FocusComposer, true); dis.fire(Action.FocusSendMessageComposer, true);
} }
}; };
@ -548,7 +552,7 @@ class LoggedInView extends React.Component<IProps, IState> {
if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) { if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) {
// synchronous dispatch so we focus before key generates input // synchronous dispatch so we focus before key generates input
dis.fire(Action.FocusComposer, true); dis.fire(Action.FocusSendMessageComposer, true);
ev.stopPropagation(); ev.stopPropagation();
// we should *not* preventDefault() here as // we should *not* preventDefault() here as
// that would prevent typing in the now-focussed composer // that would prevent typing in the now-focussed composer
@ -567,12 +571,6 @@ class LoggedInView extends React.Component<IProps, IState> {
}; };
render() { render() {
const RoomView = sdk.getComponent('structures.RoomView');
const UserView = sdk.getComponent('structures.UserView');
const GroupView = sdk.getComponent('structures.GroupView');
const MyGroups = sdk.getComponent('structures.MyGroups');
const ToastContainer = sdk.getComponent('structures.ToastContainer');
let pageElement; let pageElement;
switch (this.props.page_type) { switch (this.props.page_type) {

View file

@ -19,6 +19,8 @@ import { createClient } from "matrix-js-sdk/src/matrix";
import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { sleep, defer, IDeferred } from "matrix-js-sdk/src/utils";
// 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 _AccessibleButton.scss
import 'focus-visible'; import 'focus-visible';
// what-input helps improve keyboard accessibility // what-input helps improve keyboard accessibility
@ -34,7 +36,6 @@ import dis from "../../dispatcher/dispatcher";
import Notifier from '../../Notifier'; import Notifier from '../../Notifier';
import Modal from "../../Modal"; import Modal from "../../Modal";
import * as sdk from '../../index';
import { showRoomInviteDialog, showStartChatInviteDialog } from '../../RoomInvite'; import { showRoomInviteDialog, showStartChatInviteDialog } from '../../RoomInvite';
import * as Rooms from '../../Rooms'; import * as Rooms from '../../Rooms';
import linkifyMatrix from "../../linkify-matrix"; import linkifyMatrix from "../../linkify-matrix";
@ -55,7 +56,6 @@ import DMRoomMap from '../../utils/DMRoomMap';
import ThemeWatcher from "../../settings/watchers/ThemeWatcher"; import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
import { FontWatcher } from '../../settings/watchers/FontWatcher'; import { FontWatcher } from '../../settings/watchers/FontWatcher';
import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { storeRoomAliasInCache } from '../../RoomAliasCache';
import { defer, IDeferred, sleep } from "../../utils/promise";
import ToastStore from "../../stores/ToastStore"; import ToastStore from "../../stores/ToastStore";
import * as StorageManager from "../../utils/StorageManager"; import * as StorageManager from "../../utils/StorageManager";
import type LoggedInViewType from "./LoggedInView"; import type LoggedInViewType from "./LoggedInView";
@ -84,9 +84,27 @@ import RoomListStore from "../../stores/room-list/RoomListStore";
import { RoomUpdateCause } from "../../stores/room-list/models"; import { RoomUpdateCause } from "../../stores/room-list/models";
import defaultDispatcher from "../../dispatcher/dispatcher"; import defaultDispatcher from "../../dispatcher/dispatcher";
import SecurityCustomisations from "../../customisations/Security"; import SecurityCustomisations from "../../customisations/Security";
import Spinner from "../views/elements/Spinner";
import QuestionDialog from "../views/dialogs/QuestionDialog";
import UserSettingsDialog from '../views/dialogs/UserSettingsDialog';
import CreateGroupDialog from '../views/dialogs/CreateGroupDialog';
import CreateRoomDialog from '../views/dialogs/CreateRoomDialog';
import RoomDirectory from './RoomDirectory';
import KeySignatureUploadFailedDialog from "../views/dialogs/KeySignatureUploadFailedDialog";
import IncomingSasDialog from "../views/dialogs/IncomingSasDialog";
import CompleteSecurity from "./auth/CompleteSecurity";
import LoggedInView from './LoggedInView';
import Welcome from "../views/auth/Welcome";
import ForgotPassword from "./auth/ForgotPassword";
import E2eSetup from "./auth/E2eSetup";
import Registration from './auth/Registration';
import Login from "./auth/Login";
import ErrorBoundary from '../views/elements/ErrorBoundary';
import VerificationRequestToast from '../views/toasts/VerificationRequestToast';
import PerformanceMonitor, { PerformanceEntryNames } from "../../performance"; import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
import UIStore, { UI_EVENTS } from "../../stores/UIStore"; import UIStore, { UI_EVENTS } from "../../stores/UIStore";
import SoftLogout from './auth/SoftLogout';
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
@ -155,7 +173,12 @@ interface IRoomInfo {
/* eslint-enable camelcase */ /* eslint-enable camelcase */
interface IProps { // TODO type things better interface IProps { // TODO type things better
config: Record<string, any>; config: {
piwik: {
policyUrl: string;
};
[key: string]: any;
};
serverConfig?: ValidatedServerConfig; serverConfig?: ValidatedServerConfig;
onNewScreen: (screen: string, replaceLast: boolean) => void; onNewScreen: (screen: string, replaceLast: boolean) => void;
enableGuest?: boolean; enableGuest?: boolean;
@ -203,7 +226,7 @@ interface IState {
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
serverConfig?: ValidatedServerConfig; serverConfig?: ValidatedServerConfig;
ready: boolean; ready: boolean;
threepidInvite?: IThreepidInvite, threepidInvite?: IThreepidInvite;
roomOobData?: object; roomOobData?: object;
pendingInitialSync?: boolean; pendingInitialSync?: boolean;
justRegistered?: boolean; justRegistered?: boolean;
@ -420,7 +443,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
CountlyAnalytics.instance.trackPageChange(durationMs); CountlyAnalytics.instance.trackPageChange(durationMs);
} }
if (this.focusComposer) { if (this.focusComposer) {
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
this.focusComposer = false; this.focusComposer = false;
} }
} }
@ -518,7 +541,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onAction = (payload) => { onAction = (payload) => {
// console.log(`MatrixClientPeg.onAction: ${payload.action}`); // console.log(`MatrixClientPeg.onAction: ${payload.action}`);
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
// Start the onboarding process for certain actions // Start the onboarding process for certain actions
if (MatrixClientPeg.get() && MatrixClientPeg.get().isGuest() && if (MatrixClientPeg.get() && MatrixClientPeg.get().isGuest() &&
@ -612,8 +634,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onFinished: (confirm) => { onFinished: (confirm) => {
if (confirm) { if (confirm) {
// FIXME: controller shouldn't be loading a view :( // FIXME: controller shouldn't be loading a view :(
const Loader = sdk.getComponent("elements.Spinner"); const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
MatrixClientPeg.get().leave(payload.room_id).then(() => { MatrixClientPeg.get().leave(payload.room_id).then(() => {
modal.close(); modal.close();
@ -649,7 +670,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
case Action.ViewUserSettings: { case Action.ViewUserSettings: {
const tabPayload = payload as OpenToTabPayload; const tabPayload = payload as OpenToTabPayload;
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, Modal.createTrackedDialog('User settings', '', UserSettingsDialog,
{ initialTabId: tabPayload.initialTabId }, { initialTabId: tabPayload.initialTabId },
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true); /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
@ -662,11 +682,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.createRoom(payload.public, payload.defaultName); this.createRoom(payload.public, payload.defaultName);
break; break;
case 'view_create_group': { case 'view_create_group': {
let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog"); const prototype = SettingsStore.getValue("feature_communities_v2_prototypes");
if (SettingsStore.getValue("feature_communities_v2_prototypes")) { Modal.createTrackedDialog(
CreateGroupDialog = CreateCommunityPrototypeDialog; 'Create Community',
} '',
Modal.createTrackedDialog('Create Community', '', CreateGroupDialog); prototype ? CreateCommunityPrototypeDialog : CreateGroupDialog,
);
break; break;
} }
case Action.ViewRoomDirectory: { case Action.ViewRoomDirectory: {
@ -676,7 +697,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
room_id: SpaceStore.instance.activeSpace.roomId, room_id: SpaceStore.instance.activeSpace.roomId,
}); });
} else { } else {
const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
Modal.createTrackedDialog('Room directory', '', RoomDirectory, { Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
initialText: payload.initialText, initialText: payload.initialText,
}, 'mx_RoomDirectory_dialogWrapper', false, true); }, 'mx_RoomDirectory_dialogWrapper', false, true);
@ -1018,7 +1038,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
} }
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
defaultPublic, defaultPublic,
defaultName, defaultName,
@ -1115,7 +1134,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
private leaveRoom(roomId: string) { private leaveRoom(roomId: string) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const roomToLeave = MatrixClientPeg.get().getRoom(roomId); const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
const warnings = this.leaveRoomWarnings(roomId); const warnings = this.leaveRoomWarnings(roomId);
@ -1142,8 +1160,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const d = leaveRoomBehaviour(roomId); const d = leaveRoomBehaviour(roomId);
// FIXME: controller shouldn't be loading a view :( // FIXME: controller shouldn't be loading a view :(
const Loader = sdk.getComponent("elements.Spinner"); const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
d.finally(() => modal.close()); d.finally(() => modal.close());
dis.dispatch({ dis.dispatch({
@ -1410,7 +1427,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
showNotificationsToast(false); showNotificationsToast(false);
} }
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
this.setState({ this.setState({
ready: true, ready: true,
}); });
@ -1438,7 +1455,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}); });
}); });
cli.on('no_consent', function(message, consentUri) { cli.on('no_consent', function(message, consentUri) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('No Consent Dialog', '', QuestionDialog, { Modal.createTrackedDialog('No Consent Dialog', '', QuestionDialog, {
title: _t('Terms and Conditions'), title: _t('Terms and Conditions'),
description: <div> description: <div>
@ -1547,8 +1563,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}); });
cli.on("crypto.keySignatureUploadFailure", (failures, source, continuation) => { cli.on("crypto.keySignatureUploadFailure", (failures, source, continuation) => {
const KeySignatureUploadFailedDialog =
sdk.getComponent('views.dialogs.KeySignatureUploadFailedDialog');
Modal.createTrackedDialog( Modal.createTrackedDialog(
'Failed to upload key signatures', 'Failed to upload key signatures',
'Failed to upload key signatures', 'Failed to upload key signatures',
@ -1558,7 +1572,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
cli.on("crypto.verification.request", request => { cli.on("crypto.verification.request", request => {
if (request.verifier) { if (request.verifier) {
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
verifier: request.verifier, verifier: request.verifier,
}, null, /* priority = */ false, /* static = */ true); }, null, /* priority = */ false, /* static = */ true);
@ -1568,7 +1581,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
title: _t("Verification requested"), title: _t("Verification requested"),
icon: "verification", icon: "verification",
props: { request }, props: { request },
component: sdk.getComponent("toasts.VerificationRequestToast"), component: VerificationRequestToast,
priority: 90, priority: 90,
}); });
} }
@ -1976,21 +1989,18 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
let view = null; let view = null;
if (this.state.view === Views.LOADING) { if (this.state.view === Views.LOADING) {
const Spinner = sdk.getComponent('elements.Spinner');
view = ( view = (
<div className="mx_MatrixChat_splash"> <div className="mx_MatrixChat_splash">
<Spinner /> <Spinner />
</div> </div>
); );
} else if (this.state.view === Views.COMPLETE_SECURITY) { } else if (this.state.view === Views.COMPLETE_SECURITY) {
const CompleteSecurity = sdk.getComponent('structures.auth.CompleteSecurity');
view = ( view = (
<CompleteSecurity <CompleteSecurity
onFinished={this.onCompleteSecurityE2eSetupFinished} onFinished={this.onCompleteSecurityE2eSetupFinished}
/> />
); );
} else if (this.state.view === Views.E2E_SETUP) { } else if (this.state.view === Views.E2E_SETUP) {
const E2eSetup = sdk.getComponent('structures.auth.E2eSetup');
view = ( view = (
<E2eSetup <E2eSetup
onFinished={this.onCompleteSecurityE2eSetupFinished} onFinished={this.onCompleteSecurityE2eSetupFinished}
@ -2011,7 +2021,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
* we should go through and figure out what we actually need to pass down, as well * we should go through and figure out what we actually need to pass down, as well
* as using something like redux to avoid having a billion bits of state kicking around. * as using something like redux to avoid having a billion bits of state kicking around.
*/ */
const LoggedInView = sdk.getComponent('structures.LoggedInView');
view = ( view = (
<LoggedInView <LoggedInView
{...this.props} {...this.props}
@ -2019,14 +2028,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
ref={this.loggedInView} ref={this.loggedInView}
matrixClient={MatrixClientPeg.get()} matrixClient={MatrixClientPeg.get()}
onRoomCreated={this.onRoomCreated} onRoomCreated={this.onRoomCreated}
onCloseAllSettings={this.onCloseAllSettings}
onRegistered={this.onRegistered} onRegistered={this.onRegistered}
currentRoomId={this.state.currentRoomId} currentRoomId={this.state.currentRoomId}
/> />
); );
} else { } else {
// we think we are logged in, but are still waiting for the /sync to complete // we think we are logged in, but are still waiting for the /sync to complete
const Spinner = sdk.getComponent('elements.Spinner');
let errorBox; let errorBox;
if (this.state.syncError && !isStoreError) { if (this.state.syncError && !isStoreError) {
errorBox = <div className="mx_MatrixChat_syncError"> errorBox = <div className="mx_MatrixChat_syncError">
@ -2044,10 +2051,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
); );
} }
} else if (this.state.view === Views.WELCOME) { } else if (this.state.view === Views.WELCOME) {
const Welcome = sdk.getComponent('auth.Welcome');
view = <Welcome />; view = <Welcome />;
} else if (this.state.view === Views.REGISTER && SettingsStore.getValue(UIFeature.Registration)) { } else if (this.state.view === Views.REGISTER && SettingsStore.getValue(UIFeature.Registration)) {
const Registration = sdk.getComponent('structures.auth.Registration');
const email = ThreepidInviteStore.instance.pickBestInvite()?.toEmail; const email = ThreepidInviteStore.instance.pickBestInvite()?.toEmail;
view = ( view = (
<Registration <Registration
@ -2066,7 +2071,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
/> />
); );
} else if (this.state.view === Views.FORGOT_PASSWORD && SettingsStore.getValue(UIFeature.PasswordReset)) { } else if (this.state.view === Views.FORGOT_PASSWORD && SettingsStore.getValue(UIFeature.PasswordReset)) {
const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword');
view = ( view = (
<ForgotPassword <ForgotPassword
onComplete={this.onLoginClick} onComplete={this.onLoginClick}
@ -2077,7 +2081,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
); );
} else if (this.state.view === Views.LOGIN) { } else if (this.state.view === Views.LOGIN) {
const showPasswordReset = SettingsStore.getValue(UIFeature.PasswordReset); const showPasswordReset = SettingsStore.getValue(UIFeature.PasswordReset);
const Login = sdk.getComponent('structures.auth.Login');
view = ( view = (
<Login <Login
isSyncing={this.state.pendingInitialSync} isSyncing={this.state.pendingInitialSync}
@ -2093,7 +2096,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
/> />
); );
} else if (this.state.view === Views.SOFT_LOGOUT) { } else if (this.state.view === Views.SOFT_LOGOUT) {
const SoftLogout = sdk.getComponent('structures.auth.SoftLogout');
view = ( view = (
<SoftLogout <SoftLogout
realQueryParams={this.props.realQueryParams} realQueryParams={this.props.realQueryParams}
@ -2105,7 +2107,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
console.error(`Unknown view ${this.state.view}`); console.error(`Unknown view ${this.state.view}`);
} }
const ErrorBoundary = sdk.getComponent('elements.ErrorBoundary');
return <ErrorBoundary> return <ErrorBoundary>
{view} {view}
</ErrorBoundary>; </ErrorBoundary>;

View file

@ -24,7 +24,7 @@ interface IProps {
} }
interface IState { interface IState {
toasts: ComponentClass[], toasts: ComponentClass[];
} }
@replaceableComponent("structures.NonUrgentToastContainer") @replaceableComponent("structures.NonUrgentToastContainer")

View file

@ -23,7 +23,6 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import RateLimitedFunc from '../../ratelimitedfunc';
import GroupStore from '../../stores/GroupStore'; import GroupStore from '../../stores/GroupStore';
import { import {
RIGHT_PANEL_PHASES_NO_ARGS, RIGHT_PANEL_PHASES_NO_ARGS,
@ -48,6 +47,7 @@ import FilePanel from "./FilePanel";
import NotificationPanel from "./NotificationPanel"; import NotificationPanel from "./NotificationPanel";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
import { throttle } from 'lodash';
interface IProps { interface IProps {
room?: Room; // if showing panels for a given room, this is set room?: Room; // if showing panels for a given room, this is set
@ -73,7 +73,6 @@ interface IState {
export default class RightPanel extends React.Component<IProps, IState> { export default class RightPanel extends React.Component<IProps, IState> {
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
private readonly delayedUpdate: RateLimitedFunc;
private dispatcherRef: string; private dispatcherRef: string;
constructor(props, context) { constructor(props, context) {
@ -84,12 +83,12 @@ export default class RightPanel extends React.Component<IProps, IState> {
isUserPrivilegedInGroup: null, isUserPrivilegedInGroup: null,
member: this.getUserForPanel(), member: this.getUserForPanel(),
}; };
this.delayedUpdate = new RateLimitedFunc(() => {
this.forceUpdate();
}, 500);
} }
private readonly delayedUpdate = throttle((): void => {
this.forceUpdate();
}, 500, { leading: true, trailing: true });
// Helper function to split out the logic for getPhaseFromProps() and the constructor // Helper function to split out the logic for getPhaseFromProps() and the constructor
// as both are called at the same time in the constructor. // as both are called at the same time in the constructor.
private getUserForPanel() { private getUserForPanel() {

View file

@ -48,6 +48,9 @@ import { ActionPayload } from "../../dispatcher/payloads";
const MAX_NAME_LENGTH = 80; const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 800; const MAX_TOPIC_LENGTH = 800;
const LAST_SERVER_KEY = "mx_last_room_directory_server";
const LAST_INSTANCE_KEY = "mx_last_room_directory_instance";
function track(action: string) { function track(action: string) {
Analytics.trackEvent('RoomDirectory', action); Analytics.trackEvent('RoomDirectory', action);
} }
@ -61,7 +64,7 @@ interface IState {
loading: boolean; loading: boolean;
protocolsLoading: boolean; protocolsLoading: boolean;
error?: string; error?: string;
instanceId: string | symbol; instanceId: string;
roomServer: string; roomServer: string;
filterString: string; filterString: string;
selectedCommunityId?: string; selectedCommunityId?: string;
@ -116,6 +119,36 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
} else if (!selectedCommunityId) { } else if (!selectedCommunityId) {
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => { MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
this.protocols = response; this.protocols = response;
const myHomeserver = MatrixClientPeg.getHomeserverName();
const lsRoomServer = localStorage.getItem(LAST_SERVER_KEY);
const lsInstanceId = localStorage.getItem(LAST_INSTANCE_KEY);
let roomServer = myHomeserver;
if (
SdkConfig.get().roomDirectory?.servers?.includes(lsRoomServer) ||
SettingsStore.getValue("room_directory_servers")?.includes(lsRoomServer)
) {
roomServer = lsRoomServer;
}
let instanceId: string = null;
if (roomServer === myHomeserver && (
lsInstanceId === ALL_ROOMS ||
Object.values(this.protocols).some(p => p.instances.some(i => i.instance_id === lsInstanceId))
)) {
instanceId = lsInstanceId;
}
// Refresh the room list only if validation failed and we had to change these
if (this.state.instanceId !== instanceId || this.state.roomServer !== roomServer) {
this.setState({
protocolsLoading: false,
instanceId,
roomServer,
});
this.refreshRoomList();
return;
}
this.setState({ protocolsLoading: false }); this.setState({ protocolsLoading: false });
}, (err) => { }, (err) => {
console.warn(`error loading third party protocols: ${err}`); console.warn(`error loading third party protocols: ${err}`);
@ -150,8 +183,8 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
publicRooms: [], publicRooms: [],
loading: true, loading: true,
error: null, error: null,
instanceId: undefined, instanceId: localStorage.getItem(LAST_INSTANCE_KEY),
roomServer: MatrixClientPeg.getHomeserverName(), roomServer: localStorage.getItem(LAST_SERVER_KEY),
filterString: this.props.initialText || "", filterString: this.props.initialText || "",
selectedCommunityId, selectedCommunityId,
communityName: null, communityName: null,
@ -342,7 +375,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
} }
}; };
private onOptionChange = (server: string, instanceId?: string | symbol) => { private onOptionChange = (server: string, instanceId?: string) => {
// clear next batch so we don't try to load more rooms // clear next batch so we don't try to load more rooms
this.nextBatch = null; this.nextBatch = null;
this.setState({ this.setState({
@ -360,6 +393,14 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
// find the five gitter ones, at which point we do not want // find the five gitter ones, at which point we do not want
// to render all those rooms when switching back to 'all networks'. // to render all those rooms when switching back to 'all networks'.
// Easiest to just blow away the state & re-fetch. // Easiest to just blow away the state & re-fetch.
// We have to be careful here so that we don't set instanceId = "undefined"
localStorage.setItem(LAST_SERVER_KEY, server);
if (instanceId) {
localStorage.setItem(LAST_INSTANCE_KEY, instanceId);
} else {
localStorage.removeItem(LAST_INSTANCE_KEY);
}
}; };
private onFillRequest = (backwards: boolean) => { private onFillRequest = (backwards: boolean) => {
@ -370,7 +411,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
private onFilterChange = (alias: string) => { private onFilterChange = (alias: string) => {
this.setState({ this.setState({
filterString: alias || null, filterString: alias || "",
}); });
// don't send the request for a little bit, // don't send the request for a little bit,
@ -389,7 +430,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
private onFilterClear = () => { private onFilterClear = () => {
// update immediately // update immediately
this.setState({ this.setState({
filterString: null, filterString: "",
}, this.refreshRoomList); }, this.refreshRoomList);
if (this.filterTimeout) { if (this.filterTimeout) {

View file

@ -131,7 +131,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
switch (action) { switch (action) {
case RoomListAction.ClearSearch: case RoomListAction.ClearSearch:
this.clearInput(); this.clearInput();
defaultDispatcher.fire(Action.FocusComposer); defaultDispatcher.fire(Action.FocusSendMessageComposer);
break; break;
case RoomListAction.NextRoom: case RoomListAction.NextRoom:
case RoomListAction.PrevRoom: case RoomListAction.PrevRoom:

View file

@ -118,12 +118,12 @@ export default class RoomStatusBar extends React.PureComponent {
this.setState({ isResending: false }); this.setState({ isResending: false });
}); });
this.setState({ isResending: true }); this.setState({ isResending: true });
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
}; };
_onCancelAllClick = () => { _onCancelAllClick = () => {
Resend.cancelUnsentEvents(this.props.room); Resend.cancelUnsentEvents(this.props.room);
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
}; };
_onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => { _onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => {

View file

@ -34,16 +34,14 @@ import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
import ResizeNotifier from '../../utils/ResizeNotifier'; import ResizeNotifier from '../../utils/ResizeNotifier';
import ContentMessages from '../../ContentMessages'; import ContentMessages from '../../ContentMessages';
import Modal from '../../Modal'; import Modal from '../../Modal';
import * as sdk from '../../index';
import CallHandler, { PlaceCallType } from '../../CallHandler'; import CallHandler, { PlaceCallType } from '../../CallHandler';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import rateLimitedFunc from '../../ratelimitedfunc';
import * as Rooms from '../../Rooms'; import * as Rooms from '../../Rooms';
import eventSearch, { searchPagination } from '../../Searching'; import eventSearch, { searchPagination } from '../../Searching';
import MainSplit from './MainSplit'; import MainSplit from './MainSplit';
import RightPanel from './RightPanel'; import RightPanel from './RightPanel';
import RoomViewStore from '../../stores/RoomViewStore'; import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; import RoomScrollStateStore, { ScrollState } from '../../stores/RoomScrollStateStore';
import WidgetEchoStore from '../../stores/WidgetEchoStore'; import WidgetEchoStore from '../../stores/WidgetEchoStore';
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import { Layout } from "../../settings/Layout"; import { Layout } from "../../settings/Layout";
@ -64,7 +62,7 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
import AuxPanel from "../views/rooms/AuxPanel"; import AuxPanel from "../views/rooms/AuxPanel";
import RoomHeader from "../views/rooms/RoomHeader"; import RoomHeader from "../views/rooms/RoomHeader";
import { XOR } from "../../@types/common"; import { XOR } from "../../@types/common";
import { IThreepidInvite } from "../../stores/ThreepidInviteStore"; import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
import EffectsOverlay from "../views/elements/EffectsOverlay"; import EffectsOverlay from "../views/elements/EffectsOverlay";
import { containsEmoji } from '../../effects/utils'; import { containsEmoji } from '../../effects/utils';
import { CHAT_EFFECTS } from '../../effects'; import { CHAT_EFFECTS } from '../../effects';
@ -82,6 +80,15 @@ import { IOpts } from "../../createRoom";
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import UIStore from "../../stores/UIStore"; import UIStore from "../../stores/UIStore";
import EditorStateTransfer from "../../utils/EditorStateTransfer"; import EditorStateTransfer from "../../utils/EditorStateTransfer";
import { throttle } from "lodash";
import ErrorDialog from '../views/dialogs/ErrorDialog';
import SearchResultTile from '../views/rooms/SearchResultTile';
import Spinner from "../views/elements/Spinner";
import UploadBar from './UploadBar';
import RoomStatusBar from "./RoomStatusBar";
import MessageComposer from '../views/rooms/MessageComposer';
import JumpToBottomButton from "../views/rooms/JumpToBottomButton";
import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
const DEBUG = false; const DEBUG = false;
let debuglog = function(msg: string) {}; let debuglog = function(msg: string) {};
@ -94,22 +101,8 @@ if (DEBUG) {
} }
interface IProps { interface IProps {
threepidInvite: IThreepidInvite, threepidInvite: IThreepidInvite;
oobData?: IOOBData;
// Any data about the room that would normally come from the homeserver
// but has been passed out-of-band, eg. the room name and avatar URL
// from an email invite (a workaround for the fact that we can't
// get this information from the HS using an email invite).
// Fields:
// * name (string) The room's name
// * avatarUrl (string) The mxc:// avatar URL for the room
// * inviterName (string) The display name of the person who
// * invited us to the room
oobData?: {
name?: string;
avatarUrl?: string;
inviterName?: string;
};
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
justCreatedOpts?: IOpts; justCreatedOpts?: IOpts;
@ -680,8 +673,8 @@ export default class RoomView extends React.Component<IProps, IState> {
); );
} }
// cancel any pending calls to the rate_limited_funcs // cancel any pending calls to the throttled updated
this.updateRoomMembers.cancelPendingCall(); this.updateRoomMembers.cancel();
for (const watcher of this.settingWatchers) { for (const watcher of this.settingWatchers) {
SettingsStore.unwatchSetting(watcher); SettingsStore.unwatchSetting(watcher);
@ -830,17 +823,16 @@ export default class RoomView extends React.Component<IProps, IState> {
case Action.ComposerInsert: { case Action.ComposerInsert: {
// re-dispatch to the correct composer // re-dispatch to the correct composer
if (this.state.editState) { dis.dispatch({
dis.dispatch({ ...payload,
...payload, action: this.state.editState ? "edit_composer_insert" : "send_composer_insert",
action: "edit_composer_insert", });
}); break;
} else { }
dis.dispatch({
...payload, case Action.FocusAComposer: {
action: "send_composer_insert", // re-dispatch to the correct composer
}); dis.fire(this.state.editState ? Action.FocusEditMessageComposer : Action.FocusSendMessageComposer);
}
break; break;
} }
@ -1059,11 +1051,6 @@ export default class RoomView extends React.Component<IProps, IState> {
}); });
} }
private updateTint() {
const room = this.state.room;
if (!room) return;
}
private onAccountData = (event: MatrixEvent) => { private onAccountData = (event: MatrixEvent) => {
const type = event.getType(); const type = event.getType();
if ((type === "org.matrix.preview_urls" || type === "im.vector.web.settings") && this.state.room) { if ((type === "org.matrix.preview_urls" || type === "im.vector.web.settings") && this.state.room) {
@ -1102,7 +1089,7 @@ export default class RoomView extends React.Component<IProps, IState> {
return; return;
} }
this.updateRoomMembers(member); this.updateRoomMembers();
}; };
private onMyMembership = (room: Room, membership: string, oldMembership: string) => { private onMyMembership = (room: Room, membership: string, oldMembership: string) => {
@ -1124,10 +1111,10 @@ export default class RoomView extends React.Component<IProps, IState> {
} }
// rate limited because a power level change will emit an event for every member in the room. // rate limited because a power level change will emit an event for every member in the room.
private updateRoomMembers = rateLimitedFunc(() => { private updateRoomMembers = throttle(() => {
this.updateDMState(); this.updateDMState();
this.updateE2EStatus(this.state.room); this.updateE2EStatus(this.state.room);
}, 500); }, 500, { leading: true, trailing: true });
private checkDesktopNotifications() { private checkDesktopNotifications() {
const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount(); const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount();
@ -1263,7 +1250,7 @@ export default class RoomView extends React.Component<IProps, IState> {
ContentMessages.sharedInstance().sendContentListToRoom( ContentMessages.sharedInstance().sendContentListToRoom(
ev.dataTransfer.files, this.state.room.roomId, this.context, ev.dataTransfer.files, this.state.room.roomId, this.context,
); );
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
this.setState({ this.setState({
draggingFile: false, draggingFile: false,
@ -1271,7 +1258,7 @@ export default class RoomView extends React.Component<IProps, IState> {
}); });
}; };
private injectSticker(url, info, text) { private injectSticker(url: string, info: object, text: string) {
if (this.context.isGuest()) { if (this.context.isGuest()) {
dis.dispatch({ action: 'require_registration' }); dis.dispatch({ action: 'require_registration' });
return; return;
@ -1352,7 +1339,6 @@ export default class RoomView extends React.Component<IProps, IState> {
searchResults: results, searchResults: results,
}); });
}, (error) => { }, (error) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Search failed", error); console.error("Search failed", error);
Modal.createTrackedDialog('Search failed', '', ErrorDialog, { Modal.createTrackedDialog('Search failed', '', ErrorDialog, {
title: _t("Search failed"), title: _t("Search failed"),
@ -1368,9 +1354,6 @@ export default class RoomView extends React.Component<IProps, IState> {
} }
private getSearchResultTiles() { private getSearchResultTiles() {
const SearchResultTile = sdk.getComponent('rooms.SearchResultTile');
const Spinner = sdk.getComponent("elements.Spinner");
// XXX: todo: merge overlapping results somehow? // XXX: todo: merge overlapping results somehow?
// XXX: why doesn't searching on name work? // XXX: why doesn't searching on name work?
@ -1470,13 +1453,6 @@ export default class RoomView extends React.Component<IProps, IState> {
}); });
}; };
private onLeaveClick = () => {
dis.dispatch({
action: 'leave_room',
room_id: this.state.room.roomId,
});
};
private onForgetClick = () => { private onForgetClick = () => {
dis.dispatch({ dis.dispatch({
action: 'forget_room', action: 'forget_room',
@ -1497,7 +1473,6 @@ export default class RoomView extends React.Component<IProps, IState> {
console.error("Failed to reject invite: %s", error); console.error("Failed to reject invite: %s", error);
const msg = error.message ? error.message : JSON.stringify(error); const msg = error.message ? error.message : JSON.stringify(error);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to reject invite', '', ErrorDialog, { Modal.createTrackedDialog('Failed to reject invite', '', ErrorDialog, {
title: _t("Failed to reject invite"), title: _t("Failed to reject invite"),
description: msg, description: msg,
@ -1531,7 +1506,6 @@ export default class RoomView extends React.Component<IProps, IState> {
console.error("Failed to reject invite: %s", error); console.error("Failed to reject invite: %s", error);
const msg = error.message ? error.message : JSON.stringify(error); const msg = error.message ? error.message : JSON.stringify(error);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to reject invite', '', ErrorDialog, { Modal.createTrackedDialog('Failed to reject invite', '', ErrorDialog, {
title: _t("Failed to reject invite"), title: _t("Failed to reject invite"),
description: msg, description: msg,
@ -1578,7 +1552,7 @@ export default class RoomView extends React.Component<IProps, IState> {
} else { } else {
// Otherwise we have to jump manually // Otherwise we have to jump manually
this.messagePanel.jumpToLiveTimeline(); this.messagePanel.jumpToLiveTimeline();
dis.fire(Action.FocusComposer); dis.fire(Action.FocusSendMessageComposer);
} }
}; };
@ -1608,7 +1582,7 @@ export default class RoomView extends React.Component<IProps, IState> {
// get the current scroll position of the room, so that it can be // get the current scroll position of the room, so that it can be
// restored when we switch back to it. // restored when we switch back to it.
// //
private getScrollState() { private getScrollState(): ScrollState {
const messagePanel = this.messagePanel; const messagePanel = this.messagePanel;
if (!messagePanel) return null; if (!messagePanel) return null;
@ -1710,10 +1684,6 @@ export default class RoomView extends React.Component<IProps, IState> {
// otherwise react calls it with null on each update. // otherwise react calls it with null on each update.
private gatherTimelinePanelRef = r => { private gatherTimelinePanelRef = r => {
this.messagePanel = r; this.messagePanel = r;
if (r) {
console.log("updateTint from RoomView.gatherTimelinePanelRef");
this.updateTint();
}
}; };
private getOldRoom() { private getOldRoom() {
@ -1869,10 +1839,8 @@ export default class RoomView extends React.Component<IProps, IState> {
let isStatusAreaExpanded = true; let isStatusAreaExpanded = true;
if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) { if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) {
const UploadBar = sdk.getComponent('structures.UploadBar');
statusBar = <UploadBar room={this.state.room} />; statusBar = <UploadBar room={this.state.room} />;
} else if (!this.state.searchResults) { } else if (!this.state.searchResults) {
const RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
isStatusAreaExpanded = this.state.statusBarVisible; isStatusAreaExpanded = this.state.statusBarVisible;
statusBar = <RoomStatusBar statusBar = <RoomStatusBar
room={this.state.room} room={this.state.room}
@ -1978,12 +1946,9 @@ export default class RoomView extends React.Component<IProps, IState> {
myMembership === 'join' && !this.state.searchResults myMembership === 'join' && !this.state.searchResults
); );
if (canSpeak) { if (canSpeak) {
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
messageComposer = messageComposer =
<MessageComposer <MessageComposer
room={this.state.room} room={this.state.room}
callState={this.state.callState}
showApps={this.state.showApps}
e2eStatus={this.state.e2eStatus} e2eStatus={this.state.e2eStatus}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
replyToEvent={this.state.replyToEvent} replyToEvent={this.state.replyToEvent}
@ -2069,7 +2034,6 @@ export default class RoomView extends React.Component<IProps, IState> {
let topUnreadMessagesBar = null; let topUnreadMessagesBar = null;
// Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense // Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense
if (this.state.showTopUnreadMessagesBar && !this.state.searchResults) { if (this.state.showTopUnreadMessagesBar && !this.state.searchResults) {
const TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar');
topUnreadMessagesBar = ( topUnreadMessagesBar = (
<TopUnreadMessagesBar onScrollUpClick={this.jumpToReadMarker} onCloseClick={this.forgetReadMarker} /> <TopUnreadMessagesBar onScrollUpClick={this.jumpToReadMarker} onCloseClick={this.forgetReadMarker} />
); );
@ -2077,7 +2041,6 @@ export default class RoomView extends React.Component<IProps, IState> {
let jumpToBottom; let jumpToBottom;
// Do not show JumpToBottomButton if we have search results showing, it makes no sense // Do not show JumpToBottomButton if we have search results showing, it makes no sense
if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) { if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) {
const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton');
jumpToBottom = (<JumpToBottomButton jumpToBottom = (<JumpToBottomButton
highlight={this.state.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0} highlight={this.state.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0}
numUnreadMessages={this.state.numUnreadMessages} numUnreadMessages={this.state.numUnreadMessages}
@ -2120,7 +2083,6 @@ export default class RoomView extends React.Component<IProps, IState> {
onSearchClick={this.onSearchClick} onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick} onSettingsClick={this.onSettingsClick}
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null} onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
e2eStatus={this.state.e2eStatus} e2eStatus={this.state.e2eStatus}
onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null} onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null}
appsShown={this.state.showApps} appsShown={this.state.showApps}

View file

@ -58,7 +58,7 @@ export interface ISpaceSummaryRoom {
avatar_url?: string; avatar_url?: string;
guest_can_join: boolean; guest_can_join: boolean;
name?: string; name?: string;
num_joined_members: number num_joined_members: number;
room_id: string; room_id: string;
topic?: string; topic?: string;
world_readable: boolean; world_readable: boolean;

View file

@ -18,9 +18,9 @@ limitations under the License.
import * as React from "react"; import * as React from "react";
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import * as sdk from "../../index";
import AutoHideScrollbar from './AutoHideScrollbar'; import AutoHideScrollbar from './AutoHideScrollbar';
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import AccessibleButton from "../views/elements/AccessibleButton";
/** /**
* Represents a tab for the TabbedView. * Represents a tab for the TabbedView.
@ -82,8 +82,6 @@ export default class TabbedView extends React.Component<IProps, IState> {
} }
private _renderTabLabel(tab: Tab) { private _renderTabLabel(tab: Tab) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let classes = "mx_TabbedView_tabLabel "; let classes = "mx_TabbedView_tabLabel ";
const idx = this.props.tabs.indexOf(tab); const idx = this.props.tabs.indexOf(tab);

View file

@ -16,11 +16,13 @@ limitations under the License.
import React, { createRef, ReactNode, SyntheticEvent } from 'react'; import React, { createRef, ReactNode, SyntheticEvent } from 'react';
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { Room } from "matrix-js-sdk/src/models/room"; import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { TimelineSet } from "matrix-js-sdk/src/models/event-timeline-set"; import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set";
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; import { Direction, EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
import { TimelineWindow } from "matrix-js-sdk/src/timeline-window"; import { TimelineWindow } from "matrix-js-sdk/src/timeline-window";
import { EventType, RelationType } from 'matrix-js-sdk/src/@types/event';
import { SyncState } from 'matrix-js-sdk/src/sync.api';
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import { Layout } from "../../settings/Layout"; import { Layout } from "../../settings/Layout";
@ -30,7 +32,6 @@ import RoomContext from "../../contexts/RoomContext";
import UserActivity from "../../UserActivity"; import UserActivity from "../../UserActivity";
import Modal from "../../Modal"; import Modal from "../../Modal";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import * as sdk from "../../index";
import { Key } from '../../Keyboard'; import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer'; import Timer from '../../utils/Timer';
import shouldHideEvent from '../../shouldHideEvent'; import shouldHideEvent from '../../shouldHideEvent';
@ -39,14 +40,13 @@ import { UIFeature } from "../../settings/UIFeature";
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import { arrayFastClone } from "../../utils/arrays"; import { arrayFastClone } from "../../utils/arrays";
import MessagePanel from "./MessagePanel"; import MessagePanel from "./MessagePanel";
import { SyncState } from 'matrix-js-sdk/src/sync.api';
import { IScrollState } from "./ScrollPanel"; import { IScrollState } from "./ScrollPanel";
import { ActionPayload } from "../../dispatcher/payloads"; import { ActionPayload } from "../../dispatcher/payloads";
import { EventType } from 'matrix-js-sdk/src/@types/event';
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
import Spinner from "../views/elements/Spinner"; import Spinner from "../views/elements/Spinner";
import EditorStateTransfer from '../../utils/EditorStateTransfer'; import EditorStateTransfer from '../../utils/EditorStateTransfer';
import ErrorDialog from '../views/dialogs/ErrorDialog';
const PAGINATE_SIZE = 20; const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20; const INITIAL_SIZE = 20;
@ -65,7 +65,7 @@ interface IProps {
// representing. This may or may not have a room, depending on what it's // representing. This may or may not have a room, depending on what it's
// a timeline representing. If it has a room, we maintain RRs etc for // a timeline representing. If it has a room, we maintain RRs etc for
// that room. // that room.
timelineSet: TimelineSet; timelineSet: EventTimelineSet;
showReadReceipts?: boolean; showReadReceipts?: boolean;
// Enable managing RRs and RMs. These require the timelineSet to have a room. // Enable managing RRs and RMs. These require the timelineSet to have a room.
manageReadReceipts?: boolean; manageReadReceipts?: boolean;
@ -125,7 +125,7 @@ interface IProps {
onReadMarkerUpdated?(): void; onReadMarkerUpdated?(): void;
// callback which is called when we wish to paginate the timeline window. // callback which is called when we wish to paginate the timeline window.
onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise<boolean>, onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise<boolean>;
} }
interface IState { interface IState {
@ -388,7 +388,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
private onPaginationRequest = ( private onPaginationRequest = (
timelineWindow: TimelineWindow, timelineWindow: TimelineWindow,
direction: string, direction: Direction,
size: number, size: number,
): Promise<boolean> => { ): Promise<boolean> => {
if (this.props.onPaginationRequest) { if (this.props.onPaginationRequest) {
@ -579,7 +579,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
}); });
}; };
private onRoomTimelineReset = (room: Room, timelineSet: TimelineSet): void => { private onRoomTimelineReset = (room: Room, timelineSet: EventTimelineSet): void => {
if (timelineSet !== this.props.timelineSet) return; if (timelineSet !== this.props.timelineSet) return;
if (this.messagePanel.current && this.messagePanel.current.isAtBottom()) { if (this.messagePanel.current && this.messagePanel.current.isAtBottom()) {
@ -792,8 +792,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
// that sending an RR for the latest message will set our notif counter // that sending an RR for the latest message will set our notif counter
// to zero: it may not do this if we send an RR for somewhere before the end. // to zero: it may not do this if we send an RR for somewhere before the end.
if (this.isAtEndOfLiveTimeline()) { if (this.isAtEndOfLiveTimeline()) {
this.props.timelineSet.room.setUnreadNotificationCount('total', 0); this.props.timelineSet.room.setUnreadNotificationCount(NotificationCountType.Total, 0);
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); this.props.timelineSet.room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
dis.dispatch({ dis.dispatch({
action: 'on_room_read', action: 'on_room_read',
roomId: this.props.timelineSet.room.roomId, roomId: this.props.timelineSet.room.roomId,
@ -1096,7 +1096,6 @@ class TimelinePanel extends React.Component<IProps, IState> {
console.error( console.error(
`Error loading timeline panel at ${eventId}: ${error}`, `Error loading timeline panel at ${eventId}: ${error}`,
); );
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
let onFinished; let onFinished;
@ -1417,7 +1416,11 @@ class TimelinePanel extends React.Component<IProps, IState> {
}); });
} }
private getRelationsForEvent = (...args) => this.props.timelineSet.getRelationsForEvent(...args); private getRelationsForEvent = (
eventId: string,
relationType: RelationType,
eventType: EventType | string,
) => this.props.timelineSet.getRelationsForEvent(eventId, relationType, eventType);
render() { render() {
// just show a spinner while the timeline loads. // just show a spinner while the timeline loads.

View file

@ -26,6 +26,7 @@ import ProgressBar from "../views/elements/ProgressBar";
import AccessibleButton from "../views/elements/AccessibleButton"; import AccessibleButton from "../views/elements/AccessibleButton";
import { IUpload } from "../../models/IUpload"; import { IUpload } from "../../models/IUpload";
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import MatrixClientContext from "../../contexts/MatrixClientContext";
interface IProps { interface IProps {
room: Room; room: Room;
@ -38,6 +39,8 @@ interface IState {
@replaceableComponent("structures.UploadBar") @replaceableComponent("structures.UploadBar")
export default class UploadBar extends React.Component<IProps, IState> { export default class UploadBar extends React.Component<IProps, IState> {
static contextType = MatrixClientContext;
private dispatcherRef: string; private dispatcherRef: string;
private mounted: boolean; private mounted: boolean;
@ -82,7 +85,7 @@ export default class UploadBar extends React.Component<IProps, IState> {
private onCancelClick = (ev) => { private onCancelClick = (ev) => {
ev.preventDefault(); ev.preventDefault();
ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise); ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise, this.context);
}; };
render() { render() {

View file

@ -15,39 +15,42 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore'; import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
import SetupEncryptionBody from "./SetupEncryptionBody"; import SetupEncryptionBody from "./SetupEncryptionBody";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("structures.auth.CompleteSecurity") interface IProps {
export default class CompleteSecurity extends React.Component { onFinished: () => void;
static propTypes = { }
onFinished: PropTypes.func.isRequired,
};
constructor() { interface IState {
super(); phase: Phase;
}
@replaceableComponent("structures.auth.CompleteSecurity")
export default class CompleteSecurity extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
store.on("update", this._onStoreUpdate); store.on("update", this.onStoreUpdate);
store.start(); store.start();
this.state = { phase: store.phase }; this.state = { phase: store.phase };
} }
_onStoreUpdate = () => { private onStoreUpdate = (): void => {
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
this.setState({ phase: store.phase }); this.setState({ phase: store.phase });
}; };
componentWillUnmount() { public componentWillUnmount(): void {
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
store.off("update", this._onStoreUpdate); store.off("update", this.onStoreUpdate);
store.stop(); store.stop();
} }
render() { public render() {
const AuthPage = sdk.getComponent("auth.AuthPage"); const AuthPage = sdk.getComponent("auth.AuthPage");
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody"); const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
const { phase } = this.state; const { phase } = this.state;

View file

@ -15,20 +15,19 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import AuthPage from '../../views/auth/AuthPage'; import AuthPage from '../../views/auth/AuthPage';
import CompleteSecurityBody from '../../views/auth/CompleteSecurityBody'; import CompleteSecurityBody from '../../views/auth/CompleteSecurityBody';
import CreateCrossSigningDialog from '../../views/dialogs/security/CreateCrossSigningDialog'; import CreateCrossSigningDialog from '../../views/dialogs/security/CreateCrossSigningDialog';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("structures.auth.E2eSetup") interface IProps {
export default class E2eSetup extends React.Component { onFinished: () => void;
static propTypes = { accountPassword?: string;
onFinished: PropTypes.func.isRequired, tokenLogin?: boolean;
accountPassword: PropTypes.string, }
tokenLogin: PropTypes.bool,
};
@replaceableComponent("structures.auth.E2eSetup")
export default class E2eSetup extends React.Component<IProps> {
render() { render() {
return ( return (
<AuthPage> <AuthPage>

View file

@ -17,7 +17,6 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import Modal from "../../../Modal"; import Modal from "../../../Modal";
@ -31,27 +30,50 @@ import PassphraseField from '../../views/auth/PassphraseField';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm'; import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm';
// Phases import { IValidationResult } from "../../views/elements/Validation";
// Show the forgot password inputs
const PHASE_FORGOT = 1; enum Phase {
// Email is in the process of being sent // Show the forgot password inputs
const PHASE_SENDING_EMAIL = 2; Forgot = 1,
// Email has been sent // Email is in the process of being sent
const PHASE_EMAIL_SENT = 3; SendingEmail = 2,
// User has clicked the link in email and completed reset // Email has been sent
const PHASE_DONE = 4; EmailSent = 3,
// User has clicked the link in email and completed reset
Done = 4,
}
interface IProps {
serverConfig: ValidatedServerConfig;
onServerConfigChange: (serverConfig: ValidatedServerConfig) => void;
onLoginClick?: () => void;
onComplete: () => void;
}
interface IState {
phase: Phase;
email: string;
password: string;
password2: string;
errorText: string;
// We perform liveliness checks later, but for now suppress the errors.
// We also track the server dead errors independently of the regular errors so
// that we can render it differently, and override any other error the user may
// be seeing.
serverIsAlive: boolean;
serverErrorIsFatal: boolean;
serverDeadError: string;
passwordFieldValid: boolean;
}
@replaceableComponent("structures.auth.ForgotPassword") @replaceableComponent("structures.auth.ForgotPassword")
export default class ForgotPassword extends React.Component { export default class ForgotPassword extends React.Component<IProps, IState> {
static propTypes = { private reset: PasswordReset;
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
onServerConfigChange: PropTypes.func.isRequired,
onLoginClick: PropTypes.func,
onComplete: PropTypes.func.isRequired,
};
state = { state = {
phase: PHASE_FORGOT, phase: Phase.Forgot,
email: "", email: "",
password: "", password: "",
password2: "", password2: "",
@ -64,30 +86,31 @@ export default class ForgotPassword extends React.Component {
serverIsAlive: true, serverIsAlive: true,
serverErrorIsFatal: false, serverErrorIsFatal: false,
serverDeadError: "", serverDeadError: "",
passwordFieldValid: false,
}; };
constructor(props) { constructor(props: IProps) {
super(props); super(props);
CountlyAnalytics.instance.track("onboarding_forgot_password_begin"); CountlyAnalytics.instance.track("onboarding_forgot_password_begin");
} }
componentDidMount() { public componentDidMount() {
this.reset = null; this.reset = null;
this._checkServerLiveliness(this.props.serverConfig); this.checkServerLiveliness(this.props.serverConfig);
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(newProps) { public UNSAFE_componentWillReceiveProps(newProps: IProps): void {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
// Do a liveliness check on the new URLs // Do a liveliness check on the new URLs
this._checkServerLiveliness(newProps.serverConfig); this.checkServerLiveliness(newProps.serverConfig);
} }
async _checkServerLiveliness(serverConfig) { private async checkServerLiveliness(serverConfig): Promise<void> {
try { try {
await AutoDiscoveryUtils.validateServerConfigWithStaticUrls( await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(
serverConfig.hsUrl, serverConfig.hsUrl,
@ -98,28 +121,28 @@ export default class ForgotPassword extends React.Component {
serverIsAlive: true, serverIsAlive: true,
}); });
} catch (e) { } catch (e) {
this.setState(AutoDiscoveryUtils.authComponentStateForError(e, "forgot_password")); this.setState(AutoDiscoveryUtils.authComponentStateForError(e, "forgot_password") as IState);
} }
} }
submitPasswordReset(email, password) { public submitPasswordReset(email: string, password: string): void {
this.setState({ this.setState({
phase: PHASE_SENDING_EMAIL, phase: Phase.SendingEmail,
}); });
this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl); this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl);
this.reset.resetPassword(email, password).then(() => { this.reset.resetPassword(email, password).then(() => {
this.setState({ this.setState({
phase: PHASE_EMAIL_SENT, phase: Phase.EmailSent,
}); });
}, (err) => { }, (err) => {
this.showErrorDialog(_t('Failed to send email') + ": " + err.message); this.showErrorDialog(_t('Failed to send email') + ": " + err.message);
this.setState({ this.setState({
phase: PHASE_FORGOT, phase: Phase.Forgot,
}); });
}); });
} }
onVerify = async ev => { private onVerify = async (ev: React.MouseEvent): Promise<void> => {
ev.preventDefault(); ev.preventDefault();
if (!this.reset) { if (!this.reset) {
console.error("onVerify called before submitPasswordReset!"); console.error("onVerify called before submitPasswordReset!");
@ -127,17 +150,17 @@ export default class ForgotPassword extends React.Component {
} }
try { try {
await this.reset.checkEmailLinkClicked(); await this.reset.checkEmailLinkClicked();
this.setState({ phase: PHASE_DONE }); this.setState({ phase: Phase.Done });
} catch (err) { } catch (err) {
this.showErrorDialog(err.message); this.showErrorDialog(err.message);
} }
}; };
onSubmitForm = async ev => { private onSubmitForm = async (ev: React.FormEvent): Promise<void> => {
ev.preventDefault(); ev.preventDefault();
// refresh the server errors, just in case the server came back online // refresh the server errors, just in case the server came back online
await this._checkServerLiveliness(this.props.serverConfig); await this.checkServerLiveliness(this.props.serverConfig);
await this['password_field'].validate({ allowEmpty: false }); await this['password_field'].validate({ allowEmpty: false });
@ -172,27 +195,27 @@ export default class ForgotPassword extends React.Component {
} }
}; };
onInputChanged = (stateKey, ev) => { private onInputChanged = (stateKey: string, ev: React.FormEvent<HTMLInputElement>) => {
this.setState({ this.setState({
[stateKey]: ev.target.value, [stateKey]: ev.currentTarget.value,
}); } as any);
}; };
onLoginClick = ev => { private onLoginClick = (ev: React.MouseEvent): void => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.props.onLoginClick(); this.props.onLoginClick();
}; };
showErrorDialog(body, title) { public showErrorDialog(description: string, title?: string) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, { Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, {
title: title, title,
description: body, description,
}); });
} }
onPasswordValidate(result) { private onPasswordValidate(result: IValidationResult) {
this.setState({ this.setState({
passwordFieldValid: result.valid, passwordFieldValid: result.valid,
}); });
@ -316,16 +339,16 @@ export default class ForgotPassword extends React.Component {
let resetPasswordJsx; let resetPasswordJsx;
switch (this.state.phase) { switch (this.state.phase) {
case PHASE_FORGOT: case Phase.Forgot:
resetPasswordJsx = this.renderForgot(); resetPasswordJsx = this.renderForgot();
break; break;
case PHASE_SENDING_EMAIL: case Phase.SendingEmail:
resetPasswordJsx = this.renderSendingEmail(); resetPasswordJsx = this.renderSendingEmail();
break; break;
case PHASE_EMAIL_SENT: case Phase.EmailSent:
resetPasswordJsx = this.renderEmailSent(); resetPasswordJsx = this.renderEmailSent();
break; break;
case PHASE_DONE: case Phase.Done:
resetPasswordJsx = this.renderDone(); resetPasswordJsx = this.renderDone();
break; break;
} }

View file

@ -18,7 +18,6 @@ import React, { ReactNode } from 'react';
import { MatrixError } from "matrix-js-sdk/src/http-api"; import { MatrixError } from "matrix-js-sdk/src/http-api";
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import * as sdk from '../../../index';
import Login, { ISSOFlow, LoginFlow } from '../../../Login'; import Login, { ISSOFlow, LoginFlow } from '../../../Login';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
@ -36,6 +35,8 @@ import Spinner from "../../views/elements/Spinner";
import SSOButtons from "../../views/elements/SSOButtons"; import SSOButtons from "../../views/elements/SSOButtons";
import ServerPicker from "../../views/elements/ServerPicker"; import ServerPicker from "../../views/elements/ServerPicker";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import AuthBody from "../../views/auth/AuthBody";
import AuthHeader from "../../views/auth/AuthHeader";
// These are used in several places, and come from the js-sdk's autodiscovery // These are used in several places, and come from the js-sdk's autodiscovery
// stuff. We define them here so that they'll be picked up by i18n. // stuff. We define them here so that they'll be picked up by i18n.
@ -541,8 +542,6 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
}; };
render() { render() {
const AuthHeader = sdk.getComponent("auth.AuthHeader");
const AuthBody = sdk.getComponent("auth.AuthBody");
const loader = this.isBusy() && !this.state.busyLoggingIn ? const loader = this.isBusy() && !this.state.busyLoggingIn ?
<div className="mx_Login_loader"><Spinner /></div> : null; <div className="mx_Login_loader"><Spinner /></div> : null;

View file

@ -18,19 +18,24 @@ import { createClient } from 'matrix-js-sdk/src/matrix';
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
import classNames from "classnames"; import classNames from "classnames";
import * as Lifecycle from '../../../Lifecycle'; import * as Lifecycle from '../../../Lifecycle';
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { IMatrixClientCreds, MatrixClientPeg } from "../../../MatrixClientPeg";
import AuthPage from "../../views/auth/AuthPage"; import AuthPage from "../../views/auth/AuthPage";
import Login, { ISSOFlow } from "../../../Login"; import Login, { ISSOFlow } from "../../../Login";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import SSOButtons from "../../views/elements/SSOButtons"; import SSOButtons from "../../views/elements/SSOButtons";
import ServerPicker from '../../views/elements/ServerPicker'; import ServerPicker from '../../views/elements/ServerPicker';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import RegistrationForm from '../../views/auth/RegistrationForm';
import AccessibleButton from '../../views/elements/AccessibleButton';
import AuthBody from "../../views/auth/AuthBody";
import AuthHeader from "../../views/auth/AuthHeader";
import InteractiveAuth from "../InteractiveAuth";
import Spinner from "../../views/elements/Spinner";
interface IProps { interface IProps {
serverConfig: ValidatedServerConfig; serverConfig: ValidatedServerConfig;
@ -47,13 +52,7 @@ interface IProps {
// - The user's password, if available and applicable (may be cached in memory // - The user's password, if available and applicable (may be cached in memory
// for a short time so the user is not required to re-enter their password // for a short time so the user is not required to re-enter their password
// for operations like uploading cross-signing keys). // for operations like uploading cross-signing keys).
onLoggedIn(params: { onLoggedIn(params: IMatrixClientCreds, password: string): void;
userId: string;
deviceId: string
homeserverUrl: string;
identityServerUrl?: string;
accessToken: string;
}, password: string): void;
makeRegistrationUrl(params: { makeRegistrationUrl(params: {
/* eslint-disable camelcase */ /* eslint-disable camelcase */
client_secret: string; client_secret: string;
@ -246,7 +245,7 @@ export default class Registration extends React.Component<IProps, IState> {
} }
} }
private onFormSubmit = formVals => { private onFormSubmit = async (formVals): Promise<void> => {
this.setState({ this.setState({
errorText: "", errorText: "",
busy: true, busy: true,
@ -442,10 +441,6 @@ export default class Registration extends React.Component<IProps, IState> {
}; };
private renderRegisterComponent() { private renderRegisterComponent() {
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
const Spinner = sdk.getComponent('elements.Spinner');
const RegistrationForm = sdk.getComponent('auth.RegistrationForm');
if (this.state.matrixClient && this.state.doingUIAuth) { if (this.state.matrixClient && this.state.doingUIAuth) {
return <InteractiveAuth return <InteractiveAuth
matrixClient={this.state.matrixClient} matrixClient={this.state.matrixClient}
@ -516,10 +511,6 @@ export default class Registration extends React.Component<IProps, IState> {
} }
render() { render() {
const AuthHeader = sdk.getComponent('auth.AuthHeader');
const AuthBody = sdk.getComponent("auth.AuthBody");
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let errorText; let errorText;
const err = this.state.errorText; const err = this.state.errorText;
if (err) { if (err) {

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,33 +15,43 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog'; import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
import * as sdk from '../../../index';
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore'; import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ISecretStorageKeyInfo } from 'matrix-js-sdk/src/crypto/api';
import EncryptionPanel from "../../views/right_panel/EncryptionPanel";
import AccessibleButton from '../../views/elements/AccessibleButton';
import Spinner from '../../views/elements/Spinner';
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
function keyHasPassphrase(keyInfo) { function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean {
return ( return Boolean(
keyInfo.passphrase && keyInfo.passphrase &&
keyInfo.passphrase.salt && keyInfo.passphrase.salt &&
keyInfo.passphrase.iterations keyInfo.passphrase.iterations,
); );
} }
@replaceableComponent("structures.auth.SetupEncryptionBody") interface IProps {
export default class SetupEncryptionBody extends React.Component { onFinished: (boolean) => void;
static propTypes = { }
onFinished: PropTypes.func.isRequired,
};
constructor() { interface IState {
super(); phase: Phase;
verificationRequest: VerificationRequest;
backupInfo: IKeyBackupInfo;
}
@replaceableComponent("structures.auth.SetupEncryptionBody")
export default class SetupEncryptionBody extends React.Component<IProps, IState> {
constructor(props) {
super(props);
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
store.on("update", this._onStoreUpdate); store.on("update", this.onStoreUpdate);
store.start(); store.start();
this.state = { this.state = {
phase: store.phase, phase: store.phase,
@ -53,10 +63,10 @@ export default class SetupEncryptionBody extends React.Component {
}; };
} }
_onStoreUpdate = () => { private onStoreUpdate = () => {
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
if (store.phase === Phase.Finished) { if (store.phase === Phase.Finished) {
this.props.onFinished(); this.props.onFinished(true);
return; return;
} }
this.setState({ this.setState({
@ -66,18 +76,18 @@ export default class SetupEncryptionBody extends React.Component {
}); });
}; };
componentWillUnmount() { public componentWillUnmount() {
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
store.off("update", this._onStoreUpdate); store.off("update", this.onStoreUpdate);
store.stop(); store.stop();
} }
_onUsePassphraseClick = async () => { private onUsePassphraseClick = async () => {
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
store.usePassPhrase(); store.usePassPhrase();
} };
_onVerifyClick = () => { private onVerifyClick = () => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const userId = cli.getUserId(); const userId = cli.getUserId();
const requestPromise = cli.requestVerification(userId); const requestPromise = cli.requestVerification(userId);
@ -91,42 +101,44 @@ export default class SetupEncryptionBody extends React.Component {
request.cancel(); request.cancel();
}, },
}); });
} };
onSkipClick = () => { private onSkipClick = () => {
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
store.skip(); store.skip();
} };
onSkipConfirmClick = () => { private onSkipConfirmClick = () => {
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
store.skipConfirm(); store.skipConfirm();
} };
onSkipBackClick = () => { private onSkipBackClick = () => {
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
store.returnAfterSkip(); store.returnAfterSkip();
} };
onDoneClick = () => { private onDoneClick = () => {
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
store.done(); store.done();
} };
render() { private onEncryptionPanelClose = () => {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); this.props.onFinished(false);
};
public render() {
const { const {
phase, phase,
} = this.state; } = this.state;
if (this.state.verificationRequest) { if (this.state.verificationRequest) {
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
return <EncryptionPanel return <EncryptionPanel
layout="dialog" layout="dialog"
verificationRequest={this.state.verificationRequest} verificationRequest={this.state.verificationRequest}
onClose={this.props.onFinished} onClose={this.onEncryptionPanelClose}
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)} member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
isRoomEncrypted={false}
/>; />;
} else if (phase === Phase.Intro) { } else if (phase === Phase.Intro) {
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
@ -139,14 +151,14 @@ export default class SetupEncryptionBody extends React.Component {
let useRecoveryKeyButton; let useRecoveryKeyButton;
if (recoveryKeyPrompt) { if (recoveryKeyPrompt) {
useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this._onUsePassphraseClick}> useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this.onUsePassphraseClick}>
{recoveryKeyPrompt} {recoveryKeyPrompt}
</AccessibleButton>; </AccessibleButton>;
} }
let verifyButton; let verifyButton;
if (store.hasDevicesToVerifyAgainst) { if (store.hasDevicesToVerifyAgainst) {
verifyButton = <AccessibleButton kind="primary" onClick={this._onVerifyClick}> verifyButton = <AccessibleButton kind="primary" onClick={this.onVerifyClick}>
{ _t("Use another login") } { _t("Use another login") }
</AccessibleButton>; </AccessibleButton>;
} }
@ -217,7 +229,6 @@ export default class SetupEncryptionBody extends React.Component {
</div> </div>
); );
} else if (phase === Phase.Busy || phase === Phase.Loading) { } else if (phase === Phase.Busy || phase === Phase.Loading) {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <Spinner />; return <Spinner />;
} else { } else {
console.log(`SetupEncryptionBody: Unknown phase ${phase}`); console.log(`SetupEncryptionBody: Unknown phase ${phase}`);

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import * as Lifecycle from '../../../Lifecycle'; import * as Lifecycle from '../../../Lifecycle';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
@ -26,6 +25,12 @@ import AuthPage from "../../views/auth/AuthPage";
import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../BasePlatform"; import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../BasePlatform";
import SSOButtons from "../../views/elements/SSOButtons"; import SSOButtons from "../../views/elements/SSOButtons";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import ConfirmWipeDeviceDialog from '../../views/dialogs/ConfirmWipeDeviceDialog';
import Field from '../../views/elements/Field';
import AccessibleButton from '../../views/elements/AccessibleButton';
import Spinner from "../../views/elements/Spinner";
import AuthHeader from "../../views/auth/AuthHeader";
import AuthBody from "../../views/auth/AuthBody";
const LOGIN_VIEW = { const LOGIN_VIEW = {
LOADING: 1, LOADING: 1,
@ -49,7 +54,7 @@ interface IProps {
fragmentAfterLogin?: string; fragmentAfterLogin?: string;
// Called when the SSO login completes // Called when the SSO login completes
onTokenLoginCompleted: () => void, onTokenLoginCompleted: () => void;
} }
interface IState { interface IState {
@ -94,7 +99,6 @@ export default class SoftLogout extends React.Component<IProps, IState> {
} }
onClearAll = () => { onClearAll = () => {
const ConfirmWipeDeviceDialog = sdk.getComponent('dialogs.ConfirmWipeDeviceDialog');
Modal.createTrackedDialog('Clear Data', 'Soft Logout', ConfirmWipeDeviceDialog, { Modal.createTrackedDialog('Clear Data', 'Soft Logout', ConfirmWipeDeviceDialog, {
onFinished: (wipeData) => { onFinished: (wipeData) => {
if (!wipeData) return; if (!wipeData) return;
@ -202,7 +206,6 @@ export default class SoftLogout extends React.Component<IProps, IState> {
private renderSignInSection() { private renderSignInSection() {
if (this.state.loginView === LOGIN_VIEW.LOADING) { if (this.state.loginView === LOGIN_VIEW.LOADING) {
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />; return <Spinner />;
} }
@ -214,9 +217,6 @@ export default class SoftLogout extends React.Component<IProps, IState> {
} }
if (this.state.loginView === LOGIN_VIEW.PASSWORD) { if (this.state.loginView === LOGIN_VIEW.PASSWORD) {
const Field = sdk.getComponent("elements.Field");
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let error = null; let error = null;
if (this.state.errorText) { if (this.state.errorText) {
error = <span className='mx_Login_error'>{this.state.errorText}</span>; error = <span className='mx_Login_error'>{this.state.errorText}</span>;
@ -286,10 +286,6 @@ export default class SoftLogout extends React.Component<IProps, IState> {
} }
render() { render() {
const AuthHeader = sdk.getComponent("auth.AuthHeader");
const AuthBody = sdk.getComponent("auth.AuthBody");
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return ( return (
<AuthPage> <AuthPage>
<AuthHeader /> <AuthHeader />

View file

@ -0,0 +1,124 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Playback, PlaybackState } from "../../../voice/Playback";
import React, { createRef, ReactNode, RefObject } from "react";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import PlayPauseButton from "./PlayPauseButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { formatBytes } from "../../../utils/FormattingUtils";
import DurationClock from "./DurationClock";
import { Key } from "../../../Keyboard";
import { _t } from "../../../languageHandler";
import SeekBar from "./SeekBar";
import PlaybackClock from "./PlaybackClock";
interface IProps {
// Playback instance to render. Cannot change during component lifecycle: create
// an all-new component instead.
playback: Playback;
mediaName: string;
}
interface IState {
playbackPhase: PlaybackState;
}
@replaceableComponent("views.audio_messages.AudioPlayer")
export default class AudioPlayer extends React.PureComponent<IProps, IState> {
private playPauseRef: RefObject<PlayPauseButton> = createRef();
private seekRef: RefObject<SeekBar> = createRef();
constructor(props: IProps) {
super(props);
this.state = {
playbackPhase: PlaybackState.Decoding, // default assumption
};
// We don't need to de-register: the class handles this for us internally
this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate);
// Don't wait for the promise to complete - it will emit a progress update when it
// is done, and it's not meant to take long anyhow.
// noinspection JSIgnoredPromiseFromCall
this.props.playback.prepare();
}
private onPlaybackUpdate = (ev: PlaybackState) => {
this.setState({ playbackPhase: ev });
};
private onKeyDown = (ev: React.KeyboardEvent) => {
// stopPropagation() prevents the FocusComposer catch-all from triggering,
// but we need to do it on key down instead of press (even though the user
// interaction is typically on press).
if (ev.key === Key.SPACE) {
ev.stopPropagation();
this.playPauseRef.current?.toggleState();
} else if (ev.key === Key.ARROW_LEFT) {
ev.stopPropagation();
this.seekRef.current?.left();
} else if (ev.key === Key.ARROW_RIGHT) {
ev.stopPropagation();
this.seekRef.current?.right();
}
};
protected renderFileSize(): string {
const bytes = this.props.playback.sizeBytes;
if (!bytes) return null;
// Not translated here - we're just presenting the data which should already
// be translated if needed.
return `(${formatBytes(bytes)})`;
}
public render(): ReactNode {
// tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard
// events for accessibility
return <div className='mx_MediaBody mx_AudioPlayer_container' tabIndex={0} onKeyDown={this.onKeyDown}>
<div className='mx_AudioPlayer_primaryContainer'>
<PlayPauseButton
playback={this.props.playback}
playbackPhase={this.state.playbackPhase}
tabIndex={-1} // prevent tabbing into the button
ref={this.playPauseRef}
/>
<div className='mx_AudioPlayer_mediaInfo'>
<span className='mx_AudioPlayer_mediaName'>
{this.props.mediaName || _t("Unnamed audio")}
</span>
<div className='mx_AudioPlayer_byline'>
<DurationClock playback={this.props.playback} />
&nbsp; {/* easiest way to introduce a gap between the components */}
{ this.renderFileSize() }
</div>
</div>
</div>
<div className='mx_AudioPlayer_seek'>
<SeekBar
playback={this.props.playback}
tabIndex={-1} // prevent tabbing into the bar
playbackPhase={this.state.playbackPhase}
ref={this.seekRef}
/>
<PlaybackClock playback={this.props.playback} defaultDisplaySeconds={0} />
</div>
</div>;
}
}

View file

@ -28,7 +28,7 @@ interface IState {
* Simply converts seconds into minutes and seconds. Note that hours will not be * Simply converts seconds into minutes and seconds. Note that hours will not be
* displayed, making it possible to see "82:29". * displayed, making it possible to see "82:29".
*/ */
@replaceableComponent("views.voice_messages.Clock") @replaceableComponent("views.audio_messages.Clock")
export default class Clock extends React.Component<IProps, IState> { export default class Clock extends React.Component<IProps, IState> {
public constructor(props) { public constructor(props) {
super(props); super(props);

View file

@ -0,0 +1,55 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock";
import { Playback } from "../../../voice/Playback";
interface IProps {
playback: Playback;
}
interface IState {
durationSeconds: number;
}
/**
* A clock which shows a clip's maximum duration.
*/
@replaceableComponent("views.audio_messages.DurationClock")
export default class DurationClock extends React.PureComponent<IProps, IState> {
public constructor(props) {
super(props);
this.state = {
// we track the duration on state because we won't really know what the clip duration
// is until the first time update, and as a PureComponent we are trying to dedupe state
// updates as much as possible. This is just the easiest way to avoid a forceUpdate() or
// member property to track "did we get a duration".
durationSeconds: this.props.playback.clockInfo.durationSeconds,
};
this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate);
}
private onTimeUpdate = (time: number[]) => {
this.setState({ durationSeconds: time[1] });
};
public render() {
return <Clock seconds={this.state.durationSeconds} />;
}
}

View file

@ -1,9 +1,12 @@
/* /*
Copyright 2021 The Matrix.org Foundation C.I.C. Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -12,16 +15,13 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import Clock from "./Clock"; import { IRecordingUpdate, VoiceRecording } from "../../../voice/VoiceRecording";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock";
import { MarkedExecution } from "../../../utils/MarkedExecution"; import { MarkedExecution } from "../../../utils/MarkedExecution";
import {
IRecordingUpdate,
VoiceRecording,
} from "../../../voice/VoiceRecording";
interface IProps { interface IProps {
recorder?: VoiceRecording; recorder: VoiceRecording;
} }
interface IState { interface IState {
@ -31,7 +31,7 @@ interface IState {
/** /**
* A clock for a live recording. * A clock for a live recording.
*/ */
@replaceableComponent("views.voice_messages.LiveRecordingClock") @replaceableComponent("views.audio_messages.LiveRecordingClock")
export default class LiveRecordingClock extends React.PureComponent<IProps, IState> { export default class LiveRecordingClock extends React.PureComponent<IProps, IState> {
private seconds = 0; private seconds = 0;
private scheduledUpdate = new MarkedExecution( private scheduledUpdate = new MarkedExecution(

View file

@ -1,9 +1,12 @@
/* /*
Copyright 2021 The Matrix.org Foundation C.I.C. Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -12,16 +15,15 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import Waveform from "./Waveform"; import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../voice/VoiceRecording";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { arrayFastResample } from "../../../utils/arrays";
import { percentageOf } from "../../../utils/numbers";
import Waveform from "./Waveform";
import { MarkedExecution } from "../../../utils/MarkedExecution"; import { MarkedExecution } from "../../../utils/MarkedExecution";
import {
IRecordingUpdate,
VoiceRecording,
} from "../../../voice/VoiceRecording";
interface IProps { interface IProps {
recorder?: VoiceRecording; recorder: VoiceRecording;
} }
interface IState { interface IState {
@ -31,7 +33,7 @@ interface IState {
/** /**
* A waveform which shows the waveform of a live recording * A waveform which shows the waveform of a live recording
*/ */
@replaceableComponent("views.voice_messages.LiveRecordingWaveform") @replaceableComponent("views.audio_messages.LiveRecordingWaveform")
export default class LiveRecordingWaveform extends React.PureComponent<IProps, IState> { export default class LiveRecordingWaveform extends React.PureComponent<IProps, IState> {
public static defaultProps = { public static defaultProps = {
progress: 1, progress: 1,
@ -52,15 +54,18 @@ export default class LiveRecordingWaveform extends React.PureComponent<IProps, I
componentDidMount() { componentDidMount() {
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => { this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
this.waveform = update.waveform; const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
// The incoming data is between zero and one, but typically even screaming into a
// microphone won't send you over 0.6, so we artificially adjust the gain for the
// waveform. This results in a slightly more cinematic/animated waveform for the
// user.
this.waveform = bars.map(b => percentageOf(b, 0, 0.50));
this.scheduledUpdate.mark(); this.scheduledUpdate.mark();
}); });
} }
private updateWaveform() { private updateWaveform() {
this.setState({ this.setState({ waveform: this.waveform });
waveform: this.waveform,
});
} }
public render() { public render() {

View file

@ -21,7 +21,8 @@ import { _t } from "../../../languageHandler";
import { Playback, PlaybackState } from "../../../voice/Playback"; import { Playback, PlaybackState } from "../../../voice/Playback";
import classNames from "classnames"; import classNames from "classnames";
interface IProps { // omitted props are handled by render function
interface IProps extends Omit<React.ComponentProps<typeof AccessibleTooltipButton>, "title" | "onClick" | "disabled"> {
// Playback instance to manipulate. Cannot change during the component lifecycle. // Playback instance to manipulate. Cannot change during the component lifecycle.
playback: Playback; playback: Playback;
@ -33,19 +34,25 @@ interface IProps {
* Displays a play/pause button (activating the play/pause function of the recorder) * Displays a play/pause button (activating the play/pause function of the recorder)
* to be displayed in reference to a recording. * to be displayed in reference to a recording.
*/ */
@replaceableComponent("views.voice_messages.PlayPauseButton") @replaceableComponent("views.audio_messages.PlayPauseButton")
export default class PlayPauseButton extends React.PureComponent<IProps> { export default class PlayPauseButton extends React.PureComponent<IProps> {
public constructor(props) { public constructor(props) {
super(props); super(props);
} }
private onClick = async () => { private onClick = () => {
await this.props.playback.toggle(); // noinspection JSIgnoredPromiseFromCall
this.toggleState();
}; };
public async toggleState() {
await this.props.playback.toggle();
}
public render(): ReactNode { public render(): ReactNode {
const isPlaying = this.props.playback.isPlaying; const { playback, playbackPhase, ...restProps } = this.props;
const isDisabled = this.props.playbackPhase === PlaybackState.Decoding; const isPlaying = playback.isPlaying;
const isDisabled = playbackPhase === PlaybackState.Decoding;
const classes = classNames('mx_PlayPauseButton', { const classes = classNames('mx_PlayPauseButton', {
'mx_PlayPauseButton_play': !isPlaying, 'mx_PlayPauseButton_play': !isPlaying,
'mx_PlayPauseButton_pause': isPlaying, 'mx_PlayPauseButton_pause': isPlaying,
@ -56,6 +63,7 @@ export default class PlayPauseButton extends React.PureComponent<IProps> {
title={isPlaying ? _t("Pause") : _t("Play")} title={isPlaying ? _t("Pause") : _t("Play")}
onClick={this.onClick} onClick={this.onClick}
disabled={isDisabled} disabled={isDisabled}
{...restProps}
/>; />;
} }
} }

View file

@ -22,6 +22,11 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore";
interface IProps { interface IProps {
playback: Playback; playback: Playback;
// The default number of seconds to show when the playback has completed or
// has not started. Not used during playback, even when paused. Defaults to
// clip duration length.
defaultDisplaySeconds?: number;
} }
interface IState { interface IState {
@ -33,7 +38,7 @@ interface IState {
/** /**
* A clock for a playback of a recording. * A clock for a playback of a recording.
*/ */
@replaceableComponent("views.voice_messages.PlaybackClock") @replaceableComponent("views.audio_messages.PlaybackClock")
export default class PlaybackClock extends React.PureComponent<IProps, IState> { export default class PlaybackClock extends React.PureComponent<IProps, IState> {
public constructor(props) { public constructor(props) {
super(props); super(props);
@ -64,7 +69,11 @@ export default class PlaybackClock extends React.PureComponent<IProps, IState> {
public render() { public render() {
let seconds = this.state.seconds; let seconds = this.state.seconds;
if (this.state.playbackPhase === PlaybackState.Stopped) { if (this.state.playbackPhase === PlaybackState.Stopped) {
seconds = this.state.durationSeconds; if (Number.isFinite(this.props.defaultDisplaySeconds)) {
seconds = this.props.defaultDisplaySeconds;
} else {
seconds = this.state.durationSeconds;
}
} }
return <Clock seconds={seconds} />; return <Clock seconds={seconds} />;
} }

View file

@ -33,7 +33,7 @@ interface IState {
/** /**
* A waveform which shows the waveform of a previously recorded recording * A waveform which shows the waveform of a previously recorded recording
*/ */
@replaceableComponent("views.voice_messages.PlaybackWaveform") @replaceableComponent("views.audio_messages.PlaybackWaveform")
export default class PlaybackWaveform extends React.PureComponent<IProps, IState> { export default class PlaybackWaveform extends React.PureComponent<IProps, IState> {
public constructor(props) { public constructor(props) {
super(props); super(props);

View file

@ -20,6 +20,7 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import PlaybackWaveform from "./PlaybackWaveform"; import PlaybackWaveform from "./PlaybackWaveform";
import PlayPauseButton from "./PlayPauseButton"; import PlayPauseButton from "./PlayPauseButton";
import PlaybackClock from "./PlaybackClock"; import PlaybackClock from "./PlaybackClock";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps { interface IProps {
// Playback instance to render. Cannot change during component lifecycle: create // Playback instance to render. Cannot change during component lifecycle: create
@ -31,6 +32,7 @@ interface IState {
playbackPhase: PlaybackState; playbackPhase: PlaybackState;
} }
@replaceableComponent("views.audio_messages.RecordingPlayback")
export default class RecordingPlayback extends React.PureComponent<IProps, IState> { export default class RecordingPlayback extends React.PureComponent<IProps, IState> {
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
@ -53,7 +55,7 @@ export default class RecordingPlayback extends React.PureComponent<IProps, IStat
}; };
public render(): ReactNode { public render(): ReactNode {
return <div className='mx_VoiceMessagePrimaryContainer'> return <div className='mx_MediaBody mx_VoiceMessagePrimaryContainer'>
<PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} /> <PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} />
<PlaybackClock playback={this.props.playback} /> <PlaybackClock playback={this.props.playback} />
<PlaybackWaveform playback={this.props.playback} /> <PlaybackWaveform playback={this.props.playback} />

View file

@ -0,0 +1,112 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Playback, PlaybackState } from "../../../voice/Playback";
import React, { ChangeEvent, CSSProperties, ReactNode } from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MarkedExecution } from "../../../utils/MarkedExecution";
import { percentageOf } from "../../../utils/numbers";
interface IProps {
// Playback instance to render. Cannot change during component lifecycle: create
// an all-new component instead.
playback: Playback;
// Tab index for the underlying component. Useful if the seek bar is in a managed state.
// Defaults to zero.
tabIndex?: number;
playbackPhase: PlaybackState;
}
interface IState {
percentage: number;
}
interface ISeekCSS extends CSSProperties {
'--fillTo': number;
}
const ARROW_SKIP_SECONDS = 5; // arbitrary
@replaceableComponent("views.audio_messages.SeekBar")
export default class SeekBar extends React.PureComponent<IProps, IState> {
// 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.
private animationFrameFn = new MarkedExecution(
() => this.doUpdate(),
() => requestAnimationFrame(() => this.animationFrameFn.trigger()));
public static defaultProps = {
tabIndex: 0,
};
constructor(props: IProps) {
super(props);
this.state = {
percentage: 0,
};
// We don't need to de-register: the class handles this for us internally
this.props.playback.clockInfo.liveData.onUpdate(() => this.animationFrameFn.mark());
}
private doUpdate() {
this.setState({
percentage: percentageOf(
this.props.playback.clockInfo.timeSeconds,
0,
this.props.playback.clockInfo.durationSeconds),
});
}
public left() {
// noinspection JSIgnoredPromiseFromCall
this.props.playback.skipTo(this.props.playback.clockInfo.timeSeconds - ARROW_SKIP_SECONDS);
}
public right() {
// noinspection JSIgnoredPromiseFromCall
this.props.playback.skipTo(this.props.playback.clockInfo.timeSeconds + ARROW_SKIP_SECONDS);
}
private onChange = (ev: ChangeEvent<HTMLInputElement>) => {
// Thankfully, onChange is only called when the user changes the value, not when we
// change the value on the component. We can use this as a reliable "skip to X" function.
//
// noinspection JSIgnoredPromiseFromCall
this.props.playback.skipTo(Number(ev.target.value) * this.props.playback.clockInfo.durationSeconds);
};
public render(): ReactNode {
// We use a range input to avoid having to re-invent accessibility handling on
// a custom set of divs.
return <input
type="range"
className='mx_SeekBar'
tabIndex={this.props.tabIndex}
onChange={this.onChange}
min={0}
max={1}
value={this.state.percentage}
step={0.001}
style={{ '--fillTo': this.state.percentage } as ISeekCSS}
disabled={this.props.playbackPhase === PlaybackState.Decoding}
/>;
}
}

View file

@ -17,8 +17,13 @@ limitations under the License.
import React from "react"; import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import classNames from "classnames"; import classNames from "classnames";
import { CSSProperties } from "react";
export interface IProps { interface WaveformCSSProperties extends CSSProperties {
'--barHeight': number;
}
interface IProps {
relHeights: number[]; // relative heights (0-1) relHeights: number[]; // relative heights (0-1)
progress: number; // percent complete, 0-1, default 100% progress: number; // percent complete, 0-1, default 100%
} }
@ -34,14 +39,7 @@ interface IState {
* For CSS purposes, a mx_Waveform_bar_100pct class is added when the bar should be * For CSS purposes, a mx_Waveform_bar_100pct class is added when the bar should be
* "filled", as a demonstration of the progress property. * "filled", as a demonstration of the progress property.
*/ */
@replaceableComponent("views.audio_messages.Waveform")
import { CSSProperties } from "react";
export interface WaveformCSSProperties extends CSSProperties {
'--barHeight': number;
}
@replaceableComponent("views.voice_messages.Waveform")
export default class Waveform extends React.PureComponent<IProps, IState> { export default class Waveform extends React.PureComponent<IProps, IState> {
public static defaultProps = { public static defaultProps = {
progress: 1, progress: 1,

View file

@ -18,7 +18,6 @@ import React, { ChangeEvent, createRef, FormEvent, MouseEvent } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
@ -26,6 +25,8 @@ import Spinner from "../elements/Spinner";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { LocalisedPolicy, Policies } from '../../../Terms'; import { LocalisedPolicy, Policies } from '../../../Terms';
import Field from '../elements/Field';
import CaptchaForm from "./CaptchaForm";
/* This file contains a collection of components which are used by the /* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed * InteractiveAuth to prompt the user to enter the information needed
@ -164,8 +165,7 @@ export class PasswordAuthEntry extends React.Component<IAuthEntryProps, IPasswor
let submitButtonOrSpinner; let submitButtonOrSpinner;
if (this.props.busy) { if (this.props.busy) {
const Loader = sdk.getComponent("elements.Spinner"); submitButtonOrSpinner = <Spinner />;
submitButtonOrSpinner = <Loader />;
} else { } else {
submitButtonOrSpinner = ( submitButtonOrSpinner = (
<input type="submit" <input type="submit"
@ -185,8 +185,6 @@ export class PasswordAuthEntry extends React.Component<IAuthEntryProps, IPasswor
); );
} }
const Field = sdk.getComponent('elements.Field');
return ( return (
<div> <div>
<p>{ _t("Confirm your identity by entering your account password below.") }</p> <p>{ _t("Confirm your identity by entering your account password below.") }</p>
@ -236,13 +234,11 @@ export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps
render() { render() {
if (this.props.busy) { if (this.props.busy) {
const Loader = sdk.getComponent("elements.Spinner"); return <Spinner />;
return <Loader />;
} }
let errorText = this.props.errorText; let errorText = this.props.errorText;
const CaptchaForm = sdk.getComponent("views.auth.CaptchaForm");
let sitePublicKey; let sitePublicKey;
if (!this.props.stageParams || !this.props.stageParams.public_key) { if (!this.props.stageParams || !this.props.stageParams.public_key) {
errorText = _t( errorText = _t(
@ -390,8 +386,7 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
render() { render() {
if (this.props.busy) { if (this.props.busy) {
const Loader = sdk.getComponent("elements.Spinner"); return <Spinner />;
return <Loader />;
} }
const checkboxes = []; const checkboxes = [];
@ -590,8 +585,7 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
render() { render() {
if (this.state.requestingToken) { if (this.state.requestingToken) {
const Loader = sdk.getComponent("elements.Spinner"); return <Spinner />;
return <Loader />;
} else { } else {
const enableSubmit = Boolean(this.state.token); const enableSubmit = Boolean(this.state.token);
const submitClasses = classNames({ const submitClasses = classNames({

View file

@ -52,8 +52,8 @@ interface IProps {
interface IState { interface IState {
fieldValid: Partial<Record<LoginField, boolean>>; fieldValid: Partial<Record<LoginField, boolean>>;
loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone, loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone;
password: "", password: "";
} }
enum LoginField { enum LoginField {

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react'; import React from 'react';
import * as sdk from '../../../index';
import * as Email from '../../../email'; import * as Email from '../../../email';
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
@ -31,6 +30,7 @@ import CountlyAnalytics from "../../../CountlyAnalytics";
import Field from '../elements/Field'; import Field from '../elements/Field';
import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog'; import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import CountryDropdown from "./CountryDropdown";
enum RegistrationField { enum RegistrationField {
Email = "field_email", Email = "field_email",
@ -471,7 +471,6 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
if (!this.showPhoneNumber()) { if (!this.showPhoneNumber()) {
return null; return null;
} }
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
const phoneLabel = this.authStepIsRequired('m.login.msisdn') ? const phoneLabel = this.authStepIsRequired('m.login.msisdn') ?
_t("Phone") : _t("Phone") :
_t("Phone (optional)"); _t("Phone (optional)");

View file

@ -30,13 +30,14 @@ import { _t } from "../../../languageHandler";
import TextWithTooltip from "../elements/TextWithTooltip"; import TextWithTooltip from "../elements/TextWithTooltip";
import DMRoomMap from "../../../utils/DMRoomMap"; import DMRoomMap from "../../../utils/DMRoomMap";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IOOBData } from "../../../stores/ThreepidInviteStore";
interface IProps { interface IProps {
room: Room; room: Room;
avatarSize: number; avatarSize: number;
displayBadge?: boolean; displayBadge?: boolean;
forceCount?: boolean; forceCount?: boolean;
oobData?: object; oobData?: IOOBData;
viewAvatarOnClick?: boolean; viewAvatarOnClick?: boolean;
} }

View file

@ -24,14 +24,14 @@ import Modal from '../../../Modal';
import * as Avatar from '../../../Avatar'; import * as Avatar from '../../../Avatar';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media"; import { mediaFromMxc } from "../../../customisations/Media";
import { IOOBData } from '../../../stores/ThreepidInviteStore';
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> { interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
// Room may be left unset here, but if it is, // Room may be left unset here, but if it is,
// oobData.avatarUrl should be set (else there // oobData.avatarUrl should be set (else there
// would be nowhere to get the avatar from) // would be nowhere to get the avatar from)
room?: Room; room?: Room;
// TODO: type when js-sdk has types oobData?: IOOBData;
oobData?: any;
width?: number; width?: number;
height?: number; height?: number;
resizeMethod?: ResizeMethod; resizeMethod?: ResizeMethod;

View file

@ -1,6 +1,6 @@
/* /*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2015, 2016, 2018, 2019, 2021 The Matrix.org Foundation C.I.C. Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,12 +16,11 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { EventStatus } from 'matrix-js-sdk/src/models/event'; import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import Resend from '../../../Resend'; import Resend from '../../../Resend';
@ -29,53 +28,65 @@ import SettingsStore from '../../../settings/SettingsStore';
import { isUrlPermitted } from '../../../HtmlUtils'; import { isUrlPermitted } from '../../../HtmlUtils';
import { isContentActionable } from '../../../utils/EventUtils'; import { isContentActionable } from '../../../utils/EventUtils';
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu'; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu';
import { EventType } from "matrix-js-sdk/src/@types/event";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard"; import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
import ForwardDialog from "../dialogs/ForwardDialog"; import ForwardDialog from "../dialogs/ForwardDialog";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import ReportEventDialog from '../dialogs/ReportEventDialog';
import ViewSource from '../../structures/ViewSource';
import ConfirmRedactDialog from '../dialogs/ConfirmRedactDialog';
import ErrorDialog from '../dialogs/ErrorDialog';
import ShareDialog from '../dialogs/ShareDialog';
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
export function canCancel(eventStatus) { export function canCancel(eventStatus: EventStatus): boolean {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
} }
interface IEventTileOps {
isWidgetHidden(): boolean;
unhideWidget(): void;
}
interface IProps {
/* the MatrixEvent associated with the context menu */
mxEvent: MatrixEvent;
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
eventTileOps?: IEventTileOps;
permalinkCreator?: RoomPermalinkCreator;
/* an optional function to be called when the user clicks collapse thread, if not provided hide button */
collapseReplyThread?(): 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) */
onCloseDialog?(): void;
}
interface IState {
canRedact: boolean;
canPin: boolean;
}
@replaceableComponent("views.context_menus.MessageContextMenu") @replaceableComponent("views.context_menus.MessageContextMenu")
export default class MessageContextMenu extends React.Component { export default class MessageContextMenu extends React.Component<IProps, IState> {
static propTypes = {
/* the MatrixEvent associated with the context menu */
mxEvent: PropTypes.object.isRequired,
/* an optional EventTileOps implementation that can be used to unhide preview widgets */
eventTileOps: PropTypes.object,
/* an optional function to be called when the user clicks collapse thread, if not provided hide button */
collapseReplyThread: PropTypes.func,
/* callback called when the menu is dismissed */
onFinished: PropTypes.func,
/* if the menu is inside a dialog, we sometimes need to close that dialog after click (forwarding) */
onCloseDialog: PropTypes.func,
};
state = { state = {
canRedact: false, canRedact: false,
canPin: false, canPin: false,
}; };
componentDidMount() { componentDidMount() {
MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions); MatrixClientPeg.get().on('RoomMember.powerLevel', this.checkPermissions);
this._checkPermissions(); this.checkPermissions();
} }
componentWillUnmount() { componentWillUnmount() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli) { if (cli) {
cli.removeListener('RoomMember.powerLevel', this._checkPermissions); cli.removeListener('RoomMember.powerLevel', this.checkPermissions);
} }
} }
_checkPermissions = () => { private checkPermissions = (): void => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId()); const room = cli.getRoom(this.props.mxEvent.getRoomId());
@ -93,7 +104,7 @@ export default class MessageContextMenu extends React.Component {
this.setState({ canRedact, canPin }); this.setState({ canRedact, canPin });
}; };
_isPinned() { private isPinned(): boolean {
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, ''); const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, '');
if (!pinnedEvent) return false; if (!pinnedEvent) return false;
@ -101,38 +112,35 @@ export default class MessageContextMenu extends React.Component {
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId()); return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
} }
onResendReactionsClick = () => { private onResendReactionsClick = (): void => {
for (const reaction of this._getUnsentReactions()) { for (const reaction of this.getUnsentReactions()) {
Resend.resend(reaction); Resend.resend(reaction);
} }
this.closeMenu(); this.closeMenu();
}; };
onReportEventClick = () => { private onReportEventClick = (): void => {
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, { Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
mxEvent: this.props.mxEvent, mxEvent: this.props.mxEvent,
}, 'mx_Dialog_reportEvent'); }, 'mx_Dialog_reportEvent');
this.closeMenu(); this.closeMenu();
}; };
onViewSourceClick = () => { private onViewSourceClick = (): void => {
const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Event Source', '', ViewSource, { Modal.createTrackedDialog('View Event Source', '', ViewSource, {
mxEvent: this.props.mxEvent, mxEvent: this.props.mxEvent,
}, 'mx_Dialog_viewsource'); }, 'mx_Dialog_viewsource');
this.closeMenu(); this.closeMenu();
}; };
onRedactClick = () => { private onRedactClick = (): void => {
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, { Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
onFinished: async (proceed, reason) => { onFinished: async (proceed: boolean, reason?: string) => {
if (!proceed) return; if (!proceed) return;
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
try { try {
if (this.props.onCloseDialog) this.props.onCloseDialog(); this.props.onCloseDialog?.();
await cli.redactEvent( await cli.redactEvent(
this.props.mxEvent.getRoomId(), this.props.mxEvent.getRoomId(),
this.props.mxEvent.getId(), this.props.mxEvent.getId(),
@ -145,7 +153,6 @@ export default class MessageContextMenu extends React.Component {
// (e.g. no errcode or statusCode) as in that case the redactions end up in the // (e.g. no errcode or statusCode) as in that case the redactions end up in the
// detached queue and we show the room status bar to allow retry // detached queue and we show the room status bar to allow retry
if (typeof code !== "undefined") { if (typeof code !== "undefined") {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
// display error message stating you couldn't delete this. // display error message stating you couldn't delete this.
Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, { Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
title: _t('Error'), title: _t('Error'),
@ -158,7 +165,7 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu(); this.closeMenu();
}; };
onForwardClick = () => { private onForwardClick = (): void => {
Modal.createTrackedDialog('Forward Message', '', ForwardDialog, { Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
matrixClient: MatrixClientPeg.get(), matrixClient: MatrixClientPeg.get(),
event: this.props.mxEvent, event: this.props.mxEvent,
@ -167,12 +174,12 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu(); this.closeMenu();
}; };
onPinClick = () => { private onPinClick = (): void => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId()); const room = cli.getRoom(this.props.mxEvent.getRoomId());
const eventId = this.props.mxEvent.getId(); const eventId = this.props.mxEvent.getId();
const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.pinned || []; const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent().pinned || [];
if (pinnedIds.includes(eventId)) { if (pinnedIds.includes(eventId)) {
pinnedIds.splice(pinnedIds.indexOf(eventId), 1); pinnedIds.splice(pinnedIds.indexOf(eventId), 1);
} else { } else {
@ -188,18 +195,16 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu(); this.closeMenu();
}; };
closeMenu = () => { private closeMenu = (): void => {
if (this.props.onFinished) this.props.onFinished(); this.props.onFinished();
}; };
onUnhidePreviewClick = () => { private onUnhidePreviewClick = (): void => {
if (this.props.eventTileOps) { this.props.eventTileOps?.unhideWidget();
this.props.eventTileOps.unhideWidget();
}
this.closeMenu(); this.closeMenu();
}; };
onQuoteClick = () => { private onQuoteClick = (): void => {
dis.dispatch({ dis.dispatch({
action: Action.ComposerInsert, action: Action.ComposerInsert,
event: this.props.mxEvent, event: this.props.mxEvent,
@ -207,9 +212,8 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu(); this.closeMenu();
}; };
onPermalinkClick = (e) => { private onPermalinkClick = (e: React.MouseEvent): void => {
e.preventDefault(); e.preventDefault();
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, { Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
target: this.props.mxEvent, target: this.props.mxEvent,
permalinkCreator: this.props.permalinkCreator, permalinkCreator: this.props.permalinkCreator,
@ -217,30 +221,27 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu(); this.closeMenu();
}; };
onCollapseReplyThreadClick = () => { private onCollapseReplyThreadClick = (): void => {
this.props.collapseReplyThread(); this.props.collapseReplyThread();
this.closeMenu(); this.closeMenu();
}; };
_getReactions(filter) { private getReactions(filter: (e: MatrixEvent) => boolean): MatrixEvent[] {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId()); const room = cli.getRoom(this.props.mxEvent.getRoomId());
const eventId = this.props.mxEvent.getId(); const eventId = this.props.mxEvent.getId();
return room.getPendingEvents().filter(e => { return room.getPendingEvents().filter(e => {
const relation = e.getRelation(); const relation = e.getRelation();
return relation && return relation?.rel_type === RelationType.Annotation && relation.event_id === eventId && filter(e);
relation.rel_type === "m.annotation" &&
relation.event_id === eventId &&
filter(e);
}); });
} }
_getPendingReactions() { private getPendingReactions(): MatrixEvent[] {
return this._getReactions(e => canCancel(e.status)); return this.getReactions(e => canCancel(e.status));
} }
_getUnsentReactions() { private getUnsentReactions(): MatrixEvent[] {
return this._getReactions(e => e.status === EventStatus.NOT_SENT); return this.getReactions(e => e.status === EventStatus.NOT_SENT);
} }
render() { render() {
@ -248,16 +249,17 @@ export default class MessageContextMenu extends React.Component {
const me = cli.getUserId(); const me = cli.getUserId();
const mxEvent = this.props.mxEvent; const mxEvent = this.props.mxEvent;
const eventStatus = mxEvent.status; const eventStatus = mxEvent.status;
const unsentReactionsCount = this._getUnsentReactions().length; const unsentReactionsCount = this.getUnsentReactions().length;
let resendReactionsButton;
let redactButton; let resendReactionsButton: JSX.Element;
let forwardButton; let redactButton: JSX.Element;
let pinButton; let forwardButton: JSX.Element;
let unhidePreviewButton; let pinButton: JSX.Element;
let externalURLButton; let unhidePreviewButton: JSX.Element;
let quoteButton; let externalURLButton: JSX.Element;
let collapseReplyThread; let quoteButton: JSX.Element;
let redactItemList; let collapseReplyThread: JSX.Element;
let redactItemList: JSX.Element;
// status is SENT before remote-echo, null after // status is SENT before remote-echo, null after
const isSent = !eventStatus || eventStatus === EventStatus.SENT; const isSent = !eventStatus || eventStatus === EventStatus.SENT;
@ -296,7 +298,7 @@ export default class MessageContextMenu extends React.Component {
pinButton = ( pinButton = (
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconPin" iconClassName="mx_MessageContextMenu_iconPin"
label={ this._isPinned() ? _t('Unpin') : _t('Pin') } label={ this.isPinned() ? _t('Unpin') : _t('Pin') }
onClick={this.onPinClick} onClick={this.onPinClick}
/> />
); );
@ -327,16 +329,20 @@ export default class MessageContextMenu extends React.Component {
if (this.props.permalinkCreator) { if (this.props.permalinkCreator) {
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
} }
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
const permalinkButton = ( const permalinkButton = (
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconPermalink" iconClassName="mx_MessageContextMenu_iconPermalink"
onClick={this.onPermalinkClick} onClick={this.onPermalinkClick}
label= {_t('Share')} label= {_t('Share')}
element="a" element="a"
href={permalink} {
target="_blank" // XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
rel="noreferrer noopener" ...{
href: permalink,
target: "_blank",
rel: "noreferrer noopener",
}
}
/> />
); );
@ -351,8 +357,8 @@ export default class MessageContextMenu extends React.Component {
} }
// Bridges can provide a 'external_url' to link back to the source. // Bridges can provide a 'external_url' to link back to the source.
if (typeof (mxEvent.event.content.external_url) === "string" && if (typeof (mxEvent.getContent().external_url) === "string" &&
isUrlPermitted(mxEvent.event.content.external_url) isUrlPermitted(mxEvent.getContent().external_url)
) { ) {
externalURLButton = ( externalURLButton = (
<IconizedContextMenuOption <IconizedContextMenuOption
@ -360,9 +366,14 @@ export default class MessageContextMenu extends React.Component {
onClick={this.closeMenu} onClick={this.closeMenu}
label={ _t('Source URL') } label={ _t('Source URL') }
element="a" element="a"
target="_blank" {
rel="noreferrer noopener" // XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
href={mxEvent.event.content.external_url} ...{
target: "_blank",
rel: "noreferrer noopener",
href: mxEvent.getContent().external_url,
}
}
/> />
); );
} }
@ -377,7 +388,7 @@ export default class MessageContextMenu extends React.Component {
); );
} }
let reportEventButton; let reportEventButton: JSX.Element;
if (mxEvent.getSender() !== me) { if (mxEvent.getSender() !== me) {
reportEventButton = ( reportEventButton = (
<IconizedContextMenuOption <IconizedContextMenuOption

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