Merge remote-tracking branch 'origin/develop' into travis/voice-messages/download
This commit is contained in:
commit
5994111e5d
325 changed files with 5224 additions and 4075 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",
|
||||||
|
@ -64,8 +65,8 @@
|
||||||
"counterpart": "^0.18.6",
|
"counterpart": "^0.18.6",
|
||||||
"diff-dom": "^4.2.2",
|
"diff-dom": "^4.2.2",
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
"emojibase-data": "^5.1.1",
|
"emojibase-data": "^6.2.0",
|
||||||
"emojibase-regex": "^4.1.1",
|
"emojibase-regex": "^5.1.3",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"filesize": "6.1.0",
|
"filesize": "6.1.0",
|
||||||
|
@ -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";
|
||||||
|
@ -161,6 +162,7 @@
|
||||||
@import "./views/messages/_CreateEvent.scss";
|
@import "./views/messages/_CreateEvent.scss";
|
||||||
@import "./views/messages/_DateSeparator.scss";
|
@import "./views/messages/_DateSeparator.scss";
|
||||||
@import "./views/messages/_EventTileBubble.scss";
|
@import "./views/messages/_EventTileBubble.scss";
|
||||||
|
@import "./views/messages/_CallEvent.scss";
|
||||||
@import "./views/messages/_MEmoteBody.scss";
|
@import "./views/messages/_MEmoteBody.scss";
|
||||||
@import "./views/messages/_MFileBody.scss";
|
@import "./views/messages/_MFileBody.scss";
|
||||||
@import "./views/messages/_MImageBody.scss";
|
@import "./views/messages/_MImageBody.scss";
|
||||||
|
@ -200,6 +202,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 +266,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";
|
||||||
|
|
|
@ -118,10 +118,6 @@ limitations under the License.
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_FilePanel .mx_EventTile:hover .mx_EventTile_line {
|
|
||||||
background-color: $primary-bg-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_FilePanel_empty::before {
|
.mx_FilePanel_empty::before {
|
||||||
mask-image: url('$(res)/img/element-icons/room/files.svg');
|
mask-image: url('$(res)/img/element-icons/room/files.svg');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -30,5 +30,12 @@ limitations under the License.
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
content: '';
|
content: '';
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InfoTooltip_icon_info::before {
|
||||||
mask-image: url('$(res)/img/element-icons/info.svg');
|
mask-image: url('$(res)/img/element-icons/info.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_InfoTooltip_icon_warning::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/warning.svg');
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
154
res/css/views/messages/_CallEvent.scss
Normal file
154
res/css/views/messages/_CallEvent.scss
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
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_CallEvent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
background-color: $dark-panel-bg-color;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 10px auto;
|
||||||
|
max-width: 75%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 60px;
|
||||||
|
|
||||||
|
&.mx_CallEvent_voice {
|
||||||
|
.mx_CallEvent_type_icon::before,
|
||||||
|
.mx_CallEvent_content_button_callBack span::before,
|
||||||
|
.mx_CallEvent_content_button_answer span::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_CallEvent_video {
|
||||||
|
.mx_CallEvent_type_icon::before,
|
||||||
|
.mx_CallEvent_content_button_callBack span::before,
|
||||||
|
.mx_CallEvent_content_button_answer span::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallEvent_info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 12px;
|
||||||
|
|
||||||
|
.mx_CallEvent_info_basic {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: 10px; // To match mx_CallEvent
|
||||||
|
|
||||||
|
.mx_CallEvent_sender {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1.8rem;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallEvent_type {
|
||||||
|
font-weight: 400;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: $font-13px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.mx_CallEvent_type_icon {
|
||||||
|
height: 13px;
|
||||||
|
width: 13px;
|
||||||
|
margin-right: 5px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
height: 13px;
|
||||||
|
width: 13px;
|
||||||
|
background-color: $tertiary-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallEvent_content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
margin-right: 16px;
|
||||||
|
|
||||||
|
.mx_CallEvent_content_button {
|
||||||
|
height: 24px;
|
||||||
|
padding: 0px 12px;
|
||||||
|
margin-left: 8px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
padding: 8px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
background-color: $button-fg-color;
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallEvent_content_button_reject span::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/hangup.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallEvent_content_tooltip {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallEvent_iconButton {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-right: 8px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
background-color: $tertiary-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallEvent_silence::before {
|
||||||
|
mask-image: url('$(res)/img/voip/silence.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallEvent_unSilence::before {
|
||||||
|
mask-image: url('$(res)/img/voip/un-silence.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,154 @@ $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;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.mx_EventTile_line {
|
||||||
|
// To avoid bubble events being highlighted
|
||||||
|
background-color: inherit !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 +451,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 +584,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 +633,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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Binary file not shown.
3
res/img/element-icons/warning.svg
Normal file
3
res/img/element-icons/warning.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.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM6.9806 4.5101C6.9306 3.9401 7.3506 3.4401 7.9206 3.4001C8.4806 3.3601 8.9806 3.7801 9.0406 4.3501V4.5101L8.7206 8.5101C8.6906 8.8801 8.3806 9.1601 8.0106 9.1601H7.9506C7.6006 9.1301 7.3306 8.8601 7.3006 8.5101L6.9806 4.5101ZM8.88012 11.1202C8.88012 11.6062 8.48613 12.0002 8.00012 12.0002C7.51411 12.0002 7.12012 11.6062 7.12012 11.1202C7.12012 10.6342 7.51411 10.2402 8.00012 10.2402C8.48613 10.2402 8.88012 10.6342 8.88012 11.1202Z" fill="#8D99A5"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 713 B |
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) {
|
||||||
|
|
|
@ -99,7 +99,7 @@ const CHECK_PROTOCOLS_ATTEMPTS = 3;
|
||||||
// (and store the ID of their native room)
|
// (and store the ID of their native room)
|
||||||
export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
|
export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
|
||||||
|
|
||||||
export enum AudioID {
|
enum AudioID {
|
||||||
Ring = 'ringAudio',
|
Ring = 'ringAudio',
|
||||||
Ringback = 'ringbackAudio',
|
Ringback = 'ringbackAudio',
|
||||||
CallEnd = 'callendAudio',
|
CallEnd = 'callendAudio',
|
||||||
|
@ -142,6 +142,7 @@ export enum PlaceCallType {
|
||||||
export enum CallHandlerEvent {
|
export enum CallHandlerEvent {
|
||||||
CallsChanged = "calls_changed",
|
CallsChanged = "calls_changed",
|
||||||
CallChangeRoom = "call_change_room",
|
CallChangeRoom = "call_change_room",
|
||||||
|
SilencedCallsChanged = "silenced_calls_changed",
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CallHandler extends EventEmitter {
|
export default class CallHandler extends EventEmitter {
|
||||||
|
@ -164,6 +165,8 @@ export default class CallHandler extends EventEmitter {
|
||||||
// do the async lookup when we get new information and then store these mappings here
|
// do the async lookup when we get new information and then store these mappings here
|
||||||
private assertedIdentityNativeUsers = new Map<string, string>();
|
private assertedIdentityNativeUsers = new Map<string, string>();
|
||||||
|
|
||||||
|
private silencedCalls = new Set<string>(); // callIds
|
||||||
|
|
||||||
static sharedInstance() {
|
static sharedInstance() {
|
||||||
if (!window.mxCallHandler) {
|
if (!window.mxCallHandler) {
|
||||||
window.mxCallHandler = new CallHandler();
|
window.mxCallHandler = new CallHandler();
|
||||||
|
@ -224,6 +227,33 @@ export default class CallHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public silenceCall(callId: string) {
|
||||||
|
this.silencedCalls.add(callId);
|
||||||
|
this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
|
||||||
|
|
||||||
|
// Don't pause audio if we have calls which are still ringing
|
||||||
|
if (this.areAnyCallsUnsilenced()) return;
|
||||||
|
this.pause(AudioID.Ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
public unSilenceCall(callId: string) {
|
||||||
|
this.silencedCalls.delete(callId);
|
||||||
|
this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
|
||||||
|
this.play(AudioID.Ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isCallSilenced(callId: string): boolean {
|
||||||
|
return this.silencedCalls.has(callId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if there is at least one unsilenced call
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
private areAnyCallsUnsilenced(): boolean {
|
||||||
|
return this.calls.size > this.silencedCalls.size;
|
||||||
|
}
|
||||||
|
|
||||||
private async checkProtocols(maxTries) {
|
private async checkProtocols(maxTries) {
|
||||||
try {
|
try {
|
||||||
const protocols = await MatrixClientPeg.get().getThirdpartyProtocols();
|
const protocols = await MatrixClientPeg.get().getThirdpartyProtocols();
|
||||||
|
@ -301,6 +331,13 @@ export default class CallHandler extends EventEmitter {
|
||||||
}, true);
|
}, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public getCallById(callId: string): MatrixCall {
|
||||||
|
for (const call of this.calls.values()) {
|
||||||
|
if (call.callId === callId) return call;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
getCallForRoom(roomId: string): MatrixCall {
|
getCallForRoom(roomId: string): MatrixCall {
|
||||||
return this.calls.get(roomId) || null;
|
return this.calls.get(roomId) || null;
|
||||||
}
|
}
|
||||||
|
@ -441,6 +478,10 @@ export default class CallHandler extends EventEmitter {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newState !== CallState.Ringing) {
|
||||||
|
this.silencedCalls.delete(call.callId);
|
||||||
|
}
|
||||||
|
|
||||||
switch (newState) {
|
switch (newState) {
|
||||||
case CallState.Ringing:
|
case CallState.Ringing:
|
||||||
this.play(AudioID.Ring);
|
this.play(AudioID.Ring);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -33,7 +33,7 @@ import { IExtendedSanitizeOptions } from './@types/sanitize-html';
|
||||||
import linkifyMatrix from './linkify-matrix';
|
import linkifyMatrix from './linkify-matrix';
|
||||||
import SettingsStore from './settings/SettingsStore';
|
import SettingsStore from './settings/SettingsStore';
|
||||||
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
|
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
|
||||||
import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji";
|
import { getEmojiFromUnicode } from "./emoji";
|
||||||
import ReplyThread from "./components/views/elements/ReplyThread";
|
import ReplyThread from "./components/views/elements/ReplyThread";
|
||||||
import { mediaFromMxc } from "./customisations/Media";
|
import { mediaFromMxc } from "./customisations/Media";
|
||||||
|
|
||||||
|
@ -79,20 +79,8 @@ function mightContainEmoji(str: string): boolean {
|
||||||
* @return {String} The shortcode (such as :thumbup:)
|
* @return {String} The shortcode (such as :thumbup:)
|
||||||
*/
|
*/
|
||||||
export function unicodeToShortcode(char: string): string {
|
export function unicodeToShortcode(char: string): string {
|
||||||
const data = getEmojiFromUnicode(char);
|
const shortcodes = getEmojiFromUnicode(char).shortcodes;
|
||||||
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
|
return shortcodes.length > 0 ? `:${shortcodes[0]}:` : '';
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the unicode character for an emoji shortcode
|
|
||||||
*
|
|
||||||
* @param {String} shortcode The shortcode (such as :thumbup:)
|
|
||||||
* @return {String} The emoji character; null if none exists
|
|
||||||
*/
|
|
||||||
export function shortcodeToUnicode(shortcode: string): string {
|
|
||||||
shortcode = shortcode.slice(1, shortcode.length - 1);
|
|
||||||
const data = SHORTCODE_TO_EMOJI.get(shortcode);
|
|
||||||
return data ? data.unicode : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processHtmlForSending(html: string): string {
|
export function processHtmlForSending(html: string): string {
|
||||||
|
|
|
@ -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,9 +13,7 @@ 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 { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import * as Roles from './Roles';
|
import * as Roles from './Roles';
|
||||||
import { isValid3pidInvite } from "./RoomInvite";
|
import { isValid3pidInvite } from "./RoomInvite";
|
||||||
|
@ -32,7 +30,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 +82,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,91 +317,7 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallAnswerEvent(event): () => string | null {
|
function textForThreePidInviteEvent(event: MatrixEvent): () => string | null {
|
||||||
return () => {
|
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
|
||||||
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
|
||||||
return _t('%(senderName)s answered the call.', { senderName }) + ' ' + supported;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function textForCallHangupEvent(event): () => string | null {
|
|
||||||
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
|
||||||
const eventContent = event.getContent();
|
|
||||||
let getReason = () => "";
|
|
||||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
|
||||||
getReason = () => _t('(not supported by this browser)');
|
|
||||||
} else if (eventContent.reason) {
|
|
||||||
if (eventContent.reason === "ice_failed") {
|
|
||||||
// We couldn't establish a connection at all
|
|
||||||
getReason = () => _t('(could not connect media)');
|
|
||||||
} else if (eventContent.reason === "ice_timeout") {
|
|
||||||
// We established a connection but it died
|
|
||||||
getReason = () => _t('(connection failed)');
|
|
||||||
} else if (eventContent.reason === "user_media_failed") {
|
|
||||||
// The other side couldn't open capture devices
|
|
||||||
getReason = () => _t("(their device couldn't start the camera / microphone)");
|
|
||||||
} else if (eventContent.reason === "unknown_error") {
|
|
||||||
// An error code the other side doesn't have a way to express
|
|
||||||
// (as opposed to an error code they gave but we don't know about,
|
|
||||||
// in which case we show the error code)
|
|
||||||
getReason = () => _t("(an error occurred)");
|
|
||||||
} else if (eventContent.reason === "invite_timeout") {
|
|
||||||
getReason = () => _t('(no answer)');
|
|
||||||
} else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") {
|
|
||||||
// workaround for https://github.com/vector-im/element-web/issues/5178
|
|
||||||
// it seems Android randomly sets a reason of "user hangup" which is
|
|
||||||
// interpreted as an error code :(
|
|
||||||
// https://github.com/vector-im/riot-android/issues/2623
|
|
||||||
// Also the correct hangup code as of VoIP v1 (with underscore)
|
|
||||||
getReason = () => '';
|
|
||||||
} else {
|
|
||||||
getReason = () => _t('(unknown failure: %(reason)s)', { reason: eventContent.reason });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return () => _t('%(senderName)s ended the call.', { senderName: getSenderName() }) + ' ' + getReason();
|
|
||||||
}
|
|
||||||
|
|
||||||
function textForCallRejectEvent(event): () => string | null {
|
|
||||||
return () => {
|
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
|
||||||
return _t('%(senderName)s declined the call.', { senderName });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function textForCallInviteEvent(event): () => string | null {
|
|
||||||
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
|
||||||
// FIXME: Find a better way to determine this from the event?
|
|
||||||
let isVoice = true;
|
|
||||||
if (event.getContent().offer && event.getContent().offer.sdp &&
|
|
||||||
event.getContent().offer.sdp.indexOf('m=video') !== -1) {
|
|
||||||
isVoice = false;
|
|
||||||
}
|
|
||||||
const isSupported = MatrixClientPeg.get().supportsVoip();
|
|
||||||
|
|
||||||
// This ladder could be reduced down to a couple string variables, however other languages
|
|
||||||
// can have a hard time translating those strings. In an effort to make translations easier
|
|
||||||
// and more accurate, we break out the string-based variables to a couple booleans.
|
|
||||||
if (isVoice && isSupported) {
|
|
||||||
return () => _t("%(senderName)s placed a voice call.", {
|
|
||||||
senderName: getSenderName(),
|
|
||||||
});
|
|
||||||
} else if (isVoice && !isSupported) {
|
|
||||||
return () => _t("%(senderName)s placed a voice call. (not supported by this browser)", {
|
|
||||||
senderName: getSenderName(),
|
|
||||||
});
|
|
||||||
} else if (!isVoice && isSupported) {
|
|
||||||
return () => _t("%(senderName)s placed a video call.", {
|
|
||||||
senderName: getSenderName(),
|
|
||||||
});
|
|
||||||
} else if (!isVoice && !isSupported) {
|
|
||||||
return () => _t("%(senderName)s placed a video call. (not supported by this browser)", {
|
|
||||||
senderName: getSenderName(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function textForThreePidInviteEvent(event): () => 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 +333,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 +355,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 +437,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 +467,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,15 +560,13 @@ 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 = {
|
||||||
'm.room.message': textForMessageEvent,
|
'm.room.message': textForMessageEvent,
|
||||||
'm.call.invite': textForCallInviteEvent,
|
|
||||||
'm.call.answer': textForCallAnswerEvent,
|
|
||||||
'm.call.hangup': textForCallHangupEvent,
|
|
||||||
'm.call.reject': textForCallRejectEvent,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateHandlers: IHandlers = {
|
const stateHandlers: IHandlers = {
|
||||||
|
@ -682,14 +594,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)?.() || '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,6 @@ import { PillCompletion } from './Components';
|
||||||
import { ICompletion, ISelectionRange } from './Autocompleter';
|
import { ICompletion, ISelectionRange } from './Autocompleter';
|
||||||
import { uniq, sortBy } from 'lodash';
|
import { uniq, sortBy } from 'lodash';
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import { shortcodeToUnicode } from '../HtmlUtils';
|
|
||||||
import { EMOJI, IEmoji } from '../emoji';
|
import { EMOJI, IEmoji } from '../emoji';
|
||||||
|
|
||||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||||
|
@ -36,20 +35,18 @@ const LIMIT = 20;
|
||||||
// anchored to only match from the start of parts otherwise it'll show emoji suggestions whilst typing matrix IDs
|
// anchored to only match from the start of parts otherwise it'll show emoji suggestions whilst typing matrix IDs
|
||||||
const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w]*:?)$', 'g');
|
const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w]*:?)$', 'g');
|
||||||
|
|
||||||
interface IEmojiShort {
|
interface ISortedEmoji {
|
||||||
emoji: IEmoji;
|
emoji: IEmoji;
|
||||||
shortname: string;
|
|
||||||
_orderBy: number;
|
_orderBy: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMOJI_SHORTNAMES: IEmojiShort[] = EMOJI.sort((a, b) => {
|
const SORTED_EMOJI: ISortedEmoji[] = EMOJI.sort((a, b) => {
|
||||||
if (a.group === b.group) {
|
if (a.group === b.group) {
|
||||||
return a.order - b.order;
|
return a.order - b.order;
|
||||||
}
|
}
|
||||||
return a.group - b.group;
|
return a.group - b.group;
|
||||||
}).map((emoji, index) => ({
|
}).map((emoji, index) => ({
|
||||||
emoji,
|
emoji,
|
||||||
shortname: `:${emoji.shortcodes[0]}:`,
|
|
||||||
// Include the index so that we can preserve the original order
|
// Include the index so that we can preserve the original order
|
||||||
_orderBy: index,
|
_orderBy: index,
|
||||||
}));
|
}));
|
||||||
|
@ -64,20 +61,18 @@ function score(query, space) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class EmojiProvider extends AutocompleteProvider {
|
export default class EmojiProvider extends AutocompleteProvider {
|
||||||
matcher: QueryMatcher<IEmojiShort>;
|
matcher: QueryMatcher<ISortedEmoji>;
|
||||||
nameMatcher: QueryMatcher<IEmojiShort>;
|
nameMatcher: QueryMatcher<ISortedEmoji>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(EMOJI_REGEX);
|
super(EMOJI_REGEX);
|
||||||
this.matcher = new QueryMatcher<IEmojiShort>(EMOJI_SHORTNAMES, {
|
this.matcher = new QueryMatcher<ISortedEmoji>(SORTED_EMOJI, {
|
||||||
keys: ['emoji.emoticon', 'shortname'],
|
keys: ['emoji.emoticon'],
|
||||||
funcs: [
|
funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)],
|
||||||
(o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases
|
|
||||||
],
|
|
||||||
// For matching against ascii equivalents
|
// For matching against ascii equivalents
|
||||||
shouldMatchWordsOnly: false,
|
shouldMatchWordsOnly: false,
|
||||||
});
|
});
|
||||||
this.nameMatcher = new QueryMatcher(EMOJI_SHORTNAMES, {
|
this.nameMatcher = new QueryMatcher(SORTED_EMOJI, {
|
||||||
keys: ['emoji.annotation'],
|
keys: ['emoji.annotation'],
|
||||||
// For removing punctuation
|
// For removing punctuation
|
||||||
shouldMatchWordsOnly: true,
|
shouldMatchWordsOnly: true,
|
||||||
|
@ -105,34 +100,33 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
const sorters = [];
|
const sorters = [];
|
||||||
// make sure that emoticons come first
|
// make sure that emoticons come first
|
||||||
sorters.push((c) => score(matchedString, c.emoji.emoticon || ""));
|
sorters.push(c => score(matchedString, c.emoji.emoticon || ""));
|
||||||
|
|
||||||
// then sort by score (Infinity if matchedString not in shortname)
|
// then sort by score (Infinity if matchedString not in shortcode)
|
||||||
sorters.push((c) => score(matchedString, c.shortname));
|
sorters.push(c => score(matchedString, c.emoji.shortcodes[0]));
|
||||||
// then sort by max score of all shortcodes, trim off the `:`
|
// then sort by max score of all shortcodes, trim off the `:`
|
||||||
sorters.push((c) => Math.min(...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s))));
|
sorters.push(c => Math.min(
|
||||||
// If the matchedString is not empty, sort by length of shortname. Example:
|
...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)),
|
||||||
|
));
|
||||||
|
// If the matchedString is not empty, sort by length of shortcode. Example:
|
||||||
// matchedString = ":bookmark"
|
// matchedString = ":bookmark"
|
||||||
// completions = [":bookmark:", ":bookmark_tabs:", ...]
|
// completions = [":bookmark:", ":bookmark_tabs:", ...]
|
||||||
if (matchedString.length > 1) {
|
if (matchedString.length > 1) {
|
||||||
sorters.push((c) => c.shortname.length);
|
sorters.push(c => c.emoji.shortcodes[0].length);
|
||||||
}
|
}
|
||||||
// Finally, sort by original ordering
|
// Finally, sort by original ordering
|
||||||
sorters.push((c) => c._orderBy);
|
sorters.push(c => c._orderBy);
|
||||||
completions = sortBy(uniq(completions), sorters);
|
completions = sortBy(uniq(completions), sorters);
|
||||||
|
|
||||||
completions = completions.map(({ shortname }) => {
|
completions = completions.map(c => ({
|
||||||
const unicode = shortcodeToUnicode(shortname);
|
completion: c.emoji.unicode,
|
||||||
return {
|
|
||||||
completion: unicode,
|
|
||||||
component: (
|
component: (
|
||||||
<PillCompletion title={shortname} aria-label={unicode}>
|
<PillCompletion title={`:${c.emoji.shortcodes[0]}:`} aria-label={c.emoji.unicode}>
|
||||||
<span>{ unicode }</span>
|
<span>{ c.emoji.unicode }</span>
|
||||||
</PillCompletion>
|
</PillCompletion>
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
})).slice(0, LIMIT);
|
||||||
}).slice(0, LIMIT);
|
|
||||||
}
|
}
|
||||||
return completions;
|
return completions;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = {};
|
||||||
|
|
||||||
|
|
145
src/components/structures/CallEventGrouper.ts
Normal file
145
src/components/structures/CallEventGrouper.ts
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
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 { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { CallEvent, CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
|
|
||||||
|
export enum CallEventGrouperEvent {
|
||||||
|
StateChanged = "state_changed",
|
||||||
|
SilencedChanged = "silenced_changed",
|
||||||
|
}
|
||||||
|
|
||||||
|
const SUPPORTED_STATES = [
|
||||||
|
CallState.Connected,
|
||||||
|
CallState.Connecting,
|
||||||
|
CallState.Ringing,
|
||||||
|
];
|
||||||
|
|
||||||
|
export enum CustomCallState {
|
||||||
|
Missed = "missed",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CallEventGrouper extends EventEmitter {
|
||||||
|
private events: Set<MatrixEvent> = new Set<MatrixEvent>();
|
||||||
|
private call: MatrixCall;
|
||||||
|
public state: CallState | CustomCallState;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.setCall);
|
||||||
|
CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get invite(): MatrixEvent {
|
||||||
|
return [...this.events].find((event) => event.getType() === EventType.CallInvite);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get hangup(): MatrixEvent {
|
||||||
|
return [...this.events].find((event) => event.getType() === EventType.CallHangup);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get reject(): MatrixEvent {
|
||||||
|
return [...this.events].find((event) => event.getType() === EventType.CallReject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isVoice(): boolean {
|
||||||
|
const invite = this.invite;
|
||||||
|
if (!invite) return;
|
||||||
|
|
||||||
|
// FIXME: Find a better way to determine this from the event?
|
||||||
|
if (invite.getContent()?.offer?.sdp?.indexOf('m=video') !== -1) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get hangupReason(): string | null {
|
||||||
|
return this.hangup?.getContent()?.reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if there are only events from the other side - we missed the call
|
||||||
|
*/
|
||||||
|
private get callWasMissed(): boolean {
|
||||||
|
return ![...this.events].some((event) => event.sender?.userId === MatrixClientPeg.get().getUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private get callId(): string {
|
||||||
|
return [...this.events][0].getContent().call_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onSilencedCallsChanged = () => {
|
||||||
|
const newState = CallHandler.sharedInstance().isCallSilenced(this.callId);
|
||||||
|
this.emit(CallEventGrouperEvent.SilencedChanged, newState);
|
||||||
|
};
|
||||||
|
|
||||||
|
public answerCall = () => {
|
||||||
|
this.call?.answer();
|
||||||
|
};
|
||||||
|
|
||||||
|
public rejectCall = () => {
|
||||||
|
this.call?.reject();
|
||||||
|
};
|
||||||
|
|
||||||
|
public callBack = () => {
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: 'place_call',
|
||||||
|
type: this.isVoice ? CallType.Voice : CallType.Video,
|
||||||
|
room_id: [...this.events][0]?.getRoomId(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public toggleSilenced = () => {
|
||||||
|
const silenced = CallHandler.sharedInstance().isCallSilenced(this.callId);
|
||||||
|
silenced ?
|
||||||
|
CallHandler.sharedInstance().unSilenceCall(this.callId) :
|
||||||
|
CallHandler.sharedInstance().silenceCall(this.callId);
|
||||||
|
};
|
||||||
|
|
||||||
|
private setCallListeners() {
|
||||||
|
if (!this.call) return;
|
||||||
|
this.call.addListener(CallEvent.State, this.setState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setState = () => {
|
||||||
|
if (SUPPORTED_STATES.includes(this.call?.state)) {
|
||||||
|
this.state = this.call.state;
|
||||||
|
} else {
|
||||||
|
if (this.callWasMissed) this.state = CustomCallState.Missed;
|
||||||
|
else if (this.reject) this.state = CallState.Ended;
|
||||||
|
else if (this.hangup) this.state = CallState.Ended;
|
||||||
|
else if (this.invite && this.call) this.state = CallState.Connecting;
|
||||||
|
}
|
||||||
|
this.emit(CallEventGrouperEvent.StateChanged, this.state);
|
||||||
|
};
|
||||||
|
|
||||||
|
private setCall = () => {
|
||||||
|
if (this.call) return;
|
||||||
|
|
||||||
|
this.call = CallHandler.sharedInstance().getCallById(this.callId);
|
||||||
|
this.setCallListeners();
|
||||||
|
this.setState();
|
||||||
|
};
|
||||||
|
|
||||||
|
public add(event: MatrixEvent) {
|
||||||
|
this.events.add(event);
|
||||||
|
this.setCall();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -36,6 +36,7 @@ import DMRoomMap from "../../utils/DMRoomMap";
|
||||||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import defaultDispatcher from '../../dispatcher/dispatcher';
|
import defaultDispatcher from '../../dispatcher/dispatcher';
|
||||||
|
import CallEventGrouper from "./CallEventGrouper";
|
||||||
import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
|
import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
|
||||||
import ScrollPanel, { IScrollState } from "./ScrollPanel";
|
import ScrollPanel, { IScrollState } from "./ScrollPanel";
|
||||||
import EventListSummary from '../views/elements/EventListSummary';
|
import EventListSummary from '../views/elements/EventListSummary';
|
||||||
|
@ -54,7 +55,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 +79,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;
|
||||||
}
|
}
|
||||||
|
@ -228,6 +233,9 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
private readonly showTypingNotificationsWatcherRef: string;
|
private readonly showTypingNotificationsWatcherRef: string;
|
||||||
private eventNodes: Record<string, HTMLElement>;
|
private eventNodes: Record<string, HTMLElement>;
|
||||||
|
|
||||||
|
// A map of <callId, CallEventGrouper>
|
||||||
|
private callEventGroupers = new Map<string, CallEventGrouper>();
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
@ -239,7 +247,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 +408,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -567,9 +580,23 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
const last = (mxEv === lastShownEvent);
|
const last = (mxEv === lastShownEvent);
|
||||||
const { nextEvent, nextTile } = this.getNextEventInfo(this.props.events, i);
|
const { nextEvent, nextTile } = this.getNextEventInfo(this.props.events, i);
|
||||||
|
|
||||||
|
if (
|
||||||
|
mxEv.getType().indexOf("m.call.") === 0 ||
|
||||||
|
mxEv.getType().indexOf("org.matrix.call.") === 0
|
||||||
|
) {
|
||||||
|
const callId = mxEv.getContent().call_id;
|
||||||
|
if (this.callEventGroupers.has(callId)) {
|
||||||
|
this.callEventGroupers.get(callId).add(mxEv);
|
||||||
|
} else {
|
||||||
|
const callEventGrouper = new CallEventGrouper();
|
||||||
|
callEventGrouper.add(mxEv);
|
||||||
|
this.callEventGroupers.set(callId, callEventGrouper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 +671,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);
|
||||||
|
@ -680,6 +710,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
// it's successful: we received it.
|
// it's successful: we received it.
|
||||||
isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId();
|
isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId();
|
||||||
|
|
||||||
|
const callEventGrouper = this.callEventGroupers.get(mxEv.getContent().call_id);
|
||||||
|
|
||||||
// use txnId as key if available so that we don't remount during sending
|
// use txnId as key if available so that we don't remount during sending
|
||||||
ret.push(
|
ret.push(
|
||||||
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
|
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
|
||||||
|
@ -702,7 +734,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 +742,8 @@ 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}
|
||||||
|
callEventGrouper={callEventGrouper}
|
||||||
|
hideSender={this.props.room.getMembers().length <= 2 && this.props.layout === Layout.Bubble}
|
||||||
/>
|
/>
|
||||||
</TileErrorBoundary>,
|
</TileErrorBoundary>,
|
||||||
);
|
);
|
||||||
|
@ -946,7 +980,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 +1234,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}
|
||||||
|
|
|
@ -22,9 +22,16 @@ import Tooltip, { Alignment } from './Tooltip';
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
export enum InfoTooltipKind {
|
||||||
|
Info = "info",
|
||||||
|
Warning = "warning",
|
||||||
|
}
|
||||||
|
|
||||||
interface ITooltipProps {
|
interface ITooltipProps {
|
||||||
tooltip?: React.ReactNode;
|
tooltip?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
tooltipClassName?: string;
|
tooltipClassName?: string;
|
||||||
|
kind?: InfoTooltipKind;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -53,8 +60,12 @@ export default class InfoTooltip extends React.PureComponent<ITooltipProps, ISta
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { tooltip, children, tooltipClassName } = this.props;
|
const { tooltip, children, tooltipClassName, className, kind } = this.props;
|
||||||
const title = _t("Information");
|
const title = _t("Information");
|
||||||
|
const iconClassName = (
|
||||||
|
(kind !== InfoTooltipKind.Warning) ?
|
||||||
|
"mx_InfoTooltip_icon_info" : "mx_InfoTooltip_icon_warning"
|
||||||
|
);
|
||||||
|
|
||||||
// Tooltip are forced on the right for a more natural feel to them on info icons
|
// Tooltip are forced on the right for a more natural feel to them on info icons
|
||||||
const tip = this.state.hover ? <Tooltip
|
const tip = this.state.hover ? <Tooltip
|
||||||
|
@ -64,8 +75,12 @@ export default class InfoTooltip extends React.PureComponent<ITooltipProps, ISta
|
||||||
alignment={Alignment.Right}
|
alignment={Alignment.Right}
|
||||||
/> : <div />;
|
/> : <div />;
|
||||||
return (
|
return (
|
||||||
<div onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className="mx_InfoTooltip">
|
<div
|
||||||
<span className="mx_InfoTooltip_icon" aria-label={title} />
|
onMouseOver={this.onMouseOver}
|
||||||
|
onMouseLeave={this.onMouseLeave}
|
||||||
|
className={classNames("mx_InfoTooltip", className)}
|
||||||
|
>
|
||||||
|
<span className={classNames("mx_InfoTooltip_icon", iconClassName)} aria-label={title} />
|
||||||
{ children }
|
{ children }
|
||||||
{ tip }
|
{ tip }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -32,6 +32,8 @@ export const CATEGORY_HEADER_HEIGHT = 22;
|
||||||
export const EMOJI_HEIGHT = 37;
|
export const EMOJI_HEIGHT = 37;
|
||||||
export const EMOJIS_PER_ROW = 8;
|
export const EMOJIS_PER_ROW = 8;
|
||||||
|
|
||||||
|
const ZERO_WIDTH_JOINER = "\u200D";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
selectedEmojis?: Set<string>;
|
selectedEmojis?: Set<string>;
|
||||||
showQuickReactions?: boolean;
|
showQuickReactions?: boolean;
|
||||||
|
@ -180,7 +182,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
||||||
} else {
|
} else {
|
||||||
emojis = cat.id === "recent" ? this.recentlyUsed : DATA_BY_CATEGORY[cat.id];
|
emojis = cat.id === "recent" ? this.recentlyUsed : DATA_BY_CATEGORY[cat.id];
|
||||||
}
|
}
|
||||||
emojis = emojis.filter(emoji => emoji.filterString.includes(filter));
|
emojis = emojis.filter(emoji => this.emojiMatchesFilter(emoji, filter));
|
||||||
this.memoizedDataByCategory[cat.id] = emojis;
|
this.memoizedDataByCategory[cat.id] = emojis;
|
||||||
cat.enabled = emojis.length > 0;
|
cat.enabled = emojis.length > 0;
|
||||||
// The setState below doesn't re-render the header and we already have the refs for updateVisibility, so...
|
// The setState below doesn't re-render the header and we already have the refs for updateVisibility, so...
|
||||||
|
@ -192,6 +194,10 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
||||||
setTimeout(this.updateVisibility, 0);
|
setTimeout(this.updateVisibility, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private emojiMatchesFilter = (emoji: IEmoji, filter: string): boolean =>
|
||||||
|
[emoji.annotation, ...emoji.shortcodes, emoji.emoticon, ...emoji.unicode.split(ZERO_WIDTH_JOINER)]
|
||||||
|
.some(x => x?.includes(filter));
|
||||||
|
|
||||||
private onEnterFilter = () => {
|
private onEnterFilter = () => {
|
||||||
const btn = this.bodyRef.current.querySelector<HTMLButtonElement>(".mx_EmojiPicker_item");
|
const btn = this.bodyRef.current.querySelector<HTMLButtonElement>(".mx_EmojiPicker_item");
|
||||||
if (btn) {
|
if (btn) {
|
||||||
|
|
|
@ -27,11 +27,7 @@ interface IProps {
|
||||||
@replaceableComponent("views.emojipicker.Preview")
|
@replaceableComponent("views.emojipicker.Preview")
|
||||||
class Preview extends React.PureComponent<IProps> {
|
class Preview extends React.PureComponent<IProps> {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { unicode, annotation, shortcodes: [shortcode] } = this.props.emoji;
|
||||||
unicode = "",
|
|
||||||
annotation = "",
|
|
||||||
shortcodes: [shortcode = ""],
|
|
||||||
} = this.props.emoji || {};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_EmojiPicker_footer mx_EmojiPicker_preview">
|
<div className="mx_EmojiPicker_footer mx_EmojiPicker_preview">
|
||||||
|
|
218
src/components/views/messages/CallEvent.tsx
Normal file
218
src/components/views/messages/CallEvent.tsx
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
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 { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { _t, _td } from '../../../languageHandler';
|
||||||
|
import MemberAvatar from '../avatars/MemberAvatar';
|
||||||
|
import CallEventGrouper, { CallEventGrouperEvent, CustomCallState } from '../../structures/CallEventGrouper';
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
|
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
mxEvent: MatrixEvent;
|
||||||
|
callEventGrouper: CallEventGrouper;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
callState: CallState | CustomCallState;
|
||||||
|
silenced: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TEXTUAL_STATES: Map<CallState | CustomCallState, string> = new Map([
|
||||||
|
[CallState.Connected, _td("Connected")],
|
||||||
|
[CallState.Connecting, _td("Connecting")],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default class CallEvent extends React.Component<IProps, IState> {
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
callState: this.props.callEventGrouper.state,
|
||||||
|
silenced: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.callEventGrouper.addListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
|
||||||
|
this.props.callEventGrouper.addListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
|
||||||
|
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onSilencedChanged = (newState) => {
|
||||||
|
this.setState({ silenced: newState });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onStateChanged = (newState: CallState) => {
|
||||||
|
this.setState({ callState: newState });
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderContent(state: CallState | CustomCallState): JSX.Element {
|
||||||
|
if (state === CallState.Ringing) {
|
||||||
|
const silenceClass = classNames({
|
||||||
|
"mx_CallEvent_iconButton": true,
|
||||||
|
"mx_CallEvent_unSilence": this.state.silenced,
|
||||||
|
"mx_CallEvent_silence": !this.state.silenced,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_CallEvent_content">
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className={silenceClass}
|
||||||
|
onClick={this.props.callEventGrouper.toggleSilenced}
|
||||||
|
title={this.state.silenced ? _t("Sound on"): _t("Silence call")}
|
||||||
|
/>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_CallEvent_content_button mx_CallEvent_content_button_reject"
|
||||||
|
onClick={this.props.callEventGrouper.rejectCall}
|
||||||
|
kind="danger"
|
||||||
|
>
|
||||||
|
<span> { _t("Decline") } </span>
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_CallEvent_content_button mx_CallEvent_content_button_answer"
|
||||||
|
onClick={this.props.callEventGrouper.answerCall}
|
||||||
|
kind="primary"
|
||||||
|
>
|
||||||
|
<span> { _t("Accept") } </span>
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (state === CallState.Ended) {
|
||||||
|
const hangupReason = this.props.callEventGrouper.hangupReason;
|
||||||
|
|
||||||
|
if ([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason) {
|
||||||
|
// workaround for https://github.com/vector-im/element-web/issues/5178
|
||||||
|
// it seems Android randomly sets a reason of "user hangup" which is
|
||||||
|
// interpreted as an error code :(
|
||||||
|
// https://github.com/vector-im/riot-android/issues/2623
|
||||||
|
// Also the correct hangup code as of VoIP v1 (with underscore)
|
||||||
|
// Also, if we don't have a reason
|
||||||
|
return (
|
||||||
|
<div className="mx_CallEvent_content">
|
||||||
|
{ _t("This call has ended") }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let reason;
|
||||||
|
if (hangupReason === CallErrorCode.IceFailed) {
|
||||||
|
// We couldn't establish a connection at all
|
||||||
|
reason = _t("Could not connect media");
|
||||||
|
} else if (hangupReason === "ice_timeout") {
|
||||||
|
// We established a connection but it died
|
||||||
|
reason = _t("Connection failed");
|
||||||
|
} else if (hangupReason === CallErrorCode.NoUserMedia) {
|
||||||
|
// The other side couldn't open capture devices
|
||||||
|
reason = _t("Their device couldn't start the camera or microphone");
|
||||||
|
} else if (hangupReason === "unknown_error") {
|
||||||
|
// An error code the other side doesn't have a way to express
|
||||||
|
// (as opposed to an error code they gave but we don't know about,
|
||||||
|
// in which case we show the error code)
|
||||||
|
reason = _t("An unknown error occurred");
|
||||||
|
} else if (hangupReason === CallErrorCode.InviteTimeout) {
|
||||||
|
reason = _t("No answer");
|
||||||
|
} else if (hangupReason === CallErrorCode.UserBusy) {
|
||||||
|
reason = _t("The user you called is busy.");
|
||||||
|
} else {
|
||||||
|
reason = _t('Unknown failure: %(reason)s)', { reason: hangupReason });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_CallEvent_content">
|
||||||
|
<InfoTooltip
|
||||||
|
tooltip={reason}
|
||||||
|
className="mx_CallEvent_content_tooltip"
|
||||||
|
kind={InfoTooltipKind.Warning}
|
||||||
|
/>
|
||||||
|
{ _t("This call has failed") }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Array.from(TEXTUAL_STATES.keys()).includes(state)) {
|
||||||
|
return (
|
||||||
|
<div className="mx_CallEvent_content">
|
||||||
|
{ TEXTUAL_STATES.get(state) }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (state === CustomCallState.Missed) {
|
||||||
|
return (
|
||||||
|
<div className="mx_CallEvent_content">
|
||||||
|
{ _t("You missed this call") }
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_CallEvent_content_button mx_CallEvent_content_button_callBack"
|
||||||
|
onClick={this.props.callEventGrouper.callBack}
|
||||||
|
kind="primary"
|
||||||
|
>
|
||||||
|
<span> { _t("Call back") } </span>
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_CallEvent_content">
|
||||||
|
{ _t("The call is in an unknown state!") }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const event = this.props.mxEvent;
|
||||||
|
const sender = event.sender ? event.sender.name : event.getSender();
|
||||||
|
const isVoice = this.props.callEventGrouper.isVoice;
|
||||||
|
const callType = isVoice ? _t("Voice call") : _t("Video call");
|
||||||
|
const content = this.renderContent(this.state.callState);
|
||||||
|
const className = classNames({
|
||||||
|
mx_CallEvent: true,
|
||||||
|
mx_CallEvent_voice: isVoice,
|
||||||
|
mx_CallEvent_video: !isVoice,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div className="mx_CallEvent_info">
|
||||||
|
<MemberAvatar
|
||||||
|
member={event.sender}
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
<div className="mx_CallEvent_info_basic">
|
||||||
|
<div className="mx_CallEvent_sender">
|
||||||
|
{ sender }
|
||||||
|
</div>
|
||||||
|
<div className="mx_CallEvent_type">
|
||||||
|
<div className="mx_CallEvent_type_icon"></div>
|
||||||
|
{ callType }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{ content }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -273,13 +273,6 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
||||||
this.downloadImage();
|
this.downloadImage();
|
||||||
this.setState({ showImage: true });
|
this.setState({ showImage: true });
|
||||||
} // else don't download anything because we don't want to display anything.
|
} // else don't download anything because we don't want to display anything.
|
||||||
|
|
||||||
this._afterComponentDidMount();
|
|
||||||
}
|
|
||||||
|
|
||||||
// To be overridden by subclasses (e.g. MStickerBody) for further
|
|
||||||
// initialisation after componentDidMount
|
|
||||||
_afterComponentDidMount() {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -402,9 +395,9 @@ export default class MImageBody extends React.Component<IBodyProps, 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}
|
||||||
|
|
|
@ -44,6 +44,7 @@ import EditorStateTransfer from "../../../utils/EditorStateTransfer";
|
||||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||||
import NotificationBadge from "./NotificationBadge";
|
import NotificationBadge from "./NotificationBadge";
|
||||||
|
import CallEventGrouper from "../../structures/CallEventGrouper";
|
||||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
import { Action } from '../../../dispatcher/actions';
|
import { Action } from '../../../dispatcher/actions';
|
||||||
import MemberAvatar from '../avatars/MemberAvatar';
|
import MemberAvatar from '../avatars/MemberAvatar';
|
||||||
|
@ -60,10 +61,7 @@ const eventTileTypes = {
|
||||||
[EventType.Sticker]: 'messages.MessageEvent',
|
[EventType.Sticker]: 'messages.MessageEvent',
|
||||||
[EventType.KeyVerificationCancel]: 'messages.MKeyVerificationConclusion',
|
[EventType.KeyVerificationCancel]: 'messages.MKeyVerificationConclusion',
|
||||||
[EventType.KeyVerificationDone]: 'messages.MKeyVerificationConclusion',
|
[EventType.KeyVerificationDone]: 'messages.MKeyVerificationConclusion',
|
||||||
[EventType.CallInvite]: 'messages.TextualEvent',
|
[EventType.CallInvite]: 'messages.CallEvent',
|
||||||
[EventType.CallAnswer]: 'messages.TextualEvent',
|
|
||||||
[EventType.CallHangup]: 'messages.TextualEvent',
|
|
||||||
[EventType.CallReject]: 'messages.TextualEvent',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateEventTileTypes = {
|
const stateEventTileTypes = {
|
||||||
|
@ -170,8 +168,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------------------------------------------------.
|
||||||
|
@ -292,11 +288,17 @@ interface IProps {
|
||||||
// Helper to build permalinks for the room
|
// Helper to build permalinks for the room
|
||||||
permalinkCreator?: RoomPermalinkCreator;
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
|
|
||||||
|
// CallEventGrouper for this event
|
||||||
|
callEventGrouper?: CallEventGrouper;
|
||||||
|
|
||||||
// Symbol of the root node
|
// Symbol of the root node
|
||||||
as?: string;
|
as?: string;
|
||||||
|
|
||||||
// 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 +322,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 +432,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 +454,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 +658,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 +957,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 +977,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 +1122,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 +1133,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 }),
|
||||||
}, <>
|
}, <>
|
||||||
|
@ -1140,11 +1155,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
|
callEventGrouper={this.props.callEventGrouper}
|
||||||
/>
|
/>
|
||||||
{ keyRequestInfo }
|
{ keyRequestInfo }
|
||||||
{ reactionsRow }
|
|
||||||
{ actionBar }
|
{ actionBar }
|
||||||
</div>
|
</div>
|
||||||
|
{ reactionsRow }
|
||||||
{ msgOption }
|
{ msgOption }
|
||||||
{ avatar }
|
{ avatar }
|
||||||
</>)
|
</>)
|
||||||
|
@ -1160,7 +1176,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 +1186,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();
|
||||||
|
|
|
@ -513,7 +513,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>;
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||||
import CallHandler, { AudioID } from '../../../CallHandler';
|
import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
|
||||||
import RoomAvatar from '../avatars/RoomAvatar';
|
import RoomAvatar from '../avatars/RoomAvatar';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import { CallState } from 'matrix-js-sdk/src/webrtc/call';
|
import { CallState } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
|
@ -51,8 +51,13 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
||||||
|
};
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
|
CallHandler.sharedInstance().removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload) => {
|
private onAction = (payload: ActionPayload) => {
|
||||||
|
@ -73,6 +78,12 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onSilencedCallsChanged = () => {
|
||||||
|
const callId = this.state.incomingCall?.callId;
|
||||||
|
if (!callId) return;
|
||||||
|
this.setState({ silenced: CallHandler.sharedInstance().isCallSilenced(callId) });
|
||||||
|
};
|
||||||
|
|
||||||
private onAnswerClick: React.MouseEventHandler = (e) => {
|
private onAnswerClick: React.MouseEventHandler = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
@ -91,9 +102,10 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onSilenceClick: React.MouseEventHandler = (e) => {
|
private onSilenceClick: React.MouseEventHandler = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const newState = !this.state.silenced;
|
const callId = this.state.incomingCall.callId;
|
||||||
this.setState({ silenced: newState });
|
this.state.silenced ?
|
||||||
newState ? CallHandler.sharedInstance().pause(AudioID.Ring) : CallHandler.sharedInstance().play(AudioID.Ring);
|
CallHandler.sharedInstance().unSilenceCall(callId):
|
||||||
|
CallHandler.sharedInstance().silenceCall(callId);
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
@ -144,7 +156,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 +164,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"] {
|
||||||
|
|
44
src/emoji.ts
44
src/emoji.ts
|
@ -15,26 +15,23 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||||
|
import SHORTCODES from 'emojibase-data/en/shortcodes/iamcal.json';
|
||||||
|
|
||||||
export interface IEmoji {
|
export interface IEmoji {
|
||||||
annotation: string;
|
annotation: string;
|
||||||
group: number;
|
group?: number;
|
||||||
hexcode: string;
|
hexcode: string;
|
||||||
order: number;
|
order?: number;
|
||||||
shortcodes: string[];
|
shortcodes: string[];
|
||||||
tags: string[];
|
tags?: string[];
|
||||||
unicode: string;
|
unicode: string;
|
||||||
|
skins?: Omit<IEmoji, "shortcodes" | "tags">[]; // Currently unused
|
||||||
emoticon?: string;
|
emoticon?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IEmojiWithFilterString extends IEmoji {
|
|
||||||
filterString?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The unicode is stored without the variant selector
|
// The unicode is stored without the variant selector
|
||||||
const UNICODE_TO_EMOJI = new Map<string, IEmojiWithFilterString>(); // not exported as gets for it are handled by getEmojiFromUnicode
|
const UNICODE_TO_EMOJI = new Map<string, IEmoji>(); // not exported as gets for it are handled by getEmojiFromUnicode
|
||||||
export const EMOTICON_TO_EMOJI = new Map<string, IEmojiWithFilterString>();
|
export const EMOTICON_TO_EMOJI = new Map<string, IEmoji>();
|
||||||
export const SHORTCODE_TO_EMOJI = new Map<string, IEmojiWithFilterString>();
|
|
||||||
|
|
||||||
export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode));
|
export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode));
|
||||||
|
|
||||||
|
@ -62,17 +59,23 @@ export const DATA_BY_CATEGORY = {
|
||||||
"flags": [],
|
"flags": [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const ZERO_WIDTH_JOINER = "\u200D";
|
|
||||||
|
|
||||||
// Store various mappings from unicode/emoticon/shortcode to the Emoji objects
|
// Store various mappings from unicode/emoticon/shortcode to the Emoji objects
|
||||||
EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => {
|
export const EMOJI: IEmoji[] = EMOJIBASE.map((emojiData: Omit<IEmoji, "shortcodes">) => {
|
||||||
|
// If there's ever a gap in shortcode coverage, we fudge it by
|
||||||
|
// filling it in with the emoji's CLDR annotation
|
||||||
|
const shortcodeData = SHORTCODES[emojiData.hexcode] ??
|
||||||
|
[emojiData.annotation.toLowerCase().replace(/ /g, "_")];
|
||||||
|
|
||||||
|
const emoji: IEmoji = {
|
||||||
|
...emojiData,
|
||||||
|
// Homogenize shortcodes by ensuring that everything is an array
|
||||||
|
shortcodes: typeof shortcodeData === "string" ? [shortcodeData] : shortcodeData,
|
||||||
|
};
|
||||||
|
|
||||||
const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group];
|
const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group];
|
||||||
if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) {
|
if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) {
|
||||||
DATA_BY_CATEGORY[categoryId].push(emoji);
|
DATA_BY_CATEGORY[categoryId].push(emoji);
|
||||||
}
|
}
|
||||||
// This is used as the string to match the query against when filtering emojis
|
|
||||||
emoji.filterString = (`${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}\n` +
|
|
||||||
`${emoji.unicode.split(ZERO_WIDTH_JOINER).join("\n")}`).toLowerCase();
|
|
||||||
|
|
||||||
// Add mapping from unicode to Emoji object
|
// Add mapping from unicode to Emoji object
|
||||||
// The 'unicode' field that we use in emojibase has either
|
// The 'unicode' field that we use in emojibase has either
|
||||||
|
@ -88,12 +91,7 @@ EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => {
|
||||||
EMOTICON_TO_EMOJI.set(emoji.emoticon, emoji);
|
EMOTICON_TO_EMOJI.set(emoji.emoticon, emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emoji.shortcodes) {
|
return emoji;
|
||||||
// Add mapping from each shortcode to Emoji object
|
|
||||||
emoji.shortcodes.forEach(shortcode => {
|
|
||||||
SHORTCODE_TO_EMOJI.set(shortcode, emoji);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -107,5 +105,3 @@ EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => {
|
||||||
function stripVariation(str) {
|
function stripVariation(str) {
|
||||||
return str.replace(/[\uFE00-\uFE0F]$/, "");
|
return str.replace(/[\uFE00-\uFE0F]$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EMOJI: IEmoji[] = EMOJIBASE;
|
|
||||||
|
|
|
@ -541,22 +541,8 @@
|
||||||
"%(senderName)s changed the alternative addresses for this room.": "%(senderName)s changed the alternative addresses for this room.",
|
"%(senderName)s changed the alternative addresses for this room.": "%(senderName)s changed the alternative addresses for this room.",
|
||||||
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s changed the main and alternative addresses for this room.",
|
"%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s changed the main and alternative addresses for this room.",
|
||||||
"%(senderName)s changed the addresses for this room.": "%(senderName)s changed the addresses for this room.",
|
"%(senderName)s changed the addresses for this room.": "%(senderName)s changed the addresses for this room.",
|
||||||
"Someone": "Someone",
|
|
||||||
"(not supported by this browser)": "(not supported by this browser)",
|
|
||||||
"%(senderName)s answered the call.": "%(senderName)s answered the call.",
|
|
||||||
"(could not connect media)": "(could not connect media)",
|
|
||||||
"(connection failed)": "(connection failed)",
|
|
||||||
"(their device couldn't start the camera / microphone)": "(their device couldn't start the camera / microphone)",
|
|
||||||
"(an error occurred)": "(an error occurred)",
|
|
||||||
"(no answer)": "(no answer)",
|
|
||||||
"(unknown failure: %(reason)s)": "(unknown failure: %(reason)s)",
|
|
||||||
"%(senderName)s ended the call.": "%(senderName)s ended the call.",
|
|
||||||
"%(senderName)s declined the call.": "%(senderName)s declined the call.",
|
|
||||||
"%(senderName)s placed a voice call.": "%(senderName)s placed a voice call.",
|
|
||||||
"%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s placed a voice call. (not supported by this browser)",
|
|
||||||
"%(senderName)s placed a video call.": "%(senderName)s placed a video call.",
|
|
||||||
"%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s placed a video call. (not supported by this browser)",
|
|
||||||
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.",
|
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.",
|
||||||
|
"Someone": "Someone",
|
||||||
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.",
|
"%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.",
|
||||||
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s made future room history visible to all room members, from the point they are invited.",
|
"%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s made future room history visible to all room members, from the point they are invited.",
|
||||||
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s made future room history visible to all room members, from the point they joined.",
|
"%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s made future room history visible to all room members, from the point they joined.",
|
||||||
|
@ -823,6 +809,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 +1120,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 +1232,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",
|
||||||
|
@ -1658,7 +1640,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",
|
||||||
|
@ -1667,7 +1648,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.",
|
||||||
|
@ -1861,6 +1842,18 @@
|
||||||
"You cancelled verification.": "You cancelled verification.",
|
"You cancelled verification.": "You cancelled verification.",
|
||||||
"Verification cancelled": "Verification cancelled",
|
"Verification cancelled": "Verification cancelled",
|
||||||
"Compare emoji": "Compare emoji",
|
"Compare emoji": "Compare emoji",
|
||||||
|
"Connected": "Connected",
|
||||||
|
"This call has ended": "This call has ended",
|
||||||
|
"Could not connect media": "Could not connect media",
|
||||||
|
"Connection failed": "Connection failed",
|
||||||
|
"Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone",
|
||||||
|
"An unknown error occurred": "An unknown error occurred",
|
||||||
|
"No answer": "No answer",
|
||||||
|
"Unknown failure: %(reason)s)": "Unknown failure: %(reason)s)",
|
||||||
|
"This call has failed": "This call has failed",
|
||||||
|
"You missed this call": "You missed this call",
|
||||||
|
"Call back": "Call back",
|
||||||
|
"The call is in an unknown state!": "The call is in an unknown state!",
|
||||||
"Sunday": "Sunday",
|
"Sunday": "Sunday",
|
||||||
"Monday": "Monday",
|
"Monday": "Monday",
|
||||||
"Tuesday": "Tuesday",
|
"Tuesday": "Tuesday",
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
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