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.
"react/jsx-no-bind": "off",
"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: [{
files: [
@ -49,21 +61,16 @@ module.exports = {
"@typescript-eslint/no-explicit-any": "off",
// We'd rather not do this but we do
"@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) {
return properties.map(prop => {
const [object, property] = prop.split(".");
let [object, property] = prop.split(".");
if (object === "*") {
object = undefined;
}
return {
object,
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)
=====================================================================================================
[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",
"version": "3.24.0",
"version": "3.25.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"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:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"lint": "yarn lint:types && yarn lint:js && yarn lint:style",
"lint:js": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test",
"lint:js": "eslint --max-warnings 0 src test",
"lint:types": "tsc --noEmit --jsx react",
"lint:style": "stylelint 'res/css/**/*.scss'",
"test": "jest",
@ -55,6 +55,7 @@
"dependencies": {
"@babel/runtime": "^7.12.5",
"await-lock": "^2.1.0",
"blurhash": "^1.1.3",
"browser-encrypt-attachment": "^0.3.0",
"browser-request": "^0.3.3",
"cheerio": "^1.0.0-rc.9",
@ -78,7 +79,7 @@
"katex": "^0.12.0",
"linkifyjs": "^2.1.9",
"lodash": "^4.17.20",
"matrix-js-sdk": "12.0.0",
"matrix-js-sdk": "12.0.1",
"matrix-widget-api": "^0.1.0-beta.15",
"minimist": "^1.2.5",
"opus-recorder": "^8.0.3",
@ -90,6 +91,7 @@
"re-resizable": "^6.9.0",
"react": "^17.0.2",
"react-beautiful-dnd": "^13.1.0",
"react-blurhash": "^0.1.3",
"react-dom": "^17.0.2",
"react-focus-lock": "^2.5.0",
"react-transition-group": "^4.4.1",
@ -122,6 +124,7 @@
"@peculiar/webcrypto": "^1.1.4",
"@sinonjs/fake-timers": "^7.0.2",
"@types/classnames": "^2.2.11",
"@types/commonmark": "^0.27.4",
"@types/counterpart": "^0.18.1",
"@types/diff-match-patch": "^1.0.32",
"@types/flux": "^3.1.9",

View file

@ -37,6 +37,11 @@
@import "./structures/_ViewSource.scss";
@import "./structures/auth/_CompleteSecurity.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/_AuthButtons.scss";
@import "./views/auth/_AuthFooter.scss";
@ -52,7 +57,6 @@
@import "./views/avatars/_BaseAvatar.scss";
@import "./views/avatars/_DecoratedRoomAvatar.scss";
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
@import "./views/avatars/_PulsedAvatar.scss";
@import "./views/avatars/_WidgetAvatar.scss";
@import "./views/beta/_BetaCard.scss";
@import "./views/context_menus/_CallContextMenu.scss";
@ -165,6 +169,7 @@
@import "./views/messages/_MTextBody.scss";
@import "./views/messages/_MVideoBody.scss";
@import "./views/messages/_MVoiceMessageBody.scss";
@import "./views/messages/_MediaBody.scss";
@import "./views/messages/_MessageActionBar.scss";
@import "./views/messages/_MessageTimestamp.scss";
@import "./views/messages/_MjolnirBody.scss";
@ -196,6 +201,7 @@
@import "./views/rooms/_GroupLayout.scss";
@import "./views/rooms/_IRCLayout.scss";
@import "./views/rooms/_JumpToBottomButton.scss";
@import "./views/rooms/_LinkPreviewGroup.scss";
@import "./views/rooms/_LinkPreviewWidget.scss";
@import "./views/rooms/_MemberInfo.scss";
@import "./views/rooms/_MemberList.scss";
@ -253,12 +259,10 @@
@import "./views/toasts/_AnalyticsToast.scss";
@import "./views/toasts/_NonUrgentEchoFailureToast.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/_CallView.scss";
@import "./views/voip/_CallViewForRoom.scss";
@import "./views/voip/_CallPreview.scss";
@import "./views/voip/_DialPad.scss";
@import "./views/voip/_DialPadContextMenu.scss";
@import "./views/voip/_DialPadModal.scss";

View file

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

View file

@ -121,23 +121,51 @@ $pulse-color: $pinned-unread-color;
box-shadow: 0 0 0 0 rgba($pulse-color, 1);
animation: mx_RightPanel_indicator_pulse 2s infinite;
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 {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba($pulse-color, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 10px rgba($pulse-color, 0);
}
100% {
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 {
from {
width: 0px;
transform: scaleX(0);
}
to {
width: 32px;
transform: scaleX(1);
}
}
.mx_RoomView_fileDropTarget_image {
width: 32px;
animation: mx_RoomView_fileDropTarget_image_animation;
animation-duration: 0.5s;
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;
width: 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;
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)
// has a 1px padding on it that we want to account for.
padding: 7px 12px 7px 11px;
background-color: $voice-record-waveform-bg-color;
border-radius: 12px;
// Cheat at alignment a bit
display: flex;
align-items: center;
color: $voice-record-waveform-fg-color;
font-size: $font-14px;
line-height: $font-24px;
contain: content;
.mx_Waveform {
@ -45,7 +39,7 @@ limitations under the License.
&.mx_Waveform_bar_100pct {
// Small animation to remove the mechanical feel of progress
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;
transform: scale(1);
background: rgba($pulse-color, 1);
box-shadow: 0 0 0 0 rgba($pulse-color, 1);
animation: mx_Beta_bluePulse 2s infinite;
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 {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba($pulse-color, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 10px rgba($pulse-color, 0);
}
100% {
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;
top: 2px; // alignment
background-image: url("$(res)/img/element-icons/warning-badge.svg");
background-size: contain;
}
.mx_AccessSecretStorageDialog_reset_link {

View file

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
$timelineImageBorderRadius: 4px;
.mx_MImageBody {
display: block;
margin-right: 34px;
@ -25,7 +27,11 @@ limitations under the License.
height: 100%;
left: 0;
top: 0;
border-radius: 4px;
border-radius: $timelineImageBorderRadius;
> canvas {
border-radius: $timelineImageBorderRadius;
}
}
.mx_MImageBody_thumbnail_container {
@ -43,7 +49,7 @@ limitations under the License.
top: 50%;
}
// Inner img and TintableSvg should be centered around 0, 0
// Inner img should be centered around 0, 0
.mx_MImageBody_thumbnail_spinner > * {
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 {
align-items: center;
display: flex;
gap: 5px;
}
.mx_cryptoEvent_state {

View file

@ -477,8 +477,7 @@ $hover-select-border: 4px;
pre, code {
font-family: $monospace-font-family !important;
// deliberate constants as we're behind an invert filter
color: #333;
background-color: $header-panel-bg-color;
}
pre {
@ -488,11 +487,6 @@ $hover-select-border: 4px;
overflow-x: overlay;
overflow-y: visible;
}
code {
// deliberate constants as we're behind an invert filter
background-color: #f8f8f8;
}
}
.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 {
margin-left: 15px;
flex: 1 1 auto;
overflow-x: hidden; // cause it to wrap rather than clip
}
.mx_LinkPreviewWidget_title {
display: inline;
font-weight: bold;
white-space: normal;
}
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
.mx_LinkPreviewWidget_siteName {
display: inline;
.mx_LinkPreviewWidget_siteName {
font-weight: normal;
}
}
.mx_LinkPreviewWidget_description {
margin-top: 8px;
white-space: normal;
word-wrap: break-word;
}
.mx_LinkPreviewWidget_cancel {
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;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.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
cursor: pointer;
.mx_CallView_video {
width: 350px;
.mx_VideoFeed_remote.mx_VideoFeed_voice {
min-height: 150px;
}
.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");
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.
*/
.mx_PulsedAvatar {
@keyframes shadow-pulse {
0% {
box-shadow: 0 0 0 0px rgba($accent-color, 0.2);
}
100% {
box-shadow: 0 0 0 6px rgba($accent-color, 0);
}
}
img {
animation: shadow-pulse 1s infinite;
}
.mx_CallPreview {
position: fixed;
left: 0;
top: 0;
}

View file

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

View file

@ -15,8 +15,6 @@ limitations under the License.
*/
.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;
}

View file

@ -119,8 +119,6 @@ $voipcall-plinth-color: #394049;
$theme-button-bg-color: #e3e8f0;
$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-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;
$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-icon-color: $quaternary-fg-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:
.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 {
pre, code {
filter: invert(1);
}
pre code {
filter: none;
}
table {
tr {
background-color: #000000;
@ -303,18 +282,9 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
background-color: #080808;
}
}
blockquote {
color: #919191;
}
}
// diff highlight colors
// intentionally swapped to avoid inversion
.hljs-addition {
background: #fdd;
}
.hljs-deletion {
background: #dfd;
// highlight.js overrides
.hljs-tag {
color: inherit; // Without this they'd be weirdly blue which doesn't match the theme
}

View file

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

View file

@ -4,3 +4,4 @@
@import "../../legacy-light/css/_legacy-light.scss";
@import "_legacy-dark.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;
$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text
// Legacy theme backports
$quaternary-fg-color: #C1C6CD;
// used for dialog box text
$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-live-circle-color: #ff4b55;
$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-icon-color: $tertiary-fg-color;
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;

View file

@ -3,3 +3,4 @@
@import "_fonts.scss";
@import "_legacy-light.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-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-icon-color: $tertiary-fg-color;
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;

View file

@ -4,3 +4,4 @@
@import "_light.scss";
@import "_mods.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
yarn link
yarn install $@
yarn install --pure-lockfile $@
popd
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
pushd matrix-js-sdk
yarn link
yarn install
yarn install --pure-lockfile
popd
# Now set up the react-sdk
yarn link matrix-js-sdk
yarn link
yarn install
yarn install --pure-lockfile
yarn reskindex
# Finally, set up element-web
@ -27,6 +27,6 @@ scripts/fetchdep.sh vector-im element-web
pushd element-web
yarn link matrix-js-sdk
yarn link matrix-react-sdk
yarn install
yarn install --pure-lockfile
yarn build:res
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 * as ModernizrStatic from "modernizr";
import "@types/modernizr";
import ContentMessages from "../ContentMessages";
import { IMatrixClientPeg } from "../MatrixClientPeg";
@ -46,10 +46,10 @@ import { VoiceRecordingStore } from "../stores/VoiceRecordingStore";
import PerformanceMonitor from "../performance";
import UIStore from "../stores/UIStore";
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
declare global {
interface Window {
Modernizr: ModernizrStatic;
matrixChat: ReturnType<Renderer>;
mxMatrixClientPeg: IMatrixClientPeg;
Olm: {
@ -87,6 +87,7 @@ declare global {
mxPerformanceEntryNames: any;
mxUIStore: UIStore;
mxSetupEncryptionStore?: SetupEncryptionStore;
mxRoomScrollStateStore?: RoomScrollStateStore;
}
interface Document {

View file

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

View file

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

View file

@ -124,9 +124,9 @@ interface ThirdpartyLookupResponseFields {
}
interface ThirdpartyLookupResponse {
userid: string,
protocol: string,
fields: ThirdpartyLookupResponseFields,
userid: string;
protocol: string;
fields: ThirdpartyLookupResponseFields;
}
// 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 dis from './dispatcher/dispatcher';
import { MatrixClientPeg } from './MatrixClientPeg';
import { encode } from "blurhash";
import { MatrixClient } from "matrix-js-sdk/src/client";
import dis from './dispatcher/dispatcher';
import * as sdk from './index';
import { _t } from './languageHandler';
import Modal from './Modal';
@ -47,6 +48,8 @@ const MAX_HEIGHT = 600;
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
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 {}
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
@ -77,6 +80,7 @@ interface IThumbnail {
};
w: number;
h: number;
[BLURHASH_FIELD]: string;
};
thumbnail: Blob;
}
@ -124,7 +128,17 @@ function createThumbnail(
const canvas = document.createElement("canvas");
canvas.width = targetWidth;
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) {
resolve({
info: {
@ -136,8 +150,9 @@ function createThumbnail(
},
w: inputWidth,
h: inputHeight,
[BLURHASH_FIELD]: blurhash,
},
thumbnail: thumbnail,
thumbnail,
});
}, 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.
* @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) => {
// Load the file into an html element
const video = document.createElement("video");
video.preload = "metadata";
video.playsInline = true;
video.muted = true;
const reader = new FileReader();
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.
video.onloadeddata = function() {
video.onloadeddata = async function() {
resolve(video);
video.pause();
};
video.onerror = function(e) {
reject(e);
};
video.src = ev.target.result as string;
video.load();
video.play();
};
reader.onerror = function(e) {
reject(e);
@ -347,7 +368,7 @@ export function uploadFile(
});
(prom as IAbortablePromise<any>).abort = () => {
canceled = true;
if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise);
if (uploadPromise) matrixClient.cancelUpload(uploadPromise);
};
return prom;
} else {
@ -357,11 +378,11 @@ export function uploadFile(
const promise1 = basePromise.then(function(url) {
if (canceled) throw new UploadCanceledError();
// If the attachment isn't encrypted then include the URL directly.
return { "url": url };
return { url };
});
(promise1 as any).abort = () => {
canceled = true;
MatrixClientPeg.get().cancelUpload(basePromise);
matrixClient.cancelUpload(basePromise);
};
return promise1;
}
@ -373,7 +394,7 @@ export default class ContentMessages {
sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) {
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);
throw e;
});
@ -397,6 +418,7 @@ export default class ContentMessages {
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
if (isQuoting) {
// FIXME: Using an import will result in Element crashing
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, {
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
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
await this.ensureMediaConfigFetched();
await this.ensureMediaConfigFetched(matrixClient);
modal.close();
}
@ -431,6 +453,7 @@ export default class ContentMessages {
}
if (tooBigFiles.length > 0) {
// FIXME: Using an import will result in Element crashing
const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog");
const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Failure', '', UploadFailureDialog, {
badFiles: tooBigFiles,
@ -441,7 +464,6 @@ export default class ContentMessages {
if (!shouldContinue) return;
}
const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog");
let uploadAll = false;
// Promise to complete before sending next file into room, used for synchronisation of file-sending
// 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) {
const file = okFiles[i];
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',
'', UploadConfirmDialog, {
file,
@ -470,7 +494,7 @@ export default class ContentMessages {
return this.inprogress.filter(u => !u.canceled);
}
cancelUpload(promise: Promise<any>) {
cancelUpload(promise: Promise<any>, matrixClient: MatrixClient) {
let upload: IUpload;
for (let i = 0; i < this.inprogress.length; ++i) {
if (this.inprogress[i].promise === promise) {
@ -480,7 +504,7 @@ export default class ContentMessages {
}
if (upload) {
upload.canceled = true;
MatrixClientPeg.get().cancelUpload(upload.promise);
matrixClient.cancelUpload(upload.promise);
dis.dispatch<UploadCanceledPayload>({ action: Action.UploadCanceled, upload });
}
}
@ -545,7 +569,7 @@ export default class ContentMessages {
dis.dispatch<UploadStartedPayload>({ action: Action.UploadStarted, upload });
// Focus the composer view
dis.fire(Action.FocusComposer);
dis.fire(Action.FocusSendMessageComposer);
function onProgress(ev) {
upload.total = ev.total;
@ -584,6 +608,7 @@ export default class ContentMessages {
{ fileName: upload.fileName },
);
}
// FIXME: Using an import will result in Element crashing
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
title: _t('Upload Failed'),
@ -621,11 +646,11 @@ export default class ContentMessages {
return true;
}
private ensureMediaConfigFetched() {
private ensureMediaConfigFetched(matrixClient: MatrixClient) {
if (this.mediaConfig !== null) return;
console.log("[Media Config] Fetching");
return MatrixClientPeg.get().getMediaConfig().then((config) => {
return matrixClient.getMediaConfig().then((config) => {
console.log("[Media Config] Fetched config:", config);
return config;
}).catch(() => {

View file

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

View file

@ -160,7 +160,8 @@ export default class DeviceListener {
// which result in account data changes affecting checks below.
if (
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();
}

View file

@ -358,11 +358,11 @@ interface IOpts {
stripReplyFallback?: boolean;
returnString?: boolean;
forComposerQuote?: boolean;
ref?: React.Ref<any>;
ref?: React.Ref<HTMLSpanElement>;
}
export interface IOptsReturnNode extends IOpts {
returnString: false;
returnString: false | undefined;
}
export interface IOptsReturnString extends IOpts {
@ -403,9 +403,14 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
try {
if (highlights && highlights.length > 0) {
const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
const safeHighlights = highlights.map(function(highlight) {
return sanitizeHtml(highlight, sanitizeParams);
});
const safeHighlights = highlights
// 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.
sanitizeParams.textFilter = function(safeText) {
return highlighter.applyHighlights(safeText, safeHighlights).join('');

View file

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

View file

@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
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.
@ -15,16 +16,24 @@ limitations under the License.
*/
import * as commonmark from 'commonmark';
import {escape} from "lodash";
import { escape } from "lodash";
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
// These types of node are definitely text
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 &&
node.literal.match('^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$') != null) {
node.literal.match('^<((div|span) data-mx-maths="[^"]*"|/(div|span))>$') != null) {
return true;
}
@ -39,21 +48,12 @@ function is_allowed_html_tag(node) {
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
* comprises multiple block level elements (ie. lines),
* or false if it is only a single line.
*/
function is_multi_line(node) {
function isMultiLine(node: commonmark.Node): boolean {
let par = node;
while (par.parent) {
par = par.parent;
@ -67,6 +67,9 @@ function is_multi_line(node) {
* it's plain text.
*/
export default class Markdown {
private input: string;
private parsed: commonmark.Node;
constructor(input) {
this.input = input;
@ -74,7 +77,7 @@ export default class Markdown {
this.parsed = parser.parse(this.input);
}
isPlainText() {
isPlainText(): boolean {
const walker = this.parsed.walker();
let ev;
@ -87,7 +90,7 @@ export default class Markdown {
// 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'll just be treating it as text.
if (is_allowed_html_tag(node)) {
if (isAllowedHtmlTag(node)) {
return false;
}
} else {
@ -97,7 +100,7 @@ export default class Markdown {
return true;
}
toHTML({ externalLinks = false } = {}) {
toHTML({ externalLinks = false } = {}): string {
const renderer = new commonmark.HtmlRenderer({
safe: false,
@ -107,7 +110,7 @@ export default class Markdown {
// block quote ends up all on one line
// (https://github.com/vector-im/element-web/issues/3154)
softbreak: '<br />',
});
}) as CommonmarkHtmlRendererInternal;
// 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
@ -118,16 +121,16 @@ export default class Markdown {
//
// 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
// bare text: it's a single line of text and so should be
// 'inline', rather than unnecessarily wrapped in its own
// p tag. If, however, we have multiple nodes, each gets
// its own p tag to keep them as separate paragraphs.
if (is_multi_line(node)) {
real_paragraph.call(this, node, entering);
if (isMultiLine(node)) {
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
// if there are multiple lines in the markdown.
const isMultiLine = is_multi_line(node);
if (isMultiLine) this.cr();
*/
html_if_tag_allowed.call(this, node);
/*
*/
renderer.html_inline(node);
/*
if (isMultiLine) this.cr();
*/
*/
};
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
* which has no formatting. Otherwise it emits HTML(!).
*/
toPlaintext() {
const renderer = new commonmark.HtmlRenderer({safe: false});
const real_paragraph = renderer.paragraph;
toPlaintext(): string {
const renderer = new commonmark.HtmlRenderer({ safe: false }) as CommonmarkHtmlRendererInternal;
renderer.paragraph = function(node, entering) {
renderer.paragraph = function(node: commonmark.Node, entering: boolean) {
// as with toHTML, only append lines to paragraphs if there are
// multiple paragraphs
if (is_multi_line(node)) {
if (isMultiLine(node)) {
if (!entering && node.next) {
this.lit('\n\n');
}
}
};
renderer.html_block = function(node) {
renderer.html_block = function(node: commonmark.Node) {
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);

View file

@ -219,6 +219,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
} catch (e) {
if (e && e.name === 'InvalidCryptoStoreError') {
// The js-sdk found a crypto DB too new for it to use
// FIXME: Using an import will result in test failures
const CryptoStoreTooNewDialog =
sdk.getComponent("views.dialogs.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 EventEmitter from 'events';
interface IMediaDevices {
audioOutput: Array<MediaDeviceInfo>;
audioInput: Array<MediaDeviceInfo>;
videoInput: Array<MediaDeviceInfo>;
// XXX: MediaDeviceKind is a union type, so we make our own enum
export enum MediaDeviceKindEnum {
AudioOutput = "audiooutput",
AudioInput = "audioinput",
VideoInput = "videoinput",
}
export type IMediaDevices = Record<MediaDeviceKindEnum, Array<MediaDeviceInfo>>;
export enum MediaDeviceHandlerEvent {
AudioOutputChanged = "audio_output_changed",
}
@ -51,20 +54,14 @@ export default class MediaDeviceHandler extends EventEmitter {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const output = {
[MediaDeviceKindEnum.AudioOutput]: [],
[MediaDeviceKindEnum.AudioInput]: [],
[MediaDeviceKindEnum.VideoInput]: [],
};
const audioOutput = [];
const audioInput = [];
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 };
devices.forEach((device) => output[device.kind].push(device));
return output;
} catch (error) {
console.warn('Unable to refresh WebRTC Devices: ', error);
}
@ -106,6 +103,14 @@ export default class MediaDeviceHandler extends EventEmitter {
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 {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
}

View file

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

View file

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

View file

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

View file

@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
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 Modal from './Modal';
import * as sdk from './index';
@ -42,8 +43,8 @@ let secretStorageBeingAccessed = false;
let nonInteractive = false;
let dehydrationCache: {
key?: Uint8Array,
keyInfo?: ISecretStorageKeyInfo,
key?: Uint8Array;
keyInfo?: ISecretStorageKeyInfo;
} = {};
function isCachingAllowed(): boolean {
@ -354,6 +355,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
throw new Error("Secret storage creation canceled");
}
} else {
// FIXME: Using an import will result in test failures
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
await cli.bootstrapCrossSigning({
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 { MatrixClientPeg } from './MatrixClientPeg';
import dis from './dispatcher/dispatcher';
import * as sdk from './index';
import { _t, _td } from './languageHandler';
import Modal from './Modal';
import MultiInviter from './utils/MultiInviter';
@ -50,6 +49,12 @@ import { UIFeature } from "./settings/UIFeature";
import { CHAT_EFFECTS } from "./effects";
import CallHandler from "./CallHandler";
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
interface HTMLInputEvent extends Event {
@ -63,7 +68,6 @@ const singleMxcUpload = async (): Promise<any> => {
fileSelector.onchange = (ev: HTMLInputEvent) => {
const file = ev.target.files[0];
const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog");
Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, {
file,
onFinished: (shouldContinue) => {
@ -246,7 +250,6 @@ export const Commands = [
args: '<query>',
description: _td('Searches DuckDuckGo for results'),
runFn: function() {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
// TODO Don't explain this away, actually show a search UI here.
Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, {
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."));
}
const RoomUpgradeWarningDialog = sdk.getComponent("dialogs.RoomUpgradeWarningDialog");
const { finished } = Modal.createTrackedDialog('Slash Commands', 'upgrade room confirmation',
RoomUpgradeWarningDialog, { roomId: roomId, targetVersion: args }, /*className=*/null,
/*isPriority=*/false, /*isStatic=*/true);
@ -314,7 +315,6 @@ export const Commands = [
if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn);
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, {
title: _t('Error upgrading room'),
description: _t(
@ -434,7 +434,6 @@ export const Commands = [
const topic = topicEvents && topicEvents.getContent().topic;
const topicHtml = topic ? linkifyAndSanitizeHtml(topic) : _t('This room has no topic.');
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
Modal.createTrackedDialog('Slash Commands', 'Topic', InfoDialog, {
title: room.name,
description: <div dangerouslySetInnerHTML={{ __html: topicHtml }} />,
@ -737,7 +736,6 @@ export const Commands = [
ignoredUsers.push(userId); // de-duped internally in the js-sdk
return success(
cli.setIgnoredUsers(ignoredUsers).then(() => {
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
Modal.createTrackedDialog('Slash Commands', 'User ignored', InfoDialog, {
title: _t('Ignored user'),
description: <div>
@ -768,7 +766,6 @@ export const Commands = [
if (index !== -1) ignoredUsers.splice(index, 1);
return success(
cli.setIgnoredUsers(ignoredUsers).then(() => {
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
Modal.createTrackedDialog('Slash Commands', 'User unignored', InfoDialog, {
title: _t('Unignored user'),
description: <div>
@ -838,7 +835,6 @@ export const Commands = [
command: 'devtools',
description: _td('Opens the Developer Tools dialog'),
runFn: function(roomId) {
const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
Modal.createDialog(DevtoolsDialog, { roomId });
return success();
},
@ -943,7 +939,6 @@ export const Commands = [
await cli.setDeviceVerified(userId, deviceId, true);
// Tell the user we verified everything
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, {
title: _t('Verified key'),
description: <div>
@ -1000,8 +995,6 @@ export const Commands = [
command: "help",
description: _td("Displays list of commands with usages and descriptions"),
runFn: function() {
const SlashCommandHelpDialog = sdk.getComponent('dialogs.SlashCommandHelpDialog');
Modal.createTrackedDialog('Slash Commands', 'Help', SlashCommandHelpDialog);
return success();
},
@ -1181,7 +1174,7 @@ export const Commands = [
];
// 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 => {
CommandMap.set(cmd.command, cmd);
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
// IRC-style commands
input = input.replace(/\s+$/, '');
if (input[0] !== '/') return {}; // not a command
const bits = input.match(/^(\S+?)(?:[ \n]+((.|\n)*))?$/);
let cmd;
let args;
let cmd: string;
let args: string;
if (bits) {
cmd = bits[1].substring(1).toLowerCase();
args = bits[2];
@ -1208,6 +1201,11 @@ export function parseCommandString(input: string) {
return { cmd, args };
}
interface ICmd {
cmd?: Command;
args?: string;
}
/**
* 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.
@ -1216,7 +1214,7 @@ export function parseCommandString(input: string) {
* processing the command, or 'promise' if a request was sent out.
* 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);
if (CommandMap.has(cmd) && CommandMap.get(cmd).isEnabled()) {

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import classNames from 'classnames';
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
import { MatrixClientPeg } from './MatrixClientPeg';
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} 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 = {
[policy: string]: Policy,
[policy: string]: Policy;
};
export type TermsInteractionCallback = (
policiesAndServicePairs: {
service: Service,
policies: Policies,
service: Service;
policies: Policies;
}[],
agreedUrls: string[],
extraClassNames?: string,
@ -180,14 +181,15 @@ export async function startTermsFlow(
export function dialogTermsInteractionCallback(
policiesAndServicePairs: {
service: Service,
policies: { [policy: string]: Policy },
service: Service;
policies: { [policy: string]: Policy };
}[],
agreedUrls: string[],
extraClassNames?: string,
): Promise<string[]> {
return new Promise((resolve, reject) => {
console.log("Terms that need agreement", policiesAndServicePairs);
// FIXME: Using an import will result in test failures
const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog");
Modal.createTrackedDialog('Terms of Service', '', TermsDialog, {

View file

@ -17,10 +17,10 @@ limitations under the License.
import * as React from "react";
import classNames from "classnames";
import * as sdk from "../index";
import Modal from "../Modal";
import { _t, _td } from "../languageHandler";
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
_td("Navigation");
@ -375,7 +375,6 @@ export const toggleDialog = () => {
</div>;
});
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
activeModal = Modal.createTrackedDialog("Keyboard Shortcuts", "", InfoDialog, {
className: "mx_KeyboardShortcutsDialog",
title: _t("Keyboard Shortcuts"),

View file

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

View file

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

View file

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

View file

@ -21,8 +21,8 @@ import AutocompleteProvider from './AutocompleteProvider';
import { _t } from '../languageHandler';
import { MatrixClientPeg } from '../MatrixClientPeg';
import { PillCompletion } from './Components';
import * as sdk from '../index';
import { ICompletion, ISelectionRange } from "./Autocompleter";
import RoomAvatar from '../components/views/avatars/RoomAvatar';
const AT_ROOM_REGEX = /@\S*/g;
@ -40,8 +40,6 @@ export default class NotifProvider extends AutocompleteProvider {
force = false,
limit = -1,
): Promise<ICompletion[]> {
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
const client = MatrixClientPeg.get();
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 AutocompleteProvider from './AutocompleteProvider';
import { PillCompletion } from './Components';
import * as sdk from '../index';
import QueryMatcher from './QueryMatcher';
import { sortBy } from 'lodash';
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 { makeUserPermalink } from "../utils/permalinks/Permalinks";
import { ICompletion, ISelectionRange } from "./Autocompleter";
import MemberAvatar from '../components/views/avatars/MemberAvatar';
const USER_REGEX = /\B@\S*/g;
@ -108,8 +108,6 @@ export default class UserProvider extends AutocompleteProvider {
force = false,
limit = -1,
): Promise<ICompletion[]> {
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
// lazy-load user list into matcher
if (!this.users) this._makeUsers();

View file

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

View file

@ -19,11 +19,11 @@ import React from 'react';
import { Filter } from 'matrix-js-sdk/src/filter';
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 { Room } from 'matrix-js-sdk/src/models/room';
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
import * as sdk from '../../index';
import { MatrixClientPeg } from '../../MatrixClientPeg';
import EventIndexPeg from "../../indexing/EventIndexPeg";
import { _t } from '../../languageHandler';
@ -33,11 +33,14 @@ import DesktopBuildsNotice, { WarningKind } from "../views/elements/DesktopBuild
import { replaceableComponent } from "../../utils/replaceableComponent";
import ResizeNotifier from '../../utils/ResizeNotifier';
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
import { TileShape } from '../views/rooms/EventTile';
interface IProps {
roomId: string;
onClose: () => void;
resizeNotifier: ResizeNotifier
resizeNotifier: ResizeNotifier;
}
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 filter = new Filter(client.credentials.userId);
@ -153,7 +156,11 @@ class FilePanel extends React.Component<IProps, IState> {
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 eventIndex = EventIndexPeg.get();
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.
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
const Loader = sdk.getComponent("elements.Spinner");
const emptyState = (<div className="mx_RightPanel_empty mx_FilePanel_empty">
<h2>{_t('No files visible in this room')}</h2>
@ -259,7 +264,7 @@ class FilePanel extends React.Component<IProps, IState> {
timelineSet={this.state.timelineSet}
showUrlPreview = {false}
onPaginationRequest={this.onPaginationRequest}
tileShape="file_grid"
tileShape={TileShape.FileGrid}
resizeNotifier={this.props.resizeNotifier}
empty={emptyState}
/>
@ -272,7 +277,7 @@ class FilePanel extends React.Component<IProps, IState> {
onClose={this.props.onClose}
previousPhase={RightPanelPhases.RoomSummary}
>
<Loader />
<Spinner />
</BaseCard>
);
}

View file

@ -36,7 +36,7 @@ import FlairStore from '../../stores/FlairStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
import { makeGroupPermalink, makeUserPermalink } from "../../utils/permalinks/Permalinks";
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 AutoHideScrollbar from "./AutoHideScrollbar";
import { mediaFromMxc } from "../../customisations/Media";

View file

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

View file

@ -24,7 +24,6 @@ import { Key } from '../../Keyboard';
import PageTypes from '../../PageTypes';
import MediaDeviceHandler from '../../MediaDeviceHandler';
import { fixupColorFonts } from '../../utils/FontManager';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import { IMatrixClientCreds } from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore";
@ -48,7 +47,7 @@ import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPay
import RoomListStore from "../../stores/room-list/RoomListStore";
import NonUrgentToastContainer from "./NonUrgentToastContainer";
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
import Modal from "../../Modal";
import { ICollapseConfig } from "../../resizer/distributors/collapse";
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
@ -59,6 +58,11 @@ import { replaceableComponent } from "../../utils/replaceableComponent";
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
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)
// so each pinned message may trigger a request. Limit the number per room for sanity.
@ -78,17 +82,17 @@ interface IProps {
hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier;
// eslint-disable-next-line camelcase
page_type: string;
autoJoin: boolean;
page_type?: string;
autoJoin?: boolean;
threepidInvite?: IThreepidInvite;
roomOobData?: object;
roomOobData?: IOOBData;
currentRoomId: string;
collapseLhs: boolean;
config: {
piwik: {
policyUrl: string;
},
[key: string]: any,
};
[key: string]: any;
};
currentUserId?: string;
currentGroupId?: string;
@ -394,7 +398,7 @@ class LoggedInView extends React.Component<IProps, IState> {
// refocusing during a paste event will make the
// paste end up in the newly focused element,
// 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)) {
// synchronous dispatch so we focus before key generates input
dis.fire(Action.FocusComposer, true);
dis.fire(Action.FocusSendMessageComposer, true);
ev.stopPropagation();
// we should *not* preventDefault() here as
// that would prevent typing in the now-focussed composer
@ -567,12 +571,6 @@ class LoggedInView extends React.Component<IProps, IState> {
};
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;
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 { RoomMember } from "matrix-js-sdk/src/models/room-member";
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
import 'focus-visible';
// what-input helps improve keyboard accessibility
@ -34,7 +36,6 @@ import dis from "../../dispatcher/dispatcher";
import Notifier from '../../Notifier';
import Modal from "../../Modal";
import * as sdk from '../../index';
import { showRoomInviteDialog, showStartChatInviteDialog } from '../../RoomInvite';
import * as Rooms from '../../Rooms';
import linkifyMatrix from "../../linkify-matrix";
@ -55,7 +56,6 @@ import DMRoomMap from '../../utils/DMRoomMap';
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
import { FontWatcher } from '../../settings/watchers/FontWatcher';
import { storeRoomAliasInCache } from '../../RoomAliasCache';
import { defer, IDeferred, sleep } from "../../utils/promise";
import ToastStore from "../../stores/ToastStore";
import * as StorageManager from "../../utils/StorageManager";
import type LoggedInViewType from "./LoggedInView";
@ -84,9 +84,27 @@ import RoomListStore from "../../stores/room-list/RoomListStore";
import { RoomUpdateCause } from "../../stores/room-list/models";
import defaultDispatcher from "../../dispatcher/dispatcher";
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 UIStore, { UI_EVENTS } from "../../stores/UIStore";
import SoftLogout from './auth/SoftLogout';
/** constants for MatrixChat.state.view */
export enum Views {
@ -155,7 +173,12 @@ interface IRoomInfo {
/* eslint-enable camelcase */
interface IProps { // TODO type things better
config: Record<string, any>;
config: {
piwik: {
policyUrl: string;
};
[key: string]: any;
};
serverConfig?: ValidatedServerConfig;
onNewScreen: (screen: string, replaceLast: boolean) => void;
enableGuest?: boolean;
@ -203,7 +226,7 @@ interface IState {
resizeNotifier: ResizeNotifier;
serverConfig?: ValidatedServerConfig;
ready: boolean;
threepidInvite?: IThreepidInvite,
threepidInvite?: IThreepidInvite;
roomOobData?: object;
pendingInitialSync?: boolean;
justRegistered?: boolean;
@ -420,7 +443,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
CountlyAnalytics.instance.trackPageChange(durationMs);
}
if (this.focusComposer) {
dis.fire(Action.FocusComposer);
dis.fire(Action.FocusSendMessageComposer);
this.focusComposer = false;
}
}
@ -518,7 +541,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onAction = (payload) => {
// console.log(`MatrixClientPeg.onAction: ${payload.action}`);
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
// Start the onboarding process for certain actions
if (MatrixClientPeg.get() && MatrixClientPeg.get().isGuest() &&
@ -612,8 +634,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onFinished: (confirm) => {
if (confirm) {
// FIXME: controller shouldn't be loading a view :(
const Loader = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
MatrixClientPeg.get().leave(payload.room_id).then(() => {
modal.close();
@ -649,7 +670,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
case Action.ViewUserSettings: {
const tabPayload = payload as OpenToTabPayload;
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
Modal.createTrackedDialog('User settings', '', UserSettingsDialog,
{ initialTabId: tabPayload.initialTabId },
/*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);
break;
case 'view_create_group': {
let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
CreateGroupDialog = CreateCommunityPrototypeDialog;
}
Modal.createTrackedDialog('Create Community', '', CreateGroupDialog);
const prototype = SettingsStore.getValue("feature_communities_v2_prototypes");
Modal.createTrackedDialog(
'Create Community',
'',
prototype ? CreateCommunityPrototypeDialog : CreateGroupDialog,
);
break;
}
case Action.ViewRoomDirectory: {
@ -676,7 +697,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
room_id: SpaceStore.instance.activeSpace.roomId,
});
} else {
const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
initialText: payload.initialText,
}, '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, {
defaultPublic,
defaultName,
@ -1115,7 +1134,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
private leaveRoom(roomId: string) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
const warnings = this.leaveRoomWarnings(roomId);
@ -1142,8 +1160,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const d = leaveRoomBehaviour(roomId);
// FIXME: controller shouldn't be loading a view :(
const Loader = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
d.finally(() => modal.close());
dis.dispatch({
@ -1410,7 +1427,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
showNotificationsToast(false);
}
dis.fire(Action.FocusComposer);
dis.fire(Action.FocusSendMessageComposer);
this.setState({
ready: true,
});
@ -1438,7 +1455,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
});
cli.on('no_consent', function(message, consentUri) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('No Consent Dialog', '', QuestionDialog, {
title: _t('Terms and Conditions'),
description: <div>
@ -1547,8 +1563,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
cli.on("crypto.keySignatureUploadFailure", (failures, source, continuation) => {
const KeySignatureUploadFailedDialog =
sdk.getComponent('views.dialogs.KeySignatureUploadFailedDialog');
Modal.createTrackedDialog(
'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 => {
if (request.verifier) {
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
verifier: request.verifier,
}, null, /* priority = */ false, /* static = */ true);
@ -1568,7 +1581,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
title: _t("Verification requested"),
icon: "verification",
props: { request },
component: sdk.getComponent("toasts.VerificationRequestToast"),
component: VerificationRequestToast,
priority: 90,
});
}
@ -1976,21 +1989,18 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
let view = null;
if (this.state.view === Views.LOADING) {
const Spinner = sdk.getComponent('elements.Spinner');
view = (
<div className="mx_MatrixChat_splash">
<Spinner />
</div>
);
} else if (this.state.view === Views.COMPLETE_SECURITY) {
const CompleteSecurity = sdk.getComponent('structures.auth.CompleteSecurity');
view = (
<CompleteSecurity
onFinished={this.onCompleteSecurityE2eSetupFinished}
/>
);
} else if (this.state.view === Views.E2E_SETUP) {
const E2eSetup = sdk.getComponent('structures.auth.E2eSetup');
view = (
<E2eSetup
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
* as using something like redux to avoid having a billion bits of state kicking around.
*/
const LoggedInView = sdk.getComponent('structures.LoggedInView');
view = (
<LoggedInView
{...this.props}
@ -2019,14 +2028,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
ref={this.loggedInView}
matrixClient={MatrixClientPeg.get()}
onRoomCreated={this.onRoomCreated}
onCloseAllSettings={this.onCloseAllSettings}
onRegistered={this.onRegistered}
currentRoomId={this.state.currentRoomId}
/>
);
} else {
// we think we are logged in, but are still waiting for the /sync to complete
const Spinner = sdk.getComponent('elements.Spinner');
let errorBox;
if (this.state.syncError && !isStoreError) {
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) {
const Welcome = sdk.getComponent('auth.Welcome');
view = <Welcome />;
} else if (this.state.view === Views.REGISTER && SettingsStore.getValue(UIFeature.Registration)) {
const Registration = sdk.getComponent('structures.auth.Registration');
const email = ThreepidInviteStore.instance.pickBestInvite()?.toEmail;
view = (
<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)) {
const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword');
view = (
<ForgotPassword
onComplete={this.onLoginClick}
@ -2077,7 +2081,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
);
} else if (this.state.view === Views.LOGIN) {
const showPasswordReset = SettingsStore.getValue(UIFeature.PasswordReset);
const Login = sdk.getComponent('structures.auth.Login');
view = (
<Login
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) {
const SoftLogout = sdk.getComponent('structures.auth.SoftLogout');
view = (
<SoftLogout
realQueryParams={this.props.realQueryParams}
@ -2105,7 +2107,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
console.error(`Unknown view ${this.state.view}`);
}
const ErrorBoundary = sdk.getComponent('elements.ErrorBoundary');
return <ErrorBoundary>
{view}
</ErrorBoundary>;

View file

@ -24,7 +24,7 @@ interface IProps {
}
interface IState {
toasts: ComponentClass[],
toasts: ComponentClass[];
}
@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 dis from '../../dispatcher/dispatcher';
import RateLimitedFunc from '../../ratelimitedfunc';
import GroupStore from '../../stores/GroupStore';
import {
RIGHT_PANEL_PHASES_NO_ARGS,
@ -48,6 +47,7 @@ import FilePanel from "./FilePanel";
import NotificationPanel from "./NotificationPanel";
import ResizeNotifier from "../../utils/ResizeNotifier";
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
import { throttle } from 'lodash';
interface IProps {
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> {
static contextType = MatrixClientContext;
private readonly delayedUpdate: RateLimitedFunc;
private dispatcherRef: string;
constructor(props, context) {
@ -84,12 +83,12 @@ export default class RightPanel extends React.Component<IProps, IState> {
isUserPrivilegedInGroup: null,
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
// as both are called at the same time in the constructor.
private getUserForPanel() {

View file

@ -48,6 +48,9 @@ import { ActionPayload } from "../../dispatcher/payloads";
const MAX_NAME_LENGTH = 80;
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) {
Analytics.trackEvent('RoomDirectory', action);
}
@ -61,7 +64,7 @@ interface IState {
loading: boolean;
protocolsLoading: boolean;
error?: string;
instanceId: string | symbol;
instanceId: string;
roomServer: string;
filterString: string;
selectedCommunityId?: string;
@ -116,6 +119,36 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
} else if (!selectedCommunityId) {
MatrixClientPeg.get().getThirdpartyProtocols().then((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 });
}, (err) => {
console.warn(`error loading third party protocols: ${err}`);
@ -150,8 +183,8 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
publicRooms: [],
loading: true,
error: null,
instanceId: undefined,
roomServer: MatrixClientPeg.getHomeserverName(),
instanceId: localStorage.getItem(LAST_INSTANCE_KEY),
roomServer: localStorage.getItem(LAST_SERVER_KEY),
filterString: this.props.initialText || "",
selectedCommunityId,
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
this.nextBatch = null;
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
// to render all those rooms when switching back to 'all networks'.
// 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) => {
@ -370,7 +411,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
private onFilterChange = (alias: string) => {
this.setState({
filterString: alias || null,
filterString: alias || "",
});
// don't send the request for a little bit,
@ -389,7 +430,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
private onFilterClear = () => {
// update immediately
this.setState({
filterString: null,
filterString: "",
}, this.refreshRoomList);
if (this.filterTimeout) {

View file

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

View file

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

View file

@ -34,16 +34,14 @@ import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
import ResizeNotifier from '../../utils/ResizeNotifier';
import ContentMessages from '../../ContentMessages';
import Modal from '../../Modal';
import * as sdk from '../../index';
import CallHandler, { PlaceCallType } from '../../CallHandler';
import dis from '../../dispatcher/dispatcher';
import rateLimitedFunc from '../../ratelimitedfunc';
import * as Rooms from '../../Rooms';
import eventSearch, { searchPagination } from '../../Searching';
import MainSplit from './MainSplit';
import RightPanel from './RightPanel';
import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
import RoomScrollStateStore, { ScrollState } from '../../stores/RoomScrollStateStore';
import WidgetEchoStore from '../../stores/WidgetEchoStore';
import SettingsStore from "../../settings/SettingsStore";
import { Layout } from "../../settings/Layout";
@ -64,7 +62,7 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
import AuxPanel from "../views/rooms/AuxPanel";
import RoomHeader from "../views/rooms/RoomHeader";
import { XOR } from "../../@types/common";
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
import EffectsOverlay from "../views/elements/EffectsOverlay";
import { containsEmoji } from '../../effects/utils';
import { CHAT_EFFECTS } from '../../effects';
@ -82,6 +80,15 @@ import { IOpts } from "../../createRoom";
import { replaceableComponent } from "../../utils/replaceableComponent";
import UIStore from "../../stores/UIStore";
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;
let debuglog = function(msg: string) {};
@ -94,22 +101,8 @@ if (DEBUG) {
}
interface IProps {
threepidInvite: IThreepidInvite,
// 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;
};
threepidInvite: IThreepidInvite;
oobData?: IOOBData;
resizeNotifier: ResizeNotifier;
justCreatedOpts?: IOpts;
@ -680,8 +673,8 @@ export default class RoomView extends React.Component<IProps, IState> {
);
}
// cancel any pending calls to the rate_limited_funcs
this.updateRoomMembers.cancelPendingCall();
// cancel any pending calls to the throttled updated
this.updateRoomMembers.cancel();
for (const watcher of this.settingWatchers) {
SettingsStore.unwatchSetting(watcher);
@ -830,17 +823,16 @@ export default class RoomView extends React.Component<IProps, IState> {
case Action.ComposerInsert: {
// re-dispatch to the correct composer
if (this.state.editState) {
dis.dispatch({
...payload,
action: "edit_composer_insert",
});
} else {
dis.dispatch({
...payload,
action: "send_composer_insert",
});
}
dis.dispatch({
...payload,
action: this.state.editState ? "edit_composer_insert" : "send_composer_insert",
});
break;
}
case Action.FocusAComposer: {
// re-dispatch to the correct composer
dis.fire(this.state.editState ? Action.FocusEditMessageComposer : Action.FocusSendMessageComposer);
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) => {
const type = event.getType();
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;
}
this.updateRoomMembers(member);
this.updateRoomMembers();
};
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.
private updateRoomMembers = rateLimitedFunc(() => {
private updateRoomMembers = throttle(() => {
this.updateDMState();
this.updateE2EStatus(this.state.room);
}, 500);
}, 500, { leading: true, trailing: true });
private checkDesktopNotifications() {
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(
ev.dataTransfer.files, this.state.room.roomId, this.context,
);
dis.fire(Action.FocusComposer);
dis.fire(Action.FocusSendMessageComposer);
this.setState({
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()) {
dis.dispatch({ action: 'require_registration' });
return;
@ -1352,7 +1339,6 @@ export default class RoomView extends React.Component<IProps, IState> {
searchResults: results,
});
}, (error) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Search failed", error);
Modal.createTrackedDialog('Search failed', '', ErrorDialog, {
title: _t("Search failed"),
@ -1368,9 +1354,6 @@ export default class RoomView extends React.Component<IProps, IState> {
}
private getSearchResultTiles() {
const SearchResultTile = sdk.getComponent('rooms.SearchResultTile');
const Spinner = sdk.getComponent("elements.Spinner");
// XXX: todo: merge overlapping results somehow?
// 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 = () => {
dis.dispatch({
action: 'forget_room',
@ -1497,7 +1473,6 @@ export default class RoomView extends React.Component<IProps, IState> {
console.error("Failed to reject invite: %s", error);
const msg = error.message ? error.message : JSON.stringify(error);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to reject invite', '', ErrorDialog, {
title: _t("Failed to reject invite"),
description: msg,
@ -1531,7 +1506,6 @@ export default class RoomView extends React.Component<IProps, IState> {
console.error("Failed to reject invite: %s", error);
const msg = error.message ? error.message : JSON.stringify(error);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to reject invite', '', ErrorDialog, {
title: _t("Failed to reject invite"),
description: msg,
@ -1578,7 +1552,7 @@ export default class RoomView extends React.Component<IProps, IState> {
} else {
// Otherwise we have to jump manually
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
// restored when we switch back to it.
//
private getScrollState() {
private getScrollState(): ScrollState {
const messagePanel = this.messagePanel;
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.
private gatherTimelinePanelRef = r => {
this.messagePanel = r;
if (r) {
console.log("updateTint from RoomView.gatherTimelinePanelRef");
this.updateTint();
}
};
private getOldRoom() {
@ -1869,10 +1839,8 @@ export default class RoomView extends React.Component<IProps, IState> {
let isStatusAreaExpanded = true;
if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) {
const UploadBar = sdk.getComponent('structures.UploadBar');
statusBar = <UploadBar room={this.state.room} />;
} else if (!this.state.searchResults) {
const RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
isStatusAreaExpanded = this.state.statusBarVisible;
statusBar = <RoomStatusBar
room={this.state.room}
@ -1978,12 +1946,9 @@ export default class RoomView extends React.Component<IProps, IState> {
myMembership === 'join' && !this.state.searchResults
);
if (canSpeak) {
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
messageComposer =
<MessageComposer
room={this.state.room}
callState={this.state.callState}
showApps={this.state.showApps}
e2eStatus={this.state.e2eStatus}
resizeNotifier={this.props.resizeNotifier}
replyToEvent={this.state.replyToEvent}
@ -2069,7 +2034,6 @@ export default class RoomView extends React.Component<IProps, IState> {
let topUnreadMessagesBar = null;
// Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense
if (this.state.showTopUnreadMessagesBar && !this.state.searchResults) {
const TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar');
topUnreadMessagesBar = (
<TopUnreadMessagesBar onScrollUpClick={this.jumpToReadMarker} onCloseClick={this.forgetReadMarker} />
);
@ -2077,7 +2041,6 @@ export default class RoomView extends React.Component<IProps, IState> {
let jumpToBottom;
// Do not show JumpToBottomButton if we have search results showing, it makes no sense
if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) {
const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton');
jumpToBottom = (<JumpToBottomButton
highlight={this.state.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0}
numUnreadMessages={this.state.numUnreadMessages}
@ -2120,7 +2083,6 @@ export default class RoomView extends React.Component<IProps, IState> {
onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick}
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
e2eStatus={this.state.e2eStatus}
onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null}
appsShown={this.state.showApps}

View file

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

View file

@ -18,9 +18,9 @@ limitations under the License.
import * as React from "react";
import { _t } from '../../languageHandler';
import * as sdk from "../../index";
import AutoHideScrollbar from './AutoHideScrollbar';
import { replaceableComponent } from "../../utils/replaceableComponent";
import AccessibleButton from "../views/elements/AccessibleButton";
/**
* Represents a tab for the TabbedView.
@ -82,8 +82,6 @@ export default class TabbedView extends React.Component<IProps, IState> {
}
private _renderTabLabel(tab: Tab) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let classes = "mx_TabbedView_tabLabel ";
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 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 { TimelineSet } from "matrix-js-sdk/src/models/event-timeline-set";
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set";
import { Direction, EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
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 { Layout } from "../../settings/Layout";
@ -30,7 +32,6 @@ import RoomContext from "../../contexts/RoomContext";
import UserActivity from "../../UserActivity";
import Modal from "../../Modal";
import dis from "../../dispatcher/dispatcher";
import * as sdk from "../../index";
import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer';
import shouldHideEvent from '../../shouldHideEvent';
@ -39,14 +40,13 @@ import { UIFeature } from "../../settings/UIFeature";
import { replaceableComponent } from "../../utils/replaceableComponent";
import { arrayFastClone } from "../../utils/arrays";
import MessagePanel from "./MessagePanel";
import { SyncState } from 'matrix-js-sdk/src/sync.api';
import { IScrollState } from "./ScrollPanel";
import { ActionPayload } from "../../dispatcher/payloads";
import { EventType } from 'matrix-js-sdk/src/@types/event';
import ResizeNotifier from "../../utils/ResizeNotifier";
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
import Spinner from "../views/elements/Spinner";
import EditorStateTransfer from '../../utils/EditorStateTransfer';
import ErrorDialog from '../views/dialogs/ErrorDialog';
const PAGINATE_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
// a timeline representing. If it has a room, we maintain RRs etc for
// that room.
timelineSet: TimelineSet;
timelineSet: EventTimelineSet;
showReadReceipts?: boolean;
// Enable managing RRs and RMs. These require the timelineSet to have a room.
manageReadReceipts?: boolean;
@ -125,7 +125,7 @@ interface IProps {
onReadMarkerUpdated?(): void;
// 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 {
@ -388,7 +388,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
private onPaginationRequest = (
timelineWindow: TimelineWindow,
direction: string,
direction: Direction,
size: number,
): Promise<boolean> => {
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 (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
// to zero: it may not do this if we send an RR for somewhere before the end.
if (this.isAtEndOfLiveTimeline()) {
this.props.timelineSet.room.setUnreadNotificationCount('total', 0);
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
this.props.timelineSet.room.setUnreadNotificationCount(NotificationCountType.Total, 0);
this.props.timelineSet.room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
dis.dispatch({
action: 'on_room_read',
roomId: this.props.timelineSet.room.roomId,
@ -1096,7 +1096,6 @@ class TimelinePanel extends React.Component<IProps, IState> {
console.error(
`Error loading timeline panel at ${eventId}: ${error}`,
);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
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() {
// 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 { IUpload } from "../../models/IUpload";
import { replaceableComponent } from "../../utils/replaceableComponent";
import MatrixClientContext from "../../contexts/MatrixClientContext";
interface IProps {
room: Room;
@ -38,6 +39,8 @@ interface IState {
@replaceableComponent("structures.UploadBar")
export default class UploadBar extends React.Component<IProps, IState> {
static contextType = MatrixClientContext;
private dispatcherRef: string;
private mounted: boolean;
@ -82,7 +85,7 @@ export default class UploadBar extends React.Component<IProps, IState> {
private onCancelClick = (ev) => {
ev.preventDefault();
ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise);
ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise, this.context);
};
render() {

View file

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

View file

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

View file

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

View file

@ -18,7 +18,6 @@ import React, { ReactNode } from 'react';
import { MatrixError } from "matrix-js-sdk/src/http-api";
import { _t, _td } from '../../../languageHandler';
import * as sdk from '../../../index';
import Login, { ISSOFlow, LoginFlow } from '../../../Login';
import SdkConfig from '../../../SdkConfig';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
@ -36,6 +35,8 @@ import Spinner from "../../views/elements/Spinner";
import SSOButtons from "../../views/elements/SSOButtons";
import ServerPicker from "../../views/elements/ServerPicker";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AuthBody from "../../views/auth/AuthBody";
import AuthHeader from "../../views/auth/AuthHeader";
// 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.
@ -541,8 +542,6 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
};
render() {
const AuthHeader = sdk.getComponent("auth.AuthHeader");
const AuthBody = sdk.getComponent("auth.AuthBody");
const loader = this.isBusy() && !this.state.busyLoggingIn ?
<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 { MatrixClient } from "matrix-js-sdk/src/client";
import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
import classNames from "classnames";
import * as Lifecycle from '../../../Lifecycle';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { IMatrixClientCreds, MatrixClientPeg } from "../../../MatrixClientPeg";
import AuthPage from "../../views/auth/AuthPage";
import Login, { ISSOFlow } from "../../../Login";
import dis from "../../../dispatcher/dispatcher";
import SSOButtons from "../../views/elements/SSOButtons";
import ServerPicker from '../../views/elements/ServerPicker';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import RegistrationForm from '../../views/auth/RegistrationForm';
import AccessibleButton from '../../views/elements/AccessibleButton';
import AuthBody from "../../views/auth/AuthBody";
import AuthHeader from "../../views/auth/AuthHeader";
import InteractiveAuth from "../InteractiveAuth";
import Spinner from "../../views/elements/Spinner";
interface IProps {
serverConfig: ValidatedServerConfig;
@ -47,13 +52,7 @@ interface IProps {
// - 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 operations like uploading cross-signing keys).
onLoggedIn(params: {
userId: string;
deviceId: string
homeserverUrl: string;
identityServerUrl?: string;
accessToken: string;
}, password: string): void;
onLoggedIn(params: IMatrixClientCreds, password: string): void;
makeRegistrationUrl(params: {
/* eslint-disable camelcase */
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({
errorText: "",
busy: true,
@ -442,10 +441,6 @@ export default class Registration extends React.Component<IProps, IState> {
};
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) {
return <InteractiveAuth
matrixClient={this.state.matrixClient}
@ -516,10 +511,6 @@ export default class Registration extends React.Component<IProps, IState> {
}
render() {
const AuthHeader = sdk.getComponent('auth.AuthHeader');
const AuthBody = sdk.getComponent("auth.AuthBody");
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let errorText;
const err = this.state.errorText;
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");
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 PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
import * as sdk from '../../../index';
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
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) {
return (
function keyHasPassphrase(keyInfo: ISecretStorageKeyInfo): boolean {
return Boolean(
keyInfo.passphrase &&
keyInfo.passphrase.salt &&
keyInfo.passphrase.iterations
keyInfo.passphrase.iterations,
);
}
@replaceableComponent("structures.auth.SetupEncryptionBody")
export default class SetupEncryptionBody extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
interface IProps {
onFinished: (boolean) => void;
}
constructor() {
super();
interface IState {
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();
store.on("update", this._onStoreUpdate);
store.on("update", this.onStoreUpdate);
store.start();
this.state = {
phase: store.phase,
@ -53,10 +63,10 @@ export default class SetupEncryptionBody extends React.Component {
};
}
_onStoreUpdate = () => {
private onStoreUpdate = () => {
const store = SetupEncryptionStore.sharedInstance();
if (store.phase === Phase.Finished) {
this.props.onFinished();
this.props.onFinished(true);
return;
}
this.setState({
@ -66,18 +76,18 @@ export default class SetupEncryptionBody extends React.Component {
});
};
componentWillUnmount() {
public componentWillUnmount() {
const store = SetupEncryptionStore.sharedInstance();
store.off("update", this._onStoreUpdate);
store.off("update", this.onStoreUpdate);
store.stop();
}
_onUsePassphraseClick = async () => {
private onUsePassphraseClick = async () => {
const store = SetupEncryptionStore.sharedInstance();
store.usePassPhrase();
}
};
_onVerifyClick = () => {
private onVerifyClick = () => {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
const requestPromise = cli.requestVerification(userId);
@ -91,42 +101,44 @@ export default class SetupEncryptionBody extends React.Component {
request.cancel();
},
});
}
};
onSkipClick = () => {
private onSkipClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.skip();
}
};
onSkipConfirmClick = () => {
private onSkipConfirmClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.skipConfirm();
}
};
onSkipBackClick = () => {
private onSkipBackClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.returnAfterSkip();
}
};
onDoneClick = () => {
private onDoneClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.done();
}
};
render() {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
private onEncryptionPanelClose = () => {
this.props.onFinished(false);
};
public render() {
const {
phase,
} = this.state;
if (this.state.verificationRequest) {
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
return <EncryptionPanel
layout="dialog"
verificationRequest={this.state.verificationRequest}
onClose={this.props.onFinished}
onClose={this.onEncryptionPanelClose}
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
isRoomEncrypted={false}
/>;
} else if (phase === Phase.Intro) {
const store = SetupEncryptionStore.sharedInstance();
@ -139,14 +151,14 @@ export default class SetupEncryptionBody extends React.Component {
let useRecoveryKeyButton;
if (recoveryKeyPrompt) {
useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this._onUsePassphraseClick}>
useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this.onUsePassphraseClick}>
{recoveryKeyPrompt}
</AccessibleButton>;
}
let verifyButton;
if (store.hasDevicesToVerifyAgainst) {
verifyButton = <AccessibleButton kind="primary" onClick={this._onVerifyClick}>
verifyButton = <AccessibleButton kind="primary" onClick={this.onVerifyClick}>
{ _t("Use another login") }
</AccessibleButton>;
}
@ -217,7 +229,6 @@ export default class SetupEncryptionBody extends React.Component {
</div>
);
} else if (phase === Phase.Busy || phase === Phase.Loading) {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <Spinner />;
} else {
console.log(`SetupEncryptionBody: Unknown phase ${phase}`);

View file

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

View file

@ -1,9 +1,12 @@
/*
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.
@ -12,16 +15,15 @@ limitations under the License.
*/
import React from "react";
import Waveform from "./Waveform";
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../voice/VoiceRecording";
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 {
IRecordingUpdate,
VoiceRecording,
} from "../../../voice/VoiceRecording";
interface IProps {
recorder?: VoiceRecording;
recorder: VoiceRecording;
}
interface IState {
@ -31,7 +33,7 @@ interface IState {
/**
* 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> {
public static defaultProps = {
progress: 1,
@ -52,15 +54,18 @@ export default class LiveRecordingWaveform extends React.PureComponent<IProps, I
componentDidMount() {
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();
});
}
private updateWaveform() {
this.setState({
waveform: this.waveform,
});
this.setState({ waveform: this.waveform });
}
public render() {

View file

@ -21,7 +21,8 @@ import { _t } from "../../../languageHandler";
import { Playback, PlaybackState } from "../../../voice/Playback";
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: Playback;
@ -33,19 +34,25 @@ interface IProps {
* Displays a play/pause button (activating the play/pause function of the recorder)
* 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> {
public constructor(props) {
super(props);
}
private onClick = async () => {
await this.props.playback.toggle();
private onClick = () => {
// noinspection JSIgnoredPromiseFromCall
this.toggleState();
};
public async toggleState() {
await this.props.playback.toggle();
}
public render(): ReactNode {
const isPlaying = this.props.playback.isPlaying;
const isDisabled = this.props.playbackPhase === PlaybackState.Decoding;
const { playback, playbackPhase, ...restProps } = this.props;
const isPlaying = playback.isPlaying;
const isDisabled = playbackPhase === PlaybackState.Decoding;
const classes = classNames('mx_PlayPauseButton', {
'mx_PlayPauseButton_play': !isPlaying,
'mx_PlayPauseButton_pause': isPlaying,
@ -56,6 +63,7 @@ export default class PlayPauseButton extends React.PureComponent<IProps> {
title={isPlaying ? _t("Pause") : _t("Play")}
onClick={this.onClick}
disabled={isDisabled}
{...restProps}
/>;
}
}

View file

@ -22,6 +22,11 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore";
interface IProps {
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 {
@ -33,7 +38,7 @@ interface IState {
/**
* 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> {
public constructor(props) {
super(props);
@ -64,7 +69,11 @@ export default class PlaybackClock extends React.PureComponent<IProps, IState> {
public render() {
let seconds = this.state.seconds;
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} />;
}

View file

@ -33,7 +33,7 @@ interface IState {
/**
* 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> {
public constructor(props) {
super(props);

View file

@ -20,6 +20,7 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import PlaybackWaveform from "./PlaybackWaveform";
import PlayPauseButton from "./PlayPauseButton";
import PlaybackClock from "./PlaybackClock";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
// Playback instance to render. Cannot change during component lifecycle: create
@ -31,6 +32,7 @@ interface IState {
playbackPhase: PlaybackState;
}
@replaceableComponent("views.audio_messages.RecordingPlayback")
export default class RecordingPlayback extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);
@ -53,7 +55,7 @@ export default class RecordingPlayback extends React.PureComponent<IProps, IStat
};
public render(): ReactNode {
return <div className='mx_VoiceMessagePrimaryContainer'>
return <div className='mx_MediaBody mx_VoiceMessagePrimaryContainer'>
<PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} />
<PlaybackClock 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 { replaceableComponent } from "../../../utils/replaceableComponent";
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)
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
* "filled", as a demonstration of the progress property.
*/
import { CSSProperties } from "react";
export interface WaveformCSSProperties extends CSSProperties {
'--barHeight': number;
}
@replaceableComponent("views.voice_messages.Waveform")
@replaceableComponent("views.audio_messages.Waveform")
export default class Waveform extends React.PureComponent<IProps, IState> {
public static defaultProps = {
progress: 1,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
/*
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");
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 PropTypes from 'prop-types';
import { EventStatus } from 'matrix-js-sdk/src/models/event';
import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import Resend from '../../../Resend';
@ -29,53 +28,65 @@ import SettingsStore from '../../../settings/SettingsStore';
import { isUrlPermitted } from '../../../HtmlUtils';
import { isContentActionable } from '../../../utils/EventUtils';
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu';
import { EventType } from "matrix-js-sdk/src/@types/event";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
import ForwardDialog from "../dialogs/ForwardDialog";
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;
}
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")
export default class MessageContextMenu extends React.Component {
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,
};
export default class MessageContextMenu extends React.Component<IProps, IState> {
state = {
canRedact: false,
canPin: false,
};
componentDidMount() {
MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions);
this._checkPermissions();
MatrixClientPeg.get().on('RoomMember.powerLevel', this.checkPermissions);
this.checkPermissions();
}
componentWillUnmount() {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener('RoomMember.powerLevel', this._checkPermissions);
cli.removeListener('RoomMember.powerLevel', this.checkPermissions);
}
}
_checkPermissions = () => {
private checkPermissions = (): void => {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId());
@ -93,7 +104,7 @@ export default class MessageContextMenu extends React.Component {
this.setState({ canRedact, canPin });
};
_isPinned() {
private isPinned(): boolean {
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, '');
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());
}
onResendReactionsClick = () => {
for (const reaction of this._getUnsentReactions()) {
private onResendReactionsClick = (): void => {
for (const reaction of this.getUnsentReactions()) {
Resend.resend(reaction);
}
this.closeMenu();
};
onReportEventClick = () => {
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
private onReportEventClick = (): void => {
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
mxEvent: this.props.mxEvent,
}, 'mx_Dialog_reportEvent');
this.closeMenu();
};
onViewSourceClick = () => {
const ViewSource = sdk.getComponent('structures.ViewSource');
private onViewSourceClick = (): void => {
Modal.createTrackedDialog('View Event Source', '', ViewSource, {
mxEvent: this.props.mxEvent,
}, 'mx_Dialog_viewsource');
this.closeMenu();
};
onRedactClick = () => {
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
private onRedactClick = (): void => {
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
onFinished: async (proceed, reason) => {
onFinished: async (proceed: boolean, reason?: string) => {
if (!proceed) return;
const cli = MatrixClientPeg.get();
try {
if (this.props.onCloseDialog) this.props.onCloseDialog();
this.props.onCloseDialog?.();
await cli.redactEvent(
this.props.mxEvent.getRoomId(),
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
// detached queue and we show the room status bar to allow retry
if (typeof code !== "undefined") {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
// display error message stating you couldn't delete this.
Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
title: _t('Error'),
@ -158,7 +165,7 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu();
};
onForwardClick = () => {
private onForwardClick = (): void => {
Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
matrixClient: MatrixClientPeg.get(),
event: this.props.mxEvent,
@ -167,12 +174,12 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu();
};
onPinClick = () => {
private onPinClick = (): void => {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId());
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)) {
pinnedIds.splice(pinnedIds.indexOf(eventId), 1);
} else {
@ -188,18 +195,16 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu();
};
closeMenu = () => {
if (this.props.onFinished) this.props.onFinished();
private closeMenu = (): void => {
this.props.onFinished();
};
onUnhidePreviewClick = () => {
if (this.props.eventTileOps) {
this.props.eventTileOps.unhideWidget();
}
private onUnhidePreviewClick = (): void => {
this.props.eventTileOps?.unhideWidget();
this.closeMenu();
};
onQuoteClick = () => {
private onQuoteClick = (): void => {
dis.dispatch({
action: Action.ComposerInsert,
event: this.props.mxEvent,
@ -207,9 +212,8 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu();
};
onPermalinkClick = (e) => {
private onPermalinkClick = (e: React.MouseEvent): void => {
e.preventDefault();
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
target: this.props.mxEvent,
permalinkCreator: this.props.permalinkCreator,
@ -217,30 +221,27 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu();
};
onCollapseReplyThreadClick = () => {
private onCollapseReplyThreadClick = (): void => {
this.props.collapseReplyThread();
this.closeMenu();
};
_getReactions(filter) {
private getReactions(filter: (e: MatrixEvent) => boolean): MatrixEvent[] {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.mxEvent.getRoomId());
const eventId = this.props.mxEvent.getId();
return room.getPendingEvents().filter(e => {
const relation = e.getRelation();
return relation &&
relation.rel_type === "m.annotation" &&
relation.event_id === eventId &&
filter(e);
return relation?.rel_type === RelationType.Annotation && relation.event_id === eventId && filter(e);
});
}
_getPendingReactions() {
return this._getReactions(e => canCancel(e.status));
private getPendingReactions(): MatrixEvent[] {
return this.getReactions(e => canCancel(e.status));
}
_getUnsentReactions() {
return this._getReactions(e => e.status === EventStatus.NOT_SENT);
private getUnsentReactions(): MatrixEvent[] {
return this.getReactions(e => e.status === EventStatus.NOT_SENT);
}
render() {
@ -248,16 +249,17 @@ export default class MessageContextMenu extends React.Component {
const me = cli.getUserId();
const mxEvent = this.props.mxEvent;
const eventStatus = mxEvent.status;
const unsentReactionsCount = this._getUnsentReactions().length;
let resendReactionsButton;
let redactButton;
let forwardButton;
let pinButton;
let unhidePreviewButton;
let externalURLButton;
let quoteButton;
let collapseReplyThread;
let redactItemList;
const unsentReactionsCount = this.getUnsentReactions().length;
let resendReactionsButton: JSX.Element;
let redactButton: JSX.Element;
let forwardButton: JSX.Element;
let pinButton: JSX.Element;
let unhidePreviewButton: JSX.Element;
let externalURLButton: JSX.Element;
let quoteButton: JSX.Element;
let collapseReplyThread: JSX.Element;
let redactItemList: JSX.Element;
// status is SENT before remote-echo, null after
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
@ -296,7 +298,7 @@ export default class MessageContextMenu extends React.Component {
pinButton = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconPin"
label={ this._isPinned() ? _t('Unpin') : _t('Pin') }
label={ this.isPinned() ? _t('Unpin') : _t('Pin') }
onClick={this.onPinClick}
/>
);
@ -327,16 +329,20 @@ export default class MessageContextMenu extends React.Component {
if (this.props.permalinkCreator) {
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 = (
<IconizedContextMenuOption
iconClassName="mx_MessageContextMenu_iconPermalink"
onClick={this.onPermalinkClick}
label= {_t('Share')}
element="a"
href={permalink}
target="_blank"
rel="noreferrer noopener"
{
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
...{
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.
if (typeof (mxEvent.event.content.external_url) === "string" &&
isUrlPermitted(mxEvent.event.content.external_url)
if (typeof (mxEvent.getContent().external_url) === "string" &&
isUrlPermitted(mxEvent.getContent().external_url)
) {
externalURLButton = (
<IconizedContextMenuOption
@ -360,9 +366,14 @@ export default class MessageContextMenu extends React.Component {
onClick={this.closeMenu}
label={ _t('Source URL') }
element="a"
target="_blank"
rel="noreferrer noopener"
href={mxEvent.event.content.external_url}
{
// XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
...{
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) {
reportEventButton = (
<IconizedContextMenuOption

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