Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
commit
66a3b0fe5f
313 changed files with 4348 additions and 3855 deletions
149
CHANGELOG.md
149
CHANGELOG.md
|
@ -1,3 +1,152 @@
|
||||||
|
Changes in [3.26.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.26.0) (2021-07-19)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.26.0-rc.1...v3.26.0)
|
||||||
|
|
||||||
|
* Fix 'User' type import
|
||||||
|
[\#6376](https://github.com/matrix-org/matrix-react-sdk/pull/6376)
|
||||||
|
|
||||||
|
Changes in [3.26.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.26.0-rc.1) (2021-07-14)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.25.0...v3.26.0-rc.1)
|
||||||
|
|
||||||
|
* Fix voice messages in right panels
|
||||||
|
[\#6370](https://github.com/matrix-org/matrix-react-sdk/pull/6370)
|
||||||
|
* Use TileShape enum more universally
|
||||||
|
[\#6369](https://github.com/matrix-org/matrix-react-sdk/pull/6369)
|
||||||
|
* Translations update from Weblate
|
||||||
|
[\#6373](https://github.com/matrix-org/matrix-react-sdk/pull/6373)
|
||||||
|
* Hide world readable history option in encrypted rooms
|
||||||
|
[\#5947](https://github.com/matrix-org/matrix-react-sdk/pull/5947)
|
||||||
|
* Make the Image View buttons easier to hit
|
||||||
|
[\#6372](https://github.com/matrix-org/matrix-react-sdk/pull/6372)
|
||||||
|
* Reorder buttons in the Image View
|
||||||
|
[\#6368](https://github.com/matrix-org/matrix-react-sdk/pull/6368)
|
||||||
|
* Add VS Code to gitignore
|
||||||
|
[\#6367](https://github.com/matrix-org/matrix-react-sdk/pull/6367)
|
||||||
|
* Fix inviter exploding due to member being null
|
||||||
|
[\#6362](https://github.com/matrix-org/matrix-react-sdk/pull/6362)
|
||||||
|
* Increase sample count in voice message thumbnail
|
||||||
|
[\#6359](https://github.com/matrix-org/matrix-react-sdk/pull/6359)
|
||||||
|
* Improve arraySeed utility
|
||||||
|
[\#6360](https://github.com/matrix-org/matrix-react-sdk/pull/6360)
|
||||||
|
* Convert FontManager to TS and stub it out for tests
|
||||||
|
[\#6358](https://github.com/matrix-org/matrix-react-sdk/pull/6358)
|
||||||
|
* Adjust recording waveform behaviour for voice messages
|
||||||
|
[\#6357](https://github.com/matrix-org/matrix-react-sdk/pull/6357)
|
||||||
|
* Do not honor string power levels
|
||||||
|
[\#6245](https://github.com/matrix-org/matrix-react-sdk/pull/6245)
|
||||||
|
* Add alias and directory customisation points
|
||||||
|
[\#6343](https://github.com/matrix-org/matrix-react-sdk/pull/6343)
|
||||||
|
* Fix multiinviter user already in room and clean up code
|
||||||
|
[\#6354](https://github.com/matrix-org/matrix-react-sdk/pull/6354)
|
||||||
|
* Fix right panel not closing user info when changing rooms
|
||||||
|
[\#6341](https://github.com/matrix-org/matrix-react-sdk/pull/6341)
|
||||||
|
* Quit sticker picker on m.sticker
|
||||||
|
[\#5679](https://github.com/matrix-org/matrix-react-sdk/pull/5679)
|
||||||
|
* Don't autodetect language in inline code blocks
|
||||||
|
[\#6350](https://github.com/matrix-org/matrix-react-sdk/pull/6350)
|
||||||
|
* Make ghost button background transparent
|
||||||
|
[\#6331](https://github.com/matrix-org/matrix-react-sdk/pull/6331)
|
||||||
|
* only consider valid & loaded url previews for show N more prompt
|
||||||
|
[\#6346](https://github.com/matrix-org/matrix-react-sdk/pull/6346)
|
||||||
|
* Extract MXCs from _matrix/media/r0/ URLs for inline images in messages
|
||||||
|
[\#6335](https://github.com/matrix-org/matrix-react-sdk/pull/6335)
|
||||||
|
* Fix small visual regression with the site name on url previews
|
||||||
|
[\#6342](https://github.com/matrix-org/matrix-react-sdk/pull/6342)
|
||||||
|
* Make PIP CallView draggable/movable
|
||||||
|
[\#5952](https://github.com/matrix-org/matrix-react-sdk/pull/5952)
|
||||||
|
* Convert VoiceUserSettingsTab to TS
|
||||||
|
[\#6340](https://github.com/matrix-org/matrix-react-sdk/pull/6340)
|
||||||
|
* Simplify typescript definition for Modernizr
|
||||||
|
[\#6339](https://github.com/matrix-org/matrix-react-sdk/pull/6339)
|
||||||
|
* Remember the last used server for room directory searches
|
||||||
|
[\#6322](https://github.com/matrix-org/matrix-react-sdk/pull/6322)
|
||||||
|
* Focus composer after reacting
|
||||||
|
[\#6332](https://github.com/matrix-org/matrix-react-sdk/pull/6332)
|
||||||
|
* Fix bug which prevented more than one event getting pinned
|
||||||
|
[\#6336](https://github.com/matrix-org/matrix-react-sdk/pull/6336)
|
||||||
|
* Make DeviceListener also update on megolm key in SSSS
|
||||||
|
[\#6337](https://github.com/matrix-org/matrix-react-sdk/pull/6337)
|
||||||
|
* Improve URL previews
|
||||||
|
[\#6326](https://github.com/matrix-org/matrix-react-sdk/pull/6326)
|
||||||
|
* Don't close settings dialog when opening spaces feedback prompt
|
||||||
|
[\#6334](https://github.com/matrix-org/matrix-react-sdk/pull/6334)
|
||||||
|
* Update import location for types
|
||||||
|
[\#6330](https://github.com/matrix-org/matrix-react-sdk/pull/6330)
|
||||||
|
* Improve blurhash rendering performance
|
||||||
|
[\#6329](https://github.com/matrix-org/matrix-react-sdk/pull/6329)
|
||||||
|
* Use a proper color scheme for codeblocks
|
||||||
|
[\#6320](https://github.com/matrix-org/matrix-react-sdk/pull/6320)
|
||||||
|
* Burn `sdk.getComponent()` with 🔥
|
||||||
|
[\#6308](https://github.com/matrix-org/matrix-react-sdk/pull/6308)
|
||||||
|
* Fix instances of the Edit Message Composer's save button being wrongly
|
||||||
|
disabled
|
||||||
|
[\#6307](https://github.com/matrix-org/matrix-react-sdk/pull/6307)
|
||||||
|
* Do not generate a lockfile when running in CI
|
||||||
|
[\#6327](https://github.com/matrix-org/matrix-react-sdk/pull/6327)
|
||||||
|
* Update lockfile with correct dependencies
|
||||||
|
[\#6324](https://github.com/matrix-org/matrix-react-sdk/pull/6324)
|
||||||
|
* Clarify the keys we use when submitting rageshakes
|
||||||
|
[\#6321](https://github.com/matrix-org/matrix-react-sdk/pull/6321)
|
||||||
|
* Fix ImageView context menu
|
||||||
|
[\#6318](https://github.com/matrix-org/matrix-react-sdk/pull/6318)
|
||||||
|
* TypeScript migration
|
||||||
|
[\#6315](https://github.com/matrix-org/matrix-react-sdk/pull/6315)
|
||||||
|
* Move animation to compositor
|
||||||
|
[\#6310](https://github.com/matrix-org/matrix-react-sdk/pull/6310)
|
||||||
|
* Reorganize preferences
|
||||||
|
[\#5742](https://github.com/matrix-org/matrix-react-sdk/pull/5742)
|
||||||
|
* Fix being able to un-rotate images
|
||||||
|
[\#6313](https://github.com/matrix-org/matrix-react-sdk/pull/6313)
|
||||||
|
* Fix icon size in passphrase prompt
|
||||||
|
[\#6312](https://github.com/matrix-org/matrix-react-sdk/pull/6312)
|
||||||
|
* Use sleep & defer from js-sdk instead of duplicating it
|
||||||
|
[\#6305](https://github.com/matrix-org/matrix-react-sdk/pull/6305)
|
||||||
|
* Convert EventTimeline, EventTimelineSet and TimelineWindow to TS
|
||||||
|
[\#6295](https://github.com/matrix-org/matrix-react-sdk/pull/6295)
|
||||||
|
* Comply with new member-delimiter-style rule
|
||||||
|
[\#6306](https://github.com/matrix-org/matrix-react-sdk/pull/6306)
|
||||||
|
* Fix Test Linting
|
||||||
|
[\#6304](https://github.com/matrix-org/matrix-react-sdk/pull/6304)
|
||||||
|
* Convert Markdown to TypeScript
|
||||||
|
[\#6303](https://github.com/matrix-org/matrix-react-sdk/pull/6303)
|
||||||
|
* Convert RoomHeader to TS
|
||||||
|
[\#6302](https://github.com/matrix-org/matrix-react-sdk/pull/6302)
|
||||||
|
* Prevent RoomDirectory from exploding when filterString is wrongly nulled
|
||||||
|
[\#6296](https://github.com/matrix-org/matrix-react-sdk/pull/6296)
|
||||||
|
* Add support for blurhash (MSC2448)
|
||||||
|
[\#5099](https://github.com/matrix-org/matrix-react-sdk/pull/5099)
|
||||||
|
* Remove rateLimitedFunc
|
||||||
|
[\#6300](https://github.com/matrix-org/matrix-react-sdk/pull/6300)
|
||||||
|
* Convert some Key Verification classes to TypeScript
|
||||||
|
[\#6299](https://github.com/matrix-org/matrix-react-sdk/pull/6299)
|
||||||
|
* Typescript conversion of Composer components and more
|
||||||
|
[\#6292](https://github.com/matrix-org/matrix-react-sdk/pull/6292)
|
||||||
|
* Upgrade browserlist target versions
|
||||||
|
[\#6298](https://github.com/matrix-org/matrix-react-sdk/pull/6298)
|
||||||
|
* Fix browser crashing when searching for a malformed HTML tag
|
||||||
|
[\#6297](https://github.com/matrix-org/matrix-react-sdk/pull/6297)
|
||||||
|
* Add custom audio player
|
||||||
|
[\#6264](https://github.com/matrix-org/matrix-react-sdk/pull/6264)
|
||||||
|
* Lint MXC APIs to centralise access
|
||||||
|
[\#6293](https://github.com/matrix-org/matrix-react-sdk/pull/6293)
|
||||||
|
* Remove reminescent references to the tinter
|
||||||
|
[\#6290](https://github.com/matrix-org/matrix-react-sdk/pull/6290)
|
||||||
|
* More js-sdk type consolidation
|
||||||
|
[\#6263](https://github.com/matrix-org/matrix-react-sdk/pull/6263)
|
||||||
|
* Convert MessagePanel, TimelinePanel, ScrollPanel, and more to Typescript
|
||||||
|
[\#6243](https://github.com/matrix-org/matrix-react-sdk/pull/6243)
|
||||||
|
* Migrate to `eslint-plugin-matrix-org`
|
||||||
|
[\#6285](https://github.com/matrix-org/matrix-react-sdk/pull/6285)
|
||||||
|
* Avoid cyclic dependencies by moving watchers out of constructor
|
||||||
|
[\#6287](https://github.com/matrix-org/matrix-react-sdk/pull/6287)
|
||||||
|
* Add spacing between toast buttons with cross browser support in mind
|
||||||
|
[\#6284](https://github.com/matrix-org/matrix-react-sdk/pull/6284)
|
||||||
|
* Deprecate Tinter and TintableSVG
|
||||||
|
[\#6279](https://github.com/matrix-org/matrix-react-sdk/pull/6279)
|
||||||
|
* Migrate FilePanel to TypeScript
|
||||||
|
[\#6283](https://github.com/matrix-org/matrix-react-sdk/pull/6283)
|
||||||
|
|
||||||
Changes in [3.25.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.25.0) (2021-07-05)
|
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)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.25.0-rc.1...v3.25.0)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.25.0",
|
"version": "3.26.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -46,6 +46,7 @@
|
||||||
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||||
"lint": "yarn lint:types && yarn lint:js && yarn lint:style",
|
"lint": "yarn lint:types && yarn lint:js && yarn lint:style",
|
||||||
"lint:js": "eslint --max-warnings 0 src test",
|
"lint:js": "eslint --max-warnings 0 src test",
|
||||||
|
"lint:js-fix": "eslint --fix src test",
|
||||||
"lint:types": "tsc --noEmit --jsx react",
|
"lint:types": "tsc --noEmit --jsx react",
|
||||||
"lint:style": "stylelint 'res/css/**/*.scss'",
|
"lint:style": "stylelint 'res/css/**/*.scss'",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
|
@ -79,7 +80,7 @@
|
||||||
"katex": "^0.12.0",
|
"katex": "^0.12.0",
|
||||||
"linkifyjs": "^2.1.9",
|
"linkifyjs": "^2.1.9",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"matrix-js-sdk": "12.0.1",
|
"matrix-js-sdk": "12.1.0",
|
||||||
"matrix-widget-api": "^0.1.0-beta.15",
|
"matrix-widget-api": "^0.1.0-beta.15",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"opus-recorder": "^8.0.3",
|
"opus-recorder": "^8.0.3",
|
||||||
|
|
|
@ -149,6 +149,7 @@
|
||||||
@import "./views/elements/_StyledCheckbox.scss";
|
@import "./views/elements/_StyledCheckbox.scss";
|
||||||
@import "./views/elements/_StyledRadioButton.scss";
|
@import "./views/elements/_StyledRadioButton.scss";
|
||||||
@import "./views/elements/_SyntaxHighlight.scss";
|
@import "./views/elements/_SyntaxHighlight.scss";
|
||||||
|
@import "./views/elements/_TagComposer.scss";
|
||||||
@import "./views/elements/_TextWithTooltip.scss";
|
@import "./views/elements/_TextWithTooltip.scss";
|
||||||
@import "./views/elements/_ToggleSwitch.scss";
|
@import "./views/elements/_ToggleSwitch.scss";
|
||||||
@import "./views/elements/_Tooltip.scss";
|
@import "./views/elements/_Tooltip.scss";
|
||||||
|
@ -200,6 +201,7 @@
|
||||||
@import "./views/rooms/_EditMessageComposer.scss";
|
@import "./views/rooms/_EditMessageComposer.scss";
|
||||||
@import "./views/rooms/_EntityTile.scss";
|
@import "./views/rooms/_EntityTile.scss";
|
||||||
@import "./views/rooms/_EventTile.scss";
|
@import "./views/rooms/_EventTile.scss";
|
||||||
|
@import "./views/rooms/_EventBubbleTile.scss";
|
||||||
@import "./views/rooms/_GroupLayout.scss";
|
@import "./views/rooms/_GroupLayout.scss";
|
||||||
@import "./views/rooms/_IRCLayout.scss";
|
@import "./views/rooms/_IRCLayout.scss";
|
||||||
@import "./views/rooms/_JumpToBottomButton.scss";
|
@import "./views/rooms/_JumpToBottomButton.scss";
|
||||||
|
@ -263,9 +265,9 @@
|
||||||
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
||||||
@import "./views/verification/_VerificationShowSas.scss";
|
@import "./views/verification/_VerificationShowSas.scss";
|
||||||
@import "./views/voip/_CallContainer.scss";
|
@import "./views/voip/_CallContainer.scss";
|
||||||
|
@import "./views/voip/_CallPreview.scss";
|
||||||
@import "./views/voip/_CallView.scss";
|
@import "./views/voip/_CallView.scss";
|
||||||
@import "./views/voip/_CallViewForRoom.scss";
|
@import "./views/voip/_CallViewForRoom.scss";
|
||||||
@import "./views/voip/_CallPreview.scss";
|
|
||||||
@import "./views/voip/_DialPad.scss";
|
@import "./views/voip/_DialPad.scss";
|
||||||
@import "./views/voip/_DialPadContextMenu.scss";
|
@import "./views/voip/_DialPadContextMenu.scss";
|
||||||
@import "./views/voip/_DialPadModal.scss";
|
@import "./views/voip/_DialPadModal.scss";
|
||||||
|
|
|
@ -27,6 +27,7 @@ limitations under the License.
|
||||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=255139
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=255139
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_BaseAvatar_initial {
|
.mx_BaseAvatar_initial {
|
||||||
|
|
|
@ -16,19 +16,16 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_ReplyThread {
|
.mx_ReplyThread {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ReplyThread_show {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote.mx_ReplyThread {
|
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
border-left: 4px solid $button-bg-color;
|
border-left: 4px solid $button-bg-color;
|
||||||
|
|
||||||
|
.mx_ReplyThread_show {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
&.mx_ReplyThread_color1 {
|
&.mx_ReplyThread_color1 {
|
||||||
border-left-color: $username-variant1-color;
|
border-left-color: $username-variant1-color;
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ limitations under the License.
|
||||||
width: $font-16px;
|
width: $font-16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> input[type=radio] {
|
input[type=radio] {
|
||||||
// Remove the OS's representation
|
// Remove the OS's representation
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -112,6 +112,12 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RadioButton_innerLabel {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
top: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RadioButton_outlined {
|
.mx_RadioButton_outlined {
|
||||||
|
|
77
res/css/views/elements/_TagComposer.scss
Normal file
77
res/css/views/elements/_TagComposer.scss
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
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_TagComposer {
|
||||||
|
.mx_TagComposer_input {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.mx_Field {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0; // override from field styles
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
min-width: 70px;
|
||||||
|
padding: 0; // override from button styles
|
||||||
|
margin-left: 16px; // distance from <Field>
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Field, .mx_Field input, .mx_AccessibleButton {
|
||||||
|
// So they look related to each other by feeling the same
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TagComposer_tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 12px; // this plus 12px from the tags makes 24px from the input
|
||||||
|
|
||||||
|
.mx_TagComposer_tag {
|
||||||
|
padding: 6px 8px 8px 12px;
|
||||||
|
position: relative;
|
||||||
|
margin-right: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
// Cheaty way to get an opacified variable colour background
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: $tertiary-fg-color;
|
||||||
|
opacity: 0.15;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
// Pass through the pointer otherwise we have effectively put a whole div
|
||||||
|
// on top of the component, which makes it hard to interact with buttons.
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
background-image: url('$(res)/img/subtract.svg');
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-left: 8px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,6 @@ $timelineImageBorderRadius: 4px;
|
||||||
|
|
||||||
.mx_MImageBody {
|
.mx_MImageBody {
|
||||||
display: block;
|
display: block;
|
||||||
margin-right: 34px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MImageBody_thumbnail {
|
.mx_MImageBody_thumbnail {
|
||||||
|
@ -29,6 +28,10 @@ $timelineImageBorderRadius: 4px;
|
||||||
top: 0;
|
top: 0;
|
||||||
border-radius: $timelineImageBorderRadius;
|
border-radius: $timelineImageBorderRadius;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
> canvas {
|
> canvas {
|
||||||
border-radius: $timelineImageBorderRadius;
|
border-radius: $timelineImageBorderRadius;
|
||||||
}
|
}
|
||||||
|
@ -43,17 +46,6 @@ $timelineImageBorderRadius: 4px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MImageBody_thumbnail_spinner {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inner img should be centered around 0, 0
|
|
||||||
.mx_MImageBody_thumbnail_spinner > * {
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MImageBody_gifLabel {
|
.mx_MImageBody_gifLabel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -26,6 +26,7 @@ limitations under the License.
|
||||||
height: 24px;
|
height: 24px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
|
|
323
res/css/views/rooms/_EventBubbleTile.scss
Normal file
323
res/css/views/rooms/_EventBubbleTile.scss
Normal file
|
@ -0,0 +1,323 @@
|
||||||
|
/*
|
||||||
|
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_EventTile[data-layout=bubble],
|
||||||
|
.mx_EventTile[data-layout=bubble] ~ .mx_EventListSummary {
|
||||||
|
--avatarSize: 32px;
|
||||||
|
--gutterSize: 11px;
|
||||||
|
--cornerRadius: 12px;
|
||||||
|
--maxWidth: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile[data-layout=bubble] {
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
margin-top: var(--gutterSize);
|
||||||
|
margin-left: 50px;
|
||||||
|
margin-right: 100px;
|
||||||
|
|
||||||
|
&.mx_EventTile_continuation {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For replies */
|
||||||
|
.mx_EventTile {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
bottom: -1px;
|
||||||
|
left: -60px;
|
||||||
|
right: -60px;
|
||||||
|
z-index: -1;
|
||||||
|
background: $eventbubble-bg-hover;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_avatar {
|
||||||
|
img {
|
||||||
|
box-shadow: 0 0 0 3px $eventbubble-bg-hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SenderProfile,
|
||||||
|
.mx_EventTile_line {
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SenderProfile {
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-self=false] {
|
||||||
|
.mx_EventTile_line {
|
||||||
|
border-bottom-right-radius: var(--cornerRadius);
|
||||||
|
}
|
||||||
|
.mx_EventTile_avatar {
|
||||||
|
left: -34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageActionBar {
|
||||||
|
right: 0;
|
||||||
|
transform: translate3d(50%, 50%, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
--backgroundColor: $eventbubble-others-bg;
|
||||||
|
}
|
||||||
|
&[data-self=true] {
|
||||||
|
.mx_EventTile_line {
|
||||||
|
border-bottom-left-radius: var(--cornerRadius);
|
||||||
|
float: right;
|
||||||
|
> a {
|
||||||
|
left: auto;
|
||||||
|
right: -48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mx_SenderProfile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.mx_ReactionsRow {
|
||||||
|
float: right;
|
||||||
|
clear: right;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
/* Moving the "add reaction button" before the reactions */
|
||||||
|
> :last-child {
|
||||||
|
order: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mx_EventTile_avatar {
|
||||||
|
top: -19px; // height of the sender block
|
||||||
|
right: -35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
--backgroundColor: $eventbubble-self-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_line {
|
||||||
|
position: relative;
|
||||||
|
padding: var(--gutterSize);
|
||||||
|
border-top-left-radius: var(--cornerRadius);
|
||||||
|
border-top-right-radius: var(--cornerRadius);
|
||||||
|
background: var(--backgroundColor);
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
margin: 0 -12px 0 -9px;
|
||||||
|
> a {
|
||||||
|
position: absolute;
|
||||||
|
left: -48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_EventTile_continuation[data-self=false] .mx_EventTile_line {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
}
|
||||||
|
&.mx_EventTile_lastInSection[data-self=false] .mx_EventTile_line {
|
||||||
|
border-bottom-left-radius: var(--cornerRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_EventTile_continuation[data-self=true] .mx_EventTile_line {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
&.mx_EventTile_lastInSection[data-self=true] .mx_EventTile_line {
|
||||||
|
border-bottom-right-radius: var(--cornerRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_avatar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
line-height: 1;
|
||||||
|
img {
|
||||||
|
box-shadow: 0 0 0 3px $eventbubble-avatar-outline;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-has-reply=true] {
|
||||||
|
> .mx_EventTile_line {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ReplyThread_show {
|
||||||
|
order: 99999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ReplyThread {
|
||||||
|
margin: 0 calc(-1 * var(--gutterSize));
|
||||||
|
|
||||||
|
.mx_EventTile_reply {
|
||||||
|
max-width: 90%;
|
||||||
|
padding: 0;
|
||||||
|
> a {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--gutterSize);
|
||||||
|
.mx_EventTile_avatar {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
.mx_SenderProfile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EditMessageComposer_buttons {
|
||||||
|
position: static;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ReactionsRow {
|
||||||
|
margin-right: -18px;
|
||||||
|
margin-left: -9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ReplyThread {
|
||||||
|
border-left-width: 2px;
|
||||||
|
border-left-color: $eventbubble-reply-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_EventTile_bubbleContainer,
|
||||||
|
&.mx_EventTile_info,
|
||||||
|
& ~ .mx_EventListSummary[data-expanded=false] {
|
||||||
|
--backgroundColor: transparent;
|
||||||
|
--gutterSize: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.mx_EventTile_avatar {
|
||||||
|
position: static;
|
||||||
|
order: -1;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& ~ .mx_EventListSummary {
|
||||||
|
--maxWidth: 80%;
|
||||||
|
margin-left: calc(var(--avatarSize) + var(--gutterSize));
|
||||||
|
margin-right: calc(var(--gutterSize) + var(--avatarSize));
|
||||||
|
.mx_EventListSummary_toggle {
|
||||||
|
float: none;
|
||||||
|
margin: 0;
|
||||||
|
order: 9;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.mx_EventListSummary_avatars {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile {
|
||||||
|
margin: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_line {
|
||||||
|
margin: 0 5px;
|
||||||
|
> a {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
transform: translateX(calc(100% + 5px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageActionBar {
|
||||||
|
transform: translate3d(50%, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& ~ .mx_EventListSummary[data-expanded=false] {
|
||||||
|
padding: 0 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* events that do not require bubble layout */
|
||||||
|
& ~ .mx_EventListSummary,
|
||||||
|
&.mx_EventTile_bad {
|
||||||
|
.mx_EventTile_line {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::before {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& + .mx_EventListSummary {
|
||||||
|
.mx_EventTile {
|
||||||
|
margin-top: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventListSummary_toggle {
|
||||||
|
margin-right: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Special layout scenario for "Unable To Decrypt (UTD)" events */
|
||||||
|
&.mx_EventTile_bad > .mx_EventTile_line {
|
||||||
|
display: grid;
|
||||||
|
grid-template:
|
||||||
|
"reply reply" auto
|
||||||
|
"shield body" auto
|
||||||
|
"shield link" auto
|
||||||
|
/ auto 1fr;
|
||||||
|
.mx_EventTile_e2eIcon {
|
||||||
|
grid-area: shield;
|
||||||
|
}
|
||||||
|
.mx_UnknownBody {
|
||||||
|
grid-area: body;
|
||||||
|
}
|
||||||
|
.mx_EventTile_keyRequestInfo {
|
||||||
|
grid-area: link;
|
||||||
|
}
|
||||||
|
.mx_ReplyThread_wrapper {
|
||||||
|
grid-area: reply;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.mx_EventTile_readAvatars {
|
||||||
|
position: absolute;
|
||||||
|
right: -110px;
|
||||||
|
bottom: 0;
|
||||||
|
top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MTextBody {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -18,15 +18,14 @@ limitations under the License.
|
||||||
$left-gutter: 64px;
|
$left-gutter: 64px;
|
||||||
$hover-select-border: 4px;
|
$hover-select-border: 4px;
|
||||||
|
|
||||||
.mx_EventTile {
|
.mx_EventTile:not([data-layout=bubble]) {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
clear: both;
|
clear: both;
|
||||||
padding-top: 18px;
|
padding-top: 18px;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile.mx_EventTile_info {
|
&.mx_EventTile_info {
|
||||||
padding-top: 1px;
|
padding-top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,12 +36,12 @@ $hover-select-border: 4px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile.mx_EventTile_info .mx_EventTile_avatar {
|
&.mx_EventTile_info .mx_EventTile_avatar {
|
||||||
top: $font-6px;
|
top: $font-6px;
|
||||||
left: $left-gutter;
|
left: $left-gutter;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_continuation {
|
&.mx_EventTile_continuation {
|
||||||
padding-top: 0px !important;
|
padding-top: 0px !important;
|
||||||
|
|
||||||
&.mx_EventTile_isEditing {
|
&.mx_EventTile_isEditing {
|
||||||
|
@ -51,11 +50,11 @@ $hover-select-border: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_isEditing {
|
&.mx_EventTile_isEditing {
|
||||||
background-color: $header-panel-bg-color;
|
background-color: $header-panel-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile .mx_SenderProfile {
|
.mx_SenderProfile {
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
display: inline-block; /* anti-zalgo, with overflow hidden */
|
display: inline-block; /* anti-zalgo, with overflow hidden */
|
||||||
|
@ -70,7 +69,7 @@ $hover-select-border: 4px;
|
||||||
max-width: calc(100% - $left-gutter);
|
max-width: calc(100% - $left-gutter);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile .mx_SenderProfile .mx_Flair {
|
.mx_SenderProfile .mx_Flair {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -85,11 +84,11 @@ $hover-select-border: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_isEditing .mx_MessageTimestamp {
|
&.mx_EventTile_isEditing .mx_MessageTimestamp {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile .mx_MessageTimestamp {
|
.mx_MessageTimestamp {
|
||||||
display: block;
|
display: block;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
|
@ -97,7 +96,7 @@ $hover-select-border: 4px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_continuation .mx_EventTile_line {
|
&.mx_EventTile_continuation .mx_EventTile_line {
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,63 +106,25 @@ $hover-select-border: 4px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomView_timeline_rr_enabled,
|
|
||||||
// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter
|
|
||||||
.mx_EventListSummary {
|
|
||||||
.mx_EventTile_line {
|
|
||||||
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
|
|
||||||
margin-right: 110px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_bubbleContainer {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 100px;
|
|
||||||
|
|
||||||
.mx_EventTile_line {
|
|
||||||
margin-right: 0;
|
|
||||||
grid-column: 1 / 3;
|
|
||||||
// override default padding of mx_EventTile_line so that we can be centered
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_msgOption {
|
|
||||||
grid-column: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_reply {
|
.mx_EventTile_reply {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HACK to override line-height which is already marked important elsewhere */
|
&.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
|
||||||
.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
|
|
||||||
font-size: 48px !important;
|
|
||||||
line-height: 57px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
|
|
||||||
left: calc(-$hover-select-border);
|
left: calc(-$hover-select-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile:hover .mx_MessageActionBar,
|
|
||||||
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar,
|
|
||||||
[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar,
|
|
||||||
.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* this is used for the tile for the event which is selected via the URL.
|
/* this is used for the tile for the event which is selected via the URL.
|
||||||
* TODO: ultimately we probably want some transition on here.
|
* TODO: ultimately we probably want some transition on here.
|
||||||
*/
|
*/
|
||||||
.mx_EventTile_selected > .mx_EventTile_line {
|
&.mx_EventTile_selected > .mx_EventTile_line {
|
||||||
border-left: $accent-color 4px solid;
|
border-left: $accent-color 4px solid;
|
||||||
padding-left: calc($left-gutter - $hover-select-border);
|
padding-left: calc($left-gutter - $hover-select-border);
|
||||||
background-color: $event-selected-color;
|
background-color: $event-selected-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_highlight,
|
&.mx_EventTile_highlight,
|
||||||
.mx_EventTile_highlight .markdown-body {
|
&.mx_EventTile_highlight .markdown-body {
|
||||||
color: $event-highlight-fg-color;
|
color: $event-highlight-fg-color;
|
||||||
|
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line {
|
||||||
|
@ -171,17 +132,17 @@ $hover-select-border: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_info .mx_EventTile_line {
|
&.mx_EventTile_info .mx_EventTile_line {
|
||||||
padding-left: calc($left-gutter + 18px);
|
padding-left: calc($left-gutter + 18px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
|
&.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
|
||||||
padding-left: calc($left-gutter + 18px - $hover-select-border);
|
padding-left: calc($left-gutter + 18px - $hover-select-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile:hover .mx_EventTile_line,
|
&.mx_EventTile:hover .mx_EventTile_line,
|
||||||
.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line,
|
&.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line,
|
||||||
.mx_EventTile.focus-visible:focus-within .mx_EventTile_line {
|
&.mx_EventTile.focus-visible:focus-within .mx_EventTile_line {
|
||||||
background-color: $event-selected-color;
|
background-color: $event-selected-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,7 +186,7 @@ $hover-select-border: 4px;
|
||||||
mask-image: url('$(res)/img/element-icons/circle-sending.svg');
|
mask-image: url('$(res)/img/element-icons/circle-sending.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_contextual {
|
&.mx_EventTile_contextual {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,36 +208,6 @@ $hover-select-border: 4px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_readAvatars {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
// This aligns the avatar with the last line of the
|
|
||||||
// message. We want to move it one line up - 2.2rem
|
|
||||||
top: -2.2rem;
|
|
||||||
user-select: none;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_readAvatars .mx_BaseAvatar {
|
|
||||||
position: absolute;
|
|
||||||
display: inline-block;
|
|
||||||
height: $font-14px;
|
|
||||||
width: $font-14px;
|
|
||||||
|
|
||||||
will-change: left, top;
|
|
||||||
transition:
|
|
||||||
left var(--transition-short) ease-out,
|
|
||||||
top var(--transition-standard) ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_readAvatarRemainder {
|
|
||||||
color: $event-timestamp-color;
|
|
||||||
font-size: $font-11px;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* all the overflow-y: hidden; are to trap Zalgos -
|
/* all the overflow-y: hidden; are to trap Zalgos -
|
||||||
but they introduce an implicit overflow-x: auto.
|
but they introduce an implicit overflow-x: auto.
|
||||||
so make that explicitly hidden too to avoid random
|
so make that explicitly hidden too to avoid random
|
||||||
|
@ -314,15 +245,147 @@ $hover-select-border: 4px;
|
||||||
filter: none;
|
filter: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover.mx_EventTile_verified .mx_EventTile_line,
|
||||||
|
&:hover.mx_EventTile_unverified .mx_EventTile_line,
|
||||||
|
&:hover.mx_EventTile_unknown .mx_EventTile_line {
|
||||||
|
padding-left: calc($left-gutter - $hover-select-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover.mx_EventTile_verified .mx_EventTile_line {
|
||||||
|
border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover.mx_EventTile_unverified .mx_EventTile_line {
|
||||||
|
border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover.mx_EventTile_unknown .mx_EventTile_line {
|
||||||
|
border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
|
||||||
|
&:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
|
||||||
|
&:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
|
||||||
|
padding-left: calc($left-gutter + 18px - $hover-select-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* End to end encryption stuff */
|
||||||
|
&:hover .mx_EventTile_e2eIcon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
||||||
|
&:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
||||||
|
&:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
||||||
|
&:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
|
||||||
|
left: calc(-$hover-select-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
||||||
|
&:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon,
|
||||||
|
&:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon,
|
||||||
|
&:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon {
|
||||||
|
display: block;
|
||||||
|
left: 41px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MImageBody {
|
||||||
|
margin-right: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_EventTile_e2eIcon {
|
.mx_EventTile_e2eIcon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
top: 6px;
|
||||||
left: 44px;
|
left: 44px;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ReactionsRow {
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomView_timeline_rr_enabled {
|
||||||
|
|
||||||
|
.mx_EventTile:not([data-layout=bubble]) {
|
||||||
|
.mx_EventTile_line {
|
||||||
|
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
|
||||||
|
margin-right: 110px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_bubbleContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 100px;
|
||||||
|
|
||||||
|
.mx_EventTile_line {
|
||||||
|
margin-right: 0;
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
// override default padding of mx_EventTile_line so that we can be centered
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_msgOption {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_readAvatars {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
// This aligns the avatar with the last line of the
|
||||||
|
// message. We want to move it one line up - 2.2rem
|
||||||
|
top: -2.2rem;
|
||||||
|
user-select: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_readAvatars .mx_BaseAvatar {
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
height: $font-14px;
|
||||||
|
width: $font-14px;
|
||||||
|
|
||||||
|
will-change: left, top;
|
||||||
|
transition:
|
||||||
|
left var(--transition-short) ease-out,
|
||||||
|
top var(--transition-standard) ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_readAvatarRemainder {
|
||||||
|
color: $event-timestamp-color;
|
||||||
|
font-size: $font-11px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HACK to override line-height which is already marked important elsewhere */
|
||||||
|
.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
|
||||||
|
font-size: 48px !important;
|
||||||
|
line-height: 57px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_content .mx_EventTile_edited {
|
||||||
|
user-select: none;
|
||||||
|
font-size: $font-12px;
|
||||||
|
color: $roomtopic-color;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 9px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.mx_EventTile_e2eIcon {
|
||||||
|
position: relative;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
display: block;
|
display: block;
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
|
@ -381,87 +444,6 @@ $hover-select-border: 4px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_keyRequestInfo {
|
|
||||||
font-size: $font-12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_keyRequestInfo_text {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_keyRequestInfo_text a {
|
|
||||||
color: $primary-fg-color;
|
|
||||||
text-decoration: underline;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_keyRequestInfo_tooltip_contents p {
|
|
||||||
text-align: auto;
|
|
||||||
margin-left: 3px;
|
|
||||||
margin-right: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_keyRequestInfo_tooltip_contents p:first-child {
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_keyRequestInfo_tooltip_contents p:last-child {
|
|
||||||
margin-bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line,
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line,
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
|
|
||||||
padding-left: calc($left-gutter - $hover-select-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line {
|
|
||||||
border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line {
|
|
||||||
border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
|
|
||||||
border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
|
|
||||||
padding-left: calc($left-gutter + 18px - $hover-select-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* End to end encryption stuff */
|
|
||||||
.mx_EventTile:hover .mx_EventTile_e2eIcon {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
|
|
||||||
left: calc(-$hover-select-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon,
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon,
|
|
||||||
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon {
|
|
||||||
display: block;
|
|
||||||
left: 41px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_content .mx_EventTile_edited {
|
|
||||||
user-select: none;
|
|
||||||
font-size: $font-12px;
|
|
||||||
color: $roomtopic-color;
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 9px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Various markdown overrides */
|
/* Various markdown overrides */
|
||||||
|
|
||||||
.mx_EventTile_body pre {
|
.mx_EventTile_body pre {
|
||||||
|
@ -595,6 +577,35 @@ $hover-select-border: 4px;
|
||||||
|
|
||||||
/* end of overrides */
|
/* end of overrides */
|
||||||
|
|
||||||
|
|
||||||
|
.mx_EventTile_keyRequestInfo {
|
||||||
|
font-size: $font-12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_keyRequestInfo_text {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_keyRequestInfo_text a {
|
||||||
|
color: $primary-fg-color;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_keyRequestInfo_tooltip_contents p {
|
||||||
|
text-align: auto;
|
||||||
|
margin-left: 3px;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_keyRequestInfo_tooltip_contents p:first-child {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_keyRequestInfo_tooltip_contents p:last-child {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_EventTile_tileError {
|
.mx_EventTile_tileError {
|
||||||
color: red;
|
color: red;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -615,6 +626,13 @@ $hover-select-border: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_EventTile:hover .mx_MessageActionBar,
|
||||||
|
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar,
|
||||||
|
[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar,
|
||||||
|
.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 480px) {
|
@media only screen and (max-width: 480px) {
|
||||||
.mx_EventTile_line, .mx_EventTile_reply {
|
.mx_EventTile_line, .mx_EventTile_reply {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
|
|
|
@ -22,11 +22,9 @@ limitations under the License.
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
box-shadow: 0px -16px 32px $composer-shadow-color;
|
box-shadow: 0px -16px 32px $composer-shadow-color;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ReplyPreview_section {
|
.mx_ReplyPreview_section {
|
||||||
border-bottom: 1px solid $primary-hairline-color;
|
border-bottom: 1px solid $primary-hairline-color;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ReplyPreview_header {
|
.mx_ReplyPreview_header {
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
|
@ -52,3 +50,6 @@ limitations under the License.
|
||||||
.mx_ReplyPreview_clear {
|
.mx_ReplyPreview_clear {
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,9 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_ReplyTile {
|
.mx_ReplyTile {
|
||||||
padding-top: 2px;
|
|
||||||
padding-bottom: 2px;
|
|
||||||
font-size: $font-14px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding: 2px 0;
|
||||||
|
font-size: $font-14px;
|
||||||
line-height: $font-16px;
|
line-height: $font-16px;
|
||||||
|
|
||||||
&.mx_ReplyTile_audio .mx_MFileBody_info_icon::before {
|
&.mx_ReplyTile_audio .mx_MFileBody_info_icon::before {
|
||||||
|
@ -38,16 +37,15 @@ limitations under the License.
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ReplyTile > a {
|
> a {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ReplyTile .mx_RedactedBody {
|
.mx_RedactedBody {
|
||||||
padding: 4px 0 2px 20px;
|
padding: 4px 0 2px 20px;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
@ -58,7 +56,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
// We do reply size limiting with CSS to avoid duplicating the TextualBody component.
|
// We do reply size limiting with CSS to avoid duplicating the TextualBody component.
|
||||||
.mx_ReplyTile .mx_EventTile_content {
|
.mx_EventTile_content {
|
||||||
$reply-lines: 2;
|
$reply-lines: 2;
|
||||||
$line-height: $font-22px;
|
$line-height: $font-22px;
|
||||||
|
|
||||||
|
@ -72,8 +70,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_EventTile_body.mx_EventTile_bigEmoji {
|
.mx_EventTile_body.mx_EventTile_bigEmoji {
|
||||||
line-height: $line-height !important;
|
line-height: $line-height !important;
|
||||||
// Override the big emoji override
|
font-size: $font-14px !important; // Override the big emoji override
|
||||||
font-size: $font-14px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide line numbers
|
// Hide line numbers
|
||||||
|
@ -102,22 +99,21 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ReplyTile.mx_ReplyTile_info {
|
&.mx_ReplyTile_info {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ReplyTile .mx_SenderProfile {
|
.mx_SenderProfile {
|
||||||
color: $primary-fg-color;
|
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
display: inline-block; /* anti-zalgo, with overflow hidden */
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
padding-left: 0; /* left gutter */
|
|
||||||
padding-bottom: 0;
|
|
||||||
padding-top: 0;
|
|
||||||
margin: 0;
|
|
||||||
line-height: $font-17px;
|
line-height: $font-17px;
|
||||||
/* the next three lines, along with overflow hidden, truncate long display names */
|
|
||||||
|
display: inline-block; // anti-zalgo, with overflow hidden
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
// truncate long display names
|
||||||
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,82 +14,79 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_UserNotifSettings_tableRow {
|
.mx_UserNotifSettings {
|
||||||
display: table-row;
|
color: $primary-fg-color; // override from default settings page styles
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserNotifSettings_inputCell {
|
|
||||||
display: table-cell;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
padding-right: 8px;
|
|
||||||
width: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserNotifSettings_labelCell {
|
|
||||||
padding-bottom: 8px;
|
|
||||||
width: 400px;
|
|
||||||
display: table-cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserNotifSettings_pushRulesTableWrapper {
|
|
||||||
padding-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserNotifSettings_pushRulesTable {
|
.mx_UserNotifSettings_pushRulesTable {
|
||||||
width: 100%;
|
width: calc(100% + 12px); // +12px to line up center of 'Noisy' column with toggle switches
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
margin-top: 40px;
|
||||||
|
|
||||||
|
tr > th {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserNotifSettings_pushRulesTable thead {
|
tr > th:first-child {
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserNotifSettings_pushRulesTable tbody th {
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserNotifSettings_pushRulesTable tbody th:first-child {
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
font-size: $font-18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserNotifSettings_keywords {
|
tr > th:nth-child(n + 2) {
|
||||||
cursor: pointer;
|
color: $secondary-fg-color;
|
||||||
color: $accent-color;
|
font-size: $font-12px;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 66px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserNotifSettings_devicesTable td {
|
tr > td:nth-child(n + 2) {
|
||||||
padding-left: 20px;
|
text-align: center;
|
||||||
padding-right: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserNotifSettings_notifTable {
|
tr > td {
|
||||||
display: table;
|
padding-top: 8px;
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserNotifSettings_notifTable .mx_Spinner {
|
// Override StyledRadioButton default styles
|
||||||
position: absolute;
|
.mx_RadioButton {
|
||||||
}
|
justify-content: center;
|
||||||
|
|
||||||
.mx_NotificationSound_soundUpload {
|
.mx_RadioButton_content {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_NotificationSound_browse {
|
.mx_RadioButton_spacer {
|
||||||
color: $accent-color;
|
display: none;
|
||||||
border: 1px solid $accent-color;
|
}
|
||||||
background-color: transparent;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_NotificationSound_save {
|
.mx_UserNotifSettings_floatingSection {
|
||||||
margin-left: 5px;
|
margin-top: 40px;
|
||||||
color: white;
|
|
||||||
background-color: $accent-color;
|
& > div:first-child { // section header
|
||||||
|
font-size: $font-18px;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_NotificationSound_resetSound {
|
> table {
|
||||||
margin-top: 5px;
|
border-collapse: collapse;
|
||||||
color: white;
|
border-spacing: 0;
|
||||||
border: $warning-color;
|
margin-top: 8px;
|
||||||
background-color: $warning-color;
|
|
||||||
|
tr > td:first-child {
|
||||||
|
// Just for a bit of spacing
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_UserNotifSettings_clearNotifsButton {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TagComposer {
|
||||||
|
margin-top: 35px; // lots of distance from the last line of the table
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
3
res/img/subtract.svg
Normal file
3
res/img/subtract.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58167 12.4183 0 8 0C3.58173 0 0 3.58167 0 8C0 12.4183 3.58173 16 8 16ZM3.96967 5.0304L6.93933 8L3.96967 10.9697L5.03033 12.0304L8 9.06067L10.9697 12.0304L12.0303 10.9697L9.06067 8L12.0303 5.0304L10.9697 3.96973L8 6.93945L5.03033 3.96973L3.96967 5.0304Z" fill="#8D97A5"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 461 B |
|
@ -227,6 +227,13 @@ $groupFilterPanel-background-blur-amount: 30px;
|
||||||
|
|
||||||
$composer-shadow-color: rgba(0, 0, 0, 0.28);
|
$composer-shadow-color: rgba(0, 0, 0, 0.28);
|
||||||
|
|
||||||
|
// Bubble tiles
|
||||||
|
$eventbubble-self-bg: #143A34;
|
||||||
|
$eventbubble-others-bg: #394049;
|
||||||
|
$eventbubble-bg-hover: #433C23;
|
||||||
|
$eventbubble-avatar-outline: $bg-color;
|
||||||
|
$eventbubble-reply-color: #C1C6CD;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
|
|
@ -347,6 +347,13 @@ $appearance-tab-border-color: $input-darker-bg-color;
|
||||||
|
|
||||||
$composer-shadow-color: tranparent;
|
$composer-shadow-color: tranparent;
|
||||||
|
|
||||||
|
// Bubble tiles
|
||||||
|
$eventbubble-self-bg: #F8FDFC;
|
||||||
|
$eventbubble-others-bg: #F7F8F9;
|
||||||
|
$eventbubble-bg-hover: rgb(242, 242, 242);
|
||||||
|
$eventbubble-avatar-outline: #fff;
|
||||||
|
$eventbubble-reply-color: #C1C6CD;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
|
|
@ -349,6 +349,13 @@ $groupFilterPanel-background-blur-amount: 20px;
|
||||||
|
|
||||||
$composer-shadow-color: rgba(0, 0, 0, 0.04);
|
$composer-shadow-color: rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
// Bubble tiles
|
||||||
|
$eventbubble-self-bg: #F8FDFC;
|
||||||
|
$eventbubble-others-bg: #F7F8F9;
|
||||||
|
$eventbubble-bg-hover: #FEFCF5;
|
||||||
|
$eventbubble-avatar-outline: $primary-bg-color;
|
||||||
|
$eventbubble-reply-color: #C1C6CD;
|
||||||
|
|
||||||
// ***** Mixins! *****
|
// ***** Mixins! *****
|
||||||
|
|
||||||
@define-mixin mx_DialogButton {
|
@define-mixin mx_DialogButton {
|
||||||
|
|
4
src/@types/global.d.ts
vendored
4
src/@types/global.d.ts
vendored
|
@ -50,6 +50,8 @@ import UIStore from "../stores/UIStore";
|
||||||
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
|
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
|
||||||
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
|
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
matrixChat: ReturnType<Renderer>;
|
matrixChat: ReturnType<Renderer>;
|
||||||
|
@ -186,3 +188,5 @@ declare global {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import RoomViewStore from './stores/RoomViewStore';
|
import RoomViewStore from './stores/RoomViewStore';
|
||||||
|
import { EventSubscription } from 'fbemitter';
|
||||||
|
|
||||||
type Listener = (isActive: boolean) => void;
|
type Listener = (isActive: boolean) => void;
|
||||||
|
|
||||||
|
@ -30,7 +31,7 @@ type Listener = (isActive: boolean) => void;
|
||||||
export class ActiveRoomObserver {
|
export class ActiveRoomObserver {
|
||||||
private listeners: {[key: string]: Listener[]} = {};
|
private listeners: {[key: string]: Listener[]} = {};
|
||||||
private _activeRoomId = RoomViewStore.getRoomId();
|
private _activeRoomId = RoomViewStore.getRoomId();
|
||||||
private readonly roomStoreToken: string;
|
private readonly roomStoreToken: EventSubscription;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// TODO: We could self-destruct when the last listener goes away, or at least stop listening.
|
// TODO: We could self-destruct when the last listener goes away, or at least stop listening.
|
||||||
|
|
|
@ -270,7 +270,7 @@ export class Analytics {
|
||||||
localStorage.removeItem(LAST_VISIT_TS_KEY);
|
localStorage.removeItem(LAST_VISIT_TS_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _track(data: IData) {
|
private async track(data: IData) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
@ -304,7 +304,7 @@ export class Analytics {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ping() {
|
public ping() {
|
||||||
this._track({
|
this.track({
|
||||||
ping: "1",
|
ping: "1",
|
||||||
});
|
});
|
||||||
localStorage.setItem(LAST_VISIT_TS_KEY, String(new Date().getTime())); // update last visit ts
|
localStorage.setItem(LAST_VISIT_TS_KEY, String(new Date().getTime())); // update last visit ts
|
||||||
|
@ -324,14 +324,14 @@ export class Analytics {
|
||||||
// But continue anyway because we still want to track the change
|
// But continue anyway because we still want to track the change
|
||||||
}
|
}
|
||||||
|
|
||||||
this._track({
|
this.track({
|
||||||
gt_ms: String(generationTimeMs),
|
gt_ms: String(generationTimeMs),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public trackEvent(category: string, action: string, name?: string, value?: string) {
|
public trackEvent(category: string, action: string, name?: string, value?: string) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
this._track({
|
this.track({
|
||||||
e_c: category,
|
e_c: category,
|
||||||
e_a: action,
|
e_a: action,
|
||||||
e_n: name,
|
e_n: name,
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { User } from "matrix-js-sdk/src/models/user";
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
|
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
import { split } from "lodash";
|
||||||
|
|
||||||
import DMRoomMap from './utils/DMRoomMap';
|
import DMRoomMap from './utils/DMRoomMap';
|
||||||
import { mediaFromMxc } from "./customisations/Media";
|
import { mediaFromMxc } from "./customisations/Media";
|
||||||
|
@ -122,27 +123,13 @@ export function getInitialLetter(name: string): string {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let idx = 0;
|
|
||||||
const initial = name[0];
|
const initial = name[0];
|
||||||
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
|
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
|
||||||
idx++;
|
name = name.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// string.codePointAt(0) would do this, but that isn't supported by
|
// rely on the grapheme cluster splitter in lodash so that we don't break apart compound emojis
|
||||||
// some browsers (notably PhantomJS).
|
return split(name, "", 1)[0].toUpperCase();
|
||||||
let chars = 1;
|
|
||||||
const first = name.charCodeAt(idx);
|
|
||||||
|
|
||||||
// check if it’s the start of a surrogate pair
|
|
||||||
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
|
|
||||||
const second = name.charCodeAt(idx+1);
|
|
||||||
if (second >= 0xDC00 && second <= 0xDFFF) {
|
|
||||||
chars++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstChar = name.substring(idx, idx+chars);
|
|
||||||
return firstChar.toUpperCase();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) {
|
export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) {
|
||||||
|
|
|
@ -33,6 +33,7 @@ import { isSecretStorageBeingAccessed, accessSecretStorage } from "./SecurityMan
|
||||||
import { isSecureBackupRequired } from './utils/WellKnownUtils';
|
import { isSecureBackupRequired } from './utils/WellKnownUtils';
|
||||||
import { isLoggedIn } from './components/structures/MatrixChat';
|
import { isLoggedIn } from './components/structures/MatrixChat';
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { ActionPayload } from "./dispatcher/payloads";
|
||||||
|
|
||||||
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
@ -58,28 +59,28 @@ export default class DeviceListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
MatrixClientPeg.get().on('crypto.willUpdateDevices', this._onWillUpdateDevices);
|
MatrixClientPeg.get().on('crypto.willUpdateDevices', this.onWillUpdateDevices);
|
||||||
MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated);
|
MatrixClientPeg.get().on('crypto.devicesUpdated', this.onDevicesUpdated);
|
||||||
MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
MatrixClientPeg.get().on('deviceVerificationChanged', this.onDeviceVerificationChanged);
|
||||||
MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
MatrixClientPeg.get().on('userTrustStatusChanged', this.onUserTrustStatusChanged);
|
||||||
MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
|
MatrixClientPeg.get().on('crossSigning.keysChanged', this.onCrossSingingKeysChanged);
|
||||||
MatrixClientPeg.get().on('accountData', this._onAccountData);
|
MatrixClientPeg.get().on('accountData', this.onAccountData);
|
||||||
MatrixClientPeg.get().on('sync', this._onSync);
|
MatrixClientPeg.get().on('sync', this.onSync);
|
||||||
MatrixClientPeg.get().on('RoomState.events', this._onRoomStateEvents);
|
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
|
||||||
this.dispatcherRef = dis.register(this._onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
this._recheck();
|
this.recheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener('crypto.willUpdateDevices', this._onWillUpdateDevices);
|
MatrixClientPeg.get().removeListener('crypto.willUpdateDevices', this.onWillUpdateDevices);
|
||||||
MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated);
|
MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this.onDevicesUpdated);
|
||||||
MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
MatrixClientPeg.get().removeListener('deviceVerificationChanged', this.onDeviceVerificationChanged);
|
||||||
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this.onUserTrustStatusChanged);
|
||||||
MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
|
MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this.onCrossSingingKeysChanged);
|
||||||
MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
|
MatrixClientPeg.get().removeListener('accountData', this.onAccountData);
|
||||||
MatrixClientPeg.get().removeListener('sync', this._onSync);
|
MatrixClientPeg.get().removeListener('sync', this.onSync);
|
||||||
MatrixClientPeg.get().removeListener('RoomState.events', this._onRoomStateEvents);
|
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
|
||||||
}
|
}
|
||||||
if (this.dispatcherRef) {
|
if (this.dispatcherRef) {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
|
@ -103,15 +104,15 @@ export default class DeviceListener {
|
||||||
this.dismissed.add(d);
|
this.dismissed.add(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._recheck();
|
this.recheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissEncryptionSetup() {
|
dismissEncryptionSetup() {
|
||||||
this.dismissedThisDeviceToast = true;
|
this.dismissedThisDeviceToast = true;
|
||||||
this._recheck();
|
this.recheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
_ensureDeviceIdsAtStartPopulated() {
|
private ensureDeviceIdsAtStartPopulated() {
|
||||||
if (this.ourDeviceIdsAtStart === null) {
|
if (this.ourDeviceIdsAtStart === null) {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
this.ourDeviceIdsAtStart = new Set(
|
this.ourDeviceIdsAtStart = new Set(
|
||||||
|
@ -120,39 +121,39 @@ export default class DeviceListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => {
|
private onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => {
|
||||||
// If we didn't know about *any* devices before (ie. it's fresh login),
|
// If we didn't know about *any* devices before (ie. it's fresh login),
|
||||||
// then they are all pre-existing devices, so ignore this and set the
|
// then they are all pre-existing devices, so ignore this and set the
|
||||||
// devicesAtStart list to the devices that we see after the fetch.
|
// devicesAtStart list to the devices that we see after the fetch.
|
||||||
if (initialFetch) return;
|
if (initialFetch) return;
|
||||||
|
|
||||||
const myUserId = MatrixClientPeg.get().getUserId();
|
const myUserId = MatrixClientPeg.get().getUserId();
|
||||||
if (users.includes(myUserId)) this._ensureDeviceIdsAtStartPopulated();
|
if (users.includes(myUserId)) this.ensureDeviceIdsAtStartPopulated();
|
||||||
|
|
||||||
// No need to do a recheck here: we just need to get a snapshot of our devices
|
// No need to do a recheck here: we just need to get a snapshot of our devices
|
||||||
// before we download any new ones.
|
// before we download any new ones.
|
||||||
};
|
};
|
||||||
|
|
||||||
_onDevicesUpdated = (users: string[]) => {
|
private onDevicesUpdated = (users: string[]) => {
|
||||||
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
||||||
this._recheck();
|
this.recheck();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onDeviceVerificationChanged = (userId: string) => {
|
private onDeviceVerificationChanged = (userId: string) => {
|
||||||
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
||||||
this._recheck();
|
this.recheck();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onUserTrustStatusChanged = (userId: string) => {
|
private onUserTrustStatusChanged = (userId: string) => {
|
||||||
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
||||||
this._recheck();
|
this.recheck();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCrossSingingKeysChanged = () => {
|
private onCrossSingingKeysChanged = () => {
|
||||||
this._recheck();
|
this.recheck();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onAccountData = (ev) => {
|
private onAccountData = (ev: MatrixEvent) => {
|
||||||
// User may have:
|
// User may have:
|
||||||
// * migrated SSSS to symmetric
|
// * migrated SSSS to symmetric
|
||||||
// * uploaded keys to secret storage
|
// * uploaded keys to secret storage
|
||||||
|
@ -163,32 +164,32 @@ export default class DeviceListener {
|
||||||
ev.getType().startsWith('m.cross_signing.') ||
|
ev.getType().startsWith('m.cross_signing.') ||
|
||||||
ev.getType() === 'm.megolm_backup.v1'
|
ev.getType() === 'm.megolm_backup.v1'
|
||||||
) {
|
) {
|
||||||
this._recheck();
|
this.recheck();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onSync = (state, prevState) => {
|
private onSync = (state, prevState) => {
|
||||||
if (state === 'PREPARED' && prevState === null) this._recheck();
|
if (state === 'PREPARED' && prevState === null) this.recheck();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onRoomStateEvents = (ev: MatrixEvent) => {
|
private onRoomStateEvents = (ev: MatrixEvent) => {
|
||||||
if (ev.getType() !== "m.room.encryption") {
|
if (ev.getType() !== "m.room.encryption") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a room changes to encrypted, re-check as it may be our first
|
// If a room changes to encrypted, re-check as it may be our first
|
||||||
// encrypted room. This also catches encrypted room creation as well.
|
// encrypted room. This also catches encrypted room creation as well.
|
||||||
this._recheck();
|
this.recheck();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onAction = ({ action }) => {
|
private onAction = ({ action }: ActionPayload) => {
|
||||||
if (action !== "on_logged_in") return;
|
if (action !== "on_logged_in") return;
|
||||||
this._recheck();
|
this.recheck();
|
||||||
};
|
};
|
||||||
|
|
||||||
// The server doesn't tell us when key backup is set up, so we poll
|
// The server doesn't tell us when key backup is set up, so we poll
|
||||||
// & cache the result
|
// & cache the result
|
||||||
async _getKeyBackupInfo() {
|
private async getKeyBackupInfo() {
|
||||||
const now = (new Date()).getTime();
|
const now = (new Date()).getTime();
|
||||||
if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
|
if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
|
||||||
this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||||
|
@ -206,7 +207,7 @@ export default class DeviceListener {
|
||||||
return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId));
|
return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async _recheck() {
|
private async recheck() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
if (!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) return;
|
if (!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) return;
|
||||||
|
@ -235,7 +236,7 @@ export default class DeviceListener {
|
||||||
// Cross-signing on account but this device doesn't trust the master key (verify this session)
|
// Cross-signing on account but this device doesn't trust the master key (verify this session)
|
||||||
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
|
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
|
||||||
} else {
|
} else {
|
||||||
const backupInfo = await this._getKeyBackupInfo();
|
const backupInfo = await this.getKeyBackupInfo();
|
||||||
if (backupInfo) {
|
if (backupInfo) {
|
||||||
// No cross-signing on account but key backup available (upgrade encryption)
|
// No cross-signing on account but key backup available (upgrade encryption)
|
||||||
showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION);
|
showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION);
|
||||||
|
@ -256,7 +257,7 @@ export default class DeviceListener {
|
||||||
|
|
||||||
// This needs to be done after awaiting on downloadKeys() above, so
|
// This needs to be done after awaiting on downloadKeys() above, so
|
||||||
// we make sure we get the devices after the fetch is done.
|
// we make sure we get the devices after the fetch is done.
|
||||||
this._ensureDeviceIdsAtStartPopulated();
|
this.ensureDeviceIdsAtStartPopulated();
|
||||||
|
|
||||||
// Unverified devices that were there last time the app ran
|
// Unverified devices that were there last time the app ran
|
||||||
// (technically could just be a boolean: we don't actually
|
// (technically could just be a boolean: we don't actually
|
||||||
|
|
|
@ -105,7 +105,7 @@ export interface IMatrixClientPeg {
|
||||||
* This module provides a singleton instance of this class so the 'current'
|
* This module provides a singleton instance of this class so the 'current'
|
||||||
* Matrix Client object is available easily.
|
* Matrix Client object is available easily.
|
||||||
*/
|
*/
|
||||||
class _MatrixClientPeg implements IMatrixClientPeg {
|
class MatrixClientPegClass implements IMatrixClientPeg {
|
||||||
// These are the default options used when when the
|
// These are the default options used when when the
|
||||||
// client is started in 'start'. These can be altered
|
// client is started in 'start'. These can be altered
|
||||||
// at any time up to after the 'will_start_client'
|
// at any time up to after the 'will_start_client'
|
||||||
|
@ -300,7 +300,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!window.mxMatrixClientPeg) {
|
if (!window.mxMatrixClientPeg) {
|
||||||
window.mxMatrixClientPeg = new _MatrixClientPeg();
|
window.mxMatrixClientPeg = new MatrixClientPegClass();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MatrixClientPeg = window.mxMatrixClientPeg;
|
export const MatrixClientPeg = window.mxMatrixClientPeg;
|
||||||
|
|
|
@ -522,7 +522,7 @@ export const Commands = [
|
||||||
aliases: ['j', 'goto'],
|
aliases: ['j', 'goto'],
|
||||||
args: '<room-address>',
|
args: '<room-address>',
|
||||||
description: _td('Joins room with given address'),
|
description: _td('Joins room with given address'),
|
||||||
runFn: function(_, args) {
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
// Note: we support 2 versions of this command. The first is
|
// Note: we support 2 versions of this command. The first is
|
||||||
// the public-facing one for most users and the other is a
|
// the public-facing one for most users and the other is a
|
||||||
|
@ -1069,7 +1069,7 @@ export const Commands = [
|
||||||
command: "msg",
|
command: "msg",
|
||||||
description: _td("Sends a message to the given user"),
|
description: _td("Sends a message to the given user"),
|
||||||
args: "<user-id> <message>",
|
args: "<user-id> <message>",
|
||||||
runFn: function(_, args) {
|
runFn: function(roomId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
// matches the first whitespace delimited group and then the rest of the string
|
// matches the first whitespace delimited group and then the rest of the string
|
||||||
const matches = args.match(/^(\S+?)(?: +(.*))?$/s);
|
const matches = args.match(/^(\S+?)(?: +(.*))?$/s);
|
||||||
|
|
|
@ -13,7 +13,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
@ -32,7 +31,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
// any text to display at all. For this reason they return deferred values
|
// any text to display at all. For this reason they return deferred values
|
||||||
// to avoid the expense of looking up translations when they're not needed.
|
// to avoid the expense of looking up translations when they're not needed.
|
||||||
|
|
||||||
function textForMemberEvent(ev: MatrixEvent): () => string | null {
|
function textForMemberEvent(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean): () => string | null {
|
||||||
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
||||||
const senderName = ev.sender ? ev.sender.name : ev.getSender();
|
const senderName = ev.sender ? ev.sender.name : ev.getSender();
|
||||||
const targetName = ev.target ? ev.target.name : ev.getStateKey();
|
const targetName = ev.target ? ev.target.name : ev.getStateKey();
|
||||||
|
@ -84,7 +83,7 @@ function textForMemberEvent(ev: MatrixEvent): () => string | null {
|
||||||
return () => _t('%(senderName)s changed their profile picture', { senderName });
|
return () => _t('%(senderName)s changed their profile picture', { senderName });
|
||||||
} else if (!prevContent.avatar_url && content.avatar_url) {
|
} else if (!prevContent.avatar_url && content.avatar_url) {
|
||||||
return () => _t('%(senderName)s set a profile picture', { senderName });
|
return () => _t('%(senderName)s set a profile picture', { senderName });
|
||||||
} else if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
} else if (showHiddenEvents ?? SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
||||||
// This is a null rejoin, it will only be visible if using 'show hidden events' (labs)
|
// This is a null rejoin, it will only be visible if using 'show hidden events' (labs)
|
||||||
return () => _t("%(senderName)s made no change", { senderName });
|
return () => _t("%(senderName)s made no change", { senderName });
|
||||||
} else {
|
} else {
|
||||||
|
@ -319,7 +318,7 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallAnswerEvent(event): () => string | null {
|
function textForCallAnswerEvent(event: MatrixEvent): () => string | null {
|
||||||
return () => {
|
return () => {
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
||||||
|
@ -327,7 +326,7 @@ function textForCallAnswerEvent(event): () => string | null {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallHangupEvent(event): () => string | null {
|
function textForCallHangupEvent(event: MatrixEvent): () => string | null {
|
||||||
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
||||||
const eventContent = event.getContent();
|
const eventContent = event.getContent();
|
||||||
let getReason = () => "";
|
let getReason = () => "";
|
||||||
|
@ -364,14 +363,14 @@ function textForCallHangupEvent(event): () => string | null {
|
||||||
return () => _t('%(senderName)s ended the call.', { senderName: getSenderName() }) + ' ' + getReason();
|
return () => _t('%(senderName)s ended the call.', { senderName: getSenderName() }) + ' ' + getReason();
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallRejectEvent(event): () => string | null {
|
function textForCallRejectEvent(event: MatrixEvent): () => string | null {
|
||||||
return () => {
|
return () => {
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
return _t('%(senderName)s declined the call.', { senderName });
|
return _t('%(senderName)s declined the call.', { senderName });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallInviteEvent(event): () => string | null {
|
function textForCallInviteEvent(event: MatrixEvent): () => string | null {
|
||||||
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
||||||
// FIXME: Find a better way to determine this from the event?
|
// FIXME: Find a better way to determine this from the event?
|
||||||
let isVoice = true;
|
let isVoice = true;
|
||||||
|
@ -403,7 +402,7 @@ function textForCallInviteEvent(event): () => string | null {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForThreePidInviteEvent(event): () => string | null {
|
function textForThreePidInviteEvent(event: MatrixEvent): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
|
|
||||||
if (!isValid3pidInvite(event)) {
|
if (!isValid3pidInvite(event)) {
|
||||||
|
@ -419,7 +418,7 @@ function textForThreePidInviteEvent(event): () => string | null {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForHistoryVisibilityEvent(event): () => string | null {
|
function textForHistoryVisibilityEvent(event: MatrixEvent): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
switch (event.getContent().history_visibility) {
|
switch (event.getContent().history_visibility) {
|
||||||
case 'invited':
|
case 'invited':
|
||||||
|
@ -441,7 +440,7 @@ function textForHistoryVisibilityEvent(event): () => string | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently will only display a change if a user's power level is changed
|
// Currently will only display a change if a user's power level is changed
|
||||||
function textForPowerEvent(event): () => string | null {
|
function textForPowerEvent(event: MatrixEvent): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
if (!event.getPrevContent() || !event.getPrevContent().users ||
|
if (!event.getPrevContent() || !event.getPrevContent().users ||
|
||||||
!event.getContent() || !event.getContent().users) {
|
!event.getContent() || !event.getContent().users) {
|
||||||
|
@ -523,7 +522,7 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string
|
||||||
return () => _t("%(senderName)s changed the pinned messages for the room.", { senderName });
|
return () => _t("%(senderName)s changed the pinned messages for the room.", { senderName });
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForWidgetEvent(event): () => string | null {
|
function textForWidgetEvent(event: MatrixEvent): () => string | null {
|
||||||
const senderName = event.getSender();
|
const senderName = event.getSender();
|
||||||
const { name: prevName, type: prevType, url: prevUrl } = event.getPrevContent();
|
const { name: prevName, type: prevType, url: prevUrl } = event.getPrevContent();
|
||||||
const { name, type, url } = event.getContent() || {};
|
const { name, type, url } = event.getContent() || {};
|
||||||
|
@ -553,12 +552,12 @@ function textForWidgetEvent(event): () => string | null {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForWidgetLayoutEvent(event): () => string | null {
|
function textForWidgetLayoutEvent(event: MatrixEvent): () => string | null {
|
||||||
const senderName = event.sender?.name || event.getSender();
|
const senderName = event.sender?.name || event.getSender();
|
||||||
return () => _t("%(senderName)s has updated the widget layout", { senderName });
|
return () => _t("%(senderName)s has updated the widget layout", { senderName });
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForMjolnirEvent(event): () => string | null {
|
function textForMjolnirEvent(event: MatrixEvent): () => string | null {
|
||||||
const senderName = event.getSender();
|
const senderName = event.getSender();
|
||||||
const { entity: prevEntity } = event.getPrevContent();
|
const { entity: prevEntity } = event.getPrevContent();
|
||||||
const { entity, recommendation, reason } = event.getContent();
|
const { entity, recommendation, reason } = event.getContent();
|
||||||
|
@ -646,7 +645,9 @@ function textForMjolnirEvent(event): () => string | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IHandlers {
|
interface IHandlers {
|
||||||
[type: string]: (ev: MatrixEvent, allowJSX?: boolean) => (() => string | JSX.Element | null);
|
[type: string]:
|
||||||
|
(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean) =>
|
||||||
|
(() => string | JSX.Element | null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlers: IHandlers = {
|
const handlers: IHandlers = {
|
||||||
|
@ -682,14 +683,27 @@ for (const evType of ALL_RULE_TYPES) {
|
||||||
stateHandlers[evType] = textForMjolnirEvent;
|
stateHandlers[evType] = textForMjolnirEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasText(ev: MatrixEvent): boolean {
|
/**
|
||||||
|
* Determines whether the given event has text to display.
|
||||||
|
* @param ev The event
|
||||||
|
* @param showHiddenEvents An optional cached setting value for showHiddenEventsInTimeline
|
||||||
|
* to avoid hitting the settings store
|
||||||
|
*/
|
||||||
|
export function hasText(ev: MatrixEvent, showHiddenEvents?: boolean): boolean {
|
||||||
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||||
return Boolean(handler?.(ev));
|
return Boolean(handler?.(ev, false, showHiddenEvents));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the textual content of the given event.
|
||||||
|
* @param ev The event
|
||||||
|
* @param allowJSX Whether to output rich JSX content
|
||||||
|
* @param showHiddenEvents An optional cached setting value for showHiddenEventsInTimeline
|
||||||
|
* to avoid hitting the settings store
|
||||||
|
*/
|
||||||
export function textForEvent(ev: MatrixEvent): string;
|
export function textForEvent(ev: MatrixEvent): string;
|
||||||
export function textForEvent(ev: MatrixEvent, allowJSX: true): string | JSX.Element;
|
export function textForEvent(ev: MatrixEvent, allowJSX: true, showHiddenEvents?: boolean): string | JSX.Element;
|
||||||
export function textForEvent(ev: MatrixEvent, allowJSX = false): string | JSX.Element {
|
export function textForEvent(ev: MatrixEvent, allowJSX = false, showHiddenEvents?: boolean): string | JSX.Element {
|
||||||
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||||
return handler?.(ev, allowJSX)?.() || '';
|
return handler?.(ev, allowJSX, showHiddenEvents)?.() || '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,7 +109,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
limit = -1,
|
limit = -1,
|
||||||
): Promise<ICompletion[]> {
|
): Promise<ICompletion[]> {
|
||||||
// lazy-load user list into matcher
|
// lazy-load user list into matcher
|
||||||
if (!this.users) this._makeUsers();
|
if (!this.users) this.makeUsers();
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
const { command, range } = this.getCurrentCommand(rawQuery, selection, force);
|
const { command, range } = this.getCurrentCommand(rawQuery, selection, force);
|
||||||
|
@ -147,7 +147,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
return _t('Users');
|
return _t('Users');
|
||||||
}
|
}
|
||||||
|
|
||||||
_makeUsers() {
|
private makeUsers() {
|
||||||
const events = this.room.getLiveTimeline().getEvents();
|
const events = this.room.getLiveTimeline().getEvents();
|
||||||
const lastSpoken = {};
|
const lastSpoken = {};
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as PropTypes from 'prop-types';
|
|
||||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
|
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
|
|
||||||
import { Key } from '../../Keyboard';
|
import { Key } from '../../Keyboard';
|
||||||
import PageTypes from '../../PageTypes';
|
import PageTypes from '../../PageTypes';
|
||||||
|
@ -79,6 +79,8 @@ function canElementReceiveInput(el) {
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
matrixClient: MatrixClient;
|
matrixClient: MatrixClient;
|
||||||
|
// Called with the credentials of a registered user (if they were a ROU that
|
||||||
|
// transitioned to PWLU)
|
||||||
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
|
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
|
||||||
hideToSRUsers: boolean;
|
hideToSRUsers: boolean;
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
|
@ -140,18 +142,6 @@ interface IState {
|
||||||
class LoggedInView extends React.Component<IProps, IState> {
|
class LoggedInView extends React.Component<IProps, IState> {
|
||||||
static displayName = 'LoggedInView';
|
static displayName = 'LoggedInView';
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
|
||||||
page_type: PropTypes.string.isRequired,
|
|
||||||
onRoomCreated: PropTypes.func,
|
|
||||||
|
|
||||||
// Called with the credentials of a registered user (if they were a ROU that
|
|
||||||
// transitioned to PWLU)
|
|
||||||
onRegistered: PropTypes.func,
|
|
||||||
|
|
||||||
// and lots and lots of other stuff.
|
|
||||||
};
|
|
||||||
|
|
||||||
protected readonly _matrixClient: MatrixClient;
|
protected readonly _matrixClient: MatrixClient;
|
||||||
protected readonly _roomView: React.RefObject<any>;
|
protected readonly _roomView: React.RefObject<any>;
|
||||||
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
|
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
|
||||||
|
@ -181,10 +171,10 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.addEventListener('keydown', this._onNativeKeyDown, false);
|
document.addEventListener('keydown', this.onNativeKeyDown, false);
|
||||||
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
||||||
|
|
||||||
this._updateServerNoticeEvents();
|
this.updateServerNoticeEvents();
|
||||||
|
|
||||||
this._matrixClient.on("accountData", this.onAccountData);
|
this._matrixClient.on("accountData", this.onAccountData);
|
||||||
this._matrixClient.on("sync", this.onSync);
|
this._matrixClient.on("sync", this.onSync);
|
||||||
|
@ -200,13 +190,13 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
"useCompactLayout", null, this.onCompactLayoutChanged,
|
"useCompactLayout", null, this.onCompactLayoutChanged,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.resizer = this._createResizer();
|
this.resizer = this.createResizer();
|
||||||
this.resizer.attach();
|
this.resizer.attach();
|
||||||
this._loadResizerPreferences();
|
this.loadResizerPreferences();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('keydown', this._onNativeKeyDown, false);
|
document.removeEventListener('keydown', this.onNativeKeyDown, false);
|
||||||
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
||||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||||
this._matrixClient.removeListener("sync", this.onSync);
|
this._matrixClient.removeListener("sync", this.onSync);
|
||||||
|
@ -221,37 +211,37 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
canResetTimelineInRoom = (roomId) => {
|
public canResetTimelineInRoom = (roomId: string) => {
|
||||||
if (!this._roomView.current) {
|
if (!this._roomView.current) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return this._roomView.current.canResetTimeline();
|
return this._roomView.current.canResetTimeline();
|
||||||
};
|
};
|
||||||
|
|
||||||
_createResizer() {
|
private createResizer() {
|
||||||
let size;
|
let panelSize;
|
||||||
let collapsed;
|
let panelCollapsed;
|
||||||
const collapseConfig: ICollapseConfig = {
|
const collapseConfig: ICollapseConfig = {
|
||||||
// TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel
|
// TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel
|
||||||
toggleSize: 206 - 50,
|
toggleSize: 206 - 50,
|
||||||
onCollapsed: (_collapsed) => {
|
onCollapsed: (collapsed) => {
|
||||||
collapsed = _collapsed;
|
panelCollapsed = collapsed;
|
||||||
if (_collapsed) {
|
if (collapsed) {
|
||||||
dis.dispatch({ action: "hide_left_panel" });
|
dis.dispatch({ action: "hide_left_panel" });
|
||||||
window.localStorage.setItem("mx_lhs_size", '0');
|
window.localStorage.setItem("mx_lhs_size", '0');
|
||||||
} else {
|
} else {
|
||||||
dis.dispatch({ action: "show_left_panel" });
|
dis.dispatch({ action: "show_left_panel" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onResized: (_size) => {
|
onResized: (size) => {
|
||||||
size = _size;
|
panelSize = size;
|
||||||
this.props.resizeNotifier.notifyLeftHandleResized();
|
this.props.resizeNotifier.notifyLeftHandleResized();
|
||||||
},
|
},
|
||||||
onResizeStart: () => {
|
onResizeStart: () => {
|
||||||
this.props.resizeNotifier.startResizing();
|
this.props.resizeNotifier.startResizing();
|
||||||
},
|
},
|
||||||
onResizeStop: () => {
|
onResizeStop: () => {
|
||||||
if (!collapsed) window.localStorage.setItem("mx_lhs_size", '' + size);
|
if (!panelCollapsed) window.localStorage.setItem("mx_lhs_size", '' + panelSize);
|
||||||
this.props.resizeNotifier.stopResizing();
|
this.props.resizeNotifier.stopResizing();
|
||||||
},
|
},
|
||||||
isItemCollapsed: domNode => {
|
isItemCollapsed: domNode => {
|
||||||
|
@ -267,7 +257,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
return resizer;
|
return resizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadResizerPreferences() {
|
private loadResizerPreferences() {
|
||||||
let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10);
|
let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10);
|
||||||
if (isNaN(lhsSize)) {
|
if (isNaN(lhsSize)) {
|
||||||
lhsSize = 350;
|
lhsSize = 350;
|
||||||
|
@ -275,7 +265,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
this.resizer.forHandleAt(0).resize(lhsSize);
|
this.resizer.forHandleAt(0).resize(lhsSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAccountData = (event) => {
|
private onAccountData = (event: MatrixEvent) => {
|
||||||
if (event.getType() === "m.ignored_user_list") {
|
if (event.getType() === "m.ignored_user_list") {
|
||||||
dis.dispatch({ action: "ignore_state_changed" });
|
dis.dispatch({ action: "ignore_state_changed" });
|
||||||
}
|
}
|
||||||
|
@ -307,16 +297,16 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
|
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
|
||||||
this._updateServerNoticeEvents();
|
this.updateServerNoticeEvents();
|
||||||
} else {
|
} else {
|
||||||
this._calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent);
|
this.calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onRoomStateEvents = (ev, state) => {
|
onRoomStateEvents = (ev, state) => {
|
||||||
const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
|
const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
|
||||||
if (serverNoticeList && serverNoticeList.some(r => r.roomId === ev.getRoomId())) {
|
if (serverNoticeList && serverNoticeList.some(r => r.roomId === ev.getRoomId())) {
|
||||||
this._updateServerNoticeEvents();
|
this.updateServerNoticeEvents();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -326,7 +316,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
|
private calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
|
||||||
const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
|
const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
|
||||||
if (error) {
|
if (error) {
|
||||||
usageLimitEventContent = syncError.error.data;
|
usageLimitEventContent = syncError.error.data;
|
||||||
|
@ -346,7 +336,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateServerNoticeEvents = async () => {
|
private updateServerNoticeEvents = async () => {
|
||||||
const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
|
const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
|
||||||
if (!serverNoticeList) return [];
|
if (!serverNoticeList) return [];
|
||||||
|
|
||||||
|
@ -378,7 +368,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent();
|
const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent();
|
||||||
this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
|
this.calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
|
||||||
this.setState({
|
this.setState({
|
||||||
usageLimitEventContent,
|
usageLimitEventContent,
|
||||||
usageLimitEventTs: pinnedEventTs,
|
usageLimitEventTs: pinnedEventTs,
|
||||||
|
@ -387,7 +377,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onPaste = (ev) => {
|
private onPaste = (ev) => {
|
||||||
let canReceiveInput = false;
|
let canReceiveInput = false;
|
||||||
let element = ev.target;
|
let element = ev.target;
|
||||||
// test for all parents because the target can be a child of a contenteditable element
|
// test for all parents because the target can be a child of a contenteditable element
|
||||||
|
@ -425,22 +415,22 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
We also listen with a native listener on the document to get keydown events when no element is focused.
|
We also listen with a native listener on the document to get keydown events when no element is focused.
|
||||||
Bubbling is irrelevant here as the target is the body element.
|
Bubbling is irrelevant here as the target is the body element.
|
||||||
*/
|
*/
|
||||||
_onReactKeyDown = (ev) => {
|
private onReactKeyDown = (ev) => {
|
||||||
// events caught while bubbling up on the root element
|
// events caught while bubbling up on the root element
|
||||||
// of this component, so something must be focused.
|
// of this component, so something must be focused.
|
||||||
this._onKeyDown(ev);
|
this.onKeyDown(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onNativeKeyDown = (ev) => {
|
private onNativeKeyDown = (ev) => {
|
||||||
// only pass this if there is no focused element.
|
// only pass this if there is no focused element.
|
||||||
// if there is, _onKeyDown will be called by the
|
// if there is, onKeyDown will be called by the
|
||||||
// react keydown handler that respects the react bubbling order.
|
// react keydown handler that respects the react bubbling order.
|
||||||
if (ev.target === document.body) {
|
if (ev.target === document.body) {
|
||||||
this._onKeyDown(ev);
|
this.onKeyDown(ev);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onKeyDown = (ev) => {
|
private onKeyDown = (ev) => {
|
||||||
let handled = false;
|
let handled = false;
|
||||||
|
|
||||||
const roomAction = getKeyBindingsManager().getRoomAction(ev);
|
const roomAction = getKeyBindingsManager().getRoomAction(ev);
|
||||||
|
@ -450,7 +440,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
case RoomAction.JumpToFirstMessage:
|
case RoomAction.JumpToFirstMessage:
|
||||||
case RoomAction.JumpToLatestMessage:
|
case RoomAction.JumpToLatestMessage:
|
||||||
// pass the event down to the scroll panel
|
// pass the event down to the scroll panel
|
||||||
this._onScrollKeyPressed(ev);
|
this.onScrollKeyPressed(ev);
|
||||||
handled = true;
|
handled = true;
|
||||||
break;
|
break;
|
||||||
case RoomAction.FocusSearch:
|
case RoomAction.FocusSearch:
|
||||||
|
@ -565,7 +555,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
* dispatch a page-up/page-down/etc to the appropriate component
|
* dispatch a page-up/page-down/etc to the appropriate component
|
||||||
* @param {Object} ev The key event
|
* @param {Object} ev The key event
|
||||||
*/
|
*/
|
||||||
_onScrollKeyPressed = (ev) => {
|
private onScrollKeyPressed = (ev) => {
|
||||||
if (this._roomView.current) {
|
if (this._roomView.current) {
|
||||||
this._roomView.current.handleScrollKey(ev);
|
this._roomView.current.handleScrollKey(ev);
|
||||||
}
|
}
|
||||||
|
@ -625,8 +615,8 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
return (
|
return (
|
||||||
<MatrixClientContext.Provider value={this._matrixClient}>
|
<MatrixClientContext.Provider value={this._matrixClient}>
|
||||||
<div
|
<div
|
||||||
onPaste={this._onPaste}
|
onPaste={this.onPaste}
|
||||||
onKeyDown={this._onReactKeyDown}
|
onKeyDown={this.onReactKeyDown}
|
||||||
className='mx_MatrixChat_wrapper'
|
className='mx_MatrixChat_wrapper'
|
||||||
aria-hidden={this.props.hideToSRUsers}
|
aria-hidden={this.props.hideToSRUsers}
|
||||||
>
|
>
|
||||||
|
|
|
@ -431,7 +431,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
UNSAFE_componentWillUpdate(props, state) {
|
UNSAFE_componentWillUpdate(props, state) {
|
||||||
if (this.shouldTrackPageChange(this.state, state)) {
|
if (this.shouldTrackPageChange(this.state, state)) {
|
||||||
this.startPageChangeTimer();
|
this.startPageChangeTimer();
|
||||||
|
@ -1864,13 +1864,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
dis.dispatch({ action: 'timeline_resize' });
|
dis.dispatch({ action: 'timeline_resize' });
|
||||||
}
|
}
|
||||||
|
|
||||||
onRoomCreated(roomId: string) {
|
|
||||||
dis.dispatch({
|
|
||||||
action: "view_room",
|
|
||||||
room_id: roomId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onRegisterClick = () => {
|
onRegisterClick = () => {
|
||||||
this.showScreen("register");
|
this.showScreen("register");
|
||||||
};
|
};
|
||||||
|
@ -2043,7 +2036,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
{...this.state}
|
{...this.state}
|
||||||
ref={this.loggedInView}
|
ref={this.loggedInView}
|
||||||
matrixClient={MatrixClientPeg.get()}
|
matrixClient={MatrixClientPeg.get()}
|
||||||
onRoomCreated={this.onRoomCreated}
|
|
||||||
onRegistered={this.onRegistered}
|
onRegistered={this.onRegistered}
|
||||||
currentRoomId={this.state.currentRoomId}
|
currentRoomId={this.state.currentRoomId}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -54,7 +54,11 @@ const membershipTypes = [EventType.RoomMember, EventType.RoomThirdPartyInvite, E
|
||||||
|
|
||||||
// check if there is a previous event and it has the same sender as this event
|
// check if there is a previous event and it has the same sender as this event
|
||||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||||
function shouldFormContinuation(prevEvent: MatrixEvent, mxEvent: MatrixEvent): boolean {
|
function shouldFormContinuation(
|
||||||
|
prevEvent: MatrixEvent,
|
||||||
|
mxEvent: MatrixEvent,
|
||||||
|
showHiddenEvents: boolean,
|
||||||
|
): boolean {
|
||||||
// sanity check inputs
|
// sanity check inputs
|
||||||
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
|
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
|
||||||
// check if within the max continuation period
|
// check if within the max continuation period
|
||||||
|
@ -74,7 +78,7 @@ function shouldFormContinuation(prevEvent: MatrixEvent, mxEvent: MatrixEvent): b
|
||||||
mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false;
|
mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false;
|
||||||
|
|
||||||
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
|
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
|
||||||
if (!haveTileForEvent(prevEvent)) return false;
|
if (!haveTileForEvent(prevEvent, showHiddenEvents)) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -239,7 +243,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cache hidden events setting on mount since Settings is expensive to
|
// Cache hidden events setting on mount since Settings is expensive to
|
||||||
// query, and we check this in a hot code path.
|
// query, and we check this in a hot code path. This is also cached in
|
||||||
|
// our RoomContext, however we still need a fallback for roomless MessagePanels.
|
||||||
this.showHiddenEventsInTimeline = SettingsStore.getValue("showHiddenEventsInTimeline");
|
this.showHiddenEventsInTimeline = SettingsStore.getValue("showHiddenEventsInTimeline");
|
||||||
|
|
||||||
this.showTypingNotificationsWatcherRef =
|
this.showTypingNotificationsWatcherRef =
|
||||||
|
@ -399,17 +404,21 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
return !this.isMounted;
|
return !this.isMounted;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private get showHiddenEvents(): boolean {
|
||||||
|
return this.context?.showHiddenEventsInTimeline ?? this.showHiddenEventsInTimeline;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Implement granular (per-room) hide options
|
// TODO: Implement granular (per-room) hide options
|
||||||
public shouldShowEvent(mxEv: MatrixEvent): boolean {
|
public shouldShowEvent(mxEv: MatrixEvent): boolean {
|
||||||
if (MatrixClientPeg.get().isUserIgnored(mxEv.getSender())) {
|
if (MatrixClientPeg.get().isUserIgnored(mxEv.getSender())) {
|
||||||
return false; // ignored = no show (only happens if the ignore happens after an event was received)
|
return false; // ignored = no show (only happens if the ignore happens after an event was received)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.showHiddenEventsInTimeline) {
|
if (this.showHiddenEvents) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!haveTileForEvent(mxEv)) {
|
if (!haveTileForEvent(mxEv, this.showHiddenEvents)) {
|
||||||
return false; // no tile = no show
|
return false; // no tile = no show
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -569,7 +578,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
if (grouper) {
|
if (grouper) {
|
||||||
if (grouper.shouldGroup(mxEv)) {
|
if (grouper.shouldGroup(mxEv)) {
|
||||||
grouper.add(mxEv);
|
grouper.add(mxEv, this.showHiddenEvents);
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
// not part of group, so get the group tiles, close the
|
// not part of group, so get the group tiles, close the
|
||||||
|
@ -644,12 +653,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let willWantDateSeparator = false;
|
let willWantDateSeparator = false;
|
||||||
|
let lastInSection = true;
|
||||||
if (nextEvent) {
|
if (nextEvent) {
|
||||||
willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEvent.getDate() || new Date());
|
willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEvent.getDate() || new Date());
|
||||||
|
lastInSection = willWantDateSeparator || mxEv.getSender() !== nextEvent.getSender();
|
||||||
}
|
}
|
||||||
|
|
||||||
// is this a continuation of the previous message?
|
// is this a continuation of the previous message?
|
||||||
const continuation = !wantsDateSeparator && shouldFormContinuation(prevEvent, mxEv);
|
const continuation = !wantsDateSeparator &&
|
||||||
|
shouldFormContinuation(prevEvent, mxEv, this.showHiddenEvents);
|
||||||
|
|
||||||
const eventId = mxEv.getId();
|
const eventId = mxEv.getId();
|
||||||
const highlight = (eventId === this.props.highlightedEventId);
|
const highlight = (eventId === this.props.highlightedEventId);
|
||||||
|
@ -702,7 +714,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
isTwelveHour={this.props.isTwelveHour}
|
isTwelveHour={this.props.isTwelveHour}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
last={last}
|
last={last}
|
||||||
lastInSection={willWantDateSeparator}
|
lastInSection={lastInSection}
|
||||||
lastSuccessful={isLastSuccessful}
|
lastSuccessful={isLastSuccessful}
|
||||||
isSelectedEvent={highlight}
|
isSelectedEvent={highlight}
|
||||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
|
@ -710,6 +722,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
layout={this.props.layout}
|
layout={this.props.layout}
|
||||||
enableFlair={this.props.enableFlair}
|
enableFlair={this.props.enableFlair}
|
||||||
showReadReceipts={this.props.showReadReceipts}
|
showReadReceipts={this.props.showReadReceipts}
|
||||||
|
hideSender={this.props.room.getMembers().length <= 2 && this.props.layout === Layout.Bubble}
|
||||||
/>
|
/>
|
||||||
</TileErrorBoundary>,
|
</TileErrorBoundary>,
|
||||||
);
|
);
|
||||||
|
@ -946,7 +959,7 @@ abstract class BaseGrouper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract shouldGroup(ev: MatrixEvent): boolean;
|
public abstract shouldGroup(ev: MatrixEvent): boolean;
|
||||||
public abstract add(ev: MatrixEvent): void;
|
public abstract add(ev: MatrixEvent, showHiddenEvents?: boolean): void;
|
||||||
public abstract getTiles(): ReactNode[];
|
public abstract getTiles(): ReactNode[];
|
||||||
public abstract getNewPrevEvent(): MatrixEvent;
|
public abstract getNewPrevEvent(): MatrixEvent;
|
||||||
}
|
}
|
||||||
|
@ -1200,10 +1213,10 @@ class MemberGrouper extends BaseGrouper {
|
||||||
return membershipTypes.includes(ev.getType() as EventType);
|
return membershipTypes.includes(ev.getType() as EventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public add(ev: MatrixEvent): void {
|
public add(ev: MatrixEvent, showHiddenEvents?: boolean): void {
|
||||||
if (ev.getType() === EventType.RoomMember) {
|
if (ev.getType() === EventType.RoomMember) {
|
||||||
// We can ignore any events that don't actually have a message to display
|
// We can ignore any events that don't actually have a message to display
|
||||||
if (!hasText(ev)) return;
|
if (!hasText(ev, showHiddenEvents)) return;
|
||||||
}
|
}
|
||||||
this.readMarker = this.readMarker || this.panel.readMarkerForEvent(
|
this.readMarker = this.readMarker || this.panel.readMarkerForEvent(
|
||||||
ev.getId(),
|
ev.getId(),
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||||
import { User } from "matrix-js-sdk/src/models/user";
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
@ -152,7 +153,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line
|
||||||
if (newProps.groupId !== this.props.groupId) {
|
if (newProps.groupId !== this.props.groupId) {
|
||||||
this.unregisterGroupStore();
|
this.unregisterGroupStore();
|
||||||
this.initGroupStore(newProps.groupId);
|
this.initGroupStore(newProps.groupId);
|
||||||
|
@ -174,7 +175,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => {
|
private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember) => {
|
||||||
if (!this.props.room || member.roomId !== this.props.room.roomId) {
|
if (!this.props.room || member.roomId !== this.props.room.roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -814,7 +814,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
}) : _t("Explore rooms");
|
}) : _t("Explore rooms");
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
className={'mx_RoomDirectory_dialog'}
|
className="mx_RoomDirectory_dialog"
|
||||||
hasCancel={true}
|
hasCancel={true}
|
||||||
onFinished={this.onFinished}
|
onFinished={this.onFinished}
|
||||||
title={title}
|
title={title}
|
||||||
|
|
|
@ -166,6 +166,7 @@ export interface IState {
|
||||||
canReply: boolean;
|
canReply: boolean;
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
lowBandwidth: boolean;
|
lowBandwidth: boolean;
|
||||||
|
showHiddenEventsInTimeline: boolean;
|
||||||
showReadReceipts: boolean;
|
showReadReceipts: boolean;
|
||||||
showRedactions: boolean;
|
showRedactions: boolean;
|
||||||
showJoinLeaves: boolean;
|
showJoinLeaves: boolean;
|
||||||
|
@ -230,6 +231,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
canReply: false,
|
canReply: false,
|
||||||
layout: SettingsStore.getValue("layout"),
|
layout: SettingsStore.getValue("layout"),
|
||||||
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
|
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
|
||||||
|
showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline"),
|
||||||
showReadReceipts: true,
|
showReadReceipts: true,
|
||||||
showRedactions: true,
|
showRedactions: true,
|
||||||
showJoinLeaves: true,
|
showJoinLeaves: true,
|
||||||
|
@ -267,6 +269,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
SettingsStore.watchSetting("lowBandwidth", null, () =>
|
SettingsStore.watchSetting("lowBandwidth", null, () =>
|
||||||
this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
|
this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
|
||||||
),
|
),
|
||||||
|
SettingsStore.watchSetting("showHiddenEventsInTimeline", null, () =>
|
||||||
|
this.setState({ showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline") }),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1388,7 +1393,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!haveTileForEvent(mxEv)) {
|
if (!haveTileForEvent(mxEv, this.state.showHiddenEventsInTimeline)) {
|
||||||
// XXX: can this ever happen? It will make the result count
|
// XXX: can this ever happen? It will make the result count
|
||||||
// not match the displayed count.
|
// not match the displayed count.
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -458,7 +458,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
||||||
const numFields = 3;
|
const numFields = 3;
|
||||||
const placeholders = [_t("General"), _t("Random"), _t("Support")];
|
const placeholders = [_t("General"), _t("Random"), _t("Support")];
|
||||||
const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]);
|
const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]);
|
||||||
const fields = new Array(numFields).fill(0).map((_, i) => {
|
const fields = new Array(numFields).fill(0).map((x, i) => {
|
||||||
const name = "roomName" + i;
|
const name = "roomName" + i;
|
||||||
return <Field
|
return <Field
|
||||||
key={name}
|
key={name}
|
||||||
|
@ -625,7 +625,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
||||||
const numFields = 3;
|
const numFields = 3;
|
||||||
const fieldRefs: RefObject<Field>[] = [useRef(), useRef(), useRef()];
|
const fieldRefs: RefObject<Field>[] = [useRef(), useRef(), useRef()];
|
||||||
const [emailAddresses, setEmailAddress] = useStateArray(numFields, "");
|
const [emailAddresses, setEmailAddress] = useStateArray(numFields, "");
|
||||||
const fields = new Array(numFields).fill(0).map((_, i) => {
|
const fields = new Array(numFields).fill(0).map((x, i) => {
|
||||||
const name = "emailAddress" + i;
|
const name = "emailAddress" + i;
|
||||||
return <Field
|
return <Field
|
||||||
key={name}
|
key={name}
|
||||||
|
|
|
@ -74,7 +74,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
|
||||||
tabLocation: TabLocation.LEFT,
|
tabLocation: TabLocation.LEFT,
|
||||||
};
|
};
|
||||||
|
|
||||||
private _getActiveTabIndex() {
|
private getActiveTabIndex() {
|
||||||
if (!this.state || !this.state.activeTabIndex) return 0;
|
if (!this.state || !this.state.activeTabIndex) return 0;
|
||||||
return this.state.activeTabIndex;
|
return this.state.activeTabIndex;
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
|
||||||
* @param {Tab} tab the tab to show
|
* @param {Tab} tab the tab to show
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private _setActiveTab(tab: Tab) {
|
private setActiveTab(tab: Tab) {
|
||||||
const idx = this.props.tabs.indexOf(tab);
|
const idx = this.props.tabs.indexOf(tab);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
if (this.props.onChange) this.props.onChange(tab.id);
|
if (this.props.onChange) this.props.onChange(tab.id);
|
||||||
|
@ -94,18 +94,18 @@ export default class TabbedView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderTabLabel(tab: Tab) {
|
private renderTabLabel(tab: Tab) {
|
||||||
let classes = "mx_TabbedView_tabLabel ";
|
let classes = "mx_TabbedView_tabLabel ";
|
||||||
|
|
||||||
const idx = this.props.tabs.indexOf(tab);
|
const idx = this.props.tabs.indexOf(tab);
|
||||||
if (idx === this._getActiveTabIndex()) classes += "mx_TabbedView_tabLabel_active";
|
if (idx === this.getActiveTabIndex()) classes += "mx_TabbedView_tabLabel_active";
|
||||||
|
|
||||||
let tabIcon = null;
|
let tabIcon = null;
|
||||||
if (tab.icon) {
|
if (tab.icon) {
|
||||||
tabIcon = <span className={`mx_TabbedView_maskedIcon ${tab.icon}`} />;
|
tabIcon = <span className={`mx_TabbedView_maskedIcon ${tab.icon}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickHandler = () => this._setActiveTab(tab);
|
const onClickHandler = () => this.setActiveTab(tab);
|
||||||
|
|
||||||
const label = _t(tab.label);
|
const label = _t(tab.label);
|
||||||
return (
|
return (
|
||||||
|
@ -118,7 +118,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderTabPanel(tab: Tab): React.ReactNode {
|
private renderTabPanel(tab: Tab): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<div className="mx_TabbedView_tabPanel" key={"mx_tabpanel_" + tab.label}>
|
<div className="mx_TabbedView_tabPanel" key={"mx_tabpanel_" + tab.label}>
|
||||||
<AutoHideScrollbar className='mx_TabbedView_tabPanelContent'>
|
<AutoHideScrollbar className='mx_TabbedView_tabPanelContent'>
|
||||||
|
@ -129,8 +129,8 @@ export default class TabbedView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const labels = this.props.tabs.map(tab => this._renderTabLabel(tab));
|
const labels = this.props.tabs.map(tab => this.renderTabLabel(tab));
|
||||||
const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]);
|
const panel = this.renderTabPanel(this.props.tabs[this.getActiveTabIndex()]);
|
||||||
|
|
||||||
const tabbedViewClasses = classNames({
|
const tabbedViewClasses = classNames({
|
||||||
'mx_TabbedView': true,
|
'mx_TabbedView': true,
|
||||||
|
|
|
@ -277,7 +277,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Move into constructor
|
// TODO: [REACT-WARNING] Move into constructor
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
UNSAFE_componentWillMount() {
|
UNSAFE_componentWillMount() {
|
||||||
if (this.props.manageReadReceipts) {
|
if (this.props.manageReadReceipts) {
|
||||||
this.updateReadReceiptOnUserActivity();
|
this.updateReadReceiptOnUserActivity();
|
||||||
|
@ -290,7 +290,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
UNSAFE_componentWillReceiveProps(newProps) {
|
UNSAFE_componentWillReceiveProps(newProps) {
|
||||||
if (newProps.timelineSet !== this.props.timelineSet) {
|
if (newProps.timelineSet !== this.props.timelineSet) {
|
||||||
// throw new Error("changing timelineSet on a TimelinePanel is not supported");
|
// throw new Error("changing timelineSet on a TimelinePanel is not supported");
|
||||||
|
@ -1337,7 +1337,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const shouldIgnore = !!ev.status || // local echo
|
const shouldIgnore = !!ev.status || // local echo
|
||||||
(ignoreOwn && ev.getSender() === myUserId); // own message
|
(ignoreOwn && ev.getSender() === myUserId); // own message
|
||||||
const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev, this.context);
|
const isWithoutTile = !haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline) ||
|
||||||
|
shouldHideEvent(ev, this.context);
|
||||||
|
|
||||||
if (isWithoutTile || !node) {
|
if (isWithoutTile || !node) {
|
||||||
// don't start counting if the event should be ignored,
|
// don't start counting if the event should be ignored,
|
||||||
|
|
|
@ -37,14 +37,14 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
||||||
// toasts may dismiss themselves in their didMount if they find
|
// toasts may dismiss themselves in their didMount if they find
|
||||||
// they're already irrelevant by the time they're mounted, and
|
// they're already irrelevant by the time they're mounted, and
|
||||||
// our own componentDidMount is too late.
|
// our own componentDidMount is too late.
|
||||||
ToastStore.sharedInstance().on('update', this._onToastStoreUpdate);
|
ToastStore.sharedInstance().on('update', this.onToastStoreUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
ToastStore.sharedInstance().removeListener('update', this._onToastStoreUpdate);
|
ToastStore.sharedInstance().removeListener('update', this.onToastStoreUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onToastStoreUpdate = () => {
|
private onToastStoreUpdate = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
toasts: ToastStore.sharedInstance().getToasts(),
|
toasts: ToastStore.sharedInstance().getToasts(),
|
||||||
countSeen: ToastStore.sharedInstance().getCountSeen(),
|
countSeen: ToastStore.sharedInstance().getCountSeen(),
|
||||||
|
|
|
@ -101,7 +101,7 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
public UNSAFE_componentWillReceiveProps(newProps: IProps): void {
|
public UNSAFE_componentWillReceiveProps(newProps: IProps): void {
|
||||||
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
||||||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||||
|
|
|
@ -144,7 +144,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
UNSAFE_componentWillMount() {
|
UNSAFE_componentWillMount() {
|
||||||
this.initLoginLogic(this.props.serverConfig);
|
this.initLoginLogic(this.props.serverConfig);
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
UNSAFE_componentWillReceiveProps(newProps) {
|
UNSAFE_componentWillReceiveProps(newProps) {
|
||||||
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
||||||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||||
|
|
|
@ -141,7 +141,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
UNSAFE_componentWillReceiveProps(newProps) {
|
UNSAFE_componentWillReceiveProps(newProps) {
|
||||||
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
||||||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||||
|
|
|
@ -22,6 +22,7 @@ import ImageView from '../elements/ImageView';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import * as Avatar from '../../../Avatar';
|
import * as Avatar from '../../../Avatar';
|
||||||
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { mediaFromMxc } from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
import { IOOBData } from '../../../stores/ThreepidInviteStore';
|
import { IOOBData } from '../../../stores/ThreepidInviteStore';
|
||||||
|
@ -131,11 +132,14 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||||
const { room, oobData, viewAvatarOnClick, onClick, ...otherProps } = this.props;
|
const { room, oobData, viewAvatarOnClick, onClick, ...otherProps } = this.props;
|
||||||
|
|
||||||
const roomName = room ? room.name : oobData.name;
|
const roomName = room ? room.name : oobData.name;
|
||||||
|
// If the room is a DM, we use the other user's ID for the color hash
|
||||||
|
// in order to match the room avatar with their avatar
|
||||||
|
const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseAvatar {...otherProps}
|
<BaseAvatar {...otherProps}
|
||||||
name={roomName}
|
name={roomName}
|
||||||
idName={room ? room.roomId : null}
|
idName={idName}
|
||||||
urls={this.state.urls}
|
urls={this.state.urls}
|
||||||
onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : onClick}
|
onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : onClick}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -60,8 +60,8 @@ export default class AskInviteAnywayDialog extends React.Component<IProps> {
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
<div id='mx_Dialog_content'>
|
<div id='mx_Dialog_content'>
|
||||||
{/* eslint-disable-next-line */}
|
<p>{ _t("Unable to find profiles for the Matrix IDs listed below - " +
|
||||||
<p>{_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?")}</p>
|
"would you like to invite them anyway?") }</p>
|
||||||
<ul>
|
<ul>
|
||||||
{ errorList }
|
{ errorList }
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -187,7 +187,7 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
|
||||||
emailAddresses.push((
|
emailAddresses.push((
|
||||||
<Field
|
<Field
|
||||||
key={emailAddresses.length}
|
key={emailAddresses.length}
|
||||||
value={""}
|
value=""
|
||||||
onChange={(e) => this.onAddressChange(e, emailAddresses.length)}
|
onChange={(e) => this.onAddressChange(e, emailAddresses.length)}
|
||||||
label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
|
label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
|
||||||
placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
|
placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
|
||||||
|
|
|
@ -102,7 +102,7 @@ export default class CreateGroupDialog extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCancel = () => {
|
private onCancel = () => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ export default class CreateGroupDialog extends React.Component<IProps, IState> {
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
|
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
|
||||||
<button onClick={this._onCancel}>
|
<button onClick={this.onCancel}>
|
||||||
{ _t("Cancel") }
|
{ _t("Cancel") }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -337,7 +337,7 @@ class FilteredList extends React.PureComponent<IFilteredListProps, IFilteredList
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line
|
||||||
if (this.props.children === nextProps.children && this.props.query === nextProps.query) return;
|
if (this.props.children === nextProps.children && this.props.query === nextProps.query) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query),
|
filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query),
|
||||||
|
|
|
@ -40,7 +40,7 @@ interface IState {
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
err?: string;
|
err?: string;
|
||||||
// If we know it, the nature of the abuse, as specified by MSC3215.
|
// If we know it, the nature of the abuse, as specified by MSC3215.
|
||||||
nature?: EXTENDED_NATURE;
|
nature?: ExtendedNature;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MODERATED_BY_STATE_EVENT_TYPE = [
|
const MODERATED_BY_STATE_EVENT_TYPE = [
|
||||||
|
@ -55,22 +55,22 @@ const MODERATED_BY_STATE_EVENT_TYPE = [
|
||||||
const ABUSE_EVENT_TYPE = "org.matrix.msc3215.abuse.report";
|
const ABUSE_EVENT_TYPE = "org.matrix.msc3215.abuse.report";
|
||||||
|
|
||||||
// Standard abuse natures.
|
// Standard abuse natures.
|
||||||
enum NATURE {
|
enum Nature {
|
||||||
DISAGREEMENT = "org.matrix.msc3215.abuse.nature.disagreement",
|
Disagreement = "org.matrix.msc3215.abuse.nature.disagreement",
|
||||||
TOXIC = "org.matrix.msc3215.abuse.nature.toxic",
|
Toxic = "org.matrix.msc3215.abuse.nature.toxic",
|
||||||
ILLEGAL = "org.matrix.msc3215.abuse.nature.illegal",
|
Illegal = "org.matrix.msc3215.abuse.nature.illegal",
|
||||||
SPAM = "org.matrix.msc3215.abuse.nature.spam",
|
Spam = "org.matrix.msc3215.abuse.nature.spam",
|
||||||
OTHER = "org.matrix.msc3215.abuse.nature.other",
|
Other = "org.matrix.msc3215.abuse.nature.other",
|
||||||
}
|
}
|
||||||
|
|
||||||
enum NON_STANDARD_NATURE {
|
enum NonStandardValue {
|
||||||
// Non-standard abuse nature.
|
// Non-standard abuse nature.
|
||||||
// It should never leave the client - we use it to fallback to
|
// It should never leave the client - we use it to fallback to
|
||||||
// server-wide abuse reporting.
|
// server-wide abuse reporting.
|
||||||
ADMIN = "non-standard.abuse.nature.admin"
|
Admin = "non-standard.abuse.nature.admin"
|
||||||
}
|
}
|
||||||
|
|
||||||
type EXTENDED_NATURE = NATURE | NON_STANDARD_NATURE;
|
type ExtendedNature = Nature | NonStandardValue;
|
||||||
|
|
||||||
type Moderation = {
|
type Moderation = {
|
||||||
// The id of the moderation room.
|
// The id of the moderation room.
|
||||||
|
@ -170,7 +170,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// The user has clicked on a nature.
|
// The user has clicked on a nature.
|
||||||
private onNatureChosen = (e: React.FormEvent<HTMLInputElement>): void => {
|
private onNatureChosen = (e: React.FormEvent<HTMLInputElement>): void => {
|
||||||
this.setState({ nature: e.currentTarget.value as EXTENDED_NATURE });
|
this.setState({ nature: e.currentTarget.value as ExtendedNature });
|
||||||
};
|
};
|
||||||
|
|
||||||
// The user has clicked "cancel".
|
// The user has clicked "cancel".
|
||||||
|
@ -187,7 +187,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
||||||
// We need a nature.
|
// We need a nature.
|
||||||
// If the nature is `NATURE.OTHER` or `NON_STANDARD_NATURE.ADMIN`, we also need a `reason`.
|
// If the nature is `NATURE.OTHER` or `NON_STANDARD_NATURE.ADMIN`, we also need a `reason`.
|
||||||
if (!this.state.nature ||
|
if (!this.state.nature ||
|
||||||
((this.state.nature == NATURE.OTHER || this.state.nature == NON_STANDARD_NATURE.ADMIN)
|
((this.state.nature == Nature.Other || this.state.nature == NonStandardValue.Admin)
|
||||||
&& !reason)
|
&& !reason)
|
||||||
) {
|
) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -214,8 +214,8 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
||||||
try {
|
try {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const ev = this.props.mxEvent;
|
const ev = this.props.mxEvent;
|
||||||
if (this.moderation && this.state.nature != NON_STANDARD_NATURE.ADMIN) {
|
if (this.moderation && this.state.nature != NonStandardValue.Admin) {
|
||||||
const nature: NATURE = this.state.nature;
|
const nature: Nature = this.state.nature;
|
||||||
|
|
||||||
// Report to moderators through to the dedicated bot,
|
// Report to moderators through to the dedicated bot,
|
||||||
// as configured in the room's state events.
|
// as configured in the room's state events.
|
||||||
|
@ -274,27 +274,27 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
||||||
const homeServerName = SdkConfig.get()["validated_server_config"].hsName;
|
const homeServerName = SdkConfig.get()["validated_server_config"].hsName;
|
||||||
let subtitle;
|
let subtitle;
|
||||||
switch (this.state.nature) {
|
switch (this.state.nature) {
|
||||||
case NATURE.DISAGREEMENT:
|
case Nature.Disagreement:
|
||||||
subtitle = _t("What this user is writing is wrong.\n" +
|
subtitle = _t("What this user is writing is wrong.\n" +
|
||||||
"This will be reported to the room moderators.");
|
"This will be reported to the room moderators.");
|
||||||
break;
|
break;
|
||||||
case NATURE.TOXIC:
|
case Nature.Toxic:
|
||||||
subtitle = _t("This user is displaying toxic behaviour, " +
|
subtitle = _t("This user is displaying toxic behaviour, " +
|
||||||
"for instance by insulting other users or sharing " +
|
"for instance by insulting other users or sharing " +
|
||||||
" adult-only content in a family-friendly room " +
|
" adult-only content in a family-friendly room " +
|
||||||
" or otherwise violating the rules of this room.\n" +
|
" or otherwise violating the rules of this room.\n" +
|
||||||
"This will be reported to the room moderators.");
|
"This will be reported to the room moderators.");
|
||||||
break;
|
break;
|
||||||
case NATURE.ILLEGAL:
|
case Nature.Illegal:
|
||||||
subtitle = _t("This user is displaying illegal behaviour, " +
|
subtitle = _t("This user is displaying illegal behaviour, " +
|
||||||
"for instance by doxing people or threatening violence.\n" +
|
"for instance by doxing people or threatening violence.\n" +
|
||||||
"This will be reported to the room moderators who may escalate this to legal authorities.");
|
"This will be reported to the room moderators who may escalate this to legal authorities.");
|
||||||
break;
|
break;
|
||||||
case NATURE.SPAM:
|
case Nature.Spam:
|
||||||
subtitle = _t("This user is spamming the room with ads, links to ads or to propaganda.\n" +
|
subtitle = _t("This user is spamming the room with ads, links to ads or to propaganda.\n" +
|
||||||
"This will be reported to the room moderators.");
|
"This will be reported to the room moderators.");
|
||||||
break;
|
break;
|
||||||
case NON_STANDARD_NATURE.ADMIN:
|
case NonStandardValue.Admin:
|
||||||
if (client.isRoomEncrypted(this.props.mxEvent.getRoomId())) {
|
if (client.isRoomEncrypted(this.props.mxEvent.getRoomId())) {
|
||||||
subtitle = _t("This room is dedicated to illegal or toxic content " +
|
subtitle = _t("This room is dedicated to illegal or toxic content " +
|
||||||
"or the moderators fail to moderate illegal or toxic content.\n" +
|
"or the moderators fail to moderate illegal or toxic content.\n" +
|
||||||
|
@ -308,7 +308,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
||||||
{ homeserver: homeServerName });
|
{ homeserver: homeServerName });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case NATURE.OTHER:
|
case Nature.Other:
|
||||||
subtitle = _t("Any other reason. Please describe the problem.\n" +
|
subtitle = _t("Any other reason. Please describe the problem.\n" +
|
||||||
"This will be reported to the room moderators.");
|
"This will be reported to the room moderators.");
|
||||||
break;
|
break;
|
||||||
|
@ -327,48 +327,48 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
|
||||||
<div>
|
<div>
|
||||||
<StyledRadioButton
|
<StyledRadioButton
|
||||||
name="nature"
|
name="nature"
|
||||||
value = { NATURE.DISAGREEMENT }
|
value={Nature.Disagreement}
|
||||||
checked = { this.state.nature == NATURE.DISAGREEMENT }
|
checked={this.state.nature == Nature.Disagreement}
|
||||||
onChange={this.onNatureChosen}
|
onChange={this.onNatureChosen}
|
||||||
>
|
>
|
||||||
{ _t('Disagree') }
|
{ _t('Disagree') }
|
||||||
</StyledRadioButton>
|
</StyledRadioButton>
|
||||||
<StyledRadioButton
|
<StyledRadioButton
|
||||||
name="nature"
|
name="nature"
|
||||||
value = { NATURE.TOXIC }
|
value={Nature.Toxic}
|
||||||
checked = { this.state.nature == NATURE.TOXIC }
|
checked={this.state.nature == Nature.Toxic}
|
||||||
onChange={this.onNatureChosen}
|
onChange={this.onNatureChosen}
|
||||||
>
|
>
|
||||||
{ _t('Toxic Behaviour') }
|
{ _t('Toxic Behaviour') }
|
||||||
</StyledRadioButton>
|
</StyledRadioButton>
|
||||||
<StyledRadioButton
|
<StyledRadioButton
|
||||||
name="nature"
|
name="nature"
|
||||||
value = { NATURE.ILLEGAL }
|
value={Nature.Illegal}
|
||||||
checked = { this.state.nature == NATURE.ILLEGAL }
|
checked={this.state.nature == Nature.Illegal}
|
||||||
onChange={this.onNatureChosen}
|
onChange={this.onNatureChosen}
|
||||||
>
|
>
|
||||||
{ _t('Illegal Content') }
|
{ _t('Illegal Content') }
|
||||||
</StyledRadioButton>
|
</StyledRadioButton>
|
||||||
<StyledRadioButton
|
<StyledRadioButton
|
||||||
name="nature"
|
name="nature"
|
||||||
value = { NATURE.SPAM }
|
value={Nature.Spam}
|
||||||
checked = { this.state.nature == NATURE.SPAM }
|
checked={this.state.nature == Nature.Spam}
|
||||||
onChange={this.onNatureChosen}
|
onChange={this.onNatureChosen}
|
||||||
>
|
>
|
||||||
{ _t('Spam or propaganda') }
|
{ _t('Spam or propaganda') }
|
||||||
</StyledRadioButton>
|
</StyledRadioButton>
|
||||||
<StyledRadioButton
|
<StyledRadioButton
|
||||||
name="nature"
|
name="nature"
|
||||||
value = { NON_STANDARD_NATURE.ADMIN }
|
value={NonStandardValue.Admin}
|
||||||
checked = { this.state.nature == NON_STANDARD_NATURE.ADMIN }
|
checked={this.state.nature == NonStandardValue.Admin}
|
||||||
onChange={this.onNatureChosen}
|
onChange={this.onNatureChosen}
|
||||||
>
|
>
|
||||||
{ _t('Report the entire room') }
|
{ _t('Report the entire room') }
|
||||||
</StyledRadioButton>
|
</StyledRadioButton>
|
||||||
<StyledRadioButton
|
<StyledRadioButton
|
||||||
name="nature"
|
name="nature"
|
||||||
value = { NATURE.OTHER }
|
value={Nature.Other}
|
||||||
checked = { this.state.nature == NATURE.OTHER }
|
checked={this.state.nature == Nature.Other}
|
||||||
onChange={this.onNatureChosen}
|
onChange={this.onNatureChosen}
|
||||||
>
|
>
|
||||||
{ _t('Other') }
|
{ _t('Other') }
|
||||||
|
|
|
@ -205,13 +205,14 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
||||||
className="mx_ServerPickerDialog_otherHomeserverRadio"
|
className="mx_ServerPickerDialog_otherHomeserverRadio"
|
||||||
checked={!this.state.defaultChosen}
|
checked={!this.state.defaultChosen}
|
||||||
onChange={this.onOtherChosen}
|
onChange={this.onOtherChosen}
|
||||||
|
childrenInLabel={false}
|
||||||
>
|
>
|
||||||
<Field
|
<Field
|
||||||
type="text"
|
type="text"
|
||||||
className="mx_ServerPickerDialog_otherHomeserver"
|
className="mx_ServerPickerDialog_otherHomeserver"
|
||||||
label={_t("Other homeserver")}
|
label={_t("Other homeserver")}
|
||||||
onChange={this.onHomeserverChange}
|
onChange={this.onHomeserverChange}
|
||||||
onClick={this.onOtherChosen}
|
onFocus={this.onOtherChosen}
|
||||||
ref={this.fieldRef}
|
ref={this.fieldRef}
|
||||||
onValidate={this.onHomeserverValidate}
|
onValidate={this.onHomeserverValidate}
|
||||||
value={this.state.otherHomeserver}
|
value={this.state.otherHomeserver}
|
||||||
|
|
|
@ -81,7 +81,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
||||||
this.setState({ mjolnirEnabled: newValue });
|
this.setState({ mjolnirEnabled: newValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
_getTabs() {
|
private getTabs() {
|
||||||
const tabs = [];
|
const tabs = [];
|
||||||
|
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
|
@ -170,7 +170,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
||||||
title={_t("Settings")}
|
title={_t("Settings")}
|
||||||
>
|
>
|
||||||
<div className='mx_SettingsDialog_content'>
|
<div className='mx_SettingsDialog_content'>
|
||||||
<TabbedView tabs={this._getTabs()} initialTabId={this.props.initialTabId} />
|
<TabbedView tabs={this.getTabs()} initialTabId={this.props.initialTabId} />
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
|
|
|
@ -453,13 +453,13 @@ export default class AppTile extends React.Component {
|
||||||
title={_t('Popout widget')}
|
title={_t('Popout widget')}
|
||||||
onClick={this._onPopoutWidgetClick}
|
onClick={this._onPopoutWidgetClick}
|
||||||
/> }
|
/> }
|
||||||
{ <ContextMenuButton
|
<ContextMenuButton
|
||||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
|
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
|
||||||
label={_t("Options")}
|
label={_t("Options")}
|
||||||
isExpanded={this.state.menuDisplayed}
|
isExpanded={this.state.menuDisplayed}
|
||||||
inputRef={this._contextMenuButton}
|
inputRef={this._contextMenuButton}
|
||||||
onClick={this._onContextMenuClick}
|
onClick={this._onContextMenuClick}
|
||||||
/> }
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div> }
|
</div> }
|
||||||
{ appTileBody }
|
{ appTileBody }
|
||||||
|
|
|
@ -63,7 +63,7 @@ const EventListSummary: React.FC<IProps> = ({
|
||||||
// If we are only given few events then just pass them through
|
// If we are only given few events then just pass them through
|
||||||
if (events.length < threshold) {
|
if (events.length < threshold) {
|
||||||
return (
|
return (
|
||||||
<li className="mx_EventListSummary" data-scroll-tokens={eventIds}>
|
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={true}>
|
||||||
{ children }
|
{ children }
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@ -92,7 +92,7 @@ const EventListSummary: React.FC<IProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="mx_EventListSummary" data-scroll-tokens={eventIds}>
|
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={expanded + ""}>
|
||||||
<AccessibleButton className="mx_EventListSummary_toggle" onClick={toggleExpanded} aria-expanded={expanded}>
|
<AccessibleButton className="mx_EventListSummary_toggle" onClick={toggleExpanded} aria-expanded={expanded}>
|
||||||
{ expanded ? _t('collapse') : _t('expand') }
|
{ expanded ? _t('collapse') : _t('expand') }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
@ -101,4 +101,8 @@ const EventListSummary: React.FC<IProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
EventListSummary.defaultProps = {
|
||||||
|
startExpanded: false,
|
||||||
|
};
|
||||||
|
|
||||||
export default EventListSummary;
|
export default EventListSummary;
|
||||||
|
|
|
@ -488,8 +488,8 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={this.props.src}
|
src={this.props.src}
|
||||||
title={this.props.name}
|
|
||||||
style={style}
|
style={style}
|
||||||
|
alt={this.props.name}
|
||||||
ref={this.image}
|
ref={this.image}
|
||||||
className="mx_ImageView_image"
|
className="mx_ImageView_image"
|
||||||
draggable={true}
|
draggable={true}
|
||||||
|
|
|
@ -83,7 +83,7 @@ export default class PersistedElement extends React.Component {
|
||||||
// for this, so we bodge it by listening for document resize and
|
// for this, so we bodge it by listening for document resize and
|
||||||
// the timeline_resize action.
|
// the timeline_resize action.
|
||||||
window.addEventListener('resize', this._repositionChild);
|
window.addEventListener('resize', this._repositionChild);
|
||||||
this._dispatcherRef = dis.register(this._onAction);
|
this.dispatcherRef = dis.register(this._onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -14,14 +14,14 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { LayoutPropType } from "../../../settings/Layout";
|
import { Layout } from "../../../settings/Layout";
|
||||||
import escapeHtml from "escape-html";
|
import escapeHtml from "escape-html";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
|
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
|
||||||
|
@ -32,51 +32,54 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import Spinner from './Spinner';
|
import Spinner from './Spinner';
|
||||||
import ReplyTile from "../rooms/ReplyTile";
|
import ReplyTile from "../rooms/ReplyTile";
|
||||||
import Pill from './Pill';
|
import Pill from './Pill';
|
||||||
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// the latest event in this chain of replies
|
||||||
|
parentEv?: MatrixEvent;
|
||||||
|
// called when the ReplyThread contents has changed, including EventTiles thereof
|
||||||
|
onHeightChanged: () => void;
|
||||||
|
permalinkCreator: RoomPermalinkCreator;
|
||||||
|
// Specifies which layout to use.
|
||||||
|
layout?: Layout;
|
||||||
|
// Whether to always show a timestamp
|
||||||
|
alwaysShowTimestamps?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
// The loaded events to be rendered as linear-replies
|
||||||
|
events: MatrixEvent[];
|
||||||
|
// The latest loaded event which has not yet been shown
|
||||||
|
loadedEv: MatrixEvent;
|
||||||
|
// Whether the component is still loading more events
|
||||||
|
loading: boolean;
|
||||||
|
// Whether as error was encountered fetching a replied to event.
|
||||||
|
err: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// This component does no cycle detection, simply because the only way to make such a cycle would be to
|
// This component does no cycle detection, simply because the only way to make such a cycle would be to
|
||||||
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
|
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
|
||||||
// be low as each event being loaded (after the first) is triggered by an explicit user action.
|
// be low as each event being loaded (after the first) is triggered by an explicit user action.
|
||||||
@replaceableComponent("views.elements.ReplyThread")
|
@replaceableComponent("views.elements.ReplyThread")
|
||||||
export default class ReplyThread extends React.Component {
|
export default class ReplyThread extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
|
||||||
// the latest event in this chain of replies
|
|
||||||
parentEv: PropTypes.instanceOf(MatrixEvent),
|
|
||||||
// called when the ReplyThread contents has changed, including EventTiles thereof
|
|
||||||
onHeightChanged: PropTypes.func.isRequired,
|
|
||||||
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
|
||||||
// Specifies which layout to use.
|
|
||||||
layout: LayoutPropType,
|
|
||||||
// Whether to always show a timestamp
|
|
||||||
alwaysShowTimestamps: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
private unmounted = false;
|
||||||
|
private room: Room;
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
// The loaded events to be rendered as linear-replies
|
|
||||||
events: [],
|
events: [],
|
||||||
|
|
||||||
// The latest loaded event which has not yet been shown
|
|
||||||
loadedEv: null,
|
loadedEv: null,
|
||||||
// Whether the component is still loading more events
|
|
||||||
loading: true,
|
loading: true,
|
||||||
|
|
||||||
// Whether as error was encountered fetching a replied to event.
|
|
||||||
err: false,
|
err: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.unmounted = false;
|
|
||||||
this.room = this.context.getRoom(this.props.parentEv.getRoomId());
|
this.room = this.context.getRoom(this.props.parentEv.getRoomId());
|
||||||
|
|
||||||
this.onQuoteClick = this.onQuoteClick.bind(this);
|
|
||||||
this.canCollapse = this.canCollapse.bind(this);
|
|
||||||
this.collapse = this.collapse.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static getParentEventId(ev) {
|
public static getParentEventId(ev: MatrixEvent): string {
|
||||||
if (!ev || ev.isRedacted()) return;
|
if (!ev || ev.isRedacted()) return;
|
||||||
|
|
||||||
// XXX: For newer relations (annotations, replacements, etc.), we now
|
// XXX: For newer relations (annotations, replacements, etc.), we now
|
||||||
|
@ -92,7 +95,7 @@ export default class ReplyThread extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Part of Replies fallback support
|
// Part of Replies fallback support
|
||||||
static stripPlainReply(body) {
|
public static stripPlainReply(body: string): string {
|
||||||
// Removes lines beginning with `> ` until you reach one that doesn't.
|
// Removes lines beginning with `> ` until you reach one that doesn't.
|
||||||
const lines = body.split('\n');
|
const lines = body.split('\n');
|
||||||
while (lines.length && lines[0].startsWith('> ')) lines.shift();
|
while (lines.length && lines[0].startsWith('> ')) lines.shift();
|
||||||
|
@ -102,7 +105,7 @@ export default class ReplyThread extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Part of Replies fallback support
|
// Part of Replies fallback support
|
||||||
static stripHTMLReply(html) {
|
public static stripHTMLReply(html: string): string {
|
||||||
// Sanitize the original HTML for inclusion in <mx-reply>. We allow
|
// Sanitize the original HTML for inclusion in <mx-reply>. We allow
|
||||||
// any HTML, since the original sender could use special tags that we
|
// any HTML, since the original sender could use special tags that we
|
||||||
// don't recognize, but want to pass along to any recipients who do
|
// don't recognize, but want to pass along to any recipients who do
|
||||||
|
@ -124,7 +127,10 @@ export default class ReplyThread extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Part of Replies fallback support
|
// Part of Replies fallback support
|
||||||
static getNestedReplyText(ev, permalinkCreator) {
|
public static getNestedReplyText(
|
||||||
|
ev: MatrixEvent,
|
||||||
|
permalinkCreator: RoomPermalinkCreator,
|
||||||
|
): { body: string, html: string } {
|
||||||
if (!ev) return null;
|
if (!ev) return null;
|
||||||
|
|
||||||
let { body, formatted_body: html } = ev.getContent();
|
let { body, formatted_body: html } = ev.getContent();
|
||||||
|
@ -200,7 +206,7 @@ export default class ReplyThread extends React.Component {
|
||||||
return { body, html };
|
return { body, html };
|
||||||
}
|
}
|
||||||
|
|
||||||
static makeReplyMixIn(ev) {
|
public static makeReplyMixIn(ev: MatrixEvent) {
|
||||||
if (!ev) return {};
|
if (!ev) return {};
|
||||||
return {
|
return {
|
||||||
'm.relates_to': {
|
'm.relates_to': {
|
||||||
|
@ -211,10 +217,15 @@ export default class ReplyThread extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout, alwaysShowTimestamps) {
|
public static makeThread(
|
||||||
if (!ReplyThread.getParentEventId(parentEv)) {
|
parentEv: MatrixEvent,
|
||||||
return null;
|
onHeightChanged: () => void,
|
||||||
}
|
permalinkCreator: RoomPermalinkCreator,
|
||||||
|
ref: React.RefObject<ReplyThread>,
|
||||||
|
layout: Layout,
|
||||||
|
alwaysShowTimestamps: boolean,
|
||||||
|
): JSX.Element {
|
||||||
|
if (!ReplyThread.getParentEventId(parentEv)) return null;
|
||||||
return <ReplyThread
|
return <ReplyThread
|
||||||
parentEv={parentEv}
|
parentEv={parentEv}
|
||||||
onHeightChanged={onHeightChanged}
|
onHeightChanged={onHeightChanged}
|
||||||
|
@ -237,7 +248,7 @@ export default class ReplyThread extends React.Component {
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize() {
|
private async initialize(): Promise<void> {
|
||||||
const { parentEv } = this.props;
|
const { parentEv } = this.props;
|
||||||
// at time of making this component we checked that props.parentEv has a parentEventId
|
// at time of making this component we checked that props.parentEv has a parentEventId
|
||||||
const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv));
|
const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv));
|
||||||
|
@ -256,7 +267,7 @@ export default class ReplyThread extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextEvent(ev) {
|
private async getNextEvent(ev: MatrixEvent): Promise<MatrixEvent> {
|
||||||
try {
|
try {
|
||||||
const inReplyToEventId = ReplyThread.getParentEventId(ev);
|
const inReplyToEventId = ReplyThread.getParentEventId(ev);
|
||||||
return await this.getEvent(inReplyToEventId);
|
return await this.getEvent(inReplyToEventId);
|
||||||
|
@ -265,7 +276,7 @@ export default class ReplyThread extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEvent(eventId) {
|
private async getEvent(eventId: string): Promise<MatrixEvent> {
|
||||||
if (!eventId) return null;
|
if (!eventId) return null;
|
||||||
const event = this.room.findEventById(eventId);
|
const event = this.room.findEventById(eventId);
|
||||||
if (event) return event;
|
if (event) return event;
|
||||||
|
@ -282,15 +293,15 @@ export default class ReplyThread extends React.Component {
|
||||||
return this.room.findEventById(eventId);
|
return this.room.findEventById(eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
canCollapse() {
|
public canCollapse = (): boolean => {
|
||||||
return this.state.events.length > 1;
|
return this.state.events.length > 1;
|
||||||
}
|
};
|
||||||
|
|
||||||
collapse() {
|
public collapse = (): void => {
|
||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
};
|
||||||
|
|
||||||
async onQuoteClick() {
|
private onQuoteClick = async (): Promise<void> => {
|
||||||
const events = [this.state.loadedEv, ...this.state.events];
|
const events = [this.state.loadedEv, ...this.state.events];
|
||||||
|
|
||||||
let loadedEv = null;
|
let loadedEv = null;
|
||||||
|
@ -304,9 +315,9 @@ export default class ReplyThread extends React.Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
dis.fire(Action.FocusSendMessageComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
}
|
};
|
||||||
|
|
||||||
getReplyThreadColorClass(ev) {
|
private getReplyThreadColorClass(ev: MatrixEvent): string {
|
||||||
return getUserNameColorClass(ev.getSender()).replace("Username", "ReplyThread");
|
return getUserNameColorClass(ev.getSender()).replace("Username", "ReplyThread");
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
|
||||||
SpellCheckLanguagesDropdownIState> {
|
SpellCheckLanguagesDropdownIState> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this._onSearchChange = this._onSearchChange.bind(this);
|
this.onSearchChange = this.onSearchChange.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
|
@ -76,10 +76,8 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSearchChange(search) {
|
private onSearchChange(searchQuery: string) {
|
||||||
this.setState({
|
this.setState({ searchQuery });
|
||||||
searchQuery: search,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -117,7 +115,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
|
||||||
id="mx_LanguageDropdown"
|
id="mx_LanguageDropdown"
|
||||||
className={this.props.className}
|
className={this.props.className}
|
||||||
onOptionChange={this.props.onOptionChange}
|
onOptionChange={this.props.onOptionChange}
|
||||||
onSearchChange={this._onSearchChange}
|
onSearchChange={this.onSearchChange}
|
||||||
searchEnabled={true}
|
searchEnabled={true}
|
||||||
value={value}
|
value={value}
|
||||||
label={_t("Language Dropdown")}>
|
label={_t("Language Dropdown")}>
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
|
|
||||||
const Spinner = ({ w = 32, h = 32, message }) => (
|
|
||||||
<div className="mx_Spinner">
|
|
||||||
{ message && <React.Fragment><div className="mx_Spinner_Msg">{ message }</div> </React.Fragment> }
|
|
||||||
<div
|
|
||||||
className="mx_Spinner_icon"
|
|
||||||
style={{ width: w, height: h }}
|
|
||||||
aria-label={_t("Loading...")}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
Spinner.propTypes = {
|
|
||||||
w: PropTypes.number,
|
|
||||||
h: PropTypes.number,
|
|
||||||
message: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Spinner;
|
|
45
src/components/views/elements/Spinner.tsx
Normal file
45
src/components/views/elements/Spinner.tsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
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 { _t } from "../../../languageHandler";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
w?: number;
|
||||||
|
h?: number;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Spinner extends React.PureComponent<IProps> {
|
||||||
|
public static defaultProps: Partial<IProps> = {
|
||||||
|
w: 32,
|
||||||
|
h: 32,
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { w, h, message } = this.props;
|
||||||
|
return (
|
||||||
|
<div className="mx_Spinner">
|
||||||
|
{ message && <React.Fragment><div className="mx_Spinner_Msg">{ message }</div> </React.Fragment> }
|
||||||
|
<div
|
||||||
|
className="mx_Spinner_icon"
|
||||||
|
style={{ width: w, height: h }}
|
||||||
|
aria-label={_t("Loading...")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,10 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
outlined?: boolean;
|
outlined?: boolean;
|
||||||
|
// If true (default), the children will be contained within a <label> element
|
||||||
|
// If false, they'll be in a div. Putting interactive components that have labels
|
||||||
|
// themselves in labels can cause strange bugs like https://github.com/vector-im/element-web/issues/18031
|
||||||
|
childrenInLabel?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -29,10 +33,11 @@ interface IState {
|
||||||
export default class StyledRadioButton extends React.PureComponent<IProps, IState> {
|
export default class StyledRadioButton extends React.PureComponent<IProps, IState> {
|
||||||
public static readonly defaultProps = {
|
public static readonly defaultProps = {
|
||||||
className: '',
|
className: '',
|
||||||
|
childrenInLabel: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { children, className, disabled, outlined, ...otherProps } = this.props;
|
const { children, className, disabled, outlined, childrenInLabel, ...otherProps } = this.props;
|
||||||
const _className = classnames(
|
const _className = classnames(
|
||||||
'mx_RadioButton',
|
'mx_RadioButton',
|
||||||
className,
|
className,
|
||||||
|
@ -42,12 +47,27 @@ export default class StyledRadioButton extends React.PureComponent<IProps, IStat
|
||||||
"mx_RadioButton_checked": this.props.checked,
|
"mx_RadioButton_checked": this.props.checked,
|
||||||
"mx_RadioButton_outlined": outlined,
|
"mx_RadioButton_outlined": outlined,
|
||||||
});
|
});
|
||||||
return <label className={_className}>
|
|
||||||
|
const radioButton = <React.Fragment>
|
||||||
<input type='radio' disabled={disabled} {...otherProps} />
|
<input type='radio' disabled={disabled} {...otherProps} />
|
||||||
{ /* Used to render the radio button circle */ }
|
{ /* Used to render the radio button circle */ }
|
||||||
<div><div /></div>
|
<div><div /></div>
|
||||||
|
</React.Fragment>;
|
||||||
|
|
||||||
|
if (childrenInLabel) {
|
||||||
|
return <label className={_className}>
|
||||||
|
{ radioButton }
|
||||||
<div className="mx_RadioButton_content">{ children }</div>
|
<div className="mx_RadioButton_content">{ children }</div>
|
||||||
<div className="mx_RadioButton_spacer" />
|
<div className="mx_RadioButton_spacer" />
|
||||||
</label>;
|
</label>;
|
||||||
|
} else {
|
||||||
|
return <div className={_className}>
|
||||||
|
<label className="mx_RadioButton_innerLabel">
|
||||||
|
{ radioButton }
|
||||||
|
</label>
|
||||||
|
<div className="mx_RadioButton_content">{ children }</div>
|
||||||
|
<div className="mx_RadioButton_spacer" />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
91
src/components/views/elements/TagComposer.tsx
Normal file
91
src/components/views/elements/TagComposer.tsx
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
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, { ChangeEvent, FormEvent } from "react";
|
||||||
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import Field from "./Field";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import AccessibleButton from "./AccessibleButton";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
tags: string[];
|
||||||
|
onAdd: (tag: string) => void;
|
||||||
|
onRemove: (tag: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
newTag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple, controlled, composer for entering string tags. Contains a simple
|
||||||
|
* input, add button, and per-tag remove button.
|
||||||
|
*/
|
||||||
|
@replaceableComponent("views.elements.TagComposer")
|
||||||
|
export default class TagComposer extends React.PureComponent<IProps, IState> {
|
||||||
|
public constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
newTag: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private onInputChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({ newTag: ev.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onAdd = (ev: FormEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
if (!this.state.newTag) return;
|
||||||
|
|
||||||
|
this.props.onAdd(this.state.newTag);
|
||||||
|
this.setState({ newTag: "" });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRemove(tag: string) {
|
||||||
|
// We probably don't need to proxy this, but for
|
||||||
|
// sanity of `this` we'll do so anyways.
|
||||||
|
this.props.onRemove(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return <div className='mx_TagComposer'>
|
||||||
|
<form className='mx_TagComposer_input' onSubmit={this.onAdd}>
|
||||||
|
<Field
|
||||||
|
value={this.state.newTag}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
label={this.props.label || _t("Keyword")}
|
||||||
|
placeholder={this.props.placeholder || _t("New keyword")}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<AccessibleButton onClick={this.onAdd} kind='primary' disabled={this.props.disabled}>
|
||||||
|
{ _t("Add") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</form>
|
||||||
|
<div className='mx_TagComposer_tags'>
|
||||||
|
{ this.props.tags.map((t, i) => (<div className='mx_TagComposer_tag' key={i}>
|
||||||
|
<span>{ t }</span>
|
||||||
|
<AccessibleButton onClick={this.onRemove.bind(this, t)} disabled={this.props.disabled} />
|
||||||
|
</div>)) }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,7 +56,7 @@ export default class TextWithTooltip extends React.Component {
|
||||||
{...tooltipProps}
|
{...tooltipProps}
|
||||||
label={tooltip}
|
label={tooltip}
|
||||||
tooltipClassName={tooltipClass}
|
tooltipClassName={tooltipClass}
|
||||||
className={"mx_TextWithTooltip_tooltip"}
|
className="mx_TextWithTooltip_tooltip"
|
||||||
/> }
|
/> }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
@ -304,13 +304,6 @@ export default class MImageBody extends React.Component<IProps, IState> {
|
||||||
this.downloadImage();
|
this.downloadImage();
|
||||||
this.setState({ showImage: true });
|
this.setState({ showImage: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
this._afterComponentDidMount();
|
|
||||||
}
|
|
||||||
|
|
||||||
// To be overridden by subclasses (e.g. MStickerBody) for further
|
|
||||||
// initialisation after componentDidMount
|
|
||||||
_afterComponentDidMount() {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -440,9 +433,9 @@ export default class MImageBody extends React.Component<IProps, IState> {
|
||||||
protected getPlaceholder(width: number, height: number): JSX.Element {
|
protected getPlaceholder(width: number, height: number): JSX.Element {
|
||||||
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
|
const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
|
||||||
if (blurhash) return <Blurhash hash={blurhash} width={width} height={height} />;
|
if (blurhash) return <Blurhash hash={blurhash} width={width} height={height} />;
|
||||||
return <div className="mx_MImageBody_thumbnail_spinner">
|
return (
|
||||||
<InlineSpinner w={32} h={32} />
|
<InlineSpinner w={32} h={32} />
|
||||||
</div>;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overidden by MStickerBody
|
// Overidden by MStickerBody
|
||||||
|
|
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
|
import RoomContext from "../../../contexts/RoomContext";
|
||||||
import * as TextForEvent from "../../../TextForEvent";
|
import * as TextForEvent from "../../../TextForEvent";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
@ -26,11 +27,11 @@ interface IProps {
|
||||||
|
|
||||||
@replaceableComponent("views.messages.TextualEvent")
|
@replaceableComponent("views.messages.TextualEvent")
|
||||||
export default class TextualEvent extends React.Component<IProps> {
|
export default class TextualEvent extends React.Component<IProps> {
|
||||||
render() {
|
static contextType = RoomContext;
|
||||||
const text = TextForEvent.textForEvent(this.props.mxEvent, true);
|
|
||||||
if (!text || (text as string).length === 0) return null;
|
public render() {
|
||||||
return (
|
const text = TextForEvent.textForEvent(this.props.mxEvent, true, this.context?.showHiddenEventsInTimeline);
|
||||||
<div className="mx_TextualEvent">{ text }</div>
|
if (!text) return null;
|
||||||
);
|
return <div className="mx_TextualEvent">{ text }</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,19 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { forwardRef } from "react";
|
import React, { forwardRef } from "react";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src";
|
||||||
|
|
||||||
export default forwardRef(({ mxEvent }, ref) => {
|
interface IProps {
|
||||||
|
mxEvent: MatrixEvent;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default forwardRef(({ mxEvent, children }: IProps, ref: React.RefObject<HTMLSpanElement>) => {
|
||||||
const text = mxEvent.getContent().body;
|
const text = mxEvent.getContent().body;
|
||||||
return (
|
return (
|
||||||
<span className="mx_UnknownBody" ref={ref}>
|
<span className="mx_UnknownBody" ref={ref}>
|
||||||
{ text }
|
{ text }
|
||||||
|
{ children }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
});
|
});
|
|
@ -385,7 +385,7 @@ const UserOptionsSection: React.FC<{
|
||||||
}
|
}
|
||||||
|
|
||||||
insertPillButton = (
|
insertPillButton = (
|
||||||
<AccessibleButton onClick={onInsertPillButton} className={"mx_UserInfo_field"}>
|
<AccessibleButton onClick={onInsertPillButton} className="mx_UserInfo_field">
|
||||||
{ _t('Mention') }
|
{ _t('Mention') }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
|
|
@ -106,7 +106,7 @@ export default class RelatedGroupSettings extends React.Component {
|
||||||
<EditableItemList
|
<EditableItemList
|
||||||
id="relatedGroups"
|
id="relatedGroups"
|
||||||
items={this.state.newGroupsList}
|
items={this.state.newGroupsList}
|
||||||
className={"mx_RelatedGroupSettings"}
|
className="mx_RelatedGroupSettings"
|
||||||
newItem={this.state.newGroupId}
|
newItem={this.state.newGroupId}
|
||||||
canRemove={this.props.canSetRelatedGroups}
|
canRemove={this.props.canSetRelatedGroups}
|
||||||
canEdit={this.props.canSetRelatedGroups}
|
canEdit={this.props.canSetRelatedGroups}
|
||||||
|
|
|
@ -170,8 +170,6 @@ export function getHandlerTile(ev) {
|
||||||
return eventTileTypes[type];
|
return eventTileTypes[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_READ_AVATARS = 5;
|
|
||||||
|
|
||||||
// Our component structure for EventTiles on the timeline is:
|
// Our component structure for EventTiles on the timeline is:
|
||||||
//
|
//
|
||||||
// .-EventTile------------------------------------------------.
|
// .-EventTile------------------------------------------------.
|
||||||
|
@ -297,6 +295,9 @@ interface IProps {
|
||||||
|
|
||||||
// whether or not to always show timestamps
|
// whether or not to always show timestamps
|
||||||
alwaysShowTimestamps?: boolean;
|
alwaysShowTimestamps?: boolean;
|
||||||
|
|
||||||
|
// whether or not to display the sender
|
||||||
|
hideSender?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -320,7 +321,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
private suppressReadReceiptAnimation: boolean;
|
private suppressReadReceiptAnimation: boolean;
|
||||||
private isListeningForReceipts: boolean;
|
private isListeningForReceipts: boolean;
|
||||||
private tile = React.createRef();
|
private tile = React.createRef();
|
||||||
private replyThread = React.createRef();
|
private replyThread = React.createRef<ReplyThread>();
|
||||||
|
|
||||||
public readonly ref = createRef<HTMLElement>();
|
public readonly ref = createRef<HTMLElement>();
|
||||||
|
|
||||||
|
@ -430,7 +431,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Move into constructor
|
// TODO: [REACT-WARNING] Move into constructor
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
UNSAFE_componentWillMount() {
|
UNSAFE_componentWillMount() {
|
||||||
this.verifyEvent(this.props.mxEvent);
|
this.verifyEvent(this.props.mxEvent);
|
||||||
}
|
}
|
||||||
|
@ -452,7 +453,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
// re-check the sender verification as outgoing events progress through
|
// re-check the sender verification as outgoing events progress through
|
||||||
// the send process.
|
// the send process.
|
||||||
|
@ -656,6 +657,10 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
return <SentReceipt messageState={this.props.mxEvent.getAssociatedStatus()} />;
|
return <SentReceipt messageState={this.props.mxEvent.getAssociatedStatus()} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_READ_AVATARS = this.props.layout == Layout.Bubble
|
||||||
|
? 2
|
||||||
|
: 5;
|
||||||
|
|
||||||
// return early if there are no read receipts
|
// return early if there are no read receipts
|
||||||
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
|
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
|
||||||
// We currently must include `mx_EventTile_readAvatars` in the DOM
|
// We currently must include `mx_EventTile_readAvatars` in the DOM
|
||||||
|
@ -951,7 +956,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsSenderProfile) {
|
if (needsSenderProfile && this.props.hideSender !== true) {
|
||||||
if (!this.props.tileShape) {
|
if (!this.props.tileShape) {
|
||||||
sender = <SenderProfile onClick={this.onSenderProfileClick}
|
sender = <SenderProfile onClick={this.onSenderProfileClick}
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
|
@ -971,8 +976,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
onFocusChange={this.onActionBarFocusChange}
|
onFocusChange={this.onActionBarFocusChange}
|
||||||
/> : undefined;
|
/> : undefined;
|
||||||
|
|
||||||
const showTimestamp = this.props.mxEvent.getTs() &&
|
const showTimestamp = this.props.mxEvent.getTs()
|
||||||
(this.props.alwaysShowTimestamps || this.props.last || this.state.hover || this.state.actionBarFocused);
|
&& (this.props.alwaysShowTimestamps
|
||||||
|
|| this.props.last
|
||||||
|
|| this.state.hover
|
||||||
|
|| this.state.actionBarFocused);
|
||||||
|
|
||||||
const timestamp = showTimestamp ?
|
const timestamp = showTimestamp ?
|
||||||
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
||||||
|
|
||||||
|
@ -1112,6 +1121,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
this.props.alwaysShowTimestamps || this.state.hover,
|
this.props.alwaysShowTimestamps || this.state.hover,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
|
||||||
|
|
||||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||||
return (
|
return (
|
||||||
React.createElement(this.props.as || "li", {
|
React.createElement(this.props.as || "li", {
|
||||||
|
@ -1121,6 +1132,9 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
"aria-live": ariaLive,
|
"aria-live": ariaLive,
|
||||||
"aria-atomic": "true",
|
"aria-atomic": "true",
|
||||||
"data-scroll-tokens": scrollToken,
|
"data-scroll-tokens": scrollToken,
|
||||||
|
"data-layout": this.props.layout,
|
||||||
|
"data-self": isOwnEvent,
|
||||||
|
"data-has-reply": !!thread,
|
||||||
"onMouseEnter": () => this.setState({ hover: true }),
|
"onMouseEnter": () => this.setState({ hover: true }),
|
||||||
"onMouseLeave": () => this.setState({ hover: false }),
|
"onMouseLeave": () => this.setState({ hover: false }),
|
||||||
}, <>
|
}, <>
|
||||||
|
@ -1142,9 +1156,9 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
/>
|
/>
|
||||||
{ keyRequestInfo }
|
{ keyRequestInfo }
|
||||||
{ reactionsRow }
|
|
||||||
{ actionBar }
|
{ actionBar }
|
||||||
</div>
|
</div>
|
||||||
|
{ reactionsRow }
|
||||||
{ msgOption }
|
{ msgOption }
|
||||||
{ avatar }
|
{ avatar }
|
||||||
</>)
|
</>)
|
||||||
|
@ -1160,7 +1174,7 @@ function isMessageEvent(ev) {
|
||||||
return (messageTypes.includes(ev.getType()));
|
return (messageTypes.includes(ev.getType()));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function haveTileForEvent(e) {
|
export function haveTileForEvent(e: MatrixEvent, showHiddenEvents?: boolean) {
|
||||||
// Only messages have a tile (black-rectangle) if redacted
|
// Only messages have a tile (black-rectangle) if redacted
|
||||||
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
||||||
|
|
||||||
|
@ -1170,7 +1184,7 @@ export function haveTileForEvent(e) {
|
||||||
const handler = getHandlerTile(e);
|
const handler = getHandlerTile(e);
|
||||||
if (handler === undefined) return false;
|
if (handler === undefined) return false;
|
||||||
if (handler === 'messages.TextualEvent') {
|
if (handler === 'messages.TextualEvent') {
|
||||||
return hasText(e);
|
return hasText(e, showHiddenEvents);
|
||||||
} else if (handler === 'messages.RoomCreate') {
|
} else if (handler === 'messages.RoomCreate') {
|
||||||
return Boolean(e.getContent()['predecessor']);
|
return Boolean(e.getContent()['predecessor']);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -93,7 +93,7 @@ export default class MemberList extends React.Component<IProps, IState> {
|
||||||
this.showPresence = enablePresenceByHsUrl?.[hsUrl] ?? true;
|
this.showPresence = enablePresenceByHsUrl?.[hsUrl] ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
UNSAFE_componentWillMount() {
|
UNSAFE_componentWillMount() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
this.mounted = true;
|
this.mounted = true;
|
||||||
|
|
|
@ -18,10 +18,11 @@ import React from 'react';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import ReplyTile from './ReplyTile';
|
import ReplyTile from './ReplyTile';
|
||||||
|
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
|
import { EventSubscription } from 'fbemitter';
|
||||||
|
|
||||||
function cancelQuoting() {
|
function cancelQuoting() {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
@ -30,41 +31,46 @@ function cancelQuoting() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
permalinkCreator: RoomPermalinkCreator;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
event: MatrixEvent;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.ReplyPreview")
|
@replaceableComponent("views.rooms.ReplyPreview")
|
||||||
export default class ReplyPreview extends React.Component {
|
export default class ReplyPreview extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private unmounted = false;
|
||||||
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
private readonly roomStoreToken: EventSubscription;
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.unmounted = false;
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
event: RoomViewStore.getQuotingEvent(),
|
event: RoomViewStore.getQuotingEvent(),
|
||||||
};
|
};
|
||||||
|
|
||||||
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
|
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
|
|
||||||
// Remove RoomStore listener
|
// Remove RoomStore listener
|
||||||
if (this._roomStoreToken) {
|
if (this.roomStoreToken) {
|
||||||
this._roomStoreToken.remove();
|
this.roomStoreToken.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRoomViewStoreUpdate() {
|
private onRoomViewStoreUpdate = (): void => {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
|
|
||||||
const event = RoomViewStore.getQuotingEvent();
|
const event = RoomViewStore.getQuotingEvent();
|
||||||
if (this.state.event !== event) {
|
if (this.state.event !== event) {
|
||||||
this.setState({ event });
|
this.setState({ event });
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.state.event) return null;
|
if (!this.state.event) return null;
|
|
@ -419,7 +419,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
>
|
>
|
||||||
<IconizedContextMenuOptionList first>
|
<IconizedContextMenuOptionList first>
|
||||||
<IconizedContextMenuRadio
|
<IconizedContextMenuRadio
|
||||||
label={_t("Use default")}
|
label={_t("Global")}
|
||||||
active={state === ALL_MESSAGES}
|
active={state === ALL_MESSAGES}
|
||||||
iconClassName="mx_RoomTile_iconBell"
|
iconClassName="mx_RoomTile_iconBell"
|
||||||
onClick={this.onClickAllNotifs}
|
onClick={this.onClickAllNotifs}
|
||||||
|
@ -530,7 +530,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
) : null }
|
) : null }
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
onClick={this.onCopyRoomClick}
|
onClick={this.onCopyRoomClick}
|
||||||
label={_t("Copy Link")}
|
label={_t("Copy Room Link")}
|
||||||
iconClassName="mx_RoomTile_iconCopyLink"
|
iconClassName="mx_RoomTile_iconCopyLink"
|
||||||
/>
|
/>
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
|
|
|
@ -15,14 +15,15 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
|
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
|
||||||
import EventTile, { haveTileForEvent } from "./EventTile";
|
import RoomContext from "../../../contexts/RoomContext";
|
||||||
import DateSeparator from '../messages/DateSeparator';
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { UIFeature } from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import DateSeparator from "../messages/DateSeparator";
|
||||||
|
import EventTile, { haveTileForEvent } from "./EventTile";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// a matrix-js-sdk SearchResult containing the details of this result
|
// a matrix-js-sdk SearchResult containing the details of this result
|
||||||
|
@ -37,6 +38,8 @@ interface IProps {
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.SearchResultTile")
|
@replaceableComponent("views.rooms.SearchResultTile")
|
||||||
export default class SearchResultTile extends React.Component<IProps> {
|
export default class SearchResultTile extends React.Component<IProps> {
|
||||||
|
static contextType = RoomContext;
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const result = this.props.searchResult;
|
const result = this.props.searchResult;
|
||||||
const mxEv = result.context.getEvent();
|
const mxEv = result.context.getEvent();
|
||||||
|
@ -44,7 +47,10 @@ export default class SearchResultTile extends React.Component<IProps> {
|
||||||
|
|
||||||
const ts1 = mxEv.getTs();
|
const ts1 = mxEv.getTs();
|
||||||
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
|
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
|
||||||
|
const layout = SettingsStore.getValue("layout");
|
||||||
|
const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
|
||||||
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
|
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
|
||||||
|
const enableFlair = SettingsStore.getValue(UIFeature.Flair);
|
||||||
|
|
||||||
const timeline = result.context.getTimeline();
|
const timeline = result.context.getTimeline();
|
||||||
for (let j = 0; j < timeline.length; j++) {
|
for (let j = 0; j < timeline.length; j++) {
|
||||||
|
@ -54,26 +60,25 @@ export default class SearchResultTile extends React.Component<IProps> {
|
||||||
if (!contextual) {
|
if (!contextual) {
|
||||||
highlights = this.props.searchHighlights;
|
highlights = this.props.searchHighlights;
|
||||||
}
|
}
|
||||||
if (haveTileForEvent(ev)) {
|
if (haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline)) {
|
||||||
ret.push((
|
ret.push(
|
||||||
<EventTile
|
<EventTile
|
||||||
key={`${eventId}+${j}`}
|
key={`${eventId}+${j}`}
|
||||||
mxEvent={ev}
|
mxEvent={ev}
|
||||||
|
layout={layout}
|
||||||
contextual={contextual}
|
contextual={contextual}
|
||||||
highlights={highlights}
|
highlights={highlights}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
highlightLink={this.props.resultLink}
|
highlightLink={this.props.resultLink}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
isTwelveHour={isTwelveHour}
|
||||||
alwaysShowTimestamps={alwaysShowTimestamps}
|
alwaysShowTimestamps={alwaysShowTimestamps}
|
||||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
enableFlair={enableFlair}
|
||||||
/>
|
/>,
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<li data-scroll-tokens={eventId}>
|
return <li data-scroll-tokens={eventId}>{ ret }</li>;
|
||||||
{ ret }
|
|
||||||
</li>);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -441,7 +441,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Move this to constructor
|
// TODO: [REACT-WARNING] Move this to constructor
|
||||||
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
UNSAFE_componentWillMount() { // eslint-disable-line
|
||||||
const partCreator = new CommandPartCreator(this.props.room, this.context);
|
const partCreator = new CommandPartCreator(this.props.room, this.context);
|
||||||
const parts = this.restoreStoredEditorState(partCreator) || [];
|
const parts = this.restoreStoredEditorState(partCreator) || [];
|
||||||
this.model = new EditorModel(parts, partCreator);
|
this.model = new EditorModel(parts, partCreator);
|
||||||
|
|
|
@ -64,8 +64,8 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(_, prevState) {
|
componentDidUpdate(_, prevState) {
|
||||||
const wasVisible = this._isVisible(prevState);
|
const wasVisible = WhoIsTypingTile.isVisible(prevState);
|
||||||
const isVisible = this._isVisible(this.state);
|
const isVisible = WhoIsTypingTile.isVisible(this.state);
|
||||||
if (this.props.onShown && !wasVisible && isVisible) {
|
if (this.props.onShown && !wasVisible && isVisible) {
|
||||||
this.props.onShown();
|
this.props.onShown();
|
||||||
} else if (this.props.onHidden && wasVisible && !isVisible) {
|
} else if (this.props.onHidden && wasVisible && !isVisible) {
|
||||||
|
@ -83,12 +83,12 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
|
||||||
Object.values(this.state.delayedStopTypingTimers).forEach((t) => (t as Timer).abort());
|
Object.values(this.state.delayedStopTypingTimers).forEach((t) => (t as Timer).abort());
|
||||||
}
|
}
|
||||||
|
|
||||||
private _isVisible(state: IState): boolean {
|
private static isVisible(state: IState): boolean {
|
||||||
return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0;
|
return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isVisible = (): boolean => {
|
public isVisible = (): boolean => {
|
||||||
return this._isVisible(this.state);
|
return WhoIsTypingTile.isVisible(this.state);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRoomTimeline = (event: MatrixEvent, room: Room): void => {
|
private onRoomTimeline = (event: MatrixEvent, room: Room): void => {
|
||||||
|
|
|
@ -1,917 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2016 OpenMarket Ltd
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
|
||||||
import SettingsStore from '../../../settings/SettingsStore';
|
|
||||||
import Modal from '../../../Modal';
|
|
||||||
import {
|
|
||||||
NotificationUtils,
|
|
||||||
VectorPushRulesDefinitions,
|
|
||||||
PushRuleVectorState,
|
|
||||||
ContentRules,
|
|
||||||
} from '../../../notifications';
|
|
||||||
import SdkConfig from "../../../SdkConfig";
|
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
|
||||||
import { UIFeature } from "../../../settings/UIFeature";
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|
||||||
|
|
||||||
// TODO: this "view" component still has far too much application logic in it,
|
|
||||||
// which should be factored out to other files.
|
|
||||||
|
|
||||||
// TODO: this component also does a lot of direct poking into this.state, which
|
|
||||||
// is VERY NAUGHTY.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rules that Vector used to set in order to override the actions of default rules.
|
|
||||||
* These are used to port peoples existing overrides to match the current API.
|
|
||||||
* These can be removed and forgotten once everyone has moved to the new client.
|
|
||||||
*/
|
|
||||||
const LEGACY_RULES = {
|
|
||||||
"im.vector.rule.contains_display_name": ".m.rule.contains_display_name",
|
|
||||||
"im.vector.rule.room_one_to_one": ".m.rule.room_one_to_one",
|
|
||||||
"im.vector.rule.room_message": ".m.rule.message",
|
|
||||||
"im.vector.rule.invite_for_me": ".m.rule.invite_for_me",
|
|
||||||
"im.vector.rule.call": ".m.rule.call",
|
|
||||||
"im.vector.rule.notices": ".m.rule.suppress_notices",
|
|
||||||
};
|
|
||||||
|
|
||||||
function portLegacyActions(actions) {
|
|
||||||
const decoded = NotificationUtils.decodeActions(actions);
|
|
||||||
if (decoded !== null) {
|
|
||||||
return NotificationUtils.encodeActions(decoded);
|
|
||||||
} else {
|
|
||||||
// We don't recognise one of the actions here, so we don't try to
|
|
||||||
// canonicalise them.
|
|
||||||
return actions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@replaceableComponent("views.settings.Notifications")
|
|
||||||
export default class Notifications extends React.Component {
|
|
||||||
static phases = {
|
|
||||||
LOADING: "LOADING", // The component is loading or sending data to the hs
|
|
||||||
DISPLAY: "DISPLAY", // The component is ready and display data
|
|
||||||
ERROR: "ERROR", // There was an error
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
phase: Notifications.phases.LOADING,
|
|
||||||
masterPushRule: undefined, // The master rule ('.m.rule.master')
|
|
||||||
vectorPushRules: [], // HS default push rules displayed in Vector UI
|
|
||||||
vectorContentRules: { // Keyword push rules displayed in Vector UI
|
|
||||||
vectorState: PushRuleVectorState.ON,
|
|
||||||
rules: [],
|
|
||||||
},
|
|
||||||
externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI
|
|
||||||
externalContentRules: [], // Keyword push rules that have been defined outside Vector UI
|
|
||||||
threepids: [], // used for email notifications
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this._refreshFromServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
onEnableNotificationsChange = (checked) => {
|
|
||||||
const self = this;
|
|
||||||
this.setState({
|
|
||||||
phase: Notifications.phases.LOADING,
|
|
||||||
});
|
|
||||||
|
|
||||||
MatrixClientPeg.get().setPushRuleEnabled(
|
|
||||||
'global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked,
|
|
||||||
).then(function() {
|
|
||||||
self._refreshFromServer();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onEnableDesktopNotificationsChange = (checked) => {
|
|
||||||
SettingsStore.setValue(
|
|
||||||
"notificationsEnabled", null,
|
|
||||||
SettingLevel.DEVICE,
|
|
||||||
checked,
|
|
||||||
).finally(() => {
|
|
||||||
this.forceUpdate();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onEnableDesktopNotificationBodyChange = (checked) => {
|
|
||||||
SettingsStore.setValue(
|
|
||||||
"notificationBodyEnabled", null,
|
|
||||||
SettingLevel.DEVICE,
|
|
||||||
checked,
|
|
||||||
).finally(() => {
|
|
||||||
this.forceUpdate();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onEnableAudioNotificationsChange = (checked) => {
|
|
||||||
SettingsStore.setValue(
|
|
||||||
"audioNotificationsEnabled", null,
|
|
||||||
SettingLevel.DEVICE,
|
|
||||||
checked,
|
|
||||||
).finally(() => {
|
|
||||||
this.forceUpdate();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Returns the email pusher (pusher of type 'email') for a given
|
|
||||||
* email address. Email pushers all have the same app ID, so since
|
|
||||||
* pushers are unique over (app ID, pushkey), there will be at most
|
|
||||||
* one such pusher.
|
|
||||||
*/
|
|
||||||
getEmailPusher(pushers, address) {
|
|
||||||
if (pushers === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < pushers.length; ++i) {
|
|
||||||
if (pushers[i].kind === 'email' && pushers[i].pushkey === address) {
|
|
||||||
return pushers[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
onEnableEmailNotificationsChange = (address, checked) => {
|
|
||||||
let emailPusherPromise;
|
|
||||||
if (checked) {
|
|
||||||
const data = {};
|
|
||||||
data['brand'] = SdkConfig.get().brand;
|
|
||||||
emailPusherPromise = MatrixClientPeg.get().setPusher({
|
|
||||||
kind: 'email',
|
|
||||||
app_id: 'm.email',
|
|
||||||
pushkey: address,
|
|
||||||
app_display_name: 'Email Notifications',
|
|
||||||
device_display_name: address,
|
|
||||||
lang: navigator.language,
|
|
||||||
data: data,
|
|
||||||
append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const emailPusher = this.getEmailPusher(this.state.pushers, address);
|
|
||||||
emailPusher.kind = null;
|
|
||||||
emailPusherPromise = MatrixClientPeg.get().setPusher(emailPusher);
|
|
||||||
}
|
|
||||||
emailPusherPromise.then(() => {
|
|
||||||
this._refreshFromServer();
|
|
||||||
}, (error) => {
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
Modal.createTrackedDialog('Error saving email notification preferences', '', ErrorDialog, {
|
|
||||||
title: _t('Error saving email notification preferences'),
|
|
||||||
description: _t('An error occurred whilst saving your email notification preferences.'),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onNotifStateButtonClicked = (event) => {
|
|
||||||
// FIXME: use .bind() rather than className metadata here surely
|
|
||||||
const vectorRuleId = event.target.className.split("-")[0];
|
|
||||||
const newPushRuleVectorState = event.target.className.split("-")[1];
|
|
||||||
|
|
||||||
if ("_keywords" === vectorRuleId) {
|
|
||||||
this._setKeywordsPushRuleVectorState(newPushRuleVectorState);
|
|
||||||
} else {
|
|
||||||
const rule = this.getRule(vectorRuleId);
|
|
||||||
if (rule) {
|
|
||||||
this._setPushRuleVectorState(rule, newPushRuleVectorState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onKeywordsClicked = (event) => {
|
|
||||||
// Compute the keywords list to display
|
|
||||||
let keywords = [];
|
|
||||||
for (const i in this.state.vectorContentRules.rules) {
|
|
||||||
const rule = this.state.vectorContentRules.rules[i];
|
|
||||||
keywords.push(rule.pattern);
|
|
||||||
}
|
|
||||||
if (keywords.length) {
|
|
||||||
// As keeping the order of per-word push rules hs side is a bit tricky to code,
|
|
||||||
// display the keywords in alphabetical order to the user
|
|
||||||
keywords.sort();
|
|
||||||
|
|
||||||
keywords = keywords.join(", ");
|
|
||||||
} else {
|
|
||||||
keywords = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
|
||||||
Modal.createTrackedDialog('Keywords Dialog', '', TextInputDialog, {
|
|
||||||
title: _t('Keywords'),
|
|
||||||
description: _t('Enter keywords separated by a comma:'),
|
|
||||||
button: _t('OK'),
|
|
||||||
value: keywords,
|
|
||||||
onFinished: (shouldLeave, newValue) => {
|
|
||||||
if (shouldLeave && newValue !== keywords) {
|
|
||||||
let newKeywords = newValue.split(',');
|
|
||||||
for (const i in newKeywords) {
|
|
||||||
newKeywords[i] = newKeywords[i].trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove duplicates and empty
|
|
||||||
newKeywords = newKeywords.reduce(function(array, keyword) {
|
|
||||||
if (keyword !== "" && array.indexOf(keyword) < 0) {
|
|
||||||
array.push(keyword);
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
this._setKeywords(newKeywords);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
getRule(vectorRuleId) {
|
|
||||||
for (const i in this.state.vectorPushRules) {
|
|
||||||
const rule = this.state.vectorPushRules[i];
|
|
||||||
if (rule.vectorRuleId === vectorRuleId) {
|
|
||||||
return rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_setPushRuleVectorState(rule, newPushRuleVectorState) {
|
|
||||||
if (rule && rule.vectorState !== newPushRuleVectorState) {
|
|
||||||
this.setState({
|
|
||||||
phase: Notifications.phases.LOADING,
|
|
||||||
});
|
|
||||||
|
|
||||||
const self = this;
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
const deferreds = [];
|
|
||||||
const ruleDefinition = VectorPushRulesDefinitions[rule.vectorRuleId];
|
|
||||||
|
|
||||||
if (rule.rule) {
|
|
||||||
const actions = ruleDefinition.vectorStateToActions[newPushRuleVectorState];
|
|
||||||
|
|
||||||
if (!actions) {
|
|
||||||
// The new state corresponds to disabling the rule.
|
|
||||||
deferreds.push(cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false));
|
|
||||||
} else {
|
|
||||||
// The new state corresponds to enabling the rule and setting specific actions
|
|
||||||
deferreds.push(this._updatePushRuleActions(rule.rule, actions, true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all(deferreds).then(function() {
|
|
||||||
self._refreshFromServer();
|
|
||||||
}, function(error) {
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Failed to change settings: " + error);
|
|
||||||
Modal.createTrackedDialog('Failed to change settings', '', ErrorDialog, {
|
|
||||||
title: _t('Failed to change settings'),
|
|
||||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
|
||||||
onFinished: self._refreshFromServer,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_setKeywordsPushRuleVectorState(newPushRuleVectorState) {
|
|
||||||
// Is there really a change?
|
|
||||||
if (this.state.vectorContentRules.vectorState === newPushRuleVectorState
|
|
||||||
|| this.state.vectorContentRules.rules.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const self = this;
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
phase: Notifications.phases.LOADING,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update all rules in self.state.vectorContentRules
|
|
||||||
const deferreds = [];
|
|
||||||
for (const i in this.state.vectorContentRules.rules) {
|
|
||||||
const rule = this.state.vectorContentRules.rules[i];
|
|
||||||
|
|
||||||
let enabled; let actions;
|
|
||||||
switch (newPushRuleVectorState) {
|
|
||||||
case PushRuleVectorState.ON:
|
|
||||||
if (rule.actions.length !== 1) {
|
|
||||||
actions = PushRuleVectorState.actionsFor(PushRuleVectorState.ON);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) {
|
|
||||||
enabled = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PushRuleVectorState.LOUD:
|
|
||||||
if (rule.actions.length !== 3) {
|
|
||||||
actions = PushRuleVectorState.actionsFor(PushRuleVectorState.LOUD);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) {
|
|
||||||
enabled = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PushRuleVectorState.OFF:
|
|
||||||
enabled = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actions) {
|
|
||||||
// Note that the workaround in _updatePushRuleActions will automatically
|
|
||||||
// enable the rule
|
|
||||||
deferreds.push(this._updatePushRuleActions(rule, actions, enabled));
|
|
||||||
} else if (enabled != undefined) {
|
|
||||||
deferreds.push(cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all(deferreds).then(function(resps) {
|
|
||||||
self._refreshFromServer();
|
|
||||||
}, function(error) {
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Can't update user notification settings: " + error);
|
|
||||||
Modal.createTrackedDialog('Can\'t update user notifcation settings', '', ErrorDialog, {
|
|
||||||
title: _t('Can\'t update user notification settings'),
|
|
||||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
|
||||||
onFinished: self._refreshFromServer,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_setKeywords(newKeywords) {
|
|
||||||
this.setState({
|
|
||||||
phase: Notifications.phases.LOADING,
|
|
||||||
});
|
|
||||||
|
|
||||||
const self = this;
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
const removeDeferreds = [];
|
|
||||||
|
|
||||||
// Remove per-word push rules of keywords that are no more in the list
|
|
||||||
const vectorContentRulesPatterns = [];
|
|
||||||
for (const i in self.state.vectorContentRules.rules) {
|
|
||||||
const rule = self.state.vectorContentRules.rules[i];
|
|
||||||
|
|
||||||
vectorContentRulesPatterns.push(rule.pattern);
|
|
||||||
|
|
||||||
if (newKeywords.indexOf(rule.pattern) < 0) {
|
|
||||||
removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the keyword is part of `externalContentRules`, remove the rule
|
|
||||||
// before recreating it in the right Vector path
|
|
||||||
for (const i in self.state.externalContentRules) {
|
|
||||||
const rule = self.state.externalContentRules[i];
|
|
||||||
|
|
||||||
if (newKeywords.indexOf(rule.pattern) >= 0) {
|
|
||||||
removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onError = function(error) {
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Failed to update keywords: " + error);
|
|
||||||
Modal.createTrackedDialog('Failed to update keywords', '', ErrorDialog, {
|
|
||||||
title: _t('Failed to update keywords'),
|
|
||||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
|
||||||
onFinished: self._refreshFromServer,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Then, add the new ones
|
|
||||||
Promise.all(removeDeferreds).then(function(resps) {
|
|
||||||
const deferreds = [];
|
|
||||||
|
|
||||||
let pushRuleVectorStateKind = self.state.vectorContentRules.vectorState;
|
|
||||||
if (pushRuleVectorStateKind === PushRuleVectorState.OFF) {
|
|
||||||
// When the current global keywords rule is OFF, we need to look at
|
|
||||||
// the flavor of rules in 'vectorContentRules' to apply the same actions
|
|
||||||
// when creating the new rule.
|
|
||||||
// Thus, this new rule will join the 'vectorContentRules' set.
|
|
||||||
if (self.state.vectorContentRules.rules.length) {
|
|
||||||
pushRuleVectorStateKind = PushRuleVectorState.contentRuleVectorStateKind(
|
|
||||||
self.state.vectorContentRules.rules[0],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// ON is default
|
|
||||||
pushRuleVectorStateKind = PushRuleVectorState.ON;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const i in newKeywords) {
|
|
||||||
const keyword = newKeywords[i];
|
|
||||||
|
|
||||||
if (vectorContentRulesPatterns.indexOf(keyword) < 0) {
|
|
||||||
if (self.state.vectorContentRules.vectorState !== PushRuleVectorState.OFF) {
|
|
||||||
deferreds.push(cli.addPushRule('global', 'content', keyword, {
|
|
||||||
actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind),
|
|
||||||
pattern: keyword,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
deferreds.push(self._addDisabledPushRule('global', 'content', keyword, {
|
|
||||||
actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind),
|
|
||||||
pattern: keyword,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all(deferreds).then(function(resps) {
|
|
||||||
self._refreshFromServer();
|
|
||||||
}, onError);
|
|
||||||
}, onError);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a push rule but disabled
|
|
||||||
_addDisabledPushRule(scope, kind, ruleId, body) {
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
return cli.addPushRule(scope, kind, ruleId, body).then(() =>
|
|
||||||
cli.setPushRuleEnabled(scope, kind, ruleId, false),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if any legacy im.vector rules need to be ported to the new API
|
|
||||||
// for overriding the actions of default rules.
|
|
||||||
_portRulesToNewAPI(rulesets) {
|
|
||||||
const needsUpdate = [];
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
for (const kind in rulesets.global) {
|
|
||||||
const ruleset = rulesets.global[kind];
|
|
||||||
for (let i = 0; i < ruleset.length; ++i) {
|
|
||||||
const rule = ruleset[i];
|
|
||||||
if (rule.rule_id in LEGACY_RULES) {
|
|
||||||
console.log("Porting legacy rule", rule);
|
|
||||||
needsUpdate.push( function(kind, rule) {
|
|
||||||
return cli.setPushRuleActions(
|
|
||||||
'global', kind, LEGACY_RULES[rule.rule_id], portLegacyActions(rule.actions),
|
|
||||||
).then(() =>
|
|
||||||
cli.deletePushRule('global', kind, rule.rule_id),
|
|
||||||
).catch( (e) => {
|
|
||||||
console.warn(`Error when porting legacy rule: ${e}`);
|
|
||||||
});
|
|
||||||
}(kind, rule));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needsUpdate.length > 0) {
|
|
||||||
// If some of the rules need to be ported then wait for the porting
|
|
||||||
// to happen and then fetch the rules again.
|
|
||||||
return Promise.all(needsUpdate).then(() =>
|
|
||||||
cli.getPushRules(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Otherwise return the rules that we already have.
|
|
||||||
return rulesets;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_refreshFromServer = () => {
|
|
||||||
const self = this;
|
|
||||||
const pushRulesPromise = MatrixClientPeg.get().getPushRules().then(
|
|
||||||
self._portRulesToNewAPI,
|
|
||||||
).then(function(rulesets) {
|
|
||||||
/// XXX seriously? wtf is this?
|
|
||||||
MatrixClientPeg.get().pushRules = rulesets;
|
|
||||||
|
|
||||||
// Get homeserver default rules and triage them by categories
|
|
||||||
const ruleCategories = {
|
|
||||||
// The master rule (all notifications disabling)
|
|
||||||
'.m.rule.master': 'master',
|
|
||||||
|
|
||||||
// The default push rules displayed by Vector UI
|
|
||||||
'.m.rule.contains_display_name': 'vector',
|
|
||||||
'.m.rule.contains_user_name': 'vector',
|
|
||||||
'.m.rule.roomnotif': 'vector',
|
|
||||||
'.m.rule.room_one_to_one': 'vector',
|
|
||||||
'.m.rule.encrypted_room_one_to_one': 'vector',
|
|
||||||
'.m.rule.message': 'vector',
|
|
||||||
'.m.rule.encrypted': 'vector',
|
|
||||||
'.m.rule.invite_for_me': 'vector',
|
|
||||||
//'.m.rule.member_event': 'vector',
|
|
||||||
'.m.rule.call': 'vector',
|
|
||||||
'.m.rule.suppress_notices': 'vector',
|
|
||||||
'.m.rule.tombstone': 'vector',
|
|
||||||
|
|
||||||
// Others go to others
|
|
||||||
};
|
|
||||||
|
|
||||||
// HS default rules
|
|
||||||
const defaultRules = { master: [], vector: {}, others: [] };
|
|
||||||
|
|
||||||
for (const kind in rulesets.global) {
|
|
||||||
for (let i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) {
|
|
||||||
const r = rulesets.global[kind][i];
|
|
||||||
const cat = ruleCategories[r.rule_id];
|
|
||||||
r.kind = kind;
|
|
||||||
|
|
||||||
if (r.rule_id[0] === '.') {
|
|
||||||
if (cat === 'vector') {
|
|
||||||
defaultRules.vector[r.rule_id] = r;
|
|
||||||
} else if (cat === 'master') {
|
|
||||||
defaultRules.master.push(r);
|
|
||||||
} else {
|
|
||||||
defaultRules['others'].push(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the master rule if any defined by the hs
|
|
||||||
if (defaultRules.master.length > 0) {
|
|
||||||
self.state.masterPushRule = defaultRules.master[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse the keyword rules into our state
|
|
||||||
const contentRules = ContentRules.parseContentRules(rulesets);
|
|
||||||
self.state.vectorContentRules = {
|
|
||||||
vectorState: contentRules.vectorState,
|
|
||||||
rules: contentRules.rules,
|
|
||||||
};
|
|
||||||
self.state.externalContentRules = contentRules.externalRules;
|
|
||||||
|
|
||||||
// Build the rules displayed in the Vector UI matrix table
|
|
||||||
self.state.vectorPushRules = [];
|
|
||||||
self.state.externalPushRules = [];
|
|
||||||
|
|
||||||
const vectorRuleIds = [
|
|
||||||
'.m.rule.contains_display_name',
|
|
||||||
'.m.rule.contains_user_name',
|
|
||||||
'.m.rule.roomnotif',
|
|
||||||
'_keywords',
|
|
||||||
'.m.rule.room_one_to_one',
|
|
||||||
'.m.rule.encrypted_room_one_to_one',
|
|
||||||
'.m.rule.message',
|
|
||||||
'.m.rule.encrypted',
|
|
||||||
'.m.rule.invite_for_me',
|
|
||||||
//'im.vector.rule.member_event',
|
|
||||||
'.m.rule.call',
|
|
||||||
'.m.rule.suppress_notices',
|
|
||||||
'.m.rule.tombstone',
|
|
||||||
];
|
|
||||||
for (const i in vectorRuleIds) {
|
|
||||||
const vectorRuleId = vectorRuleIds[i];
|
|
||||||
|
|
||||||
if (vectorRuleId === '_keywords') {
|
|
||||||
// keywords needs a special handling
|
|
||||||
// For Vector UI, this is a single global push rule but translated in Matrix,
|
|
||||||
// it corresponds to all content push rules (stored in self.state.vectorContentRule)
|
|
||||||
self.state.vectorPushRules.push({
|
|
||||||
"vectorRuleId": "_keywords",
|
|
||||||
"description": (
|
|
||||||
<span>
|
|
||||||
{ _t('Messages containing <span>keywords</span>',
|
|
||||||
{},
|
|
||||||
{ 'span': (sub) =>
|
|
||||||
<span className="mx_UserNotifSettings_keywords" onClick={ self.onKeywordsClicked }>{sub}</span>,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
"vectorState": self.state.vectorContentRules.vectorState,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const ruleDefinition = VectorPushRulesDefinitions[vectorRuleId];
|
|
||||||
const rule = defaultRules.vector[vectorRuleId];
|
|
||||||
|
|
||||||
const vectorState = ruleDefinition.ruleToVectorState(rule);
|
|
||||||
|
|
||||||
//console.log("Refreshing vectorPushRules for " + vectorRuleId +", "+ ruleDefinition.description +", " + rule +", " + vectorState);
|
|
||||||
|
|
||||||
self.state.vectorPushRules.push({
|
|
||||||
"vectorRuleId": vectorRuleId,
|
|
||||||
"description": _t(ruleDefinition.description), // Text from VectorPushRulesDefinitions.js
|
|
||||||
"rule": rule,
|
|
||||||
"vectorState": vectorState,
|
|
||||||
});
|
|
||||||
|
|
||||||
// if there was a rule which we couldn't parse, add it to the external list
|
|
||||||
if (rule && !vectorState) {
|
|
||||||
rule.description = ruleDefinition.description;
|
|
||||||
self.state.externalPushRules.push(rule);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the rules not managed by Vector UI
|
|
||||||
const otherRulesDescriptions = {
|
|
||||||
'.m.rule.message': _t('Notify for all other messages/rooms'),
|
|
||||||
'.m.rule.fallback': _t('Notify me for anything else'),
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const i in defaultRules.others) {
|
|
||||||
const rule = defaultRules.others[i];
|
|
||||||
const ruleDescription = otherRulesDescriptions[rule.rule_id];
|
|
||||||
|
|
||||||
// Show enabled default rules that was modified by the user
|
|
||||||
if (ruleDescription && rule.enabled && !rule.default) {
|
|
||||||
rule.description = ruleDescription;
|
|
||||||
self.state.externalPushRules.push(rule);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const pushersPromise = MatrixClientPeg.get().getPushers().then(function(resp) {
|
|
||||||
self.setState({ pushers: resp.pushers });
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all([pushRulesPromise, pushersPromise]).then(function() {
|
|
||||||
self.setState({
|
|
||||||
phase: Notifications.phases.DISPLAY,
|
|
||||||
});
|
|
||||||
}, function(error) {
|
|
||||||
console.error(error);
|
|
||||||
self.setState({
|
|
||||||
phase: Notifications.phases.ERROR,
|
|
||||||
});
|
|
||||||
}).finally(() => {
|
|
||||||
// actually explicitly update our state having been deep-manipulating it
|
|
||||||
self.setState({
|
|
||||||
masterPushRule: self.state.masterPushRule,
|
|
||||||
vectorContentRules: self.state.vectorContentRules,
|
|
||||||
vectorPushRules: self.state.vectorPushRules,
|
|
||||||
externalContentRules: self.state.externalContentRules,
|
|
||||||
externalPushRules: self.state.externalPushRules,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
MatrixClientPeg.get().getThreePids().then((r) => this.setState({ threepids: r.threepids }));
|
|
||||||
};
|
|
||||||
|
|
||||||
_onClearNotifications = () => {
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
cli.getRooms().forEach(r => {
|
|
||||||
if (r.getUnreadNotificationCount() > 0) {
|
|
||||||
const events = r.getLiveTimeline().getEvents();
|
|
||||||
if (events.length) cli.sendReadReceipt(events.pop());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
_updatePushRuleActions(rule, actions, enabled) {
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
return cli.setPushRuleActions(
|
|
||||||
'global', rule.kind, rule.rule_id, actions,
|
|
||||||
).then( function() {
|
|
||||||
// Then, if requested, enabled or disabled the rule
|
|
||||||
if (undefined != enabled) {
|
|
||||||
return cli.setPushRuleEnabled(
|
|
||||||
'global', rule.kind, rule.rule_id, enabled,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderNotifRulesTableRow(title, className, pushRuleVectorState) {
|
|
||||||
return (
|
|
||||||
<tr key={ className }>
|
|
||||||
<th>
|
|
||||||
{ title }
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<th>
|
|
||||||
<input className= {className + "-" + PushRuleVectorState.OFF}
|
|
||||||
type="radio"
|
|
||||||
checked={ pushRuleVectorState === PushRuleVectorState.OFF }
|
|
||||||
onChange={ this.onNotifStateButtonClicked } />
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<th>
|
|
||||||
<input className= {className + "-" + PushRuleVectorState.ON}
|
|
||||||
type="radio"
|
|
||||||
checked={ pushRuleVectorState === PushRuleVectorState.ON }
|
|
||||||
onChange={ this.onNotifStateButtonClicked } />
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<th>
|
|
||||||
<input className= {className + "-" + PushRuleVectorState.LOUD}
|
|
||||||
type="radio"
|
|
||||||
checked={ pushRuleVectorState === PushRuleVectorState.LOUD }
|
|
||||||
onChange={ this.onNotifStateButtonClicked } />
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderNotifRulesTableRows() {
|
|
||||||
const rows = [];
|
|
||||||
for (const i in this.state.vectorPushRules) {
|
|
||||||
const rule = this.state.vectorPushRules[i];
|
|
||||||
if (rule.rule === undefined && rule.vectorRuleId.startsWith(".m.")) {
|
|
||||||
console.warn(`Skipping render of rule ${rule.vectorRuleId} due to no underlying rule`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
//console.log("rendering: " + rule.description + ", " + rule.vectorRuleId + ", " + rule.vectorState);
|
|
||||||
rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState));
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasEmailPusher(pushers, address) {
|
|
||||||
if (pushers === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < pushers.length; ++i) {
|
|
||||||
if (pushers[i].kind === 'email' && pushers[i].pushkey === address) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
emailNotificationsRow(address, label) {
|
|
||||||
return <LabelledToggleSwitch value={this.hasEmailPusher(this.state.pushers, address)}
|
|
||||||
onChange={this.onEnableEmailNotificationsChange.bind(this, address)}
|
|
||||||
label={label} key={`emailNotif_${label}`} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let spinner;
|
|
||||||
if (this.state.phase === Notifications.phases.LOADING) {
|
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
|
||||||
spinner = <Loader />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let masterPushRuleDiv;
|
|
||||||
if (this.state.masterPushRule) {
|
|
||||||
masterPushRuleDiv = <LabelledToggleSwitch value={!this.state.masterPushRule.enabled}
|
|
||||||
onChange={this.onEnableNotificationsChange}
|
|
||||||
label={_t('Enable notifications for this account')} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let clearNotificationsButton;
|
|
||||||
if (MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0)) {
|
|
||||||
clearNotificationsButton = <AccessibleButton onClick={this._onClearNotifications} kind='danger'>
|
|
||||||
{_t("Clear notifications")}
|
|
||||||
</AccessibleButton>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When enabled, the master rule inhibits all existing rules
|
|
||||||
// So do not show all notification settings
|
|
||||||
if (this.state.masterPushRule && this.state.masterPushRule.enabled) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{masterPushRuleDiv}
|
|
||||||
|
|
||||||
<div className="mx_UserNotifSettings_notifTable">
|
|
||||||
{ _t('All notifications are currently disabled for all targets.') }
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{clearNotificationsButton}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const emailThreepids = this.state.threepids.filter((tp) => tp.medium === "email");
|
|
||||||
let emailNotificationsRows;
|
|
||||||
if (emailThreepids.length > 0) {
|
|
||||||
emailNotificationsRows = emailThreepids.map((threePid) => this.emailNotificationsRow(
|
|
||||||
threePid.address, `${_t('Enable email notifications')} (${threePid.address})`,
|
|
||||||
));
|
|
||||||
} else if (SettingsStore.getValue(UIFeature.ThirdPartyID)) {
|
|
||||||
emailNotificationsRows = <div>
|
|
||||||
{ _t('Add an email address to configure email notifications') }
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build external push rules
|
|
||||||
const externalRules = [];
|
|
||||||
for (const i in this.state.externalPushRules) {
|
|
||||||
const rule = this.state.externalPushRules[i];
|
|
||||||
externalRules.push(<li>{ _t(rule.description) }</li>);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show keywords not displayed by the vector UI as a single external push rule
|
|
||||||
let externalKeywords = [];
|
|
||||||
for (const i in this.state.externalContentRules) {
|
|
||||||
const rule = this.state.externalContentRules[i];
|
|
||||||
externalKeywords.push(rule.pattern);
|
|
||||||
}
|
|
||||||
if (externalKeywords.length) {
|
|
||||||
externalKeywords = externalKeywords.join(", ");
|
|
||||||
externalRules.push(<li>
|
|
||||||
{_t('Notifications on the following keywords follow rules which can’t be displayed here:') }
|
|
||||||
{ externalKeywords }
|
|
||||||
</li>);
|
|
||||||
}
|
|
||||||
|
|
||||||
let devicesSection;
|
|
||||||
if (this.state.pushers === undefined) {
|
|
||||||
devicesSection = <div className="error">{ _t('Unable to fetch notification target list') }</div>;
|
|
||||||
} else if (this.state.pushers.length === 0) {
|
|
||||||
devicesSection = null;
|
|
||||||
} else {
|
|
||||||
// TODO: It would be great to be able to delete pushers from here too,
|
|
||||||
// and this wouldn't be hard to add.
|
|
||||||
const rows = [];
|
|
||||||
for (let i = 0; i < this.state.pushers.length; ++i) {
|
|
||||||
rows.push(<tr key={ i }>
|
|
||||||
<td>{this.state.pushers[i].app_display_name}</td>
|
|
||||||
<td>{this.state.pushers[i].device_display_name}</td>
|
|
||||||
</tr>);
|
|
||||||
}
|
|
||||||
devicesSection = (<table className="mx_UserNotifSettings_devicesTable">
|
|
||||||
<tbody>
|
|
||||||
{rows}
|
|
||||||
</tbody>
|
|
||||||
</table>);
|
|
||||||
}
|
|
||||||
if (devicesSection) {
|
|
||||||
devicesSection = (<div>
|
|
||||||
<h3>{ _t('Notification targets') }</h3>
|
|
||||||
{ devicesSection }
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
let advancedSettings;
|
|
||||||
if (externalRules.length) {
|
|
||||||
const brand = SdkConfig.get().brand;
|
|
||||||
advancedSettings = (
|
|
||||||
<div>
|
|
||||||
<h3>{ _t('Advanced notification settings') }</h3>
|
|
||||||
{ _t('There are advanced notifications which are not shown here.') }<br />
|
|
||||||
{_t(
|
|
||||||
'You might have configured them in a client other than %(brand)s. ' +
|
|
||||||
'You cannot tune them in %(brand)s but they still apply.',
|
|
||||||
{ brand },
|
|
||||||
)}
|
|
||||||
<ul>
|
|
||||||
{ externalRules }
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
|
|
||||||
{masterPushRuleDiv}
|
|
||||||
|
|
||||||
<div className="mx_UserNotifSettings_notifTable">
|
|
||||||
|
|
||||||
{ spinner }
|
|
||||||
|
|
||||||
<LabelledToggleSwitch value={SettingsStore.getValue("notificationsEnabled")}
|
|
||||||
onChange={this.onEnableDesktopNotificationsChange}
|
|
||||||
label={_t('Enable desktop notifications for this session')} />
|
|
||||||
|
|
||||||
<LabelledToggleSwitch value={SettingsStore.getValue("notificationBodyEnabled")}
|
|
||||||
onChange={this.onEnableDesktopNotificationBodyChange}
|
|
||||||
label={_t('Show message in desktop notification')} />
|
|
||||||
|
|
||||||
<LabelledToggleSwitch value={SettingsStore.getValue("audioNotificationsEnabled")}
|
|
||||||
onChange={this.onEnableAudioNotificationsChange}
|
|
||||||
label={_t('Enable audible notifications for this session')} />
|
|
||||||
|
|
||||||
{ emailNotificationsRows }
|
|
||||||
|
|
||||||
<div className="mx_UserNotifSettings_pushRulesTableWrapper">
|
|
||||||
<table className="mx_UserNotifSettings_pushRulesTable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th width="55%"></th>
|
|
||||||
<th width="15%">{ _t('Off') }</th>
|
|
||||||
<th width="15%">{ _t('On') }</th>
|
|
||||||
<th width="15%">{ _t('Noisy') }</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
{ this.renderNotifRulesTableRows() }
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ advancedSettings }
|
|
||||||
|
|
||||||
{ devicesSection }
|
|
||||||
|
|
||||||
{ clearNotificationsButton }
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
647
src/components/views/settings/Notifications.tsx
Normal file
647
src/components/views/settings/Notifications.tsx
Normal file
|
@ -0,0 +1,647 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 - 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 Spinner from "../elements/Spinner";
|
||||||
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
import { IAnnotatedPushRule, IPusher, PushRuleAction, PushRuleKind, RuleId } from "matrix-js-sdk/src/@types/PushRules";
|
||||||
|
import {
|
||||||
|
ContentRules,
|
||||||
|
IContentRules,
|
||||||
|
PushRuleVectorState,
|
||||||
|
VectorPushRulesDefinitions,
|
||||||
|
VectorState,
|
||||||
|
} from "../../../notifications";
|
||||||
|
import { _t, TranslatedString } from "../../../languageHandler";
|
||||||
|
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
||||||
|
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import StyledRadioButton from "../elements/StyledRadioButton";
|
||||||
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
|
import SdkConfig from "../../../SdkConfig";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import TagComposer from "../elements/TagComposer";
|
||||||
|
import { objectClone } from "../../../utils/objects";
|
||||||
|
import { arrayDiff } from "../../../utils/arrays";
|
||||||
|
|
||||||
|
// TODO: this "view" component still has far too much application logic in it,
|
||||||
|
// which should be factored out to other files.
|
||||||
|
|
||||||
|
enum Phase {
|
||||||
|
Loading = "loading",
|
||||||
|
Ready = "ready",
|
||||||
|
Persisting = "persisting", // technically a meta-state for Ready, but whatever
|
||||||
|
Error = "error",
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RuleClass {
|
||||||
|
Master = "master",
|
||||||
|
|
||||||
|
// The vector sections map approximately to UI sections
|
||||||
|
VectorGlobal = "vector_global",
|
||||||
|
VectorMentions = "vector_mentions",
|
||||||
|
VectorOther = "vector_other",
|
||||||
|
Other = "other", // unknown rules, essentially
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEYWORD_RULE_ID = "_keywords"; // used as a placeholder "Rule ID" throughout this component
|
||||||
|
const KEYWORD_RULE_CATEGORY = RuleClass.VectorMentions;
|
||||||
|
|
||||||
|
// This array doesn't care about categories: it's just used for a simple sort
|
||||||
|
const RULE_DISPLAY_ORDER: string[] = [
|
||||||
|
// Global
|
||||||
|
RuleId.DM,
|
||||||
|
RuleId.EncryptedDM,
|
||||||
|
RuleId.Message,
|
||||||
|
RuleId.EncryptedMessage,
|
||||||
|
|
||||||
|
// Mentions
|
||||||
|
RuleId.ContainsDisplayName,
|
||||||
|
RuleId.ContainsUserName,
|
||||||
|
RuleId.AtRoomNotification,
|
||||||
|
|
||||||
|
// Other
|
||||||
|
RuleId.InviteToSelf,
|
||||||
|
RuleId.IncomingCall,
|
||||||
|
RuleId.SuppressNotices,
|
||||||
|
RuleId.Tombstone,
|
||||||
|
];
|
||||||
|
|
||||||
|
interface IVectorPushRule {
|
||||||
|
ruleId: RuleId | typeof KEYWORD_RULE_ID | string;
|
||||||
|
rule?: IAnnotatedPushRule;
|
||||||
|
description: TranslatedString | string;
|
||||||
|
vectorState: VectorState;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
phase: Phase;
|
||||||
|
|
||||||
|
// Optional stuff is required when `phase === Ready`
|
||||||
|
masterPushRule?: IAnnotatedPushRule;
|
||||||
|
vectorKeywordRuleInfo?: IContentRules;
|
||||||
|
vectorPushRules?: {
|
||||||
|
[category in RuleClass]?: IVectorPushRule[];
|
||||||
|
};
|
||||||
|
pushers?: IPusher[];
|
||||||
|
threepids?: IThreepid[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Notifications extends React.PureComponent<IProps, IState> {
|
||||||
|
public constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
phase: Phase.Loading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private get isInhibited(): boolean {
|
||||||
|
// Caution: The master rule's enabled state is inverted from expectation. When
|
||||||
|
// the master rule is *enabled* it means all other rules are *disabled* (or
|
||||||
|
// inhibited). Conversely, when the master rule is *disabled* then all other rules
|
||||||
|
// are *enabled* (or operate fine).
|
||||||
|
return this.state.masterPushRule?.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
// noinspection JSIgnoredPromiseFromCall
|
||||||
|
this.refreshFromServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshFromServer() {
|
||||||
|
try {
|
||||||
|
const newState = (await Promise.all([
|
||||||
|
this.refreshRules(),
|
||||||
|
this.refreshPushers(),
|
||||||
|
this.refreshThreepids(),
|
||||||
|
])).reduce((p, c) => Object.assign(c, p), {});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
...newState,
|
||||||
|
phase: Phase.Ready,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error setting up notifications for settings: ", e);
|
||||||
|
this.setState({ phase: Phase.Error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshRules(): Promise<Partial<IState>> {
|
||||||
|
const ruleSets = await MatrixClientPeg.get().getPushRules();
|
||||||
|
|
||||||
|
const categories = {
|
||||||
|
[RuleId.Master]: RuleClass.Master,
|
||||||
|
|
||||||
|
[RuleId.DM]: RuleClass.VectorGlobal,
|
||||||
|
[RuleId.EncryptedDM]: RuleClass.VectorGlobal,
|
||||||
|
[RuleId.Message]: RuleClass.VectorGlobal,
|
||||||
|
[RuleId.EncryptedMessage]: RuleClass.VectorGlobal,
|
||||||
|
|
||||||
|
[RuleId.ContainsDisplayName]: RuleClass.VectorMentions,
|
||||||
|
[RuleId.ContainsUserName]: RuleClass.VectorMentions,
|
||||||
|
[RuleId.AtRoomNotification]: RuleClass.VectorMentions,
|
||||||
|
|
||||||
|
[RuleId.InviteToSelf]: RuleClass.VectorOther,
|
||||||
|
[RuleId.IncomingCall]: RuleClass.VectorOther,
|
||||||
|
[RuleId.SuppressNotices]: RuleClass.VectorOther,
|
||||||
|
[RuleId.Tombstone]: RuleClass.VectorOther,
|
||||||
|
|
||||||
|
// Everything maps to a generic "other" (unknown rule)
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultRules: {
|
||||||
|
[k in RuleClass]: IAnnotatedPushRule[];
|
||||||
|
} = {
|
||||||
|
[RuleClass.Master]: [],
|
||||||
|
[RuleClass.VectorGlobal]: [],
|
||||||
|
[RuleClass.VectorMentions]: [],
|
||||||
|
[RuleClass.VectorOther]: [],
|
||||||
|
[RuleClass.Other]: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const k in ruleSets.global) {
|
||||||
|
// noinspection JSUnfilteredForInLoop
|
||||||
|
const kind = k as PushRuleKind;
|
||||||
|
for (const r of ruleSets.global[kind]) {
|
||||||
|
const rule: IAnnotatedPushRule = Object.assign(r, { kind });
|
||||||
|
const category = categories[rule.rule_id] ?? RuleClass.Other;
|
||||||
|
|
||||||
|
if (rule.rule_id[0] === '.') {
|
||||||
|
defaultRules[category].push(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const preparedNewState: Partial<IState> = {};
|
||||||
|
if (defaultRules.master.length > 0) {
|
||||||
|
preparedNewState.masterPushRule = defaultRules.master[0];
|
||||||
|
} else {
|
||||||
|
// XXX: Can this even happen? How do we safely recover?
|
||||||
|
throw new Error("Failed to locate a master push rule");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse keyword rules
|
||||||
|
preparedNewState.vectorKeywordRuleInfo = ContentRules.parseContentRules(ruleSets);
|
||||||
|
|
||||||
|
// Prepare rendering for all of our known rules
|
||||||
|
preparedNewState.vectorPushRules = {};
|
||||||
|
const vectorCategories = [RuleClass.VectorGlobal, RuleClass.VectorMentions, RuleClass.VectorOther];
|
||||||
|
for (const category of vectorCategories) {
|
||||||
|
preparedNewState.vectorPushRules[category] = [];
|
||||||
|
for (const rule of defaultRules[category]) {
|
||||||
|
const definition = VectorPushRulesDefinitions[rule.rule_id];
|
||||||
|
const vectorState = definition.ruleToVectorState(rule);
|
||||||
|
preparedNewState.vectorPushRules[category].push({
|
||||||
|
ruleId: rule.rule_id,
|
||||||
|
rule, vectorState,
|
||||||
|
description: _t(definition.description),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quickly sort the rules for display purposes
|
||||||
|
preparedNewState.vectorPushRules[category].sort((a, b) => {
|
||||||
|
let idxA = RULE_DISPLAY_ORDER.indexOf(a.ruleId);
|
||||||
|
let idxB = RULE_DISPLAY_ORDER.indexOf(b.ruleId);
|
||||||
|
|
||||||
|
// Assume unknown things go at the end
|
||||||
|
if (idxA < 0) idxA = RULE_DISPLAY_ORDER.length;
|
||||||
|
if (idxB < 0) idxB = RULE_DISPLAY_ORDER.length;
|
||||||
|
|
||||||
|
return idxA - idxB;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (category === KEYWORD_RULE_CATEGORY) {
|
||||||
|
preparedNewState.vectorPushRules[category].push({
|
||||||
|
ruleId: KEYWORD_RULE_ID,
|
||||||
|
description: _t("Messages containing keywords"),
|
||||||
|
vectorState: preparedNewState.vectorKeywordRuleInfo.vectorState,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return preparedNewState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshPushers(): Promise<Partial<IState>> {
|
||||||
|
return MatrixClientPeg.get().getPushers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshThreepids(): Promise<Partial<IState>> {
|
||||||
|
return MatrixClientPeg.get().getThreePids();
|
||||||
|
}
|
||||||
|
|
||||||
|
private showSaveError() {
|
||||||
|
Modal.createTrackedDialog('Error saving notification preferences', '', ErrorDialog, {
|
||||||
|
title: _t('Error saving notification preferences'),
|
||||||
|
description: _t('An error occurred whilst saving your notification preferences.'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMasterRuleChanged = async (checked: boolean) => {
|
||||||
|
this.setState({ phase: Phase.Persisting });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const masterRule = this.state.masterPushRule;
|
||||||
|
await MatrixClientPeg.get().setPushRuleEnabled('global', masterRule.kind, masterRule.rule_id, !checked);
|
||||||
|
await this.refreshFromServer();
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({ phase: Phase.Error });
|
||||||
|
console.error("Error updating master push rule:", e);
|
||||||
|
this.showSaveError();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onEmailNotificationsChanged = async (email: string, checked: boolean) => {
|
||||||
|
this.setState({ phase: Phase.Persisting });
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (checked) {
|
||||||
|
await MatrixClientPeg.get().setPusher({
|
||||||
|
kind: "email",
|
||||||
|
app_id: "m.email",
|
||||||
|
pushkey: email,
|
||||||
|
app_display_name: "Email Notifications",
|
||||||
|
device_display_name: email,
|
||||||
|
lang: navigator.language,
|
||||||
|
data: {
|
||||||
|
brand: SdkConfig.get().brand,
|
||||||
|
},
|
||||||
|
|
||||||
|
// We always append for email pushers since we don't want to stop other
|
||||||
|
// accounts notifying to the same email address
|
||||||
|
append: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const pusher = this.state.pushers.find(p => p.kind === "email" && p.pushkey === email);
|
||||||
|
pusher.kind = null; // flag for delete
|
||||||
|
await MatrixClientPeg.get().setPusher(pusher);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.refreshFromServer();
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({ phase: Phase.Error });
|
||||||
|
console.error("Error updating email pusher:", e);
|
||||||
|
this.showSaveError();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDesktopNotificationsChanged = async (checked: boolean) => {
|
||||||
|
await SettingsStore.setValue("notificationsEnabled", null, SettingLevel.DEVICE, checked);
|
||||||
|
this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue()
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDesktopShowBodyChanged = async (checked: boolean) => {
|
||||||
|
await SettingsStore.setValue("notificationBodyEnabled", null, SettingLevel.DEVICE, checked);
|
||||||
|
this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue()
|
||||||
|
};
|
||||||
|
|
||||||
|
private onAudioNotificationsChanged = async (checked: boolean) => {
|
||||||
|
await SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, checked);
|
||||||
|
this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue()
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRadioChecked = async (rule: IVectorPushRule, checkedState: VectorState) => {
|
||||||
|
this.setState({ phase: Phase.Persisting });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
if (rule.ruleId === KEYWORD_RULE_ID) {
|
||||||
|
// Update all the keywords
|
||||||
|
for (const rule of this.state.vectorKeywordRuleInfo.rules) {
|
||||||
|
let enabled: boolean;
|
||||||
|
let actions: PushRuleAction[];
|
||||||
|
if (checkedState === VectorState.On) {
|
||||||
|
if (rule.actions.length !== 1) { // XXX: Magic number
|
||||||
|
actions = PushRuleVectorState.actionsFor(checkedState);
|
||||||
|
}
|
||||||
|
if (this.state.vectorKeywordRuleInfo.vectorState === VectorState.Off) {
|
||||||
|
enabled = true;
|
||||||
|
}
|
||||||
|
} else if (checkedState === VectorState.Loud) {
|
||||||
|
if (rule.actions.length !== 3) { // XXX: Magic number
|
||||||
|
actions = PushRuleVectorState.actionsFor(checkedState);
|
||||||
|
}
|
||||||
|
if (this.state.vectorKeywordRuleInfo.vectorState === VectorState.Off) {
|
||||||
|
enabled = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actions) {
|
||||||
|
await cli.setPushRuleActions('global', rule.kind, rule.rule_id, actions);
|
||||||
|
}
|
||||||
|
if (enabled !== undefined) {
|
||||||
|
await cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const definition = VectorPushRulesDefinitions[rule.ruleId];
|
||||||
|
const actions = definition.vectorStateToActions[checkedState];
|
||||||
|
if (!actions) {
|
||||||
|
await cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false);
|
||||||
|
} else {
|
||||||
|
await cli.setPushRuleActions('global', rule.rule.kind, rule.rule.rule_id, actions);
|
||||||
|
await cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.refreshFromServer();
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({ phase: Phase.Error });
|
||||||
|
console.error("Error updating push rule:", e);
|
||||||
|
this.showSaveError();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onClearNotificationsClicked = () => {
|
||||||
|
MatrixClientPeg.get().getRooms().forEach(r => {
|
||||||
|
if (r.getUnreadNotificationCount() > 0) {
|
||||||
|
const events = r.getLiveTimeline().getEvents();
|
||||||
|
if (events.length) {
|
||||||
|
// noinspection JSIgnoredPromiseFromCall
|
||||||
|
MatrixClientPeg.get().sendReadReceipt(events[events.length - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private async setKeywords(keywords: string[], originalRules: IAnnotatedPushRule[]) {
|
||||||
|
try {
|
||||||
|
// De-duplicate and remove empties
|
||||||
|
keywords = Array.from(new Set(keywords)).filter(k => !!k);
|
||||||
|
const oldKeywords = Array.from(new Set(originalRules.map(r => r.pattern))).filter(k => !!k);
|
||||||
|
|
||||||
|
// Note: Technically because of the UI interaction (at the time of writing), the diff
|
||||||
|
// will only ever be +/-1 so we don't really have to worry about efficiently handling
|
||||||
|
// tons of keyword changes.
|
||||||
|
|
||||||
|
const diff = arrayDiff(oldKeywords, keywords);
|
||||||
|
|
||||||
|
for (const word of diff.removed) {
|
||||||
|
for (const rule of originalRules.filter(r => r.pattern === word)) {
|
||||||
|
await MatrixClientPeg.get().deletePushRule('global', rule.kind, rule.rule_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ruleVectorState = this.state.vectorKeywordRuleInfo.vectorState;
|
||||||
|
if (ruleVectorState === VectorState.Off) {
|
||||||
|
// When the current global keywords rule is OFF, we need to look at
|
||||||
|
// the flavor of existing rules to apply the same actions
|
||||||
|
// when creating the new rule.
|
||||||
|
if (originalRules.length) {
|
||||||
|
ruleVectorState = PushRuleVectorState.contentRuleVectorStateKind(originalRules[0]);
|
||||||
|
} else {
|
||||||
|
ruleVectorState = VectorState.On; // default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const kind = PushRuleKind.ContentSpecific;
|
||||||
|
for (const word of diff.added) {
|
||||||
|
await MatrixClientPeg.get().addPushRule('global', kind, word, {
|
||||||
|
actions: PushRuleVectorState.actionsFor(ruleVectorState),
|
||||||
|
pattern: word,
|
||||||
|
});
|
||||||
|
if (ruleVectorState === VectorState.Off) {
|
||||||
|
await MatrixClientPeg.get().setPushRuleEnabled('global', kind, word, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.refreshFromServer();
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({ phase: Phase.Error });
|
||||||
|
console.error("Error updating keyword push rules:", e);
|
||||||
|
this.showSaveError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onKeywordAdd = (keyword: string) => {
|
||||||
|
const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules);
|
||||||
|
|
||||||
|
// We add the keyword immediately as a sort of local echo effect
|
||||||
|
this.setState({
|
||||||
|
phase: Phase.Persisting,
|
||||||
|
vectorKeywordRuleInfo: {
|
||||||
|
...this.state.vectorKeywordRuleInfo,
|
||||||
|
rules: [
|
||||||
|
...this.state.vectorKeywordRuleInfo.rules,
|
||||||
|
|
||||||
|
// XXX: Horrible assumption that we don't need the remaining fields
|
||||||
|
{ pattern: keyword } as IAnnotatedPushRule,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}, async () => {
|
||||||
|
await this.setKeywords(this.state.vectorKeywordRuleInfo.rules.map(r => r.pattern), originalRules);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onKeywordRemove = (keyword: string) => {
|
||||||
|
const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules);
|
||||||
|
|
||||||
|
// We remove the keyword immediately as a sort of local echo effect
|
||||||
|
this.setState({
|
||||||
|
phase: Phase.Persisting,
|
||||||
|
vectorKeywordRuleInfo: {
|
||||||
|
...this.state.vectorKeywordRuleInfo,
|
||||||
|
rules: this.state.vectorKeywordRuleInfo.rules.filter(r => r.pattern !== keyword),
|
||||||
|
},
|
||||||
|
}, async () => {
|
||||||
|
await this.setKeywords(this.state.vectorKeywordRuleInfo.rules.map(r => r.pattern), originalRules);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderTopSection() {
|
||||||
|
const masterSwitch = <LabelledToggleSwitch
|
||||||
|
value={!this.isInhibited}
|
||||||
|
label={_t("Enable for this account")}
|
||||||
|
onChange={this.onMasterRuleChanged}
|
||||||
|
disabled={this.state.phase === Phase.Persisting}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
// If all the rules are inhibited, don't show anything.
|
||||||
|
if (this.isInhibited) {
|
||||||
|
return masterSwitch;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailSwitches = this.state.threepids.filter(t => t.medium === ThreepidMedium.Email)
|
||||||
|
.map(e => <LabelledToggleSwitch
|
||||||
|
key={e.address}
|
||||||
|
value={this.state.pushers.some(p => p.kind === "email" && p.pushkey === e.address)}
|
||||||
|
label={_t("Enable email notifications for %(email)s", { email: e.address })}
|
||||||
|
onChange={this.onEmailNotificationsChanged.bind(this, e.address)}
|
||||||
|
disabled={this.state.phase === Phase.Persisting}
|
||||||
|
/>);
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{ masterSwitch }
|
||||||
|
|
||||||
|
<LabelledToggleSwitch
|
||||||
|
value={SettingsStore.getValue("notificationsEnabled")}
|
||||||
|
onChange={this.onDesktopNotificationsChanged}
|
||||||
|
label={_t('Enable desktop notifications for this session')}
|
||||||
|
disabled={this.state.phase === Phase.Persisting}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabelledToggleSwitch
|
||||||
|
value={SettingsStore.getValue("notificationBodyEnabled")}
|
||||||
|
onChange={this.onDesktopShowBodyChanged}
|
||||||
|
label={_t('Show message in desktop notification')}
|
||||||
|
disabled={this.state.phase === Phase.Persisting}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabelledToggleSwitch
|
||||||
|
value={SettingsStore.getValue("audioNotificationsEnabled")}
|
||||||
|
onChange={this.onAudioNotificationsChanged}
|
||||||
|
label={_t('Enable audible notifications for this session')}
|
||||||
|
disabled={this.state.phase === Phase.Persisting}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{ emailSwitches }
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderCategory(category: RuleClass) {
|
||||||
|
if (category !== RuleClass.VectorOther && this.isInhibited) {
|
||||||
|
return null; // nothing to show for the section
|
||||||
|
}
|
||||||
|
|
||||||
|
let clearNotifsButton: JSX.Element;
|
||||||
|
if (
|
||||||
|
category === RuleClass.VectorOther
|
||||||
|
&& MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0)
|
||||||
|
) {
|
||||||
|
clearNotifsButton = <AccessibleButton
|
||||||
|
onClick={this.onClearNotificationsClicked}
|
||||||
|
kind='danger'
|
||||||
|
className='mx_UserNotifSettings_clearNotifsButton'
|
||||||
|
>{ _t("Clear notifications") }</AccessibleButton>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category === RuleClass.VectorOther && this.isInhibited) {
|
||||||
|
// only render the utility buttons (if needed)
|
||||||
|
if (clearNotifsButton) {
|
||||||
|
return <div className='mx_UserNotifSettings_floatingSection'>
|
||||||
|
<div>{ _t("Other") }</div>
|
||||||
|
{ clearNotifsButton }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let keywordComposer: JSX.Element;
|
||||||
|
if (category === RuleClass.VectorMentions) {
|
||||||
|
keywordComposer = <TagComposer
|
||||||
|
tags={this.state.vectorKeywordRuleInfo?.rules.map(r => r.pattern)}
|
||||||
|
onAdd={this.onKeywordAdd}
|
||||||
|
onRemove={this.onKeywordRemove}
|
||||||
|
disabled={this.state.phase === Phase.Persisting}
|
||||||
|
label={_t("Keyword")}
|
||||||
|
placeholder={_t("New keyword")}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeRadio = (r: IVectorPushRule, s: VectorState) => (
|
||||||
|
<StyledRadioButton
|
||||||
|
key={r.ruleId}
|
||||||
|
name={r.ruleId}
|
||||||
|
checked={r.vectorState === s}
|
||||||
|
onChange={this.onRadioChecked.bind(this, r, s)}
|
||||||
|
disabled={this.state.phase === Phase.Persisting}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const rows = this.state.vectorPushRules[category].map(r => <tr key={category + r.ruleId}>
|
||||||
|
<td>{ r.description }</td>
|
||||||
|
<td>{ makeRadio(r, VectorState.Off) }</td>
|
||||||
|
<td>{ makeRadio(r, VectorState.On) }</td>
|
||||||
|
<td>{ makeRadio(r, VectorState.Loud) }</td>
|
||||||
|
</tr>);
|
||||||
|
|
||||||
|
let sectionName: TranslatedString;
|
||||||
|
switch (category) {
|
||||||
|
case RuleClass.VectorGlobal:
|
||||||
|
sectionName = _t("Global");
|
||||||
|
break;
|
||||||
|
case RuleClass.VectorMentions:
|
||||||
|
sectionName = _t("Mentions & keywords");
|
||||||
|
break;
|
||||||
|
case RuleClass.VectorOther:
|
||||||
|
sectionName = _t("Other");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Developer error: Unnamed notifications section: " + category);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<table className='mx_UserNotifSettings_pushRulesTable'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{ sectionName }</th>
|
||||||
|
<th>{ _t("Off") }</th>
|
||||||
|
<th>{ _t("On") }</th>
|
||||||
|
<th>{ _t("Noisy") }</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{ rows }
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{ clearNotifsButton }
|
||||||
|
{ keywordComposer }
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderTargets() {
|
||||||
|
if (this.isInhibited) return null; // no targets if there's no notifications
|
||||||
|
|
||||||
|
const rows = this.state.pushers.map(p => <tr key={p.kind+p.pushkey}>
|
||||||
|
<td>{ p.app_display_name }</td>
|
||||||
|
<td>{ p.device_display_name }</td>
|
||||||
|
</tr>);
|
||||||
|
|
||||||
|
if (!rows.length) return null; // no targets to show
|
||||||
|
|
||||||
|
return <div className='mx_UserNotifSettings_floatingSection'>
|
||||||
|
<div>{ _t("Notification targets") }</div>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{ rows }
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (this.state.phase === Phase.Loading) {
|
||||||
|
// Ends up default centered
|
||||||
|
return <Spinner />;
|
||||||
|
} else if (this.state.phase === Phase.Error) {
|
||||||
|
return <p>{ _t("There was an error loading your notification settings.") }</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className='mx_UserNotifSettings'>
|
||||||
|
{ this.renderTopSection() }
|
||||||
|
{ this.renderCategory(RuleClass.VectorGlobal) }
|
||||||
|
{ this.renderCategory(RuleClass.VectorMentions) }
|
||||||
|
{ this.renderCategory(RuleClass.VectorOther) }
|
||||||
|
{ this.renderTargets() }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,7 +35,7 @@ interface SpellCheckLanguagesIState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> {
|
export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> {
|
||||||
_onRemove = (e) => {
|
private onRemove = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellChe
|
||||||
return (
|
return (
|
||||||
<div className="mx_ExistingSpellCheckLanguage">
|
<div className="mx_ExistingSpellCheckLanguage">
|
||||||
<span className="mx_ExistingSpellCheckLanguage_language">{ this.props.language }</span>
|
<span className="mx_ExistingSpellCheckLanguage_language">{ this.props.language }</span>
|
||||||
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
|
<AccessibleButton onClick={this.onRemove} kind="danger_sm">
|
||||||
{ _t("Remove") }
|
{ _t("Remove") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -63,12 +63,12 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRemoved = (language) => {
|
private onRemoved = (language: string) => {
|
||||||
const languages = this.props.languages.filter((e) => e !== language);
|
const languages = this.props.languages.filter((e) => e !== language);
|
||||||
this.props.onLanguagesChange(languages);
|
this.props.onLanguagesChange(languages);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onAddClick = (e) => {
|
private onAddClick = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -81,18 +81,18 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
|
||||||
this.props.onLanguagesChange(this.props.languages);
|
this.props.onLanguagesChange(this.props.languages);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onNewLanguageChange = (language: string) => {
|
private onNewLanguageChange = (language: string) => {
|
||||||
if (this.state.newLanguage === language) return;
|
if (this.state.newLanguage === language) return;
|
||||||
this.setState({ newLanguage: language });
|
this.setState({ newLanguage: language });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const existingSpellCheckLanguages = this.props.languages.map((e) => {
|
const existingSpellCheckLanguages = this.props.languages.map((e) => {
|
||||||
return <ExistingSpellCheckLanguage language={e} onRemoved={this._onRemoved} key={e} />;
|
return <ExistingSpellCheckLanguage language={e} onRemoved={this.onRemoved} key={e} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
const addButton = (
|
const addButton = (
|
||||||
<AccessibleButton onClick={this._onAddClick} kind="primary">
|
<AccessibleButton onClick={this.onAddClick} kind="primary">
|
||||||
{ _t("Add") }
|
{ _t("Add") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
@ -100,11 +100,11 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
|
||||||
return (
|
return (
|
||||||
<div className="mx_SpellCheckLanguages">
|
<div className="mx_SpellCheckLanguages">
|
||||||
{ existingSpellCheckLanguages }
|
{ existingSpellCheckLanguages }
|
||||||
<form onSubmit={this._onAddClick} noValidate={true}>
|
<form onSubmit={this.onAddClick} noValidate={true}>
|
||||||
<SpellCheckLanguagesDropdown
|
<SpellCheckLanguagesDropdown
|
||||||
className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
|
className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
|
||||||
value={this.state.newLanguage}
|
value={this.state.newLanguage}
|
||||||
onOptionChange={this._onNewLanguageChange} />
|
onOptionChange={this.onNewLanguageChange} />
|
||||||
{ addButton }
|
{ addButton }
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -78,7 +78,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Move this to constructor
|
// TODO: [REACT-WARNING] Move this to constructor
|
||||||
async UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
async UNSAFE_componentWillMount() { // eslint-disable-line
|
||||||
MatrixClientPeg.get().on("RoomState.events", this.onStateEvent);
|
MatrixClientPeg.get().on("RoomState.events", this.onStateEvent);
|
||||||
|
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||||
|
|
|
@ -37,6 +37,8 @@ import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
||||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||||
import { Layout } from "../../../../../settings/Layout";
|
import { Layout } from "../../../../../settings/Layout";
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import StyledRadioButton from '../../../elements/StyledRadioButton';
|
||||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
import { compare } from "../../../../../utils/strings";
|
import { compare } from "../../../../../utils/strings";
|
||||||
|
|
||||||
|
@ -241,6 +243,19 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
this.setState({ customThemeUrl: e.target.value });
|
this.setState({ customThemeUrl: e.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onLayoutChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
let layout;
|
||||||
|
switch (e.target.value) {
|
||||||
|
case "irc": layout = Layout.IRC; break;
|
||||||
|
case "group": layout = Layout.Group; break;
|
||||||
|
case "bubble": layout = Layout.Bubble; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ layout: layout });
|
||||||
|
|
||||||
|
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, layout);
|
||||||
|
};
|
||||||
|
|
||||||
private onIRCLayoutChange = (enabled: boolean) => {
|
private onIRCLayoutChange = (enabled: boolean) => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
this.setState({ layout: Layout.IRC });
|
this.setState({ layout: Layout.IRC });
|
||||||
|
@ -373,6 +388,77 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderLayoutSection = () => {
|
||||||
|
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Layout">
|
||||||
|
<span className="mx_SettingsTab_subheading">{ _t("Message layout") }</span>
|
||||||
|
|
||||||
|
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons">
|
||||||
|
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
||||||
|
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.IRC,
|
||||||
|
})}>
|
||||||
|
<EventTilePreview
|
||||||
|
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||||
|
message={this.MESSAGE_PREVIEW_TEXT}
|
||||||
|
layout={Layout.IRC}
|
||||||
|
userId={this.state.userId}
|
||||||
|
displayName={this.state.displayName}
|
||||||
|
avatarUrl={this.state.avatarUrl}
|
||||||
|
/>
|
||||||
|
<StyledRadioButton
|
||||||
|
name="layout"
|
||||||
|
value="irc"
|
||||||
|
checked={this.state.layout === Layout.IRC}
|
||||||
|
onChange={this.onLayoutChange}
|
||||||
|
>
|
||||||
|
{ _t("IRC") }
|
||||||
|
</StyledRadioButton>
|
||||||
|
</div>
|
||||||
|
<div className="mx_AppearanceUserSettingsTab_spacer" />
|
||||||
|
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
||||||
|
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.Group,
|
||||||
|
})}>
|
||||||
|
<EventTilePreview
|
||||||
|
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||||
|
message={this.MESSAGE_PREVIEW_TEXT}
|
||||||
|
layout={Layout.Group}
|
||||||
|
userId={this.state.userId}
|
||||||
|
displayName={this.state.displayName}
|
||||||
|
avatarUrl={this.state.avatarUrl}
|
||||||
|
/>
|
||||||
|
<StyledRadioButton
|
||||||
|
name="layout"
|
||||||
|
value="group"
|
||||||
|
checked={this.state.layout == Layout.Group}
|
||||||
|
onChange={this.onLayoutChange}
|
||||||
|
>
|
||||||
|
{ _t("Modern") }
|
||||||
|
</StyledRadioButton>
|
||||||
|
</div>
|
||||||
|
<div className="mx_AppearanceUserSettingsTab_spacer" />
|
||||||
|
<div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
|
||||||
|
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout === Layout.Bubble,
|
||||||
|
})}>
|
||||||
|
<EventTilePreview
|
||||||
|
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
|
||||||
|
message={this.MESSAGE_PREVIEW_TEXT}
|
||||||
|
layout={Layout.Bubble}
|
||||||
|
userId={this.state.userId}
|
||||||
|
displayName={this.state.displayName}
|
||||||
|
avatarUrl={this.state.avatarUrl}
|
||||||
|
/>
|
||||||
|
<StyledRadioButton
|
||||||
|
name="layout"
|
||||||
|
value="bubble"
|
||||||
|
checked={this.state.layout == Layout.Bubble}
|
||||||
|
onChange={this.onLayoutChange}
|
||||||
|
>
|
||||||
|
{ _t("Message bubbles") }
|
||||||
|
</StyledRadioButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
private renderAdvancedSection() {
|
private renderAdvancedSection() {
|
||||||
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
|
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
|
||||||
|
|
||||||
|
@ -396,14 +482,17 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
name="useCompactLayout"
|
name="useCompactLayout"
|
||||||
level={SettingLevel.DEVICE}
|
level={SettingLevel.DEVICE}
|
||||||
useCheckbox={true}
|
useCheckbox={true}
|
||||||
disabled={this.state.layout == Layout.IRC}
|
disabled={this.state.layout !== Layout.Group}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{ !SettingsStore.getValue("feature_new_layout_switcher") ?
|
||||||
<StyledCheckbox
|
<StyledCheckbox
|
||||||
checked={this.state.layout == Layout.IRC}
|
checked={this.state.layout == Layout.IRC}
|
||||||
onChange={(ev) => this.onIRCLayoutChange(ev.target.checked)}
|
onChange={(ev) => this.onIRCLayoutChange(ev.target.checked)}
|
||||||
>
|
>
|
||||||
{ _t("Enable experimental, compact IRC style layout") }
|
{ _t("Enable experimental, compact IRC style layout") }
|
||||||
</StyledCheckbox>
|
</StyledCheckbox> : null
|
||||||
|
}
|
||||||
|
|
||||||
<SettingsFlag
|
<SettingsFlag
|
||||||
name="useSystemFont"
|
name="useSystemFont"
|
||||||
|
@ -444,6 +533,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
{ _t("Appearance Settings only affect this %(brand)s session.", { brand }) }
|
{ _t("Appearance Settings only affect this %(brand)s session.", { brand }) }
|
||||||
</div>
|
</div>
|
||||||
{ this.renderThemeSection() }
|
{ this.renderThemeSection() }
|
||||||
|
{ SettingsStore.getValue("feature_new_layout_switcher") ? this.renderLayoutSection() : null }
|
||||||
{ this.renderFontSection() }
|
{ this.renderFontSection() }
|
||||||
{ this.renderAdvancedSection() }
|
{ this.renderAdvancedSection() }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,17 +16,12 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import * as sdk from "../../../../../index";
|
|
||||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
|
import Notifications from "../../Notifications";
|
||||||
|
|
||||||
@replaceableComponent("views.settings.tabs.user.NotificationUserSettingsTab")
|
@replaceableComponent("views.settings.tabs.user.NotificationUserSettingsTab")
|
||||||
export default class NotificationUserSettingsTab extends React.Component {
|
export default class NotificationUserSettingsTab extends React.Component {
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const Notifications = sdk.getComponent("views.settings.Notifications");
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab mx_NotificationUserSettingsTab">
|
<div className="mx_SettingsTab mx_NotificationUserSettingsTab">
|
||||||
<div className="mx_SettingsTab_heading">{ _t("Notifications") }</div>
|
<div className="mx_SettingsTab_heading">{ _t("Notifications") }</div>
|
|
@ -60,14 +60,14 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
|
||||||
this.setState({ counter });
|
this.setState({ counter });
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
request.on("change", this._checkRequestIsPending);
|
request.on("change", this.checkRequestIsPending);
|
||||||
// We should probably have a separate class managing the active verification toasts,
|
// We should probably have a separate class managing the active verification toasts,
|
||||||
// rather than monitoring this in the toast component itself, since we'll get problems
|
// rather than monitoring this in the toast component itself, since we'll get problems
|
||||||
// like the toasdt not going away when the verification is cancelled unless it's the
|
// like the toasdt not going away when the verification is cancelled unless it's the
|
||||||
// one on the top (ie. the one that's mounted).
|
// one on the top (ie. the one that's mounted).
|
||||||
// As a quick & dirty fix, check the toast is still relevant when it mounts (this prevents
|
// As a quick & dirty fix, check the toast is still relevant when it mounts (this prevents
|
||||||
// a toast hanging around after logging in if you did a verification as part of login).
|
// a toast hanging around after logging in if you did a verification as part of login).
|
||||||
this._checkRequestIsPending();
|
this.checkRequestIsPending();
|
||||||
|
|
||||||
if (request.isSelfVerification) {
|
if (request.isSelfVerification) {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
@ -83,10 +83,10 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
clearInterval(this.intervalHandle);
|
clearInterval(this.intervalHandle);
|
||||||
const { request } = this.props;
|
const { request } = this.props;
|
||||||
request.off("change", this._checkRequestIsPending);
|
request.off("change", this.checkRequestIsPending);
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkRequestIsPending = () => {
|
private checkRequestIsPending = () => {
|
||||||
const { request } = this.props;
|
const { request } = this.props;
|
||||||
if (!request.canAccept) {
|
if (!request.canAccept) {
|
||||||
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
|
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import UIStore from '../../../stores/UIStore';
|
import UIStore from '../../../stores/UIStore';
|
||||||
import { lerp } from '../../../utils/AnimationUtils';
|
import { lerp } from '../../../utils/AnimationUtils';
|
||||||
import { MarkedExecution } from '../../../utils/MarkedExecution';
|
import { MarkedExecution } from '../../../utils/MarkedExecution';
|
||||||
|
import { EventSubscription } from 'fbemitter';
|
||||||
|
|
||||||
const PIP_VIEW_WIDTH = 336;
|
const PIP_VIEW_WIDTH = 336;
|
||||||
const PIP_VIEW_HEIGHT = 232;
|
const PIP_VIEW_HEIGHT = 232;
|
||||||
|
@ -108,7 +109,7 @@ function getPrimarySecondaryCalls(calls: MatrixCall[]): [MatrixCall, MatrixCall[
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("views.voip.CallPreview")
|
@replaceableComponent("views.voip.CallPreview")
|
||||||
export default class CallPreview extends React.Component<IProps, IState> {
|
export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
private roomStoreToken: any;
|
private roomStoreToken: EventSubscription;
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private settingsWatcherRef: string;
|
private settingsWatcherRef: string;
|
||||||
private callViewWrapper = createRef<HTMLDivElement>();
|
private callViewWrapper = createRef<HTMLDivElement>();
|
||||||
|
@ -240,7 +241,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
this.scheduledUpdate.mark();
|
this.scheduledUpdate.mark();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRoomViewStoreUpdate = (payload) => {
|
private onRoomViewStoreUpdate = () => {
|
||||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||||
|
|
||||||
const roomId = RoomViewStore.getRoomId();
|
const roomId = RoomViewStore.getRoomId();
|
||||||
|
|
|
@ -605,7 +605,9 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
transferee: transfereeName,
|
transferee: transfereeName,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
a: sub => <AccessibleButton kind="link" onClick={this.onTransferClick}>{sub}</AccessibleButton>,
|
a: sub => <AccessibleButton kind="link" onClick={this.onTransferClick}>
|
||||||
|
{ sub }
|
||||||
|
</AccessibleButton>,
|
||||||
},
|
},
|
||||||
) }
|
) }
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -144,7 +144,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_IncomingCallBox_buttons">
|
<div className="mx_IncomingCallBox_buttons">
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className={"mx_IncomingCallBox_decline"}
|
className="mx_IncomingCallBox_decline"
|
||||||
onClick={this.onRejectClick}
|
onClick={this.onRejectClick}
|
||||||
kind="danger"
|
kind="danger"
|
||||||
>
|
>
|
||||||
|
@ -152,7 +152,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<div className="mx_IncomingCallBox_spacer" />
|
<div className="mx_IncomingCallBox_spacer" />
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className={"mx_IncomingCallBox_accept"}
|
className="mx_IncomingCallBox_accept"
|
||||||
onClick={this.onAnswerClick}
|
onClick={this.onAnswerClick}
|
||||||
kind="primary"
|
kind="primary"
|
||||||
>
|
>
|
||||||
|
|
|
@ -41,6 +41,7 @@ const RoomContext = createContext<IState>({
|
||||||
canReply: false,
|
canReply: false,
|
||||||
layout: Layout.Group,
|
layout: Layout.Group,
|
||||||
lowBandwidth: false,
|
lowBandwidth: false,
|
||||||
|
showHiddenEventsInTimeline: false,
|
||||||
showReadReceipts: true,
|
showReadReceipts: true,
|
||||||
showRedactions: true,
|
showRedactions: true,
|
||||||
showJoinLeaves: true,
|
showJoinLeaves: true,
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
|
@ -247,11 +248,11 @@ export function findDMForUser(client: MatrixClient, userId: string): Room {
|
||||||
* NOTE: this assumes you've just created the room and there's not been an opportunity
|
* NOTE: this assumes you've just created the room and there's not been an opportunity
|
||||||
* for other code to run, so we shouldn't miss RoomState.newMember when it comes by.
|
* for other code to run, so we shouldn't miss RoomState.newMember when it comes by.
|
||||||
*/
|
*/
|
||||||
export async function _waitForMember(client: MatrixClient, roomId: string, userId: string, opts = { timeout: 1500 }) {
|
export async function waitForMember(client: MatrixClient, roomId: string, userId: string, opts = { timeout: 1500 }) {
|
||||||
const { timeout } = opts;
|
const { timeout } = opts;
|
||||||
let handler;
|
let handler;
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
handler = function(_event, _roomstate, member) {
|
handler = function(_, __, member: RoomMember) { // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
if (member.userId !== userId) return;
|
if (member.userId !== userId) return;
|
||||||
if (member.roomId !== roomId) return;
|
if (member.roomId !== roomId) return;
|
||||||
resolve(true);
|
resolve(true);
|
||||||
|
@ -324,7 +325,7 @@ export async function ensureDMExists(client: MatrixClient, userId: string): Prom
|
||||||
}
|
}
|
||||||
|
|
||||||
roomId = await createRoom({ encryption, dmUserId: userId, spinner: false, andView: false });
|
roomId = await createRoom({ encryption, dmUserId: userId, spinner: false, andView: false });
|
||||||
await _waitForMember(client, roomId, userId);
|
await waitForMember(client, roomId, userId);
|
||||||
}
|
}
|
||||||
return roomId;
|
return roomId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -274,7 +274,7 @@ abstract class PillPart extends BasePart implements IPillPart {
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper method for subclasses
|
// helper method for subclasses
|
||||||
_setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string) {
|
protected setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string) {
|
||||||
const avatarBackground = `url('${avatarUrl}')`;
|
const avatarBackground = `url('${avatarUrl}')`;
|
||||||
const avatarLetter = `'${initialLetter}'`;
|
const avatarLetter = `'${initialLetter}'`;
|
||||||
// check if the value is changing,
|
// check if the value is changing,
|
||||||
|
@ -354,7 +354,7 @@ class RoomPillPart extends PillPart {
|
||||||
initialLetter = Avatar.getInitialLetter(this.room ? this.room.name : this.resourceId);
|
initialLetter = Avatar.getInitialLetter(this.room ? this.room.name : this.resourceId);
|
||||||
avatarUrl = Avatar.defaultAvatarUrlForString(this.room ? this.room.roomId : this.resourceId);
|
avatarUrl = Avatar.defaultAvatarUrlForString(this.room ? this.room.roomId : this.resourceId);
|
||||||
}
|
}
|
||||||
this._setAvatarVars(node, avatarUrl, initialLetter);
|
this.setAvatarVars(node, avatarUrl, initialLetter);
|
||||||
}
|
}
|
||||||
|
|
||||||
get type(): IPillPart["type"] {
|
get type(): IPillPart["type"] {
|
||||||
|
@ -399,7 +399,7 @@ class UserPillPart extends PillPart {
|
||||||
if (avatarUrl === defaultAvatarUrl) {
|
if (avatarUrl === defaultAvatarUrl) {
|
||||||
initialLetter = Avatar.getInitialLetter(name);
|
initialLetter = Avatar.getInitialLetter(name);
|
||||||
}
|
}
|
||||||
this._setAvatarVars(node, avatarUrl, initialLetter);
|
this.setAvatarVars(node, avatarUrl, initialLetter);
|
||||||
}
|
}
|
||||||
|
|
||||||
get type(): IPillPart["type"] {
|
get type(): IPillPart["type"] {
|
||||||
|
|
|
@ -822,6 +822,7 @@
|
||||||
"Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices",
|
"Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices",
|
||||||
"Enable advanced debugging for the room list": "Enable advanced debugging for the room list",
|
"Enable advanced debugging for the room list": "Enable advanced debugging for the room list",
|
||||||
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
||||||
|
"New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)",
|
||||||
"Font size": "Font size",
|
"Font size": "Font size",
|
||||||
"Use custom size": "Use custom size",
|
"Use custom size": "Use custom size",
|
||||||
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
|
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
|
||||||
|
@ -1133,33 +1134,24 @@
|
||||||
"Connecting to integration manager...": "Connecting to integration manager...",
|
"Connecting to integration manager...": "Connecting to integration manager...",
|
||||||
"Cannot connect to integration manager": "Cannot connect to integration manager",
|
"Cannot connect to integration manager": "Cannot connect to integration manager",
|
||||||
"The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
|
"The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
|
||||||
"Error saving email notification preferences": "Error saving email notification preferences",
|
"Messages containing keywords": "Messages containing keywords",
|
||||||
"An error occurred whilst saving your email notification preferences.": "An error occurred whilst saving your email notification preferences.",
|
"Error saving notification preferences": "Error saving notification preferences",
|
||||||
"Keywords": "Keywords",
|
"An error occurred whilst saving your notification preferences.": "An error occurred whilst saving your notification preferences.",
|
||||||
"Enter keywords separated by a comma:": "Enter keywords separated by a comma:",
|
"Enable for this account": "Enable for this account",
|
||||||
"Failed to change settings": "Failed to change settings",
|
"Enable email notifications for %(email)s": "Enable email notifications for %(email)s",
|
||||||
"Can't update user notification settings": "Can't update user notification settings",
|
|
||||||
"Failed to update keywords": "Failed to update keywords",
|
|
||||||
"Messages containing <span>keywords</span>": "Messages containing <span>keywords</span>",
|
|
||||||
"Notify for all other messages/rooms": "Notify for all other messages/rooms",
|
|
||||||
"Notify me for anything else": "Notify me for anything else",
|
|
||||||
"Enable notifications for this account": "Enable notifications for this account",
|
|
||||||
"Clear notifications": "Clear notifications",
|
|
||||||
"All notifications are currently disabled for all targets.": "All notifications are currently disabled for all targets.",
|
|
||||||
"Enable email notifications": "Enable email notifications",
|
|
||||||
"Add an email address to configure email notifications": "Add an email address to configure email notifications",
|
|
||||||
"Notifications on the following keywords follow rules which can’t be displayed here:": "Notifications on the following keywords follow rules which can’t be displayed here:",
|
|
||||||
"Unable to fetch notification target list": "Unable to fetch notification target list",
|
|
||||||
"Notification targets": "Notification targets",
|
|
||||||
"Advanced notification settings": "Advanced notification settings",
|
|
||||||
"There are advanced notifications which are not shown here.": "There are advanced notifications which are not shown here.",
|
|
||||||
"You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.",
|
|
||||||
"Enable desktop notifications for this session": "Enable desktop notifications for this session",
|
"Enable desktop notifications for this session": "Enable desktop notifications for this session",
|
||||||
"Show message in desktop notification": "Show message in desktop notification",
|
"Show message in desktop notification": "Show message in desktop notification",
|
||||||
"Enable audible notifications for this session": "Enable audible notifications for this session",
|
"Enable audible notifications for this session": "Enable audible notifications for this session",
|
||||||
|
"Clear notifications": "Clear notifications",
|
||||||
|
"Keyword": "Keyword",
|
||||||
|
"New keyword": "New keyword",
|
||||||
|
"Global": "Global",
|
||||||
|
"Mentions & keywords": "Mentions & keywords",
|
||||||
"Off": "Off",
|
"Off": "Off",
|
||||||
"On": "On",
|
"On": "On",
|
||||||
"Noisy": "Noisy",
|
"Noisy": "Noisy",
|
||||||
|
"Notification targets": "Notification targets",
|
||||||
|
"There was an error loading your notification settings.": "There was an error loading your notification settings.",
|
||||||
"Failed to save your profile": "Failed to save your profile",
|
"Failed to save your profile": "Failed to save your profile",
|
||||||
"The operation could not be completed": "The operation could not be completed",
|
"The operation could not be completed": "The operation could not be completed",
|
||||||
"<a>Upgrade</a> to your own domain": "<a>Upgrade</a> to your own domain",
|
"<a>Upgrade</a> to your own domain": "<a>Upgrade</a> to your own domain",
|
||||||
|
@ -1254,6 +1246,10 @@
|
||||||
"Custom theme URL": "Custom theme URL",
|
"Custom theme URL": "Custom theme URL",
|
||||||
"Add theme": "Add theme",
|
"Add theme": "Add theme",
|
||||||
"Theme": "Theme",
|
"Theme": "Theme",
|
||||||
|
"Message layout": "Message layout",
|
||||||
|
"IRC": "IRC",
|
||||||
|
"Modern": "Modern",
|
||||||
|
"Message bubbles": "Message bubbles",
|
||||||
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
|
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
|
||||||
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
|
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
|
||||||
"Customise your appearance": "Customise your appearance",
|
"Customise your appearance": "Customise your appearance",
|
||||||
|
@ -1660,7 +1656,6 @@
|
||||||
"Show %(count)s more|other": "Show %(count)s more",
|
"Show %(count)s more|other": "Show %(count)s more",
|
||||||
"Show %(count)s more|one": "Show %(count)s more",
|
"Show %(count)s more|one": "Show %(count)s more",
|
||||||
"Show less": "Show less",
|
"Show less": "Show less",
|
||||||
"Use default": "Use default",
|
|
||||||
"All messages": "All messages",
|
"All messages": "All messages",
|
||||||
"Mentions & Keywords": "Mentions & Keywords",
|
"Mentions & Keywords": "Mentions & Keywords",
|
||||||
"Notification options": "Notification options",
|
"Notification options": "Notification options",
|
||||||
|
@ -1669,7 +1664,7 @@
|
||||||
"Favourite": "Favourite",
|
"Favourite": "Favourite",
|
||||||
"Low Priority": "Low Priority",
|
"Low Priority": "Low Priority",
|
||||||
"Invite People": "Invite People",
|
"Invite People": "Invite People",
|
||||||
"Copy Link": "Copy Link",
|
"Copy Room Link": "Copy Room Link",
|
||||||
"Leave Room": "Leave Room",
|
"Leave Room": "Leave Room",
|
||||||
"Room options": "Room options",
|
"Room options": "Room options",
|
||||||
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
|
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
|
||||||
|
|
|
@ -67,7 +67,7 @@ export function getUserLanguage(): string {
|
||||||
|
|
||||||
// Function which only purpose is to mark that a string is translatable
|
// Function which only purpose is to mark that a string is translatable
|
||||||
// Does not actually do anything. It's helpful for automatic extraction of translatable strings
|
// Does not actually do anything. It's helpful for automatic extraction of translatable strings
|
||||||
export function _td(s: string): string {
|
export function _td(s: string): string { // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,6 +132,8 @@ export type TranslatedString = string | React.ReactNode;
|
||||||
*
|
*
|
||||||
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
|
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
|
||||||
*/
|
*/
|
||||||
|
// eslint-next-line @typescript-eslint/naming-convention
|
||||||
|
// eslint-nexline @typescript-eslint/naming-convention
|
||||||
export function _t(text: string, variables?: IVariables): string;
|
export function _t(text: string, variables?: IVariables): string;
|
||||||
export function _t(text: string, variables: IVariables, tags: Tags): React.ReactNode;
|
export function _t(text: string, variables: IVariables, tags: Tags): React.ReactNode;
|
||||||
export function _t(text: string, variables?: IVariables, tags?: Tags): TranslatedString {
|
export function _t(text: string, variables?: IVariables, tags?: Tags): TranslatedString {
|
||||||
|
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
import { ALL_RULE_TYPES, BanList } from "./BanList";
|
import { ALL_RULE_TYPES, BanList } from "./BanList";
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
@ -21,19 +22,17 @@ import { _t } from "../languageHandler";
|
||||||
import dis from "../dispatcher/dispatcher";
|
import dis from "../dispatcher/dispatcher";
|
||||||
import { SettingLevel } from "../settings/SettingLevel";
|
import { SettingLevel } from "../settings/SettingLevel";
|
||||||
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
import { ActionPayload } from "../dispatcher/payloads";
|
||||||
|
|
||||||
// TODO: Move this and related files to the js-sdk or something once finalized.
|
// TODO: Move this and related files to the js-sdk or something once finalized.
|
||||||
|
|
||||||
export class Mjolnir {
|
export class Mjolnir {
|
||||||
static _instance: Mjolnir = null;
|
private static instance: Mjolnir = null;
|
||||||
|
|
||||||
_lists: BanList[] = [];
|
private _lists: BanList[] = []; // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
_roomIds: string[] = [];
|
private _roomIds: string[] = []; // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
_mjolnirWatchRef = null;
|
private mjolnirWatchRef: string = null;
|
||||||
_dispatcherRef = null;
|
private dispatcherRef: string = null;
|
||||||
|
|
||||||
constructor() {
|
|
||||||
}
|
|
||||||
|
|
||||||
get roomIds(): string[] {
|
get roomIds(): string[] {
|
||||||
return this._roomIds;
|
return this._roomIds;
|
||||||
|
@ -44,16 +43,16 @@ export class Mjolnir {
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
this._mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this._onListsChanged.bind(this));
|
this.mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this.onListsChanged.bind(this));
|
||||||
|
|
||||||
this._dispatcherRef = dis.register(this._onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'do_after_sync_prepared',
|
action: 'do_after_sync_prepared',
|
||||||
deferred_action: { action: 'setup_mjolnir' },
|
deferred_action: { action: 'setup_mjolnir' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAction = (payload) => {
|
private onAction = (payload: ActionPayload) => {
|
||||||
if (payload['action'] === 'setup_mjolnir') {
|
if (payload['action'] === 'setup_mjolnir') {
|
||||||
console.log("Setting up Mjolnir: after sync");
|
console.log("Setting up Mjolnir: after sync");
|
||||||
this.setup();
|
this.setup();
|
||||||
|
@ -62,23 +61,23 @@ export class Mjolnir {
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
if (!MatrixClientPeg.get()) return;
|
if (!MatrixClientPeg.get()) return;
|
||||||
this._updateLists(SettingsStore.getValue("mjolnirRooms"));
|
this.updateLists(SettingsStore.getValue("mjolnirRooms"));
|
||||||
MatrixClientPeg.get().on("RoomState.events", this._onEvent);
|
MatrixClientPeg.get().on("RoomState.events", this.onEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (this._mjolnirWatchRef) {
|
if (this.mjolnirWatchRef) {
|
||||||
SettingsStore.unwatchSetting(this._mjolnirWatchRef);
|
SettingsStore.unwatchSetting(this.mjolnirWatchRef);
|
||||||
this._mjolnirWatchRef = null;
|
this.mjolnirWatchRef = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._dispatcherRef) {
|
if (this.dispatcherRef) {
|
||||||
dis.unregister(this._dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
this._dispatcherRef = null;
|
this.dispatcherRef = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!MatrixClientPeg.get()) return;
|
if (!MatrixClientPeg.get()) return;
|
||||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onEvent);
|
MatrixClientPeg.get().removeListener("RoomState.events", this.onEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrCreatePersonalList(): Promise<BanList> {
|
async getOrCreatePersonalList(): Promise<BanList> {
|
||||||
|
@ -132,20 +131,20 @@ export class Mjolnir {
|
||||||
this._lists = this._lists.filter(b => b.roomId !== roomId);
|
this._lists = this._lists.filter(b => b.roomId !== roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onEvent = (event) => {
|
private onEvent = (event: MatrixEvent) => {
|
||||||
if (!MatrixClientPeg.get()) return;
|
if (!MatrixClientPeg.get()) return;
|
||||||
if (!this._roomIds.includes(event.getRoomId())) return;
|
if (!this._roomIds.includes(event.getRoomId())) return;
|
||||||
if (!ALL_RULE_TYPES.includes(event.getType())) return;
|
if (!ALL_RULE_TYPES.includes(event.getType())) return;
|
||||||
|
|
||||||
this._updateLists(this._roomIds);
|
this.updateLists(this._roomIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onListsChanged(settingName, roomId, atLevel, newValue) {
|
private onListsChanged(settingName: string, roomId: string, atLevel: SettingLevel, newValue: string[]) {
|
||||||
// We know that ban lists are only recorded at one level so we don't need to re-eval them
|
// We know that ban lists are only recorded at one level so we don't need to re-eval them
|
||||||
this._updateLists(newValue);
|
this.updateLists(newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateLists(listRoomIds: string[]) {
|
private updateLists(listRoomIds: string[]) {
|
||||||
if (!MatrixClientPeg.get()) return;
|
if (!MatrixClientPeg.get()) return;
|
||||||
|
|
||||||
console.log("Updating Mjolnir ban lists to: " + listRoomIds);
|
console.log("Updating Mjolnir ban lists to: " + listRoomIds);
|
||||||
|
@ -182,10 +181,10 @@ export class Mjolnir {
|
||||||
}
|
}
|
||||||
|
|
||||||
static sharedInstance(): Mjolnir {
|
static sharedInstance(): Mjolnir {
|
||||||
if (!Mjolnir._instance) {
|
if (!Mjolnir.instance) {
|
||||||
Mjolnir._instance = new Mjolnir();
|
Mjolnir.instance = new Mjolnir();
|
||||||
}
|
}
|
||||||
return Mjolnir._instance;
|
return Mjolnir.instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,13 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PushRuleVectorState, State } from "./PushRuleVectorState";
|
import { PushRuleVectorState, VectorState } from "./PushRuleVectorState";
|
||||||
import { IExtendedPushRule, IRuleSets } from "./types";
|
import { IAnnotatedPushRule, IPushRules, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules";
|
||||||
|
|
||||||
export interface IContentRules {
|
export interface IContentRules {
|
||||||
vectorState: State;
|
vectorState: VectorState;
|
||||||
rules: IExtendedPushRule[];
|
rules: IAnnotatedPushRule[];
|
||||||
externalRules: IExtendedPushRule[];
|
externalRules: IAnnotatedPushRule[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SCOPE = "global";
|
export const SCOPE = "global";
|
||||||
|
@ -39,9 +38,9 @@ export class ContentRules {
|
||||||
* externalRules: a list of other keyword rules, with states other than
|
* externalRules: a list of other keyword rules, with states other than
|
||||||
* vectorState
|
* vectorState
|
||||||
*/
|
*/
|
||||||
static parseContentRules(rulesets: IRuleSets): IContentRules {
|
public static parseContentRules(rulesets: IPushRules): IContentRules {
|
||||||
// first categorise the keyword rules in terms of their actions
|
// first categorise the keyword rules in terms of their actions
|
||||||
const contentRules = this._categoriseContentRules(rulesets);
|
const contentRules = ContentRules.categoriseContentRules(rulesets);
|
||||||
|
|
||||||
// Decide which content rules to display in Vector UI.
|
// Decide which content rules to display in Vector UI.
|
||||||
// Vector displays a single global rule for a list of keywords
|
// Vector displays a single global rule for a list of keywords
|
||||||
|
@ -59,7 +58,7 @@ export class ContentRules {
|
||||||
|
|
||||||
if (contentRules.loud.length) {
|
if (contentRules.loud.length) {
|
||||||
return {
|
return {
|
||||||
vectorState: State.Loud,
|
vectorState: VectorState.Loud,
|
||||||
rules: contentRules.loud,
|
rules: contentRules.loud,
|
||||||
externalRules: [
|
externalRules: [
|
||||||
...contentRules.loud_but_disabled,
|
...contentRules.loud_but_disabled,
|
||||||
|
@ -70,33 +69,33 @@ export class ContentRules {
|
||||||
};
|
};
|
||||||
} else if (contentRules.loud_but_disabled.length) {
|
} else if (contentRules.loud_but_disabled.length) {
|
||||||
return {
|
return {
|
||||||
vectorState: State.Off,
|
vectorState: VectorState.Off,
|
||||||
rules: contentRules.loud_but_disabled,
|
rules: contentRules.loud_but_disabled,
|
||||||
externalRules: [...contentRules.on, ...contentRules.on_but_disabled, ...contentRules.other],
|
externalRules: [...contentRules.on, ...contentRules.on_but_disabled, ...contentRules.other],
|
||||||
};
|
};
|
||||||
} else if (contentRules.on.length) {
|
} else if (contentRules.on.length) {
|
||||||
return {
|
return {
|
||||||
vectorState: State.On,
|
vectorState: VectorState.On,
|
||||||
rules: contentRules.on,
|
rules: contentRules.on,
|
||||||
externalRules: [...contentRules.on_but_disabled, ...contentRules.other],
|
externalRules: [...contentRules.on_but_disabled, ...contentRules.other],
|
||||||
};
|
};
|
||||||
} else if (contentRules.on_but_disabled.length) {
|
} else if (contentRules.on_but_disabled.length) {
|
||||||
return {
|
return {
|
||||||
vectorState: State.Off,
|
vectorState: VectorState.Off,
|
||||||
rules: contentRules.on_but_disabled,
|
rules: contentRules.on_but_disabled,
|
||||||
externalRules: contentRules.other,
|
externalRules: contentRules.other,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
vectorState: State.On,
|
vectorState: VectorState.On,
|
||||||
rules: [],
|
rules: [],
|
||||||
externalRules: contentRules.other,
|
externalRules: contentRules.other,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static _categoriseContentRules(rulesets: IRuleSets) {
|
private static categoriseContentRules(rulesets: IPushRules) {
|
||||||
const contentRules: Record<"on"|"on_but_disabled"|"loud"|"loud_but_disabled"|"other", IExtendedPushRule[]> = {
|
const contentRules: Record<"on"|"on_but_disabled"|"loud"|"loud_but_disabled"|"other", IAnnotatedPushRule[]> = {
|
||||||
on: [],
|
on: [],
|
||||||
on_but_disabled: [],
|
on_but_disabled: [],
|
||||||
loud: [],
|
loud: [],
|
||||||
|
@ -109,7 +108,7 @@ export class ContentRules {
|
||||||
const r = rulesets.global[kind][i];
|
const r = rulesets.global[kind][i];
|
||||||
|
|
||||||
// check it's not a default rule
|
// check it's not a default rule
|
||||||
if (r.rule_id[0] === '.' || kind !== "content") {
|
if (r.rule_id[0] === '.' || kind !== PushRuleKind.ContentSpecific) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,14 +116,14 @@ export class ContentRules {
|
||||||
r.kind = kind;
|
r.kind = kind;
|
||||||
|
|
||||||
switch (PushRuleVectorState.contentRuleVectorStateKind(r)) {
|
switch (PushRuleVectorState.contentRuleVectorStateKind(r)) {
|
||||||
case State.On:
|
case VectorState.On:
|
||||||
if (r.enabled) {
|
if (r.enabled) {
|
||||||
contentRules.on.push(r);
|
contentRules.on.push(r);
|
||||||
} else {
|
} else {
|
||||||
contentRules.on_but_disabled.push(r);
|
contentRules.on_but_disabled.push(r);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case State.Loud:
|
case VectorState.Loud:
|
||||||
if (r.enabled) {
|
if (r.enabled) {
|
||||||
contentRules.loud.push(r);
|
contentRules.loud.push(r);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Action, Actions } from "./types";
|
import { PushRuleAction, PushRuleActionName, TweakHighlight, TweakSound } from "matrix-js-sdk/src/@types/PushRules";
|
||||||
|
|
||||||
interface IEncodedActions {
|
interface IEncodedActions {
|
||||||
notify: boolean;
|
notify: boolean;
|
||||||
|
@ -30,23 +29,23 @@ export class NotificationUtils {
|
||||||
// "highlight: true/false,
|
// "highlight: true/false,
|
||||||
// }
|
// }
|
||||||
// to a list of push actions.
|
// to a list of push actions.
|
||||||
static encodeActions(action: IEncodedActions) {
|
static encodeActions(action: IEncodedActions): PushRuleAction[] {
|
||||||
const notify = action.notify;
|
const notify = action.notify;
|
||||||
const sound = action.sound;
|
const sound = action.sound;
|
||||||
const highlight = action.highlight;
|
const highlight = action.highlight;
|
||||||
if (notify) {
|
if (notify) {
|
||||||
const actions: Action[] = [Actions.Notify];
|
const actions: PushRuleAction[] = [PushRuleActionName.Notify];
|
||||||
if (sound) {
|
if (sound) {
|
||||||
actions.push({ "set_tweak": "sound", "value": sound });
|
actions.push({ "set_tweak": "sound", "value": sound } as TweakSound);
|
||||||
}
|
}
|
||||||
if (highlight) {
|
if (highlight) {
|
||||||
actions.push({ "set_tweak": "highlight" });
|
actions.push({ "set_tweak": "highlight" } as TweakHighlight);
|
||||||
} else {
|
} else {
|
||||||
actions.push({ "set_tweak": "highlight", "value": false });
|
actions.push({ "set_tweak": "highlight", "value": false } as TweakHighlight);
|
||||||
}
|
}
|
||||||
return actions;
|
return actions;
|
||||||
} else {
|
} else {
|
||||||
return [Actions.DontNotify];
|
return [PushRuleActionName.DontNotify];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,16 +55,16 @@ export class NotificationUtils {
|
||||||
// "highlight: true/false,
|
// "highlight: true/false,
|
||||||
// }
|
// }
|
||||||
// If the actions couldn't be decoded then returns null.
|
// If the actions couldn't be decoded then returns null.
|
||||||
static decodeActions(actions: Action[]): IEncodedActions {
|
static decodeActions(actions: PushRuleAction[]): IEncodedActions {
|
||||||
let notify = false;
|
let notify = false;
|
||||||
let sound = null;
|
let sound = null;
|
||||||
let highlight = false;
|
let highlight = false;
|
||||||
|
|
||||||
for (let i = 0; i < actions.length; ++i) {
|
for (let i = 0; i < actions.length; ++i) {
|
||||||
const action = actions[i];
|
const action = actions[i];
|
||||||
if (action === Actions.Notify) {
|
if (action === PushRuleActionName.Notify) {
|
||||||
notify = true;
|
notify = true;
|
||||||
} else if (action === Actions.DontNotify) {
|
} else if (action === PushRuleActionName.DontNotify) {
|
||||||
notify = false;
|
notify = false;
|
||||||
} else if (typeof action === "object") {
|
} else if (typeof action === "object") {
|
||||||
if (action.set_tweak === "sound") {
|
if (action.set_tweak === "sound") {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,9 +16,9 @@ limitations under the License.
|
||||||
|
|
||||||
import { StandardActions } from "./StandardActions";
|
import { StandardActions } from "./StandardActions";
|
||||||
import { NotificationUtils } from "./NotificationUtils";
|
import { NotificationUtils } from "./NotificationUtils";
|
||||||
import { IPushRule } from "./types";
|
import { IPushRule } from "matrix-js-sdk/src/@types/PushRules";
|
||||||
|
|
||||||
export enum State {
|
export enum VectorState {
|
||||||
/** The push rule is disabled */
|
/** The push rule is disabled */
|
||||||
Off = "off",
|
Off = "off",
|
||||||
/** The user will receive push notification for this rule */
|
/** The user will receive push notification for this rule */
|
||||||
|
@ -31,26 +30,26 @@ export enum State {
|
||||||
|
|
||||||
export class PushRuleVectorState {
|
export class PushRuleVectorState {
|
||||||
// Backwards compatibility (things should probably be using the enum above instead)
|
// Backwards compatibility (things should probably be using the enum above instead)
|
||||||
static OFF = State.Off;
|
static OFF = VectorState.Off;
|
||||||
static ON = State.On;
|
static ON = VectorState.On;
|
||||||
static LOUD = State.Loud;
|
static LOUD = VectorState.Loud;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum for state of a push rule as defined by the Vector UI.
|
* Enum for state of a push rule as defined by the Vector UI.
|
||||||
* @readonly
|
* @readonly
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
static states = State;
|
static states = VectorState;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a PushRuleVectorState to a list of actions
|
* Convert a PushRuleVectorState to a list of actions
|
||||||
*
|
*
|
||||||
* @return [object] list of push-rule actions
|
* @return [object] list of push-rule actions
|
||||||
*/
|
*/
|
||||||
static actionsFor(pushRuleVectorState: State) {
|
static actionsFor(pushRuleVectorState: VectorState) {
|
||||||
if (pushRuleVectorState === State.On) {
|
if (pushRuleVectorState === VectorState.On) {
|
||||||
return StandardActions.ACTION_NOTIFY;
|
return StandardActions.ACTION_NOTIFY;
|
||||||
} else if (pushRuleVectorState === State.Loud) {
|
} else if (pushRuleVectorState === VectorState.Loud) {
|
||||||
return StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND;
|
return StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +61,7 @@ export class PushRuleVectorState {
|
||||||
* category or in PushRuleVectorState.LOUD, regardless of its enabled
|
* category or in PushRuleVectorState.LOUD, regardless of its enabled
|
||||||
* state. Returns null if it does not match these categories.
|
* state. Returns null if it does not match these categories.
|
||||||
*/
|
*/
|
||||||
static contentRuleVectorStateKind(rule: IPushRule): State {
|
static contentRuleVectorStateKind(rule: IPushRule): VectorState {
|
||||||
const decoded = NotificationUtils.decodeActions(rule.actions);
|
const decoded = NotificationUtils.decodeActions(rule.actions);
|
||||||
|
|
||||||
if (!decoded) {
|
if (!decoded) {
|
||||||
|
@ -80,10 +79,10 @@ export class PushRuleVectorState {
|
||||||
let stateKind = null;
|
let stateKind = null;
|
||||||
switch (tweaks) {
|
switch (tweaks) {
|
||||||
case 0:
|
case 0:
|
||||||
stateKind = State.On;
|
stateKind = VectorState.On;
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
stateKind = State.Loud;
|
stateKind = VectorState.Loud;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return stateKind;
|
return stateKind;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,19 +16,24 @@ limitations under the License.
|
||||||
|
|
||||||
import { _td } from '../languageHandler';
|
import { _td } from '../languageHandler';
|
||||||
import { StandardActions } from "./StandardActions";
|
import { StandardActions } from "./StandardActions";
|
||||||
import { PushRuleVectorState } from "./PushRuleVectorState";
|
import { PushRuleVectorState, VectorState } from "./PushRuleVectorState";
|
||||||
import { NotificationUtils } from "./NotificationUtils";
|
import { NotificationUtils } from "./NotificationUtils";
|
||||||
|
import { PushRuleAction, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules";
|
||||||
|
|
||||||
|
type StateToActionsMap = {
|
||||||
|
[state in VectorState]?: PushRuleAction[];
|
||||||
|
};
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
kind: Kind;
|
kind: PushRuleKind;
|
||||||
description: string;
|
description: string;
|
||||||
vectorStateToActions: Action;
|
vectorStateToActions: StateToActionsMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
class VectorPushRuleDefinition {
|
class VectorPushRuleDefinition {
|
||||||
private kind: Kind;
|
private kind: PushRuleKind;
|
||||||
private description: string;
|
private description: string;
|
||||||
private vectorStateToActions: Action;
|
public readonly vectorStateToActions: StateToActionsMap;
|
||||||
|
|
||||||
constructor(opts: IProps) {
|
constructor(opts: IProps) {
|
||||||
this.kind = opts.kind;
|
this.kind = opts.kind;
|
||||||
|
@ -73,73 +77,62 @@ class VectorPushRuleDefinition {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Kind {
|
|
||||||
Override = "override",
|
|
||||||
Underride = "underride",
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Action {
|
|
||||||
on: StandardActions;
|
|
||||||
loud: StandardActions;
|
|
||||||
off: StandardActions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The descriptions of rules managed by the Vector UI.
|
* The descriptions of rules managed by the Vector UI.
|
||||||
*/
|
*/
|
||||||
export const VectorPushRulesDefinitions = {
|
export const VectorPushRulesDefinitions = {
|
||||||
// Messages containing user's display name
|
// Messages containing user's display name
|
||||||
".m.rule.contains_display_name": new VectorPushRuleDefinition({
|
".m.rule.contains_display_name": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Override,
|
kind: PushRuleKind.Override,
|
||||||
description: _td("Messages containing my display name"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Messages containing my display name"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
||||||
off: StandardActions.ACTION_DISABLED,
|
[VectorState.Off]: StandardActions.ACTION_DISABLED,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Messages containing user's username (localpart/MXID)
|
// Messages containing user's username (localpart/MXID)
|
||||||
".m.rule.contains_user_name": new VectorPushRuleDefinition({
|
".m.rule.contains_user_name": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Override,
|
kind: PushRuleKind.Override,
|
||||||
description: _td("Messages containing my username"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Messages containing my username"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
||||||
off: StandardActions.ACTION_DISABLED,
|
[VectorState.Off]: StandardActions.ACTION_DISABLED,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Messages containing @room
|
// Messages containing @room
|
||||||
".m.rule.roomnotif": new VectorPushRuleDefinition({
|
".m.rule.roomnotif": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Override,
|
kind: PushRuleKind.Override,
|
||||||
description: _td("Messages containing @room"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Messages containing @room"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_HIGHLIGHT,
|
[VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT,
|
||||||
off: StandardActions.ACTION_DISABLED,
|
[VectorState.Off]: StandardActions.ACTION_DISABLED,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Messages just sent to the user in a 1:1 room
|
// Messages just sent to the user in a 1:1 room
|
||||||
".m.rule.room_one_to_one": new VectorPushRuleDefinition({
|
".m.rule.room_one_to_one": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Underride,
|
kind: PushRuleKind.Underride,
|
||||||
description: _td("Messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: {
|
vectorStateToActions: {
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||||
off: StandardActions.ACTION_DONT_NOTIFY,
|
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Encrypted messages just sent to the user in a 1:1 room
|
// Encrypted messages just sent to the user in a 1:1 room
|
||||||
".m.rule.encrypted_room_one_to_one": new VectorPushRuleDefinition({
|
".m.rule.encrypted_room_one_to_one": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Underride,
|
kind: PushRuleKind.Underride,
|
||||||
description: _td("Encrypted messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Encrypted messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: {
|
vectorStateToActions: {
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||||
off: StandardActions.ACTION_DONT_NOTIFY,
|
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -147,12 +140,12 @@ export const VectorPushRulesDefinitions = {
|
||||||
// 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined
|
// 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined
|
||||||
// By opposition, all other room messages are from group chat rooms.
|
// By opposition, all other room messages are from group chat rooms.
|
||||||
".m.rule.message": new VectorPushRuleDefinition({
|
".m.rule.message": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Underride,
|
kind: PushRuleKind.Underride,
|
||||||
description: _td("Messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: {
|
vectorStateToActions: {
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||||
off: StandardActions.ACTION_DONT_NOTIFY,
|
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -160,57 +153,57 @@ export const VectorPushRulesDefinitions = {
|
||||||
// Encrypted 1:1 room messages are catched by the .m.rule.encrypted_room_one_to_one rule if any defined
|
// Encrypted 1:1 room messages are catched by the .m.rule.encrypted_room_one_to_one rule if any defined
|
||||||
// By opposition, all other room messages are from group chat rooms.
|
// By opposition, all other room messages are from group chat rooms.
|
||||||
".m.rule.encrypted": new VectorPushRuleDefinition({
|
".m.rule.encrypted": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Underride,
|
kind: PushRuleKind.Underride,
|
||||||
description: _td("Encrypted messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Encrypted messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: {
|
vectorStateToActions: {
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||||
off: StandardActions.ACTION_DONT_NOTIFY,
|
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Invitation for the user
|
// Invitation for the user
|
||||||
".m.rule.invite_for_me": new VectorPushRuleDefinition({
|
".m.rule.invite_for_me": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Underride,
|
kind: PushRuleKind.Underride,
|
||||||
description: _td("When I'm invited to a room"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("When I'm invited to a room"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: {
|
vectorStateToActions: {
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||||
off: StandardActions.ACTION_DISABLED,
|
[VectorState.Off]: StandardActions.ACTION_DISABLED,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Incoming call
|
// Incoming call
|
||||||
".m.rule.call": new VectorPushRuleDefinition({
|
".m.rule.call": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Underride,
|
kind: PushRuleKind.Underride,
|
||||||
description: _td("Call invitation"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Call invitation"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: {
|
vectorStateToActions: {
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_NOTIFY_RING_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_RING_SOUND,
|
||||||
off: StandardActions.ACTION_DISABLED,
|
[VectorState.Off]: StandardActions.ACTION_DISABLED,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Notifications from bots
|
// Notifications from bots
|
||||||
".m.rule.suppress_notices": new VectorPushRuleDefinition({
|
".m.rule.suppress_notices": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Override,
|
kind: PushRuleKind.Override,
|
||||||
description: _td("Messages sent by bot"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Messages sent by bot"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: {
|
vectorStateToActions: {
|
||||||
// .m.rule.suppress_notices is a "negative" rule, we have to invert its enabled value for vector UI
|
// .m.rule.suppress_notices is a "negative" rule, we have to invert its enabled value for vector UI
|
||||||
on: StandardActions.ACTION_DISABLED,
|
[VectorState.On]: StandardActions.ACTION_DISABLED,
|
||||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||||
off: StandardActions.ACTION_DONT_NOTIFY,
|
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Room upgrades (tombstones)
|
// Room upgrades (tombstones)
|
||||||
".m.rule.tombstone": new VectorPushRuleDefinition({
|
".m.rule.tombstone": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Override,
|
kind: PushRuleKind.Override,
|
||||||
description: _td("When rooms are upgraded"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("When rooms are upgraded"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_HIGHLIGHT,
|
[VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT,
|
||||||
off: StandardActions.ACTION_DISABLED,
|
[VectorState.Off]: StandardActions.ACTION_DISABLED,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,114 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export enum NotificationSetting {
|
|
||||||
AllMessages = "all_messages", // .m.rule.message = notify
|
|
||||||
DirectMessagesMentionsKeywords = "dm_mentions_keywords", // .m.rule.message = mark_unread. This is the new default.
|
|
||||||
MentionsKeywordsOnly = "mentions_keywords", // .m.rule.message = mark_unread; .m.rule.room_one_to_one = mark_unread
|
|
||||||
Never = "never", // .m.rule.master = enabled (dont_notify)
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISoundTweak {
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
set_tweak: "sound";
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
export interface IHighlightTweak {
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
set_tweak: "highlight";
|
|
||||||
value?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Tweak = ISoundTweak | IHighlightTweak;
|
|
||||||
|
|
||||||
export enum Actions {
|
|
||||||
Notify = "notify",
|
|
||||||
DontNotify = "dont_notify", // no-op
|
|
||||||
Coalesce = "coalesce", // unused
|
|
||||||
MarkUnread = "mark_unread", // new
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Action = Actions | Tweak;
|
|
||||||
|
|
||||||
// Push rule kinds in descending priority order
|
|
||||||
export enum Kind {
|
|
||||||
Override = "override",
|
|
||||||
ContentSpecific = "content",
|
|
||||||
RoomSpecific = "room",
|
|
||||||
SenderSpecific = "sender",
|
|
||||||
Underride = "underride",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IEventMatchCondition {
|
|
||||||
kind: "event_match";
|
|
||||||
key: string;
|
|
||||||
pattern: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IContainsDisplayNameCondition {
|
|
||||||
kind: "contains_display_name";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IRoomMemberCountCondition {
|
|
||||||
kind: "room_member_count";
|
|
||||||
is: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISenderNotificationPermissionCondition {
|
|
||||||
kind: "sender_notification_permission";
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Condition =
|
|
||||||
IEventMatchCondition |
|
|
||||||
IContainsDisplayNameCondition |
|
|
||||||
IRoomMemberCountCondition |
|
|
||||||
ISenderNotificationPermissionCondition;
|
|
||||||
|
|
||||||
export enum RuleIds {
|
|
||||||
MasterRule = ".m.rule.master", // The master rule (all notifications disabling)
|
|
||||||
MessageRule = ".m.rule.message",
|
|
||||||
EncryptedMessageRule = ".m.rule.encrypted",
|
|
||||||
RoomOneToOneRule = ".m.rule.room_one_to_one",
|
|
||||||
EncryptedRoomOneToOneRule = ".m.rule.room_one_to_one",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IPushRule {
|
|
||||||
enabled: boolean;
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
rule_id: RuleIds | string;
|
|
||||||
actions: Action[];
|
|
||||||
default: boolean;
|
|
||||||
conditions?: Condition[]; // only applicable to `underride` and `override` rules
|
|
||||||
pattern?: string; // only applicable to `content` rules
|
|
||||||
}
|
|
||||||
|
|
||||||
// push rule extended with kind, used by ContentRules and js-sdk's pushprocessor
|
|
||||||
export interface IExtendedPushRule extends IPushRule {
|
|
||||||
kind: Kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IPushRuleSet {
|
|
||||||
override: IPushRule[];
|
|
||||||
content: IPushRule[];
|
|
||||||
room: IPushRule[];
|
|
||||||
sender: IPushRule[];
|
|
||||||
underride: IPushRule[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IRuleSets {
|
|
||||||
global: IPushRuleSet;
|
|
||||||
}
|
|
|
@ -203,7 +203,7 @@ export default async function sendBugReport(bugReportEndpoint: string, opts: IOp
|
||||||
const body = await collectBugReport(opts);
|
const body = await collectBugReport(opts);
|
||||||
|
|
||||||
progressCallback(_t("Uploading logs"));
|
progressCallback(_t("Uploading logs"));
|
||||||
await _submitReport(bugReportEndpoint, body, progressCallback);
|
await submitReport(bugReportEndpoint, body, progressCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -289,10 +289,10 @@ export async function submitFeedback(
|
||||||
body.append(k, extraData[k]);
|
body.append(k, extraData[k]);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {});
|
await submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _submitReport(endpoint: string, body: FormData, progressCallback: (string) => void) {
|
function submitReport(endpoint: string, body: FormData, progressCallback: (str: string) => void) {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const req = new XMLHttpRequest();
|
const req = new XMLHttpRequest();
|
||||||
req.open("POST", endpoint);
|
req.open("POST", endpoint);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
Copyright 2021 Quirin Götz <codeworks@supercable.onl>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -19,7 +20,8 @@ import PropTypes from 'prop-types';
|
||||||
/* TODO: This should be later reworked into something more generic */
|
/* TODO: This should be later reworked into something more generic */
|
||||||
export enum Layout {
|
export enum Layout {
|
||||||
IRC = "irc",
|
IRC = "irc",
|
||||||
Group = "group"
|
Group = "group",
|
||||||
|
Bubble = "bubble",
|
||||||
}
|
}
|
||||||
|
|
||||||
/* We need this because multiple components are still using JavaScript */
|
/* We need this because multiple components are still using JavaScript */
|
||||||
|
|
|
@ -41,6 +41,7 @@ import { Layout } from "./Layout";
|
||||||
import ReducedMotionController from './controllers/ReducedMotionController';
|
import ReducedMotionController from './controllers/ReducedMotionController';
|
||||||
import IncompatibleController from "./controllers/IncompatibleController";
|
import IncompatibleController from "./controllers/IncompatibleController";
|
||||||
import SdkConfig from "../SdkConfig";
|
import SdkConfig from "../SdkConfig";
|
||||||
|
import NewLayoutSwitcherController from './controllers/NewLayoutSwitcherController';
|
||||||
|
|
||||||
// These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
|
// These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
|
||||||
const LEVELS_ROOM_SETTINGS = [
|
const LEVELS_ROOM_SETTINGS = [
|
||||||
|
@ -321,6 +322,13 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
displayName: _td("Show info about bridges in room settings"),
|
displayName: _td("Show info about bridges in room settings"),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"feature_new_layout_switcher": {
|
||||||
|
isFeature: true,
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
displayName: _td("New layout switcher (with message bubbles)"),
|
||||||
|
default: false,
|
||||||
|
controller: new NewLayoutSwitcherController(),
|
||||||
|
},
|
||||||
"RoomList.backgroundImage": {
|
"RoomList.backgroundImage": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
default: null,
|
default: null,
|
||||||
|
|
26
src/settings/controllers/NewLayoutSwitcherController.ts
Normal file
26
src/settings/controllers/NewLayoutSwitcherController.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
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 SettingController from "./SettingController";
|
||||||
|
import { SettingLevel } from "../SettingLevel";
|
||||||
|
import SettingsStore from "../SettingsStore";
|
||||||
|
import { Layout } from "../Layout";
|
||||||
|
|
||||||
|
export default class NewLayoutSwitcherController extends SettingController {
|
||||||
|
public onChange(level: SettingLevel, roomId: string, newValue: any) {
|
||||||
|
// On disabling switch back to Layout.Group if Layout.Bubble
|
||||||
|
if (!newValue && SettingsStore.getValue("layout") == Layout.Bubble) {
|
||||||
|
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -49,7 +49,7 @@ class GroupFilterOrderStore extends Store {
|
||||||
this.__emitChange();
|
this.__emitChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
__onDispatch(payload) {
|
__onDispatch(payload) { // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
// Initialise state after initial sync
|
// Initialise state after initial sync
|
||||||
case 'view_room': {
|
case 'view_room': {
|
||||||
|
|
|
@ -44,7 +44,7 @@ class LifecycleStore extends Store<ActionPayload> {
|
||||||
this.__emitChange();
|
this.__emitChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected __onDispatch(payload: ActionPayload) {
|
protected __onDispatch(payload: ActionPayload) { // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'do_after_sync_prepared':
|
case 'do_after_sync_prepared':
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|
|
@ -144,7 +144,7 @@ export default class RightPanelStore extends Store<ActionPayload> {
|
||||||
this.__emitChange();
|
this.__emitChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
__onDispatch(payload: ActionPayload) {
|
__onDispatch(payload: ActionPayload) { // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'view_room':
|
case 'view_room':
|
||||||
if (payload.room_id === this.lastRoomId) break; // skip this transition, probably a permalink
|
if (payload.room_id === this.lastRoomId) break; // skip this transition, probably a permalink
|
||||||
|
|
|
@ -96,7 +96,7 @@ class RoomViewStore extends Store<ActionPayload> {
|
||||||
this.__emitChange();
|
this.__emitChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
__onDispatch(payload) {
|
__onDispatch(payload) { // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
// view_room:
|
// view_room:
|
||||||
// - room_alias: '#somealias:matrix.org'
|
// - room_alias: '#somealias:matrix.org'
|
||||||
|
@ -429,7 +429,7 @@ class RoomViewStore extends Store<ActionPayload> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let singletonRoomViewStore = null;
|
let singletonRoomViewStore: RoomViewStore = null;
|
||||||
if (!singletonRoomViewStore) {
|
if (!singletonRoomViewStore) {
|
||||||
singletonRoomViewStore = new RoomViewStore();
|
singletonRoomViewStore = new RoomViewStore();
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue