Merge remote-tracking branch 'upstream/develop' into feature/collapse-pinned-mels/17938

This commit is contained in:
Šimon Brandner 2021-07-22 07:51:58 +02:00
commit 2df4f7b859
No known key found for this signature in database
GPG key ID: 55C211A1226CB17D
483 changed files with 11911 additions and 7811 deletions

View file

@ -1,3 +1,15 @@
<!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst before submitting your pull request --> <!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md before submitting your pull request -->
<!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst#sign-off --> <!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md#sign-off -->
<!-- To specify text for the changelog entry (otherwise the PR title will be used):
Notes:
Changes in this project generate changelog entries in element-web by default.
To suppress this:
element-web notes: none
...or to specify different notes:
element-web notes: <notes>
-->

View file

@ -1,5 +1,8 @@
name: Develop name: Develop
on: on:
# These tests won't work for non-develop branches at the moment as they
# won't pull in the right versions of other repos, so they're only enabled
# on develop.
push: push:
branches: [develop] branches: [develop]
pull_request: pull_request:

3
.gitignore vendored
View file

@ -15,3 +15,6 @@ package-lock.json
.DS_Store .DS_Store
*.tmp *.tmp
.vscode
.vscode/

View file

@ -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)

6
__mocks__/FontManager.js Normal file
View file

@ -0,0 +1,6 @@
// Stub out FontManager for tests as it doesn't validate anything we don't already know given
// our fixed test environment and it requires the installation of node-canvas.
module.exports = {
fixupColorFonts: () => Promise.resolve(),
};

1
__mocks__/workerMock.js Normal file
View file

@ -0,0 +1 @@
module.exports = jest.fn();

View file

@ -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",
@ -126,6 +127,7 @@
"@types/classnames": "^2.2.11", "@types/classnames": "^2.2.11",
"@types/commonmark": "^0.27.4", "@types/commonmark": "^0.27.4",
"@types/counterpart": "^0.18.1", "@types/counterpart": "^0.18.1",
"@types/css-font-loading-module": "^0.0.6",
"@types/diff-match-patch": "^1.0.32", "@types/diff-match-patch": "^1.0.32",
"@types/flux": "^3.1.9", "@types/flux": "^3.1.9",
"@types/jest": "^26.0.20", "@types/jest": "^26.0.20",
@ -186,7 +188,8 @@
"\\$webapp/i18n/languages.json": "<rootDir>/__mocks__/languages.json", "\\$webapp/i18n/languages.json": "<rootDir>/__mocks__/languages.json",
"decoderWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js", "decoderWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js", "decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js" "waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js"
}, },
"transformIgnorePatterns": [ "transformIgnorePatterns": [
"/node_modules/(?!matrix-js-sdk).+$" "/node_modules/(?!matrix-js-sdk).+$"

View file

@ -120,6 +120,7 @@
@import "./views/elements/_AddressTile.scss"; @import "./views/elements/_AddressTile.scss";
@import "./views/elements/_DesktopBuildsNotice.scss"; @import "./views/elements/_DesktopBuildsNotice.scss";
@import "./views/elements/_DesktopCapturerSourcePicker.scss"; @import "./views/elements/_DesktopCapturerSourcePicker.scss";
@import "./views/elements/_DialPadBackspaceButton.scss";
@import "./views/elements/_DirectorySearchBox.scss"; @import "./views/elements/_DirectorySearchBox.scss";
@import "./views/elements/_Dropdown.scss"; @import "./views/elements/_Dropdown.scss";
@import "./views/elements/_EditableItemList.scss"; @import "./views/elements/_EditableItemList.scss";
@ -148,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";
@ -160,9 +162,11 @@
@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";
@import "./views/messages/_MImageReplyBody.scss";
@import "./views/messages/_MJitsiWidgetEvent.scss"; @import "./views/messages/_MJitsiWidgetEvent.scss";
@import "./views/messages/_MNoticeBody.scss"; @import "./views/messages/_MNoticeBody.scss";
@import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MStickerBody.scss";
@ -198,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";
@ -212,6 +217,7 @@
@import "./views/rooms/_PinnedEventTile.scss"; @import "./views/rooms/_PinnedEventTile.scss";
@import "./views/rooms/_PresenceLabel.scss"; @import "./views/rooms/_PresenceLabel.scss";
@import "./views/rooms/_ReplyPreview.scss"; @import "./views/rooms/_ReplyPreview.scss";
@import "./views/rooms/_ReplyTile.scss";
@import "./views/rooms/_RoomBreadcrumbs.scss"; @import "./views/rooms/_RoomBreadcrumbs.scss";
@import "./views/rooms/_RoomHeader.scss"; @import "./views/rooms/_RoomHeader.scss";
@import "./views/rooms/_RoomList.scss"; @import "./views/rooms/_RoomList.scss";
@ -260,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";

View file

@ -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');
} }

View file

@ -1,6 +1,7 @@
/* /*
Copyright 2017 Travis Ralston Copyright 2017 Travis Ralston
Copyright 2019 New Vector Ltd Copyright 2019 New Vector Ltd
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -20,7 +21,6 @@ limitations under the License.
padding: 0 0 0 16px; padding: 0 0 0 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
@ -28,11 +28,93 @@ limitations under the License.
margin-top: 8px; margin-top: 8px;
} }
.mx_TabbedView_tabLabels { .mx_TabbedView_tabsOnLeft {
flex-direction: column;
position: absolute;
.mx_TabbedView_tabLabels {
width: 170px; width: 170px;
max-width: 170px; max-width: 170px;
color: $tab-label-fg-color;
position: fixed; position: fixed;
}
.mx_TabbedView_tabPanel {
margin-left: 240px; // 170px sidebar + 70px padding
flex-direction: column;
}
.mx_TabbedView_tabLabel_active {
background-color: $tab-label-active-bg-color;
color: $tab-label-active-fg-color;
}
.mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before {
background-color: $tab-label-active-icon-bg-color;
}
.mx_TabbedView_maskedIcon {
width: 16px;
height: 16px;
margin-left: 8px;
margin-right: 16px;
}
.mx_TabbedView_maskedIcon::before {
mask-size: 16px;
width: 16px;
height: 16px;
}
}
.mx_TabbedView_tabsOnTop {
flex-direction: column;
.mx_TabbedView_tabLabels {
display: flex;
margin-bottom: 8px;
}
.mx_TabbedView_tabLabel {
padding-left: 0px;
padding-right: 52px;
.mx_TabbedView_tabLabel_text {
font-size: 15px;
color: $tertiary-fg-color;
}
}
.mx_TabbedView_tabPanel {
flex-direction: row;
}
.mx_TabbedView_tabLabel_active {
color: $accent-color;
.mx_TabbedView_tabLabel_text {
color: $accent-color;
}
}
.mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before {
background-color: $accent-color;
}
.mx_TabbedView_maskedIcon {
width: 22px;
height: 22px;
margin-left: 0px;
margin-right: 8px;
}
.mx_TabbedView_maskedIcon::before {
mask-size: 22px;
width: inherit;
height: inherit;
}
}
.mx_TabbedView_tabLabels {
color: $tab-label-fg-color;
} }
.mx_TabbedView_tabLabel { .mx_TabbedView_tabLabel {
@ -46,43 +128,25 @@ limitations under the License.
position: relative; position: relative;
} }
.mx_TabbedView_tabLabel_active {
background-color: $tab-label-active-bg-color;
color: $tab-label-active-fg-color;
}
.mx_TabbedView_maskedIcon { .mx_TabbedView_maskedIcon {
margin-left: 8px;
margin-right: 16px;
width: 16px;
height: 16px;
display: inline-block; display: inline-block;
} }
.mx_TabbedView_maskedIcon::before { .mx_TabbedView_maskedIcon::before {
display: inline-block; display: inline-block;
background-color: $tab-label-icon-bg-color; background-color: $icon-button-color;
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-size: 16px;
width: 16px;
height: 16px;
mask-position: center; mask-position: center;
content: ''; content: '';
} }
.mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before {
background-color: $tab-label-active-icon-bg-color;
}
.mx_TabbedView_tabLabel_text { .mx_TabbedView_tabLabel_text {
vertical-align: middle; vertical-align: middle;
} }
.mx_TabbedView_tabPanel { .mx_TabbedView_tabPanel {
margin-left: 240px; // 170px sidebar + 70px padding
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
flex-direction: column;
min-height: 0; // firefox min-height: 0; // firefox
} }

View file

@ -49,4 +49,8 @@ limitations under the License.
padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended. padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended.
padding-left: 8px; // isolate from recording circle / play control padding-left: 8px; // isolate from recording circle / play control
} }
&.mx_VoiceMessagePrimaryContainer_noWaveform {
max-width: 162px; // with all the padding this results in 185px wide
}
} }

View file

@ -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 {

View file

@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_InviteDialog_transferWrapper .mx_Dialog {
padding-bottom: 16px;
}
.mx_InviteDialog_addressBar { .mx_InviteDialog_addressBar {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -286,16 +290,41 @@ limitations under the License.
} }
} }
.mx_InviteDialog { .mx_InviteDialog_other {
// Prevent the dialog from jumping around randomly when elements change. // Prevent the dialog from jumping around randomly when elements change.
height: 600px; height: 600px;
padding-left: 20px; // the design wants some padding on the left padding-left: 20px; // the design wants some padding on the left
display: flex;
.mx_InviteDialog_userSections {
height: calc(100% - 115px); // mx_InviteDialog's height minus some for the upper and lower elements
}
}
.mx_InviteDialog_content {
height: calc(100% - 36px); // full height minus the size of the header
overflow: hidden;
}
.mx_InviteDialog_transfer {
width: 496px;
height: 466px;
flex-direction: column; flex-direction: column;
.mx_InviteDialog_content { .mx_InviteDialog_content {
overflow: hidden; flex-direction: column;
height: 100%;
.mx_TabbedView {
height: calc(100% - 60px);
}
overflow: visible;
}
.mx_InviteDialog_addressBar {
margin-top: 8px;
}
input[type="checkbox"] {
margin-right: 8px;
} }
} }
@ -303,7 +332,6 @@ limitations under the License.
margin-top: 4px; margin-top: 4px;
overflow-y: auto; overflow-y: auto;
padding: 0 45px 4px 0; padding: 0 45px 4px 0;
height: calc(100% - 115px); // mx_InviteDialog's height minus some for the upper and lower elements
} }
.mx_InviteDialog_hasFooter .mx_InviteDialog_userSections { .mx_InviteDialog_hasFooter .mx_InviteDialog_userSections {
@ -318,6 +346,74 @@ limitations under the License.
padding: 0; padding: 0;
} }
.mx_InviteDialog_dialPad .mx_InviteDialog_dialPadField {
border-top: 0;
border-left: 0;
border-right: 0;
border-radius: 0;
margin-top: 0;
border-color: $quaternary-fg-color;
input {
font-size: 18px;
font-weight: 600;
padding-top: 0;
}
}
.mx_InviteDialog_dialPad .mx_InviteDialog_dialPadField:focus-within {
border-color: $accent-color;
}
.mx_InviteDialog_dialPadField .mx_Field_postfix {
/* Remove border separator between postfix and field content */
border-left: none;
}
.mx_InviteDialog_dialPad {
width: 224px;
margin-top: 16px;
margin-left: auto;
margin-right: auto;
}
.mx_InviteDialog_dialPad .mx_DialPad {
row-gap: 16px;
column-gap: 48px;
margin-left: auto;
margin-right: auto;
}
.mx_InviteDialog_transferConsultConnect {
padding-top: 16px;
/* This wants a drop shadow the full width of the dialog, so relative-position it
* and make it wider, then compensate with padding
*/
position: relative;
width: 496px;
left: -24px;
padding-left: 24px;
padding-right: 24px;
border-top: 1px solid $message-body-panel-bg-color;
display: flex;
flex-direction: row;
align-items: center;
}
.mx_InviteDialog_transferConsultConnect_pushRight {
margin-left: auto;
}
.mx_InviteDialog_userDirectoryIcon::before {
mask-image: url('$(res)/img/voip/tab-userdirectory.svg');
}
.mx_InviteDialog_dialPadIcon::before {
mask-image: url('$(res)/img/voip/tab-dialpad.svg');
}
.mx_InviteDialog_multiInviterError { .mx_InviteDialog_multiInviterError {
> h4 { > h4 {
font-size: $font-15px; font-size: $font-15px;

View file

@ -72,7 +72,7 @@ limitations under the License.
.mx_AccessibleButton_kind_danger_outline { .mx_AccessibleButton_kind_danger_outline {
color: $button-danger-bg-color; color: $button-danger-bg-color;
background-color: $button-secondary-bg-color; background-color: transparent;
border: 1px solid $button-danger-bg-color; border: 1px solid $button-danger-bg-color;
} }

View file

@ -0,0 +1,40 @@
/*
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_DialPadBackspaceButton {
position: relative;
height: 28px;
width: 28px;
&::before {
/* force this element to appear on the DOM */
content: "";
background-color: #8D97A5;
width: inherit;
height: inherit;
top: 0px;
left: 0px;
position: absolute;
display: inline-block;
vertical-align: middle;
mask-image: url('$(res)/img/element-icons/call/delete.svg');
mask-position: 8px;
mask-size: 20px;
mask-repeat: no-repeat;
}
}

View file

@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
$button-size: 32px;
$icon-size: 22px;
$button-gap: 24px;
.mx_ImageView { .mx_ImageView {
display: flex; display: flex;
width: 100%; width: 100%;
@ -66,16 +70,17 @@ limitations under the License.
pointer-events: initial; pointer-events: initial;
display: flex; display: flex;
align-items: center; align-items: center;
gap: calc($button-gap - ($button-size - $icon-size));
} }
.mx_ImageView_button { .mx_ImageView_button {
margin-left: 24px; padding: calc(($button-size - $icon-size) / 2);
display: block; display: block;
&::before { &::before {
content: ''; content: '';
height: 22px; height: $icon-size;
width: 22px; width: $icon-size;
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-size: contain; mask-size: contain;
mask-position: center; mask-position: center;
@ -109,11 +114,12 @@ limitations under the License.
} }
.mx_ImageView_button_close { .mx_ImageView_button_close {
padding: calc($button-size - $button-size);
border-radius: 100%; border-radius: 100%;
background: #21262c; // same on all themes background: #21262c; // same on all themes
&::before { &::before {
width: 32px; width: $button-size;
height: 32px; height: $button-size;
mask-image: url('$(res)/img/image-view/close.svg'); mask-image: url('$(res)/img/image-view/close.svg');
mask-size: 40%; mask-size: 40%;
} }

View file

@ -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');
}

View file

@ -16,22 +16,45 @@ limitations under the License.
.mx_ReplyThread { .mx_ReplyThread {
margin-top: 0; margin-top: 0;
}
.mx_ReplyThread .mx_DateSeparator {
font-size: 1em !important;
margin-top: 0;
margin-bottom: 0;
padding-bottom: 1px;
bottom: -5px;
}
.mx_ReplyThread_show {
cursor: pointer;
}
blockquote.mx_ReplyThread {
margin-left: 0; margin-left: 0;
margin-right: 0;
margin-bottom: 8px;
padding-left: 10px; padding-left: 10px;
border-left: 4px solid $blockquote-bar-color; border-left: 4px solid $button-bg-color;
.mx_ReplyThread_show {
cursor: pointer;
}
&.mx_ReplyThread_color1 {
border-left-color: $username-variant1-color;
}
&.mx_ReplyThread_color2 {
border-left-color: $username-variant2-color;
}
&.mx_ReplyThread_color3 {
border-left-color: $username-variant3-color;
}
&.mx_ReplyThread_color4 {
border-left-color: $username-variant4-color;
}
&.mx_ReplyThread_color5 {
border-left-color: $username-variant5-color;
}
&.mx_ReplyThread_color6 {
border-left-color: $username-variant6-color;
}
&.mx_ReplyThread_color7 {
border-left-color: $username-variant7-color;
}
&.mx_ReplyThread_color8 {
border-left-color: $username-variant8-color;
}
} }

View file

@ -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 {

View 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;
}
}
}

View 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');
}
}
}

View file

@ -83,12 +83,12 @@ limitations under the License.
mask-size: cover; mask-size: cover;
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg'); mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
background-color: $message-body-panel-icon-fg-color; background-color: $message-body-panel-icon-fg-color;
width: 13px; width: 15px;
height: 15px; height: 15px;
position: absolute; position: absolute;
top: 8px; top: 8px;
left: 9px; left: 8px;
} }
} }

View file

@ -18,17 +18,16 @@ $timelineImageBorderRadius: 4px;
.mx_MImageBody { .mx_MImageBody {
display: block; display: block;
margin-right: 34px;
} }
.mx_MImageBody_thumbnail { .mx_MImageBody_thumbnail {
position: absolute; object-fit: contain;
width: 100%;
height: 100%;
left: 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 +42,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;

View file

@ -0,0 +1,37 @@
/*
Copyright 2020 Tulir Asokan <tulir@maunium.net>
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_MImageReplyBody {
display: flex;
.mx_MImageBody_thumbnail_container {
flex: 1;
margin-right: 4px;
}
.mx_MImageReplyBody_info {
flex: 1;
.mx_MImageReplyBody_sender {
grid-area: sender;
}
.mx_MImageReplyBody_filename {
grid-area: filename;
}
}
}

View file

@ -107,3 +107,12 @@ limitations under the License.
.mx_MessageActionBar_cancelButton::after { .mx_MessageActionBar_cancelButton::after {
mask-image: url('$(res)/img/element-icons/trashcan.svg'); mask-image: url('$(res)/img/element-icons/trashcan.svg');
} }
.mx_MessageActionBar_downloadButton::after {
mask-size: 14px;
mask-image: url('$(res)/img/download.svg');
}
.mx_MessageActionBar_downloadButton.mx_MessageActionBar_downloadSpinnerButton::after {
background-color: transparent; // hide the download icon mask
}

View file

@ -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: '';

View 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%;
}
}

View file

@ -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,44 +18,43 @@ 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;
} }
.mx_EventTile_avatar { .mx_EventTile_avatar {
top: 14px; top: 14px;
left: 8px; left: 8px;
cursor: pointer; cursor: pointer;
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 {
padding-top: 5px !important; padding-top: 5px !important;
margin-top: -5px; margin-top: -5px;
} }
} }
.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 */
@ -68,9 +67,9 @@ $hover-select-border: 4px;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
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;
@ -83,124 +82,90 @@ $hover-select-border: 4px;
margin-right: 2px; margin-right: 2px;
border-radius: 8px; border-radius: 8px;
} }
} }
.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;
text-align: center; text-align: center;
user-select: none; user-select: none;
} }
.mx_EventTile_continuation .mx_EventTile_line { &.mx_EventTile_continuation .mx_EventTile_line {
clear: both; clear: both;
} }
.mx_EventTile_line, .mx_EventTile_reply { .mx_EventTile_line, .mx_EventTile_reply {
position: relative; position: relative;
padding-left: $left-gutter; padding-left: $left-gutter;
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 { .mx_EventTile_reply {
grid-column: 2;
}
}
.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, /* this is used for the tile for the event which is selected via the URL.
.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.
* 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 {
background-color: $event-highlight-bg-color; background-color: $event-highlight-bg-color;
} }
} }
.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_EventListSummary .mx_EventTile_line {
padding-left: calc($left-gutter);
}
&.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;
} }
.mx_EventTile_searchHighlight { .mx_EventTile_searchHighlight {
background-color: $accent-color; background-color: $accent-color;
color: $accent-fg-color; color: $accent-fg-color;
border-radius: 5px; border-radius: 5px;
padding-left: 2px; padding-left: 2px;
padding-right: 2px; padding-right: 2px;
cursor: pointer; cursor: pointer;
} }
.mx_EventTile_searchHighlight a { .mx_EventTile_searchHighlight a {
background-color: $accent-color; background-color: $accent-color;
color: $accent-fg-color; color: $accent-fg-color;
} }
.mx_EventTile_receiptSent, .mx_EventTile_receiptSent,
.mx_EventTile_receiptSending { .mx_EventTile_receiptSending {
// We don't use `position: relative` on the element because then it won't line // We don't use `position: relative` on the element because then it won't line
// up with the other read receipts // up with the other read receipts
@ -217,19 +182,19 @@ $hover-select-border: 4px;
left: 0; left: 0;
right: 0; right: 0;
} }
} }
.mx_EventTile_receiptSent::before { .mx_EventTile_receiptSent::before {
mask-image: url('$(res)/img/element-icons/circle-sent.svg'); mask-image: url('$(res)/img/element-icons/circle-sent.svg');
} }
.mx_EventTile_receiptSending::before { .mx_EventTile_receiptSending::before {
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;
} }
.mx_EventTile_msgOption { .mx_EventTile_msgOption {
float: right; float: right;
text-align: right; text-align: right;
position: relative; position: relative;
@ -241,10 +206,142 @@ $hover-select-border: 4px;
height: 1px; height: 1px;
margin-right: 10px; margin-right: 10px;
}
.mx_EventTile_msgOption a {
text-decoration: none;
}
/* De-zalgoing */
.mx_EventTile_body {
overflow-y: hidden;
}
&: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 {
position: absolute;
top: 6px;
left: 44px;
bottom: 0;
right: 0;
}
.mx_ReactionsRow {
margin: 0;
padding: 6px 60px;
}
} }
.mx_EventTile_msgOption a { /* all the overflow-y: hidden; are to trap Zalgos -
text-decoration: none; but they introduce an implicit overflow-x: auto.
so make that explicitly hidden too to avoid random
horizontal scrollbars occasionally appearing, like in
https://github.com/vector-im/vector-web/issues/1154 */
.mx_EventTile_content {
overflow-y: hidden;
overflow-x: hidden;
margin-right: 34px;
}
/* Spoiler stuff */
.mx_EventTile_spoiler {
cursor: pointer;
}
.mx_EventTile_spoiler_reason {
color: $event-timestamp-color;
font-size: $font-11px;
}
.mx_EventTile_spoiler_content {
filter: blur(5px) saturate(0.1) sepia(1);
transition-duration: 0.5s;
}
.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content {
filter: none;
}
.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 { .mx_EventTile_readAvatars {
@ -277,52 +374,27 @@ $hover-select-border: 4px;
position: absolute; position: absolute;
} }
/* all the overflow-y: hidden; are to trap Zalgos - /* HACK to override line-height which is already marked important elsewhere */
but they introduce an implicit overflow-x: auto. .mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
so make that explicitly hidden too to avoid random font-size: 48px !important;
horizontal scrollbars occasionally appearing, like in line-height: 57px !important;
https://github.com/vector-im/vector-web/issues/1154
*/
.mx_EventTile_content {
display: block;
overflow-y: hidden;
overflow-x: hidden;
margin-right: 34px;
} }
/* De-zalgoing */ .mx_EventTile_content .mx_EventTile_edited {
.mx_EventTile_body { user-select: none;
overflow-y: hidden; font-size: $font-12px;
} color: $roomtopic-color;
display: inline-block;
/* Spoiler stuff */ margin-left: 9px;
.mx_EventTile_spoiler {
cursor: pointer; cursor: pointer;
} }
.mx_EventTile_spoiler_reason {
color: $event-timestamp-color;
font-size: $font-11px;
}
.mx_EventTile_spoiler_content {
filter: blur(5px) saturate(0.1) sepia(1);
transition-duration: 0.5s;
}
.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content {
filter: none;
}
.mx_EventTile_e2eIcon { .mx_EventTile_e2eIcon {
position: absolute; position: relative;
top: 6px;
left: 44px;
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 +453,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 {
@ -480,6 +471,11 @@ $hover-select-border: 4px;
background-color: $header-panel-bg-color; background-color: $header-panel-bg-color;
} }
pre code > * {
display: inline-block;
width: 100%;
}
pre { pre {
// have to use overlay rather than auto otherwise Linux and Windows // have to use overlay rather than auto otherwise Linux and Windows
// Chrome gets very confused about vertical spacing: // Chrome gets very confused about vertical spacing:
@ -595,6 +591,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 +640,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;

View file

@ -198,8 +198,9 @@ $irc-line-height: $font-18px;
.mx_ReplyThread { .mx_ReplyThread {
margin: 0; margin: 0;
.mx_SenderProfile { .mx_SenderProfile {
order: unset;
max-width: unset;
width: unset; width: unset;
max-width: var(--name-width);
background: transparent; background: transparent;
} }

View file

@ -22,28 +22,34 @@ 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: 12px; margin: 8px;
color: $primary-fg-color; color: $primary-fg-color;
font-weight: 400; font-weight: 400;
opacity: 0.4; opacity: 0.4;
} }
.mx_ReplyPreview_title { .mx_ReplyPreview_tile {
margin: 0 8px;
}
.mx_ReplyPreview_title {
float: left; float: left;
} }
.mx_ReplyPreview_cancel { .mx_ReplyPreview_cancel {
float: right; float: right;
cursor: pointer; cursor: pointer;
display: flex;
}
.mx_ReplyPreview_clear {
clear: both;
}
}
} }
.mx_ReplyPreview_clear {
clear: both;
}

View file

@ -0,0 +1,119 @@
/*
Copyright 2020 Tulir Asokan <tulir@maunium.net>
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_ReplyTile {
position: relative;
padding: 2px 0;
font-size: $font-14px;
line-height: $font-16px;
&.mx_ReplyTile_audio .mx_MFileBody_info_icon::before {
mask-image: url("$(res)/img/element-icons/speaker.svg");
}
&.mx_ReplyTile_video .mx_MFileBody_info_icon::before {
mask-image: url("$(res)/img/element-icons/call/video-call.svg");
}
.mx_MFileBody {
.mx_MFileBody_info {
margin: 5px 0;
}
.mx_MFileBody_download {
display: none;
}
}
> a {
display: flex;
flex-direction: column;
text-decoration: none;
color: $primary-fg-color;
}
.mx_RedactedBody {
padding: 4px 0 2px 20px;
&::before {
height: 13px;
width: 13px;
top: 5px;
}
}
// We do reply size limiting with CSS to avoid duplicating the TextualBody component.
.mx_EventTile_content {
$reply-lines: 2;
$line-height: $font-22px;
pointer-events: none;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: $reply-lines;
line-height: $line-height;
.mx_EventTile_body.mx_EventTile_bigEmoji {
line-height: $line-height !important;
font-size: $font-14px !important; // Override the big emoji override
}
// Hide line numbers
.mx_EventTile_lineNumbers {
display: none;
}
// Hack to cut content in <pre> tags too
.mx_EventTile_pre_container > pre {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: $reply-lines;
padding: 4px;
}
.markdown-body blockquote,
.markdown-body dl,
.markdown-body ol,
.markdown-body p,
.markdown-body pre,
.markdown-body table,
.markdown-body ul {
margin-bottom: 4px;
}
}
&.mx_ReplyTile_info {
padding-top: 0;
}
.mx_SenderProfile {
font-size: $font-14px;
line-height: $font-17px;
display: inline-block; // anti-zalgo, with overflow hidden
padding: 0;
margin: 0;
// truncate long display names
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}

View file

@ -193,6 +193,10 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/settings.svg'); mask-image: url('$(res)/img/element-icons/settings.svg');
} }
.mx_RoomTile_iconCopyLink::before {
mask-image: url('$(res)/img/element-icons/link.svg');
}
.mx_RoomTile_iconInvite::before { .mx_RoomTile_iconInvite::before {
mask-image: url('$(res)/img/element-icons/room/invite.svg'); mask-image: url('$(res)/img/element-icons/room/invite.svg');
} }

View file

@ -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 { .mx_UserNotifSettings_pushRulesTable {
display: table-cell; width: calc(100% + 12px); // +12px to line up center of 'Noisy' column with toggle switches
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 {
width: 100%;
table-layout: fixed; table-layout: fixed;
} border-collapse: collapse;
border-spacing: 0;
margin-top: 40px;
.mx_UserNotifSettings_pushRulesTable thead { tr > th {
font-weight: bold; font-weight: $font-semi-bold;
} }
.mx_UserNotifSettings_pushRulesTable tbody th { tr > th:first-child {
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;
}
.mx_NotificationSound_resetSound { & > div:first-child { // section header
margin-top: 5px; font-size: $font-18px;
color: white; font-weight: $font-semi-bold;
border: $warning-color; }
background-color: $warning-color;
> table {
border-collapse: collapse;
border-spacing: 0;
margin-top: 8px;
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
}
} }

View file

@ -15,8 +15,7 @@ limitations under the License.
*/ */
.mx_AppearanceUserSettingsTab_fontSlider, .mx_AppearanceUserSettingsTab_fontSlider,
.mx_AppearanceUserSettingsTab_fontSlider_preview, .mx_AppearanceUserSettingsTab_fontSlider_preview {
.mx_AppearanceUserSettingsTab_Layout {
@mixin mx_Settings_fullWidthField; @mixin mx_Settings_fullWidthField;
} }
@ -45,6 +44,11 @@ limitations under the License.
border-radius: 10px; border-radius: 10px;
padding: 0 16px 9px 16px; padding: 0 16px 9px 16px;
pointer-events: none; pointer-events: none;
display: flow-root;
.mx_EventTile[data-layout=bubble] {
margin-top: 30px;
}
.mx_EventTile_msgOption { .mx_EventTile_msgOption {
display: none; display: none;
@ -154,13 +158,10 @@ limitations under the License.
.mx_AppearanceUserSettingsTab_Layout_RadioButtons { .mx_AppearanceUserSettingsTab_Layout_RadioButtons {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 24px;
color: $primary-fg-color; color: $primary-fg-color;
.mx_AppearanceUserSettingsTab_spacer {
width: 24px;
}
> .mx_AppearanceUserSettingsTab_Layout_RadioButton { > .mx_AppearanceUserSettingsTab_Layout_RadioButton {
flex-grow: 0; flex-grow: 0;
flex-shrink: 1; flex-shrink: 1;
@ -210,6 +211,21 @@ limitations under the License.
.mx_RadioButton_checked { .mx_RadioButton_checked {
background-color: rgba($accent-color, 0.08); background-color: rgba($accent-color, 0.08);
} }
.mx_EventTile {
margin: 0;
&[data-layout=bubble] {
margin-right: 40px;
}
&[data-layout=irc] {
> a {
display: none;
}
}
.mx_EventTile_line {
max-width: 90%;
}
}
} }
.mx_AppearanceUserSettingsTab_Advanced { .mx_AppearanceUserSettingsTab_Advanced {

View file

@ -16,11 +16,21 @@ limitations under the License.
.mx_DialPad { .mx_DialPad {
display: grid; display: grid;
row-gap: 16px;
column-gap: 0px;
margin-top: 24px;
margin-left: auto;
margin-right: auto;
/* squeeze the dial pad buttons together horizontally */
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 16px;
} }
.mx_DialPad_button { .mx_DialPad_button {
display: flex;
flex-direction: column;
justify-content: center;
width: 40px; width: 40px;
height: 40px; height: 40px;
background-color: $dialpad-button-bg-color; background-color: $dialpad-button-bg-color;
@ -29,10 +39,19 @@ limitations under the License.
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
line-height: 40px; margin-left: auto;
margin-right: auto;
} }
.mx_DialPad_deleteButton, .mx_DialPad_dialButton { .mx_DialPad_button .mx_DialPad_buttonSubText {
font-size: 8px;
}
.mx_DialPad_dialButton {
/* Always show the dial button in the center grid column */
grid-column: 2;
background-color: $accent-color;
&::before { &::before {
content: ''; content: '';
display: inline-block; display: inline-block;
@ -42,21 +61,7 @@ limitations under the License.
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-size: 20px; mask-size: 20px;
mask-position: center; mask-position: center;
background-color: $primary-bg-color; background-color: #FFF; // on all themes
}
}
.mx_DialPad_deleteButton {
background-color: $notice-primary-color;
&::before {
mask-image: url('$(res)/img/element-icons/call/delete.svg');
mask-position: 9px; // delete icon is right-heavy so have to be slightly to the left to look centered
}
}
.mx_DialPad_dialButton {
background-color: $accent-color;
&::before {
mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
} }
} }

View file

@ -14,10 +14,40 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_DialPadContextMenu_dialPad .mx_DialPad {
row-gap: 16px;
column-gap: 32px;
}
.mx_DialPadContextMenuWrapper {
padding: 15px;
}
.mx_DialPadContextMenu_header { .mx_DialPadContextMenu_header {
margin-top: 12px; border: none;
margin-left: 12px; margin-top: 32px;
margin-right: 12px; margin-left: 20px;
margin-right: 20px;
/* a separator between the input line and the dial buttons */
border-bottom: 1px solid $quaternary-fg-color;
transition: border-bottom 0.25s;
}
.mx_DialPadContextMenu_cancel {
float: right;
mask: url('$(res)/img/feather-customised/cancel.svg');
mask-repeat: no-repeat;
mask-position: center;
mask-size: cover;
width: 14px;
height: 14px;
background-color: $dialog-close-fg-color;
cursor: pointer;
}
.mx_DialPadContextMenu_header:focus-within {
border-bottom: 1px solid $accent-color;
} }
.mx_DialPadContextMenu_title { .mx_DialPadContextMenu_title {
@ -30,7 +60,6 @@ limitations under the License.
height: 1.5em; height: 1.5em;
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
max-width: 150px;
border: none; border: none;
margin: 0px; margin: 0px;
} }
@ -38,7 +67,7 @@ limitations under the License.
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
overflow: hidden; overflow: hidden;
max-width: 150px; max-width: 185px;
text-align: left; text-align: left;
direction: rtl; direction: rtl;
padding: 8px 0px; padding: 8px 0px;
@ -48,13 +77,3 @@ limitations under the License.
.mx_DialPadContextMenu_dialPad { .mx_DialPadContextMenu_dialPad {
margin: 16px; margin: 16px;
} }
.mx_DialPadContextMenu_horizSep {
position: relative;
&::before {
content: '';
position: absolute;
width: 100%;
border-bottom: 1px solid $input-darker-bg-color;
}
}

View file

@ -19,14 +19,23 @@ limitations under the License.
} }
.mx_DialPadModal { .mx_DialPadModal {
width: 192px; width: 292px;
height: 368px; height: 370px;
padding: 16px 0px 0px 0px;
} }
.mx_DialPadModal_header { .mx_DialPadModal_header {
margin-top: 12px; margin-top: 32px;
margin-left: 12px; margin-left: 40px;
margin-right: 12px; margin-right: 40px;
/* a separator between the input line and the dial buttons */
border-bottom: 1px solid $quaternary-fg-color;
transition: border-bottom 0.25s;
}
.mx_DialPadModal_header:focus-within {
border-bottom: 1px solid $accent-color;
} }
.mx_DialPadModal_title { .mx_DialPadModal_title {
@ -45,11 +54,18 @@ limitations under the License.
height: 14px; height: 14px;
background-color: $dialog-close-fg-color; background-color: $dialog-close-fg-color;
cursor: pointer; cursor: pointer;
margin-right: 16px;
} }
.mx_DialPadModal_field { .mx_DialPadModal_field {
border: none; border: none;
margin: 0px; margin: 0px;
height: 30px;
}
.mx_DialPadModal_field .mx_Field_postfix {
/* Remove border separator between postfix and field content */
border-left: none;
} }
.mx_DialPadModal_field input { .mx_DialPadModal_field input {
@ -62,13 +78,3 @@ limitations under the License.
margin-right: 16px; margin-right: 16px;
margin-top: 16px; margin-top: 16px;
} }
.mx_DialPadModal_horizSep {
position: relative;
&::before {
content: '';
position: absolute;
width: 100%;
border-bottom: 1px solid $input-darker-bg-color;
}
}

View file

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.97991 1.48403L4 4.80062L1 4.80062C0.447715 4.80062 0 5.24834 0 5.80062V10.2006C0 10.7529 0.447714 11.2006 0.999999 11.2006L4 11.2006L7.97991 14.5172C8.30557 14.7886 8.8 14.557 8.8 14.1331V1.86814C8.8 1.44422 8.30557 1.21265 7.97991 1.48403Z" fill="#737D8C"/>
<path d="M14.1258 2.79107C13.8998 2.50044 13.4809 2.44808 13.1903 2.67413C12.9 2.89992 12.8475 3.3181 13.0726 3.6087L13.0731 3.60935L13.0738 3.61021L13.0829 3.62231C13.0917 3.63418 13.1059 3.65355 13.1248 3.68011C13.1625 3.73326 13.2187 3.81496 13.2872 3.92256C13.4243 4.13812 13.6097 4.45554 13.7955 4.85371C14.169 5.65407 14.5329 6.75597 14.5329 8.00036C14.5329 9.24475 14.169 10.3466 13.7955 11.147C13.6097 11.5452 13.4243 11.8626 13.2872 12.0782C13.2187 12.1858 13.1625 12.2675 13.1248 12.3206C13.1059 12.3472 13.0917 12.3665 13.0829 12.3784L13.0738 12.3905L13.0731 12.3914L13.0725 12.3921C12.8475 12.6827 12.9 13.1008 13.1903 13.3266C13.4809 13.5526 13.8998 13.5003 14.1258 13.2097L13.629 12.8232C14.1258 13.2096 14.1258 13.2097 14.1258 13.2097L14.1272 13.2079L14.1291 13.2055L14.1346 13.1982L14.1523 13.1748C14.1669 13.1552 14.187 13.1277 14.2119 13.0926C14.2617 13.0225 14.3305 12.9221 14.4121 12.794C14.5749 12.5381 14.7895 12.1698 15.0037 11.7109C15.4302 10.7969 15.8663 9.49883 15.8663 8.00036C15.8663 6.50189 15.4302 5.20379 15.0037 4.28987C14.7895 3.83089 14.5749 3.4626 14.4121 3.20673C14.3305 3.07862 14.2617 2.97818 14.2119 2.90811C14.187 2.87306 14.1669 2.84556 14.1523 2.82596L14.1346 2.80249L14.1291 2.79525L14.1272 2.79278L14.1264 2.79183C14.1264 2.79183 14.1258 2.79107 13.5996 3.20036L14.1258 2.79107Z" fill="#737D8C"/>
<path d="M11.7264 5.19121C11.5004 4.90058 11.0815 4.84823 10.7909 5.07427C10.501 5.29973 10.4482 5.71698 10.6722 6.00752L10.6745 6.01057C10.6775 6.01457 10.6831 6.02223 10.691 6.03338C10.7069 6.05572 10.7318 6.09189 10.7628 6.14057C10.8249 6.23827 10.9103 6.38426 10.9961 6.56815C11.1696 6.93993 11.3335 7.44183 11.3335 8.00051C11.3335 8.55918 11.1696 9.06108 10.9961 9.43287C10.9103 9.61675 10.8249 9.76275 10.7628 9.86045C10.7318 9.90912 10.7069 9.94529 10.691 9.96763C10.6831 9.97879 10.6775 9.98645 10.6745 9.99044L10.6722 9.9935C10.4482 10.284 10.501 10.7013 10.7909 10.9267C11.0815 11.1528 11.5004 11.1004 11.7264 10.8098L11.2002 10.4005C11.7264 10.8098 11.7264 10.8098 11.7264 10.8098L11.7276 10.8083L11.7291 10.8064L11.7329 10.8014L11.7439 10.7868C11.7526 10.7751 11.7642 10.7593 11.7781 10.7396C11.806 10.7004 11.8436 10.6455 11.8876 10.5763C11.9755 10.4383 12.0901 10.2414 12.2043 9.99672C12.4308 9.51136 12.6669 8.81326 12.6669 8.00051C12.6669 7.18775 12.4308 6.48965 12.2043 6.0043C12.0901 5.75961 11.9755 5.56275 11.8876 5.42473C11.8436 5.35555 11.806 5.30065 11.7781 5.26138C11.7642 5.24173 11.7526 5.22596 11.7439 5.21422L11.7329 5.19964L11.7291 5.19465L11.7276 5.19274L11.727 5.19193C11.727 5.19193 11.7264 5.19121 11.2002 5.60051L11.7264 5.19121Z" fill="#737D8C"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View 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
View 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

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 19C10.9 19 10 19.9 10 21C10 22.1 10.9 23 12 23C13.1 23 14 22.1 14 21C14 19.9 13.1 19 12 19ZM6 1C4.9 1 4 1.9 4 3C4 4.1 4.9 5 6 5C7.1 5 8 4.1 8 3C8 1.9 7.1 1 6 1ZM6 7C4.9 7 4 7.9 4 9C4 10.1 4.9 11 6 11C7.1 11 8 10.1 8 9C8 7.9 7.1 7 6 7ZM6 13C4.9 13 4 13.9 4 15C4 16.1 4.9 17 6 17C7.1 17 8 16.1 8 15C8 13.9 7.1 13 6 13ZM18 5C19.1 5 20 4.1 20 3C20 1.9 19.1 1 18 1C16.9 1 16 1.9 16 3C16 4.1 16.9 5 18 5ZM12 13C10.9 13 10 13.9 10 15C10 16.1 10.9 17 12 17C13.1 17 14 16.1 14 15C14 13.9 13.1 13 12 13ZM18 13C16.9 13 16 13.9 16 15C16 16.1 16.9 17 18 17C19.1 17 20 16.1 20 15C20 13.9 19.1 13 18 13ZM18 7C16.9 7 16 7.9 16 9C16 10.1 16.9 11 18 11C19.1 11 20 10.1 20 9C20 7.9 19.1 7 18 7ZM12 7C10.9 7 10 7.9 10 9C10 10.1 10.9 11 12 11C13.1 11 14 10.1 14 9C14 7.9 13.1 7 12 7ZM12 1C10.9 1 10 1.9 10 3C10 4.1 10.9 5 12 5C13.1 5 14 4.1 14 3C14 1.9 13.1 1 12 1Z" fill="#8D97A5"/>
</svg>

After

Width:  |  Height:  |  Size: 979 B

View file

@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="path-1-inside-1" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.1502 21.1214C16.3946 22.3074 14.2782 23 12 23C9.52367 23 7.23845 22.1817 5.4 20.8008C2.72821 18.794 1 15.5988 1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 15.797 21.0762 19.1446 18.1502 21.1214ZM12 12.55C13.8225 12.55 15.3 10.9494 15.3 8.975C15.3 7.00058 13.8225 5.4 12 5.4C10.1775 5.4 8.7 7.00058 8.7 8.975C8.7 10.9494 10.1775 12.55 12 12.55ZM12 20.8C14.3782 20.8 16.536 19.8566 18.1197 18.3237C17.1403 15.9056 14.7693 14.2 12 14.2C9.23066 14.2 6.85969 15.9056 5.88028 18.3237C7.46399 19.8566 9.62183 20.8 12 20.8Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.1502 21.1214C16.3946 22.3074 14.2782 23 12 23C9.52367 23 7.23845 22.1817 5.4 20.8008C2.72821 18.794 1 15.5988 1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 15.797 21.0762 19.1446 18.1502 21.1214ZM12 12.55C13.8225 12.55 15.3 10.9494 15.3 8.975C15.3 7.00058 13.8225 5.4 12 5.4C10.1775 5.4 8.7 7.00058 8.7 8.975C8.7 10.9494 10.1775 12.55 12 12.55ZM12 20.8C14.3782 20.8 16.536 19.8566 18.1197 18.3237C17.1403 15.9056 14.7693 14.2 12 14.2C9.23066 14.2 6.85969 15.9056 5.88028 18.3237C7.46399 19.8566 9.62183 20.8 12 20.8Z" fill="#8D97A5"/>
<path d="M18.1502 21.1214L18.9339 22.2814L18.1502 21.1214ZM5.4 20.8008L4.55919 21.9202H4.55919L5.4 20.8008ZM18.1197 18.3237L19.0934 19.3296L19.7717 18.6731L19.4173 17.7981L18.1197 18.3237ZM5.88028 18.3237L4.58268 17.7981L4.22829 18.6731L4.90659 19.3296L5.88028 18.3237ZM12 24.4C14.5662 24.4 16.9541 23.619 18.9339 22.2814L17.3665 19.9613C15.835 20.9959 13.9902 21.6 12 21.6V24.4ZM4.55919 21.9202C6.63176 23.477 9.21011 24.4 12 24.4V21.6C9.83723 21.6 7.84514 20.8865 6.24081 19.6814L4.55919 21.9202ZM-0.399998 12C-0.399998 16.0577 1.55052 19.6603 4.55919 21.9202L6.24081 19.6814C3.90591 17.9276 2.4 15.1399 2.4 12H-0.399998ZM12 -0.399998C5.15167 -0.399998 -0.399998 5.15167 -0.399998 12H2.4C2.4 6.69807 6.69807 2.4 12 2.4V-0.399998ZM24.4 12C24.4 5.15167 18.8483 -0.399998 12 -0.399998V2.4C17.3019 2.4 21.6 6.69807 21.6 12H24.4ZM18.9339 22.2814C22.2288 20.0554 24.4 16.2815 24.4 12H21.6C21.6 15.3124 19.9236 18.2337 17.3665 19.9613L18.9339 22.2814ZM13.9 8.975C13.9 10.2838 12.9459 11.15 12 11.15V13.95C14.6991 13.95 16.7 11.615 16.7 8.975H13.9ZM12 6.8C12.9459 6.8 13.9 7.66616 13.9 8.975H16.7C16.7 6.335 14.6991 4 12 4V6.8ZM10.1 8.975C10.1 7.66616 11.0541 6.8 12 6.8V4C9.30086 4 7.3 6.335 7.3 8.975H10.1ZM12 11.15C11.0541 11.15 10.1 10.2838 10.1 8.975H7.3C7.3 11.615 9.30086 13.95 12 13.95V11.15ZM17.146 17.3178C15.8129 18.6081 14.0004 19.4 12 19.4V22.2C14.756 22.2 17.2591 21.1051 19.0934 19.3296L17.146 17.3178ZM12 15.6C14.1797 15.6 16.0494 16.9415 16.8221 18.8493L19.4173 17.7981C18.2312 14.8697 15.359 12.8 12 12.8V15.6ZM7.17788 18.8493C7.95058 16.9415 9.8203 15.6 12 15.6V12.8C8.64102 12.8 5.7688 14.8697 4.58268 17.7981L7.17788 18.8493ZM12 19.4C9.99963 19.4 8.18709 18.6081 6.85397 17.3178L4.90659 19.3296C6.74088 21.1051 9.24402 22.2 12 22.2V19.4Z" fill="#8D97A5" mask="url(#path-1-inside-1)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -118,7 +118,7 @@ $voipcall-plinth-color: #394049;
// ******************** // ********************
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$dialpad-button-bg-color: #6F7882; $dialpad-button-bg-color: #394049;
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
$roomlist-filter-active-bg-color: $bg-color; $roomlist-filter-active-bg-color: $bg-color;
@ -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 {
@ -288,3 +295,11 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
.hljs-tag { .hljs-tag {
color: inherit; // Without this they'd be weirdly blue which doesn't match the theme color: inherit; // Without this they'd be weirdly blue which doesn't match the theme
} }
.hljs-addition {
background: #1a4b59;
}
.hljs-deletion {
background: #53232a;
}

View file

@ -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 {

View file

@ -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 {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { JSXElementConstructor } from "react"; import React, { JSXElementConstructor } from "react";
// Based on https://stackoverflow.com/a/53229857/3532235 // Based on https://stackoverflow.com/a/53229857/3532235
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>]?: never}; export type Without<T, U> = {[P in Exclude<keyof T, keyof U>]?: never};
@ -22,3 +22,4 @@ export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<
export type Writeable<T> = { -readonly [P in keyof T]: T[P] }; export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor<any>; export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
export type ReactAnyComponent = React.Component | React.ExoticComponent;

View file

@ -15,6 +15,8 @@ limitations under the License.
*/ */
import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
// Load types for the WG CSS Font Loading APIs https://github.com/Microsoft/TypeScript/issues/13569
import "@types/css-font-loading-module";
import "@types/modernizr"; import "@types/modernizr";
import ContentMessages from "../ContentMessages"; import ContentMessages from "../ContentMessages";
@ -48,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>;
@ -88,6 +92,7 @@ declare global {
mxUIStore: UIStore; mxUIStore: UIStore;
mxSetupEncryptionStore?: SetupEncryptionStore; mxSetupEncryptionStore?: SetupEncryptionStore;
mxRoomScrollStateStore?: RoomScrollStateStore; mxRoomScrollStateStore?: RoomScrollStateStore;
mxOnRecaptchaLoaded?: () => void;
} }
interface Document { interface Document {
@ -112,7 +117,7 @@ declare global {
} }
interface StorageEstimate { interface StorageEstimate {
usageDetails?: {[key: string]: number}; usageDetails?: { [key: string]: number };
} }
interface HTMLAudioElement { interface HTMLAudioElement {
@ -183,4 +188,21 @@ declare global {
parameterDescriptors?: AudioParamDescriptor[]; parameterDescriptors?: AudioParamDescriptor[];
} }
); );
// eslint-disable-next-line no-var
var grecaptcha:
| undefined
| {
reset: (id: string) => void;
render: (
divId: string,
options: {
sitekey: string;
callback: (response: string) => void;
},
) => string;
isReady: () => boolean;
};
} }
/* eslint-enable @typescript-eslint/naming-convention */

23
src/@types/worker-loader.d.ts vendored Normal file
View file

@ -0,0 +1,23 @@
/*
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.
*/
declare module "*.worker.ts" {
class WebpackWorker extends Worker {
constructor();
}
export default WebpackWorker;
}

View file

@ -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.

View file

@ -248,7 +248,7 @@ export default class AddThreepid {
/** /**
* Takes a phone number verification code as entered by the user and validates * Takes a phone number verification code as entered by the user and validates
* it with the ID server, then if successful, adds the phone number. * it with the identity server, then if successful, adds the phone number.
* @param {string} msisdnToken phone number verification code as entered by the user * @param {string} msisdnToken phone number verification code as entered by the user
* @return {Promise} Resolves if the phone number was added. Rejects with an object * @return {Promise} Resolves if the phone number was added. Rejects with an object
* with a "message" property which contains a human-readable message detailing why * with a "message" property which contains a human-readable message detailing why

View file

@ -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,
@ -395,17 +395,17 @@ export class Analytics {
Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, { Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, {
title: _t('Analytics'), title: _t('Analytics'),
description: <div className="mx_AnalyticsModal"> description: <div className="mx_AnalyticsModal">
<div>{_t('The information being sent to us to help make %(brand)s better includes:', { <div>{ _t('The information being sent to us to help make %(brand)s better includes:', {
brand: SdkConfig.get().brand, brand: SdkConfig.get().brand,
})}</div> }) }</div>
<table> <table>
{ rows.map((row) => <tr key={row[0]}> { rows.map((row) => <tr key={row[0]}>
<td>{_t( <td>{ _t(
customVariables[row[0]].expl, customVariables[row[0]].expl,
customVariables[row[0]].getTextVariables ? customVariables[row[0]].getTextVariables ?
customVariables[row[0]].getTextVariables() : customVariables[row[0]].getTextVariables() :
null, null,
)}</td> ) }</td>
{ row[1] !== undefined && <td><code>{ row[1] }</code></td> } { row[1] !== undefined && <td><code>{ row[1] }</code></td> }
</tr>) } </tr>) }
{ otherVariables.map((item, index) => { otherVariables.map((item, index) =>

View file

@ -18,10 +18,11 @@ 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";
import SettingsStore from "./settings/SettingsStore"; import SpaceStore from "./stores/SpaceStore";
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already // Not to be used for BaseAvatar urls as that has similar default avatar fallback already
export function avatarUrlForMember( export function avatarUrlForMember(
@ -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 its 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) {
@ -153,7 +140,7 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi
} }
// space rooms cannot be DMs so skip the rest // space rooms cannot be DMs so skip the rest
if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) return null; if (SpaceStore.spacesEnabled && room.isSpaceRoom()) return null;
let otherMember = null; let otherMember = null;
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId); const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);

60
src/BlurhashEncoder.ts Normal file
View file

@ -0,0 +1,60 @@
/*
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 { defer, IDeferred } from "matrix-js-sdk/src/utils";
// @ts-ignore - `.ts` is needed here to make TS happy
import BlurhashWorker from "./workers/blurhash.worker.ts";
interface IBlurhashWorkerResponse {
seq: number;
blurhash: string;
}
export class BlurhashEncoder {
private static internalInstance = new BlurhashEncoder();
public static get instance(): BlurhashEncoder {
return BlurhashEncoder.internalInstance;
}
private readonly worker: Worker;
private seq = 0;
private pendingDeferredMap = new Map<number, IDeferred<string>>();
constructor() {
this.worker = new BlurhashWorker();
this.worker.onmessage = this.onMessage;
}
private onMessage = (ev: MessageEvent<IBlurhashWorkerResponse>) => {
const { seq, blurhash } = ev.data;
const deferred = this.pendingDeferredMap.get(seq);
if (deferred) {
this.pendingDeferredMap.delete(seq);
deferred.resolve(blurhash);
}
};
public getBlurhash(imageData: ImageData): Promise<string> {
const seq = this.seq++;
const deferred = defer<string>();
this.pendingDeferredMap.set(seq, deferred);
this.worker.postMessage({ seq, imageData });
return deferred.promise;
}
}

View file

@ -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 {
@ -154,7 +155,7 @@ export default class CallHandler extends EventEmitter {
private supportsPstnProtocol = null; private supportsPstnProtocol = null;
private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol
private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native
private pstnSupportCheckTimer: NodeJS.Timeout; // number actually because we're in the browser private pstnSupportCheckTimer: number;
// For rooms we've been invited to, true if they're from virtual user, false if we've checked and they aren't. // For rooms we've been invited to, true if they're from virtual user, false if we've checked and they aren't.
private invitedRoomsAreVirtual = new Map<string, boolean>(); private invitedRoomsAreVirtual = new Map<string, boolean>();
private invitedRoomCheckInProgress = false; private invitedRoomCheckInProgress = false;
@ -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;
} }
@ -394,7 +431,7 @@ export default class CallHandler extends EventEmitter {
} }
private setCallListeners(call: MatrixCall) { private setCallListeners(call: MatrixCall) {
let mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); let mappedRoomId = this.roomIdForCall(call);
call.on(CallEvent.Error, (err: CallError) => { call.on(CallEvent.Error, (err: CallError) => {
if (!this.matchesCallForThisRoom(call)) return; if (!this.matchesCallForThisRoom(call)) return;
@ -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);
@ -615,23 +656,23 @@ export default class CallHandler extends EventEmitter {
private showICEFallbackPrompt() { private showICEFallbackPrompt() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const code = sub => <code>{sub}</code>; const code = sub => <code>{ sub }</code>;
Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, { Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
title: _t("Call failed due to misconfigured server"), title: _t("Call failed due to misconfigured server"),
description: <div> description: <div>
<p>{_t( <p>{ _t(
"Please ask the administrator of your homeserver " + "Please ask the administrator of your homeserver " +
"(<code>%(homeserverDomain)s</code>) to configure a TURN server in " + "(<code>%(homeserverDomain)s</code>) to configure a TURN server in " +
"order for calls to work reliably.", "order for calls to work reliably.",
{ homeserverDomain: cli.getDomain() }, { code }, { homeserverDomain: cli.getDomain() }, { code },
)}</p> ) }</p>
<p>{_t( <p>{ _t(
"Alternatively, you can try to use the public server at " + "Alternatively, you can try to use the public server at " +
"<code>turn.matrix.org</code>, but this will not be as reliable, and " + "<code>turn.matrix.org</code>, but this will not be as reliable, and " +
"it will share your IP address with that server. You can also manage " + "it will share your IP address with that server. You can also manage " +
"this in Settings.", "this in Settings.",
null, { code }, null, { code },
)}</p> ) }</p>
</div>, </div>,
button: _t('Try using turn.matrix.org'), button: _t('Try using turn.matrix.org'),
cancelButton: _t('OK'), cancelButton: _t('OK'),
@ -649,19 +690,19 @@ export default class CallHandler extends EventEmitter {
if (call.type === CallType.Voice) { if (call.type === CallType.Voice) {
title = _t("Unable to access microphone"); title = _t("Unable to access microphone");
description = <div> description = <div>
{_t( { _t(
"Call failed because microphone could not be accessed. " + "Call failed because microphone could not be accessed. " +
"Check that a microphone is plugged in and set up correctly.", "Check that a microphone is plugged in and set up correctly.",
)} ) }
</div>; </div>;
} else if (call.type === CallType.Video) { } else if (call.type === CallType.Video) {
title = _t("Unable to access webcam / microphone"); title = _t("Unable to access webcam / microphone");
description = <div> description = <div>
{_t("Call failed because webcam or microphone could not be accessed. Check that:")} { _t("Call failed because webcam or microphone could not be accessed. Check that:") }
<ul> <ul>
<li>{_t("A microphone and webcam are plugged in and set up correctly")}</li> <li>{ _t("A microphone and webcam are plugged in and set up correctly") }</li>
<li>{_t("Permission is granted to use the webcam")}</li> <li>{ _t("Permission is granted to use the webcam") }</li>
<li>{_t("No other application is using the webcam")}</li> <li>{ _t("No other application is using the webcam") }</li>
</ul> </ul>
</div>; </div>;
} }
@ -871,6 +912,12 @@ export default class CallHandler extends EventEmitter {
case Action.DialNumber: case Action.DialNumber:
this.dialNumber(payload.number); this.dialNumber(payload.number);
break; break;
case Action.TransferCallToMatrixID:
this.startTransferToMatrixID(payload.call, payload.destination, payload.consultFirst);
break;
case Action.TransferCallToPhoneNumber:
this.startTransferToPhoneNumber(payload.call, payload.destination, payload.consultFirst);
break;
} }
}; };
@ -905,6 +952,48 @@ export default class CallHandler extends EventEmitter {
}); });
} }
private async startTransferToPhoneNumber(call: MatrixCall, destination: string, consultFirst: boolean) {
const results = await this.pstnLookup(destination);
if (!results || results.length === 0 || !results[0].userid) {
Modal.createTrackedDialog('', '', ErrorDialog, {
title: _t("Unable to transfer call"),
description: _t("There was an error looking up the phone number"),
});
return;
}
await this.startTransferToMatrixID(call, results[0].userid, consultFirst);
}
private async startTransferToMatrixID(call: MatrixCall, destination: string, consultFirst: boolean) {
if (consultFirst) {
const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), destination);
dis.dispatch({
action: 'place_call',
type: call.type,
room_id: dmRoomId,
transferee: call,
});
dis.dispatch({
action: 'view_room',
room_id: dmRoomId,
should_peek: false,
joining: false,
});
} else {
try {
await call.transfer(destination);
} catch (e) {
console.log("Failed to transfer call", e);
Modal.createTrackedDialog('Failed to transfer call', '', ErrorDialog, {
title: _t('Transfer Failed'),
description: _t('Failed to transfer call'),
});
}
}
}
setActiveCallRoomId(activeCallRoomId: string) { setActiveCallRoomId(activeCallRoomId: string) {
logger.info("Setting call in room " + activeCallRoomId + " active"); logger.info("Setting call in room " + activeCallRoomId + " active");

View file

@ -17,7 +17,6 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { encode } from "blurhash";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
@ -28,7 +27,6 @@ import RoomViewStore from './stores/RoomViewStore';
import encrypt from "browser-encrypt-attachment"; import encrypt from "browser-encrypt-attachment";
import extractPngChunks from "png-chunks-extract"; import extractPngChunks from "png-chunks-extract";
import Spinner from "./components/views/elements/Spinner"; import Spinner from "./components/views/elements/Spinner";
import { Action } from "./dispatcher/actions"; import { Action } from "./dispatcher/actions";
import CountlyAnalytics from "./CountlyAnalytics"; import CountlyAnalytics from "./CountlyAnalytics";
import { import {
@ -39,7 +37,8 @@ import {
UploadStartedPayload, UploadStartedPayload,
} from "./dispatcher/payloads/UploadPayload"; } from "./dispatcher/payloads/UploadPayload";
import { IUpload } from "./models/IUpload"; import { IUpload } from "./models/IUpload";
import { IImageInfo } from "matrix-js-sdk/src/@types/partials"; import { IAbortablePromise, IImageInfo } from "matrix-js-sdk/src/@types/partials";
import { BlurhashEncoder } from "./BlurhashEncoder";
const MAX_WIDTH = 800; const MAX_WIDTH = 800;
const MAX_HEIGHT = 600; const MAX_HEIGHT = 600;
@ -85,10 +84,6 @@ interface IThumbnail {
thumbnail: Blob; thumbnail: Blob;
} }
interface IAbortablePromise<T> extends Promise<T> {
abort(): void;
}
/** /**
* Create a thumbnail for a image DOM element. * Create a thumbnail for a image DOM element.
* The image will be smaller than MAX_WIDTH and MAX_HEIGHT. * The image will be smaller than MAX_WIDTH and MAX_HEIGHT.
@ -107,13 +102,12 @@ interface IAbortablePromise<T> extends Promise<T> {
* @return {Promise} A promise that resolves with an object with an info key * @return {Promise} A promise that resolves with an object with an info key
* and a thumbnail key. * and a thumbnail key.
*/ */
function createThumbnail( async function createThumbnail(
element: ThumbnailableElement, element: ThumbnailableElement,
inputWidth: number, inputWidth: number,
inputHeight: number, inputHeight: number,
mimeType: string, mimeType: string,
): Promise<IThumbnail> { ): Promise<IThumbnail> {
return new Promise((resolve) => {
let targetWidth = inputWidth; let targetWidth = inputWidth;
let targetHeight = inputHeight; let targetHeight = inputHeight;
if (targetHeight > MAX_HEIGHT) { if (targetHeight > MAX_HEIGHT) {
@ -125,22 +119,32 @@ function createThumbnail(
targetWidth = MAX_WIDTH; targetWidth = MAX_WIDTH;
} }
const canvas = document.createElement("canvas"); let canvas: HTMLCanvasElement | OffscreenCanvas;
if (window.OffscreenCanvas) {
canvas = new window.OffscreenCanvas(targetWidth, targetHeight);
} else {
canvas = document.createElement("canvas");
canvas.width = targetWidth; canvas.width = targetWidth;
canvas.height = targetHeight; canvas.height = targetHeight;
}
const context = canvas.getContext("2d"); const context = canvas.getContext("2d");
context.drawImage(element, 0, 0, targetWidth, targetHeight); context.drawImage(element, 0, 0, targetWidth, targetHeight);
let thumbnailPromise: Promise<Blob>;
if (window.OffscreenCanvas) {
thumbnailPromise = (canvas as OffscreenCanvas).convertToBlob({ type: mimeType });
} else {
thumbnailPromise = new Promise<Blob>(resolve => (canvas as HTMLCanvasElement).toBlob(resolve, mimeType));
}
const imageData = context.getImageData(0, 0, targetWidth, targetHeight); const imageData = context.getImageData(0, 0, targetWidth, targetHeight);
const blurhash = encode( // thumbnailPromise and blurhash promise are being awaited concurrently
imageData.data, const blurhash = await BlurhashEncoder.instance.getBlurhash(imageData);
imageData.width, const thumbnail = await thumbnailPromise;
imageData.height,
// use 4 components on the longer dimension, if square then both return {
imageData.width >= imageData.height ? 4 : 3,
imageData.height >= imageData.width ? 4 : 3,
);
canvas.toBlob(function(thumbnail) {
resolve({
info: { info: {
thumbnail_info: { thumbnail_info: {
w: targetWidth, w: targetWidth,
@ -153,9 +157,7 @@ function createThumbnail(
[BLURHASH_FIELD]: blurhash, [BLURHASH_FIELD]: blurhash,
}, },
thumbnail, thumbnail,
}); };
}, mimeType);
});
} }
/** /**
@ -333,7 +335,7 @@ export function uploadFile(
roomId: string, roomId: string,
file: File | Blob, file: File | Blob,
progressHandler?: any, // TODO: Types progressHandler?: any, // TODO: Types
): Promise<{url?: string, file?: any}> { // TODO: Types ): IAbortablePromise<{url?: string, file?: any}> { // TODO: Types
let canceled = false; let canceled = false;
if (matrixClient.isRoomEncrypted(roomId)) { if (matrixClient.isRoomEncrypted(roomId)) {
// If the room is encrypted then encrypt the file before uploading it. // If the room is encrypted then encrypt the file before uploading it.
@ -365,8 +367,8 @@ export function uploadFile(
encryptInfo.mimetype = file.type; encryptInfo.mimetype = file.type;
} }
return { "file": encryptInfo }; return { "file": encryptInfo };
}); }) as IAbortablePromise<{ file: any }>;
(prom as IAbortablePromise<any>).abort = () => { prom.abort = () => {
canceled = true; canceled = true;
if (uploadPromise) matrixClient.cancelUpload(uploadPromise); if (uploadPromise) matrixClient.cancelUpload(uploadPromise);
}; };
@ -379,8 +381,8 @@ export function uploadFile(
if (canceled) throw new UploadCanceledError(); if (canceled) throw new UploadCanceledError();
// If the attachment isn't encrypted then include the URL directly. // If the attachment isn't encrypted then include the URL directly.
return { url }; return { url };
}); }) as IAbortablePromise<{ url: string }>;
(promise1 as any).abort = () => { promise1.abort = () => {
canceled = true; canceled = true;
matrixClient.cancelUpload(basePromise); matrixClient.cancelUpload(basePromise);
}; };
@ -423,10 +425,10 @@ export default class ContentMessages {
const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, { const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, {
title: _t('Replying With Files'), title: _t('Replying With Files'),
description: ( description: (
<div>{_t( <div>{ _t(
'At this time it is not possible to reply with a file. ' + 'At this time it is not possible to reply with a file. ' +
'Would you like to upload this file without replying?', 'Would you like to upload this file without replying?',
)}</div> ) }</div>
), ),
hasCancelButton: true, hasCancelButton: true,
button: _t("Continue"), button: _t("Continue"),
@ -551,10 +553,10 @@ export default class ContentMessages {
content.msgtype = 'm.file'; content.msgtype = 'm.file';
resolve(); resolve();
} }
}); }) as IAbortablePromise<void>;
// create temporary abort handler for before the actual upload gets passed off to js-sdk // create temporary abort handler for before the actual upload gets passed off to js-sdk
(prom as IAbortablePromise<any>).abort = () => { prom.abort = () => {
upload.canceled = true; upload.canceled = true;
}; };
@ -583,9 +585,7 @@ export default class ContentMessages {
// XXX: upload.promise must be the promise that // XXX: upload.promise must be the promise that
// is returned by uploadFile as it has an abort() // is returned by uploadFile as it has an abort()
// method hacked onto it. // method hacked onto it.
upload.promise = uploadFile( upload.promise = uploadFile(matrixClient, roomId, file, onProgress);
matrixClient, roomId, file, onProgress,
);
return upload.promise.then(function(result) { return upload.promise.then(function(result) {
content.file = result.file; content.file = result.file;
content.url = result.url; content.url = result.url;

View file

@ -364,8 +364,8 @@ export default class CountlyAnalytics {
private initTime = CountlyAnalytics.getTimestamp(); private initTime = CountlyAnalytics.getTimestamp();
private firstPage = true; private firstPage = true;
private heartbeatIntervalId: NodeJS.Timeout; private heartbeatIntervalId: number;
private activityIntervalId: NodeJS.Timeout; private activityIntervalId: number;
private trackTime = true; private trackTime = true;
private lastBeat: number; private lastBeat: number;
private storedDuration = 0; private storedDuration = 0;

View file

@ -46,8 +46,8 @@ export class DecryptionFailureTracker {
}; };
// Set to an interval ID when `start` is called // Set to an interval ID when `start` is called
public checkInterval: NodeJS.Timeout = null; public checkInterval: number = null;
public trackInterval: NodeJS.Timeout = null; public trackInterval: number = null;
// Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`. // Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`.
static TRACK_INTERVAL_MS = 60000; static TRACK_INTERVAL_MS = 60000;

View file

@ -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

View file

@ -25,7 +25,6 @@ import _linkifyElement from 'linkifyjs/element';
import _linkifyString from 'linkifyjs/string'; import _linkifyString from 'linkifyjs/string';
import classNames from 'classnames'; import classNames from 'classnames';
import EMOJIBASE_REGEX from 'emojibase-regex'; import EMOJIBASE_REGEX from 'emojibase-regex';
import url from 'url';
import katex from 'katex'; import katex from 'katex';
import { AllHtmlEntities } from 'html-entities'; import { AllHtmlEntities } from 'html-entities';
import { IContent } from 'matrix-js-sdk/src/models/event'; import { IContent } from 'matrix-js-sdk/src/models/event';
@ -34,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";
@ -58,7 +57,9 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet']; export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet', 'matrix'];
const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/;
/* /*
* Return true if the given string contains emoji * Return true if the given string contains emoji
@ -78,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 ? `:${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 {
@ -151,10 +140,8 @@ export function getHtmlText(insaneHtml: string): string {
*/ */
export function isUrlPermitted(inputUrl: string): boolean { export function isUrlPermitted(inputUrl: string): boolean {
try { try {
const parsed = url.parse(inputUrl);
if (!parsed.protocol) return false;
// URL parser protocol includes the trailing colon // URL parser protocol includes the trailing colon
return PERMITTED_URL_SCHEMES.includes(parsed.protocol.slice(0, -1)); return PERMITTED_URL_SCHEMES.includes(new URL(inputUrl).protocol.slice(0, -1));
} catch (e) { } catch (e) {
return false; return false;
} }
@ -176,18 +163,31 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
return { tagName, attribs }; return { tagName, attribs };
}, },
'img': function(tagName: string, attribs: sanitizeHtml.Attributes) { 'img': function(tagName: string, attribs: sanitizeHtml.Attributes) {
let src = attribs.src;
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag // Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
// because transformTags is used _before_ we filter by allowedSchemesByTag and // because transformTags is used _before_ we filter by allowedSchemesByTag and
// we don't want to allow images with `https?` `src`s. // we don't want to allow images with `https?` `src`s.
// We also drop inline images (as if they were not present at all) when the "show // We also drop inline images (as if they were not present at all) when the "show
// images" preference is disabled. Future work might expose some UI to reveal them // images" preference is disabled. Future work might expose some UI to reveal them
// like standalone image events have. // like standalone image events have.
if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) { if (!src || !SettingsStore.getValue("showImages")) {
return { tagName, attribs: {} }; return { tagName, attribs: {} };
} }
if (!src.startsWith("mxc://")) {
const match = MEDIA_API_MXC_REGEX.exec(src);
if (match) {
src = `mxc://${match[1]}/${match[2]}`;
}
}
if (!src.startsWith("mxc://")) {
return { tagName, attribs: {} };
}
const width = Number(attribs.width) || 800; const width = Number(attribs.width) || 800;
const height = Number(attribs.height) || 600; const height = Number(attribs.height) || 600;
attribs.src = mediaFromMxc(attribs.src).getThumbnailOfSourceHttp(width, height); attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height);
return { tagName, attribs }; return { tagName, attribs };
}, },
'code': function(tagName: string, attribs: sanitizeHtml.Attributes) { 'code': function(tagName: string, attribs: sanitizeHtml.Attributes) {

View file

@ -127,7 +127,7 @@ export default class IdentityAuthClient {
await this._matrixClient.getIdentityAccount(token); await this._matrixClient.getIdentityAccount(token);
} catch (e) { } catch (e) {
if (e.errcode === "M_TERMS_NOT_SIGNED") { if (e.errcode === "M_TERMS_NOT_SIGNED") {
console.log("Identity Server requires new terms to be agreed to"); console.log("Identity server requires new terms to be agreed to");
await startTermsFlow([new Service( await startTermsFlow([new Service(
SERVICE_TYPES.IS, SERVICE_TYPES.IS,
identityServerUrl, identityServerUrl,
@ -149,17 +149,17 @@ export default class IdentityAuthClient {
title: _t("Identity server has no terms of service"), title: _t("Identity server has no terms of service"),
description: ( description: (
<div> <div>
<p>{_t( <p>{ _t(
"This action requires accessing the default identity server " + "This action requires accessing the default identity server " +
"<server /> to validate an email address or phone number, " + "<server /> to validate an email address or phone number, " +
"but the server does not have any terms of service.", {}, "but the server does not have any terms of service.", {},
{ {
server: () => <b>{abbreviateUrl(identityServerUrl)}</b>, server: () => <b>{ abbreviateUrl(identityServerUrl) }</b>,
}, },
)}</p> ) }</p>
<p>{_t( <p>{ _t(
"Only continue if you trust the owner of the server.", "Only continue if you trust the owner of the server.",
)}</p> ) }</p>
</div> </div>
), ),
button: _t("Trust"), button: _t("Trust"),

View file

@ -21,6 +21,7 @@ import { createClient } from 'matrix-js-sdk/src/matrix';
import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes"; import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes";
import { QueryDict } from 'matrix-js-sdk/src/utils';
import { IMatrixClientCreds, MatrixClientPeg } from './MatrixClientPeg'; import { IMatrixClientCreds, MatrixClientPeg } from './MatrixClientPeg';
import SecurityCustomisations from "./customisations/Security"; import SecurityCustomisations from "./customisations/Security";
@ -65,7 +66,7 @@ interface ILoadSessionOpts {
guestIsUrl?: string; guestIsUrl?: string;
ignoreGuest?: boolean; ignoreGuest?: boolean;
defaultDeviceDisplayName?: string; defaultDeviceDisplayName?: string;
fragmentQueryParams?: Record<string, string>; fragmentQueryParams?: QueryDict;
} }
/** /**
@ -118,8 +119,8 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
) { ) {
console.log("Using guest access credentials"); console.log("Using guest access credentials");
return doSetLoggedIn({ return doSetLoggedIn({
userId: fragmentQueryParams.guest_user_id, userId: fragmentQueryParams.guest_user_id as string,
accessToken: fragmentQueryParams.guest_access_token, accessToken: fragmentQueryParams.guest_access_token as string,
homeserverUrl: guestHsUrl, homeserverUrl: guestHsUrl,
identityServerUrl: guestIsUrl, identityServerUrl: guestIsUrl,
guest: true, guest: true,
@ -173,7 +174,7 @@ export async function getStoredSessionOwner(): Promise<[string, boolean]> {
* login, else false * login, else false
*/ */
export function attemptTokenLogin( export function attemptTokenLogin(
queryParams: Record<string, string>, queryParams: QueryDict,
defaultDeviceDisplayName?: string, defaultDeviceDisplayName?: string,
fragmentAfterLogin?: string, fragmentAfterLogin?: string,
): Promise<boolean> { ): Promise<boolean> {
@ -198,7 +199,7 @@ export function attemptTokenLogin(
homeserver, homeserver,
identityServer, identityServer,
"m.login.token", { "m.login.token", {
token: queryParams.loginToken, token: queryParams.loginToken as string,
initial_device_display_name: defaultDeviceDisplayName, initial_device_display_name: defaultDeviceDisplayName,
}, },
).then(function(creds) { ).then(function(creds) {

View file

@ -17,8 +17,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ICreateClientOpts } from 'matrix-js-sdk/src/matrix'; import { ICreateClientOpts, PendingEventOrdering } from 'matrix-js-sdk/src/matrix';
import { MatrixClient } from 'matrix-js-sdk/src/client'; import { IStartClientOpts, MatrixClient } from 'matrix-js-sdk/src/client';
import { MemoryStore } from 'matrix-js-sdk/src/store/memory'; import { MemoryStore } from 'matrix-js-sdk/src/store/memory';
import * as utils from 'matrix-js-sdk/src/utils'; import * as utils from 'matrix-js-sdk/src/utils';
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
@ -47,25 +47,8 @@ export interface IMatrixClientCreds {
freshLogin?: boolean; freshLogin?: boolean;
} }
// TODO: Move this to the js-sdk
export interface IOpts {
initialSyncLimit?: number;
pendingEventOrdering?: "detached" | "chronological";
lazyLoadMembers?: boolean;
clientWellKnownPollPeriod?: number;
}
export interface IMatrixClientPeg { export interface IMatrixClientPeg {
opts: IOpts; opts: IStartClientOpts;
/**
* Sets the script href passed to the IndexedDB web worker
* If set, a separate web worker will be started to run the IndexedDB
* queries on.
*
* @param {string} script href to the script to be passed to the web worker
*/
setIndexedDbWorkerScript(script: string): void;
/** /**
* Return the server name of the user's homeserver * Return the server name of the user's homeserver
@ -122,12 +105,12 @@ 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'
// event is finished processing. // event is finished processing.
public opts: IOpts = { public opts: IStartClientOpts = {
initialSyncLimit: 20, initialSyncLimit: 20,
}; };
@ -141,10 +124,6 @@ class _MatrixClientPeg implements IMatrixClientPeg {
constructor() { constructor() {
} }
public setIndexedDbWorkerScript(script: string): void {
createMatrixClient.indexedDbWorkerScript = script;
}
public get(): MatrixClient { public get(): MatrixClient {
return this.matrixClient; return this.matrixClient;
} }
@ -231,7 +210,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
const opts = utils.deepCopy(this.opts); const opts = utils.deepCopy(this.opts);
// the react sdk doesn't work without this, so don't allow // the react sdk doesn't work without this, so don't allow
opts.pendingEventOrdering = "detached"; opts.pendingEventOrdering = PendingEventOrdering.Detached;
opts.lazyLoadMembers = true; opts.lazyLoadMembers = true;
opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours
@ -321,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;

View file

@ -378,7 +378,7 @@ export class ModalManager {
const dialog = ( const dialog = (
<div className={classes}> <div className={classes}>
<div className="mx_Dialog"> <div className="mx_Dialog">
{modal.elem} { modal.elem }
</div> </div>
<div className="mx_Dialog_background" onClick={this.onBackgroundClick} /> <div className="mx_Dialog_background" onClick={this.onBackgroundClick} />
</div> </div>

View file

@ -328,7 +328,7 @@ export const Notifier = {
onEvent: function(ev: MatrixEvent) { onEvent: function(ev: MatrixEvent) {
if (!this.isSyncing) return; // don't alert for any messages initially if (!this.isSyncing) return; // don't alert for any messages initially
if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return; if (ev.getSender() === MatrixClientPeg.get().credentials.userId) return;
MatrixClientPeg.get().decryptEventIfNeeded(ev); MatrixClientPeg.get().decryptEventIfNeeded(ev);

View file

@ -17,6 +17,7 @@ limitations under the License.
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClientPeg } from './MatrixClientPeg'; import { MatrixClientPeg } from './MatrixClientPeg';
import AliasCustomisations from './customisations/Alias';
/** /**
* Given a room object, return the alias we should use for it, * Given a room object, return the alias we should use for it,
@ -28,7 +29,18 @@ import { MatrixClientPeg } from './MatrixClientPeg';
* @returns {string} A display alias for the given room * @returns {string} A display alias for the given room
*/ */
export function getDisplayAliasForRoom(room: Room): string { export function getDisplayAliasForRoom(room: Room): string {
return room.getCanonicalAlias() || room.getAltAliases()[0]; return getDisplayAliasForAliasSet(
room.getCanonicalAlias(), room.getAltAliases(),
);
}
// The various display alias getters should all feed through this one path so
// there's a single place to change the logic.
export function getDisplayAliasForAliasSet(canonicalAlias: string, altAliases: string[]): string {
if (AliasCustomisations.getDisplayAliasForAliasSet) {
return AliasCustomisations.getDisplayAliasForAliasSet(canonicalAlias, altAliases);
}
return canonicalAlias || altAliases?.[0];
} }
export function looksLikeDirectMessageRoom(room: Room, myUserId: string): boolean { export function looksLikeDirectMessageRoom(room: Room, myUserId: string): boolean {
@ -72,10 +84,8 @@ export function guessAndSetDMRoom(room: Room, isDirect: boolean): Promise<void>
this room as a DM room this room as a DM room
* @returns {object} A promise * @returns {object} A promise
*/ */
export function setDMRoom(roomId: string, userId: string): Promise<void> { export async function setDMRoom(roomId: string, userId: string): Promise<void> {
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) return;
return Promise.resolve();
}
const mDirectEvent = MatrixClientPeg.get().getAccountData('m.direct'); const mDirectEvent = MatrixClientPeg.get().getAccountData('m.direct');
let dmRoomMap = {}; let dmRoomMap = {};
@ -104,7 +114,7 @@ export function setDMRoom(roomId: string, userId: string): Promise<void> {
dmRoomMap[userId] = roomList; dmRoomMap[userId] = roomList;
} }
return MatrixClientPeg.get().setAccountData('m.direct', dmRoomMap); await MatrixClientPeg.get().setAccountData('m.direct', dmRoomMap);
} }
/** /**

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. 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.
@ -14,26 +14,42 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {
IResultRoomEvents,
ISearchRequestBody,
ISearchResponse,
ISearchResult,
ISearchResults,
SearchOrderBy,
} from "matrix-js-sdk/src/@types/search";
import { IRoomEventFilter } from "matrix-js-sdk/src/filter";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { ISearchArgs } from "./indexing/BaseEventIndexManager";
import EventIndexPeg from "./indexing/EventIndexPeg"; import EventIndexPeg from "./indexing/EventIndexPeg";
import { MatrixClientPeg } from "./MatrixClientPeg"; import { MatrixClientPeg } from "./MatrixClientPeg";
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
const SEARCH_LIMIT = 10; const SEARCH_LIMIT = 10;
async function serverSideSearch(term, roomId = undefined) { async function serverSideSearch(
term: string,
roomId: string = undefined,
): Promise<{ response: ISearchResponse, query: ISearchRequestBody }> {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const filter = { const filter: IRoomEventFilter = {
limit: SEARCH_LIMIT, limit: SEARCH_LIMIT,
}; };
if (roomId !== undefined) filter.rooms = [roomId]; if (roomId !== undefined) filter.rooms = [roomId];
const body = { const body: ISearchRequestBody = {
search_categories: { search_categories: {
room_events: { room_events: {
search_term: term, search_term: term,
filter: filter, filter: filter,
order_by: "recent", order_by: SearchOrderBy.Recent,
event_context: { event_context: {
before_limit: 1, before_limit: 1,
after_limit: 1, after_limit: 1,
@ -45,31 +61,26 @@ async function serverSideSearch(term, roomId = undefined) {
const response = await client.search({ body: body }); const response = await client.search({ body: body });
const result = { return { response, query: body };
response: response,
query: body,
};
return result;
} }
async function serverSideSearchProcess(term, roomId = undefined) { async function serverSideSearchProcess(term: string, roomId: string = undefined): Promise<ISearchResults> {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const result = await serverSideSearch(term, roomId); const result = await serverSideSearch(term, roomId);
// The js-sdk method backPaginateRoomEventsSearch() uses _query internally // The js-sdk method backPaginateRoomEventsSearch() uses _query internally
// so we're reusing the concept here since we wan't to delegate the // so we're reusing the concept here since we want to delegate the
// pagination back to backPaginateRoomEventsSearch() in some cases. // pagination back to backPaginateRoomEventsSearch() in some cases.
const searchResult = { const searchResults: ISearchResults = {
_query: result.query, _query: result.query,
results: [], results: [],
highlights: [], highlights: [],
}; };
return client.processRoomEventsSearch(searchResult, result.response); return client.processRoomEventsSearch(searchResults, result.response);
} }
function compareEvents(a, b) { function compareEvents(a: ISearchResult, b: ISearchResult): number {
const aEvent = a.result; const aEvent = a.result;
const bEvent = b.result; const bEvent = b.result;
@ -79,7 +90,7 @@ function compareEvents(a, b) {
return 0; return 0;
} }
async function combinedSearch(searchTerm) { async function combinedSearch(searchTerm: string): Promise<ISearchResults> {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
// Create two promises, one for the local search, one for the // Create two promises, one for the local search, one for the
@ -111,10 +122,10 @@ async function combinedSearch(searchTerm) {
// returns since that one can be either a server-side one, a local one or a // returns since that one can be either a server-side one, a local one or a
// fake one to fetch the remaining cached events. See the docs for // fake one to fetch the remaining cached events. See the docs for
// combineEvents() for an explanation why we need to cache events. // combineEvents() for an explanation why we need to cache events.
const emptyResult = { const emptyResult: ISeshatSearchResults = {
seshatQuery: localQuery, seshatQuery: localQuery,
_query: serverQuery, _query: serverQuery,
serverSideNextBatch: serverResponse.next_batch, serverSideNextBatch: serverResponse.search_categories.room_events.next_batch,
cachedEvents: [], cachedEvents: [],
oldestEventFrom: "server", oldestEventFrom: "server",
results: [], results: [],
@ -125,7 +136,7 @@ async function combinedSearch(searchTerm) {
const combinedResult = combineResponses(emptyResult, localResponse, serverResponse.search_categories.room_events); const combinedResult = combineResponses(emptyResult, localResponse, serverResponse.search_categories.room_events);
// Let the client process the combined result. // Let the client process the combined result.
const response = { const response: ISearchResponse = {
search_categories: { search_categories: {
room_events: combinedResult, room_events: combinedResult,
}, },
@ -139,10 +150,14 @@ async function combinedSearch(searchTerm) {
return result; return result;
} }
async function localSearch(searchTerm, roomId = undefined, processResult = true) { async function localSearch(
searchTerm: string,
roomId: string = undefined,
processResult = true,
): Promise<{ response: IResultRoomEvents, query: ISearchArgs }> {
const eventIndex = EventIndexPeg.get(); const eventIndex = EventIndexPeg.get();
const searchArgs = { const searchArgs: ISearchArgs = {
search_term: searchTerm, search_term: searchTerm,
before_limit: 1, before_limit: 1,
after_limit: 1, after_limit: 1,
@ -167,11 +182,18 @@ async function localSearch(searchTerm, roomId = undefined, processResult = true)
return result; return result;
} }
async function localSearchProcess(searchTerm, roomId = undefined) { export interface ISeshatSearchResults extends ISearchResults {
seshatQuery?: ISearchArgs;
cachedEvents?: ISearchResult[];
oldestEventFrom?: "local" | "server";
serverSideNextBatch?: string;
}
async function localSearchProcess(searchTerm: string, roomId: string = undefined): Promise<ISeshatSearchResults> {
const emptyResult = { const emptyResult = {
results: [], results: [],
highlights: [], highlights: [],
}; } as ISeshatSearchResults;
if (searchTerm === "") return emptyResult; if (searchTerm === "") return emptyResult;
@ -179,7 +201,7 @@ async function localSearchProcess(searchTerm, roomId = undefined) {
emptyResult.seshatQuery = result.query; emptyResult.seshatQuery = result.query;
const response = { const response: ISearchResponse = {
search_categories: { search_categories: {
room_events: result.response, room_events: result.response,
}, },
@ -192,7 +214,7 @@ async function localSearchProcess(searchTerm, roomId = undefined) {
return processedResult; return processedResult;
} }
async function localPagination(searchResult) { async function localPagination(searchResult: ISeshatSearchResults): Promise<ISeshatSearchResults> {
const eventIndex = EventIndexPeg.get(); const eventIndex = EventIndexPeg.get();
const searchArgs = searchResult.seshatQuery; const searchArgs = searchResult.seshatQuery;
@ -221,10 +243,10 @@ async function localPagination(searchResult) {
return result; return result;
} }
function compareOldestEvents(firstResults, secondResults) { function compareOldestEvents(firstResults: ISearchResult[], secondResults: ISearchResult[]): number {
try { try {
const oldestFirstEvent = firstResults.results[firstResults.results.length - 1].result; const oldestFirstEvent = firstResults[firstResults.length - 1].result;
const oldestSecondEvent = secondResults.results[secondResults.results.length - 1].result; const oldestSecondEvent = secondResults[secondResults.length - 1].result;
if (oldestFirstEvent.origin_server_ts <= oldestSecondEvent.origin_server_ts) { if (oldestFirstEvent.origin_server_ts <= oldestSecondEvent.origin_server_ts) {
return -1; return -1;
@ -236,7 +258,12 @@ function compareOldestEvents(firstResults, secondResults) {
} }
} }
function combineEventSources(previousSearchResult, response, a, b) { function combineEventSources(
previousSearchResult: ISeshatSearchResults,
response: IResultRoomEvents,
a: ISearchResult[],
b: ISearchResult[],
): void {
// Merge event sources and sort the events. // Merge event sources and sort the events.
const combinedEvents = a.concat(b).sort(compareEvents); const combinedEvents = a.concat(b).sort(compareEvents);
// Put half of the events in the response, and cache the other half. // Put half of the events in the response, and cache the other half.
@ -353,8 +380,12 @@ function combineEventSources(previousSearchResult, response, a, b) {
* different event sources. * different event sources.
* *
*/ */
function combineEvents(previousSearchResult, localEvents = undefined, serverEvents = undefined) { function combineEvents(
const response = {}; previousSearchResult: ISeshatSearchResults,
localEvents: IResultRoomEvents = undefined,
serverEvents: IResultRoomEvents = undefined,
): IResultRoomEvents {
const response = {} as IResultRoomEvents;
const cachedEvents = previousSearchResult.cachedEvents; const cachedEvents = previousSearchResult.cachedEvents;
let oldestEventFrom = previousSearchResult.oldestEventFrom; let oldestEventFrom = previousSearchResult.oldestEventFrom;
@ -364,7 +395,7 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven
// This is a first search call, combine the events from the server and // This is a first search call, combine the events from the server and
// the local index. Note where our oldest event came from, we shall // the local index. Note where our oldest event came from, we shall
// fetch the next batch of events from the other source. // fetch the next batch of events from the other source.
if (compareOldestEvents(localEvents, serverEvents) < 0) { if (compareOldestEvents(localEvents.results, serverEvents.results) < 0) {
oldestEventFrom = "local"; oldestEventFrom = "local";
} }
@ -375,7 +406,7 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven
// meaning that our oldest event was on the server. // meaning that our oldest event was on the server.
// Change the source of the oldest event if our local event is older // Change the source of the oldest event if our local event is older
// than the cached one. // than the cached one.
if (compareOldestEvents(localEvents, cachedEvents) < 0) { if (compareOldestEvents(localEvents.results, cachedEvents) < 0) {
oldestEventFrom = "local"; oldestEventFrom = "local";
} }
combineEventSources(previousSearchResult, response, localEvents.results, cachedEvents); combineEventSources(previousSearchResult, response, localEvents.results, cachedEvents);
@ -384,7 +415,7 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven
// meaning that our oldest event was in the local index. // meaning that our oldest event was in the local index.
// Change the source of the oldest event if our server event is older // Change the source of the oldest event if our server event is older
// than the cached one. // than the cached one.
if (compareOldestEvents(serverEvents, cachedEvents) < 0) { if (compareOldestEvents(serverEvents.results, cachedEvents) < 0) {
oldestEventFrom = "server"; oldestEventFrom = "server";
} }
combineEventSources(previousSearchResult, response, serverEvents.results, cachedEvents); combineEventSources(previousSearchResult, response, serverEvents.results, cachedEvents);
@ -412,7 +443,11 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven
* @return {object} A response object that combines the events from the * @return {object} A response object that combines the events from the
* different event sources. * different event sources.
*/ */
function combineResponses(previousSearchResult, localEvents = undefined, serverEvents = undefined) { function combineResponses(
previousSearchResult: ISeshatSearchResults,
localEvents: IResultRoomEvents = undefined,
serverEvents: IResultRoomEvents = undefined,
): IResultRoomEvents {
// Combine our events first. // Combine our events first.
const response = combineEvents(previousSearchResult, localEvents, serverEvents); const response = combineEvents(previousSearchResult, localEvents, serverEvents);
@ -454,42 +489,51 @@ function combineResponses(previousSearchResult, localEvents = undefined, serverE
return response; return response;
} }
function restoreEncryptionInfo(searchResultSlice = []) { interface IEncryptedSeshatEvent {
curve25519Key: string;
ed25519Key: string;
algorithm: string;
forwardingCurve25519KeyChain: string[];
}
function restoreEncryptionInfo(searchResultSlice: SearchResult[] = []): void {
for (let i = 0; i < searchResultSlice.length; i++) { for (let i = 0; i < searchResultSlice.length; i++) {
const timeline = searchResultSlice[i].context.getTimeline(); const timeline = searchResultSlice[i].context.getTimeline();
for (let j = 0; j < timeline.length; j++) { for (let j = 0; j < timeline.length; j++) {
const ev = timeline[j]; const mxEv = timeline[j];
const ev = mxEv.event as IEncryptedSeshatEvent;
if (ev.event.curve25519Key) { if (ev.curve25519Key) {
ev.makeEncrypted( mxEv.makeEncrypted(
"m.room.encrypted", EventType.RoomMessageEncrypted,
{ algorithm: ev.event.algorithm }, { algorithm: ev.algorithm },
ev.event.curve25519Key, ev.curve25519Key,
ev.event.ed25519Key, ev.ed25519Key,
); );
ev.forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain; // @ts-ignore
mxEv.forwardingCurve25519KeyChain = ev.forwardingCurve25519KeyChain;
delete ev.event.curve25519Key; delete ev.curve25519Key;
delete ev.event.ed25519Key; delete ev.ed25519Key;
delete ev.event.algorithm; delete ev.algorithm;
delete ev.event.forwardingCurve25519KeyChain; delete ev.forwardingCurve25519KeyChain;
} }
} }
} }
} }
async function combinedPagination(searchResult) { async function combinedPagination(searchResult: ISeshatSearchResults): Promise<ISeshatSearchResults> {
const eventIndex = EventIndexPeg.get(); const eventIndex = EventIndexPeg.get();
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const searchArgs = searchResult.seshatQuery; const searchArgs = searchResult.seshatQuery;
const oldestEventFrom = searchResult.oldestEventFrom; const oldestEventFrom = searchResult.oldestEventFrom;
let localResult; let localResult: IResultRoomEvents;
let serverSideResult; let serverSideResult: ISearchResponse;
// Fetch events from the local index if we have a token for itand if it's // Fetch events from the local index if we have a token for it and if it's
// the local indexes turn or the server has exhausted its results. // the local indexes turn or the server has exhausted its results.
if (searchArgs.next_batch && (!searchResult.serverSideNextBatch || oldestEventFrom === "server")) { if (searchArgs.next_batch && (!searchResult.serverSideNextBatch || oldestEventFrom === "server")) {
localResult = await eventIndex.search(searchArgs); localResult = await eventIndex.search(searchArgs);
@ -502,7 +546,7 @@ async function combinedPagination(searchResult) {
serverSideResult = await client.search(body); serverSideResult = await client.search(body);
} }
let serverEvents; let serverEvents: IResultRoomEvents;
if (serverSideResult) { if (serverSideResult) {
serverEvents = serverSideResult.search_categories.room_events; serverEvents = serverSideResult.search_categories.room_events;
@ -532,8 +576,8 @@ async function combinedPagination(searchResult) {
return result; return result;
} }
function eventIndexSearch(term, roomId = undefined) { function eventIndexSearch(term: string, roomId: string = undefined): Promise<ISearchResults> {
let searchPromise; let searchPromise: Promise<ISearchResults>;
if (roomId !== undefined) { if (roomId !== undefined) {
if (MatrixClientPeg.get().isRoomEncrypted(roomId)) { if (MatrixClientPeg.get().isRoomEncrypted(roomId)) {
@ -554,7 +598,7 @@ function eventIndexSearch(term, roomId = undefined) {
return searchPromise; return searchPromise;
} }
function eventIndexSearchPagination(searchResult) { function eventIndexSearchPagination(searchResult: ISeshatSearchResults): Promise<ISeshatSearchResults> {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const seshatQuery = searchResult.seshatQuery; const seshatQuery = searchResult.seshatQuery;
@ -580,7 +624,7 @@ function eventIndexSearchPagination(searchResult) {
} }
} }
export function searchPagination(searchResult) { export function searchPagination(searchResult: ISearchResults): Promise<ISearchResults> {
const eventIndex = EventIndexPeg.get(); const eventIndex = EventIndexPeg.get();
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
@ -590,7 +634,7 @@ export function searchPagination(searchResult) {
else return eventIndexSearchPagination(searchResult); else return eventIndexSearchPagination(searchResult);
} }
export default function eventSearch(term, roomId = undefined) { export default function eventSearch(term: string, roomId: string = undefined): Promise<ISearchResults> {
const eventIndex = EventIndexPeg.get(); const eventIndex = EventIndexPeg.get();
if (eventIndex === null) return serverSideSearchProcess(term, roomId); if (eventIndex === null) return serverSideSearchProcess(term, roomId);

View file

@ -480,14 +480,14 @@ export const Commands = [
'Identity server', 'Identity server',
QuestionDialog, { QuestionDialog, {
title: _t("Use an identity server"), title: _t("Use an identity server"),
description: <p>{_t( description: <p>{ _t(
"Use an identity server to invite by email. " + "Use an identity server to invite by email. " +
"Click continue to use the default identity server " + "Click continue to use the default identity server " +
"(%(defaultIdentityServerName)s) or manage in Settings.", "(%(defaultIdentityServerName)s) or manage in Settings.",
{ {
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl), defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
}, },
)}</p>, ) }</p>,
button: _t("Continue"), button: _t("Continue"),
}, },
); );
@ -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);

View file

@ -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): () => 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): () => 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 {
@ -127,7 +125,7 @@ function textForMemberEvent(ev): () => string | null {
} }
} }
function textForTopicEvent(ev): () => string | null { function textForTopicEvent(ev: MatrixEvent): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return () => _t('%(senderDisplayName)s changed the topic to "%(topic)s".', { return () => _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
senderDisplayName, senderDisplayName,
@ -135,7 +133,7 @@ function textForTopicEvent(ev): () => string | null {
}); });
} }
function textForRoomNameEvent(ev): () => string | null { function textForRoomNameEvent(ev: MatrixEvent): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) { if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
@ -154,12 +152,12 @@ function textForRoomNameEvent(ev): () => string | null {
}); });
} }
function textForTombstoneEvent(ev): () => string | null { function textForTombstoneEvent(ev: MatrixEvent): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return () => _t('%(senderDisplayName)s upgraded this room.', { senderDisplayName }); return () => _t('%(senderDisplayName)s upgraded this room.', { senderDisplayName });
} }
function textForJoinRulesEvent(ev): () => string | null { function textForJoinRulesEvent(ev: MatrixEvent): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
switch (ev.getContent().join_rule) { switch (ev.getContent().join_rule) {
case "public": case "public":
@ -179,7 +177,7 @@ function textForJoinRulesEvent(ev): () => string | null {
} }
} }
function textForGuestAccessEvent(ev): () => string | null { function textForGuestAccessEvent(ev: MatrixEvent): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
switch (ev.getContent().guest_access) { switch (ev.getContent().guest_access) {
case "can_join": case "can_join":
@ -195,7 +193,7 @@ function textForGuestAccessEvent(ev): () => string | null {
} }
} }
function textForRelatedGroupsEvent(ev): () => string | null { function textForRelatedGroupsEvent(ev: MatrixEvent): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const groups = ev.getContent().groups || []; const groups = ev.getContent().groups || [];
const prevGroups = ev.getPrevContent().groups || []; const prevGroups = ev.getPrevContent().groups || [];
@ -225,7 +223,7 @@ function textForRelatedGroupsEvent(ev): () => string | null {
} }
} }
function textForServerACLEvent(ev): () => string | null { function textForServerACLEvent(ev: MatrixEvent): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const prevContent = ev.getPrevContent(); const prevContent = ev.getPrevContent();
const current = ev.getContent(); const current = ev.getContent();
@ -255,7 +253,7 @@ function textForServerACLEvent(ev): () => string | null {
return getText; return getText;
} }
function textForMessageEvent(ev): () => string | null { function textForMessageEvent(ev: MatrixEvent): () => string | null {
return () => { return () => {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
let message = senderDisplayName + ': ' + ev.getContent().body; let message = senderDisplayName + ': ' + ev.getContent().body;
@ -268,7 +266,7 @@ function textForMessageEvent(ev): () => string | null {
}; };
} }
function textForCanonicalAliasEvent(ev): () => string | null { function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null {
const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const oldAlias = ev.getPrevContent().alias; const oldAlias = ev.getPrevContent().alias;
const oldAltAliases = ev.getPrevContent().alt_aliases || []; const oldAltAliases = ev.getPrevContent().alt_aliases || [];
@ -319,91 +317,7 @@ function textForCanonicalAliasEvent(ev): () => 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,13 +355,14 @@ 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) {
return null; return null;
} }
const userDefault = event.getContent().users_default || 0; const previousUserDefault = event.getPrevContent().users_default || 0;
const currentUserDefault = event.getContent().users_default || 0;
// Construct set of userIds // Construct set of userIds
const users = []; const users = [];
Object.keys(event.getContent().users).forEach( Object.keys(event.getContent().users).forEach(
@ -463,9 +378,16 @@ function textForPowerEvent(event): () => string | null {
const diffs = []; const diffs = [];
users.forEach((userId) => { users.forEach((userId) => {
// Previous power level // Previous power level
const from = event.getPrevContent().users[userId]; let from = event.getPrevContent().users[userId];
if (!Number.isInteger(from)) {
from = previousUserDefault;
}
// Current power level // Current power level
const to = event.getContent().users[userId]; let to = event.getContent().users[userId];
if (!Number.isInteger(to)) {
to = currentUserDefault;
}
if (from === previousUserDefault && to === currentUserDefault) { return; }
if (to !== from) { if (to !== from) {
diffs.push({ userId, from, to }); diffs.push({ userId, from, to });
} }
@ -479,8 +401,8 @@ function textForPowerEvent(event): () => string | null {
powerLevelDiffText: diffs.map(diff => powerLevelDiffText: diffs.map(diff =>
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', { _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
userId: diff.userId, userId: diff.userId,
fromPowerLevel: Roles.textualPowerLevel(diff.from, userDefault), fromPowerLevel: Roles.textualPowerLevel(diff.from, previousUserDefault),
toPowerLevel: Roles.textualPowerLevel(diff.to, userDefault), toPowerLevel: Roles.textualPowerLevel(diff.to, currentUserDefault),
}), }),
).join(", "), ).join(", "),
}); });
@ -515,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() || {};
@ -545,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();
@ -638,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 = {
@ -674,14 +594,27 @@ for (const evType of ALL_RULE_TYPES) {
stateHandlers[evType] = textForMjolnirEvent; stateHandlers[evType] = textForMjolnirEvent;
} }
export function hasText(ev): 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)?.() || '';
} }

View file

@ -30,7 +30,7 @@ import { haveTileForEvent } from "./components/views/rooms/EventTile";
* @returns {boolean} True if the given event should affect the unread message count * @returns {boolean} True if the given event should affect the unread message count
*/ */
export function eventTriggersUnreadCount(ev: MatrixEvent): boolean { export function eventTriggersUnreadCount(ev: MatrixEvent): boolean {
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) { if (ev.getSender() === MatrixClientPeg.get().credentials.userId) {
return false; return false;
} }
@ -63,9 +63,7 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean {
// https://github.com/vector-im/element-web/issues/2427 // https://github.com/vector-im/element-web/issues/2427
// ...and possibly some of the others at // ...and possibly some of the others at
// https://github.com/vector-im/element-web/issues/3363 // https://github.com/vector-im/element-web/issues/3363
if (room.timeline.length && if (room.timeline.length && room.timeline[room.timeline.length - 1].getSender() === myUserId) {
room.timeline[room.timeline.length - 1].sender &&
room.timeline[room.timeline.length - 1].sender.userId === myUserId) {
return false; return false;
} }

View file

@ -370,8 +370,8 @@ export const toggleDialog = () => {
const sections = categoryOrder.map(category => { const sections = categoryOrder.map(category => {
const list = shortcuts[category]; const list = shortcuts[category];
return <div className="mx_KeyboardShortcutsDialog_category" key={category}> return <div className="mx_KeyboardShortcutsDialog_category" key={category}>
<h3>{_t(category)}</h3> <h3>{ _t(category) }</h3>
<div>{list.map(shortcut => <Shortcut key={shortcut.description} shortcut={shortcut} />)}</div> <div>{ list.map(shortcut => <Shortcut key={shortcut.description} shortcut={shortcut} />) }</div>
</div>; </div>;
}); });

View file

@ -62,9 +62,9 @@ const Toolbar: React.FC<IProps> = ({ children, ...props }) => {
}; };
return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}> return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
{({ onKeyDownHandler }) => <div {...props} onKeyDown={onKeyDownHandler} role="toolbar"> { ({ onKeyDownHandler }) => <div {...props} onKeyDown={onKeyDownHandler} role="toolbar">
{ children } { children }
</div>} </div> }
</RovingTabIndexProvider>; </RovingTabIndexProvider>;
}; };

View file

@ -15,8 +15,10 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import * as sdk from '../../../../index';
import PropTypes from 'prop-types'; import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import Spinner from "../../../../components/views/elements/Spinner";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import dis from "../../../../dispatcher/dispatcher"; import dis from "../../../../dispatcher/dispatcher";
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
@ -24,46 +26,44 @@ import SettingsStore from "../../../../settings/SettingsStore";
import EventIndexPeg from "../../../../indexing/EventIndexPeg"; import EventIndexPeg from "../../../../indexing/EventIndexPeg";
import { Action } from "../../../../dispatcher/actions"; import { Action } from "../../../../dispatcher/actions";
import { SettingLevel } from "../../../../settings/SettingLevel"; import { SettingLevel } from "../../../../settings/SettingLevel";
interface IProps {
onFinished: (success: boolean) => void;
}
interface IState {
disabling: boolean;
}
/* /*
* Allows the user to disable the Event Index. * Allows the user to disable the Event Index.
*/ */
export default class DisableEventIndexDialog extends React.Component { export default class DisableEventIndexDialog extends React.Component<IProps, IState> {
static propTypes = { constructor(props: IProps) {
onFinished: PropTypes.func.isRequired,
}
constructor(props) {
super(props); super(props);
this.state = { this.state = {
disabling: false, disabling: false,
}; };
} }
_onDisable = async () => { private onDisable = async (): Promise<void> => {
this.setState({ this.setState({
disabling: true, disabling: true,
}); });
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false); await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false);
await EventIndexPeg.deleteEventIndex(); await EventIndexPeg.deleteEventIndex();
this.props.onFinished(); this.props.onFinished(true);
dis.fire(Action.ViewUserSettings); dis.fire(Action.ViewUserSettings);
} };
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('elements.Spinner');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
public render(): React.ReactNode {
return ( return (
<BaseDialog onFinished={this.props.onFinished} title={_t("Are you sure?")}> <BaseDialog onFinished={this.props.onFinished} title={_t("Are you sure?")}>
{_t("If disabled, messages from encrypted rooms won't appear in search results.")} { _t("If disabled, messages from encrypted rooms won't appear in search results.") }
{this.state.disabling ? <Spinner /> : <div />} { this.state.disabling ? <Spinner /> : <div /> }
<DialogButtons <DialogButtons
primaryButton={_t('Disable')} primaryButton={_t('Disable')}
onPrimaryButtonClick={this._onDisable} onPrimaryButtonClick={this.onDisable}
primaryButtonClass="danger" primaryButtonClass="danger"
cancelButtonClass="warning" cancelButtonClass="warning"
onCancel={this.props.onFinished} onCancel={this.props.onFinished}

View file

@ -134,8 +134,9 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
} }
private onDisable = async () => { private onDisable = async () => {
Modal.createTrackedDialogAsync("Disable message search", "Disable message search", const DisableEventIndexDialog = (await import("./DisableEventIndexDialog")).default;
import("./DisableEventIndexDialog"), Modal.createTrackedDialog("Disable message search", "Disable message search",
DisableEventIndexDialog,
null, null, /* priority = */ false, /* static = */ true, null, null, /* priority = */ false, /* static = */ true,
); );
}; };
@ -161,19 +162,19 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
const eventIndexingSettings = ( const eventIndexingSettings = (
<div> <div>
{_t( { _t(
"%(brand)s is securely caching encrypted messages locally for them " + "%(brand)s is securely caching encrypted messages locally for them " +
"to appear in search results:", "to appear in search results:",
{ brand }, { brand },
)} ) }
<div className='mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_subsectionText'>
{crawlerState}<br /> { crawlerState }<br />
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}<br /> { _t("Space used:") } { formatBytes(this.state.eventIndexSize, 0) }<br />
{_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}<br /> { _t("Indexed messages:") } { formatCountLong(this.state.eventCount) }<br />
{_t("Indexed rooms:")} {_t("%(doneRooms)s out of %(totalRooms)s", { { _t("Indexed rooms:") } { _t("%(doneRooms)s out of %(totalRooms)s", {
doneRooms: formatCountLong(doneRooms), doneRooms: formatCountLong(doneRooms),
totalRooms: formatCountLong(this.state.roomCount), totalRooms: formatCountLong(this.state.roomCount),
})} <br /> }) } <br />
<Field <Field
label={_t('Message downloading sleep time(ms)')} label={_t('Message downloading sleep time(ms)')}
type='number' type='number'
@ -188,7 +189,7 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={_t("Message search")} title={_t("Message search")}
> >
{eventIndexingSettings} { eventIndexingSettings }
<DialogButtons <DialogButtons
primaryButton={_t("Done")} primaryButton={_t("Done")}
onPrimaryButtonClick={this.props.onFinished} onPrimaryButtonClick={this.props.onFinished}

View file

@ -232,15 +232,15 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseNextClick}> return <form onSubmit={this._onPassPhraseNextClick}>
<p>{_t( <p>{ _t(
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {}, "<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
{ b: sub => <b>{sub}</b> }, { b: sub => <b>{ sub }</b> },
)}</p> ) }</p>
<p>{_t( <p>{ _t(
"We'll store an encrypted copy of your keys on our server. " + "We'll store an encrypted copy of your keys on our server. " +
"Secure your backup with a Security Phrase.", "Secure your backup with a Security Phrase.",
)}</p> ) }</p>
<p>{_t("For maximum security, this should be different from your account password.")}</p> <p>{ _t("For maximum security, this should be different from your account password.") }</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer"> <div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer"> <div className="mx_CreateKeyBackupDialog_passPhraseContainer">
@ -268,9 +268,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
/> />
<details> <details>
<summary>{_t("Advanced")}</summary> <summary>{ _t("Advanced") }</summary>
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} > <AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
{_t("Set up with a Security Key")} { _t("Set up with a Security Key") }
</AccessibleButton> </AccessibleButton>
</details> </details>
</form>; </form>;
@ -299,19 +299,19 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
let passPhraseMatch = null; let passPhraseMatch = null;
if (matchText) { if (matchText) {
passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch"> passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch">
<div>{matchText}</div> <div>{ matchText }</div>
<div> <div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}> <AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
{changeText} { changeText }
</AccessibleButton> </AccessibleButton>
</div> </div>
</div>; </div>;
} }
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseConfirmNextClick}> return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t( <p>{ _t(
"Enter your Security Phrase a second time to confirm it.", "Enter your Security Phrase a second time to confirm it.",
)}</p> ) }</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer"> <div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer"> <div className="mx_CreateKeyBackupDialog_passPhraseContainer">
<div> <div>
@ -323,7 +323,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
autoFocus={true} autoFocus={true}
/> />
</div> </div>
{passPhraseMatch} { passPhraseMatch }
</div> </div>
</div> </div>
<DialogButtons <DialogButtons
@ -337,27 +337,27 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_renderPhaseShowKey() { _renderPhaseShowKey() {
return <div> return <div>
<p>{_t( <p>{ _t(
"Your Security Key is a safety net - you can use it to restore " + "Your Security Key is a safety net - you can use it to restore " +
"access to your encrypted messages if you forget your Security Phrase.", "access to your encrypted messages if you forget your Security Phrase.",
)}</p> ) }</p>
<p>{_t( <p>{ _t(
"Keep a copy of it somewhere secure, like a password manager or even a safe.", "Keep a copy of it somewhere secure, like a password manager or even a safe.",
)}</p> ) }</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer"> <div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKeyHeader"> <div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
{_t("Your Security Key")} { _t("Your Security Key") }
</div> </div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer"> <div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKey"> <div className="mx_CreateKeyBackupDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._keyBackupInfo.recovery_key}</code> <code ref={this._collectRecoveryKeyNode}>{ this._keyBackupInfo.recovery_key }</code>
</div> </div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons"> <div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
<button className="mx_Dialog_primary" onClick={this._onCopyClick}> <button className="mx_Dialog_primary" onClick={this._onCopyClick}>
{_t("Copy")} { _t("Copy") }
</button> </button>
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}> <button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
{_t("Download")} { _t("Download") }
</button> </button>
</div> </div>
</div> </div>
@ -370,26 +370,26 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
if (this.state.copied) { if (this.state.copied) {
introText = _t( introText = _t(
"Your Security Key has been <b>copied to your clipboard</b>, paste it to:", "Your Security Key has been <b>copied to your clipboard</b>, paste it to:",
{}, { b: s => <b>{s}</b> }, {}, { b: s => <b>{ s }</b> },
); );
} else if (this.state.downloaded) { } else if (this.state.downloaded) {
introText = _t( introText = _t(
"Your Security Key is in your <b>Downloads</b> folder.", "Your Security Key is in your <b>Downloads</b> folder.",
{}, { b: s => <b>{s}</b> }, {}, { b: s => <b>{ s }</b> },
); );
} }
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div> return <div>
{introText} { introText }
<ul> <ul>
<li>{_t("<b>Print it</b> and store it somewhere safe", {}, { b: s => <b>{s}</b> })}</li> <li>{ _t("<b>Print it</b> and store it somewhere safe", {}, { b: s => <b>{ s }</b> }) }</li>
<li>{_t("<b>Save it</b> on a USB key or backup drive", {}, { b: s => <b>{s}</b> })}</li> <li>{ _t("<b>Save it</b> on a USB key or backup drive", {}, { b: s => <b>{ s }</b> }) }</li>
<li>{_t("<b>Copy it</b> to your personal cloud storage", {}, { b: s => <b>{s}</b> })}</li> <li>{ _t("<b>Copy it</b> to your personal cloud storage", {}, { b: s => <b>{ s }</b> }) }</li>
</ul> </ul>
<DialogButtons primaryButton={_t("Continue")} <DialogButtons primaryButton={_t("Continue")}
onPrimaryButtonClick={this._createBackup} onPrimaryButtonClick={this._createBackup}
hasCancel={false}> hasCancel={false}>
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button> <button onClick={this._onKeepItSafeBackClick}>{ _t("Back") }</button>
</DialogButtons> </DialogButtons>
</div>; </div>;
} }
@ -404,9 +404,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_renderPhaseDone() { _renderPhaseDone() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div> return <div>
<p>{_t( <p>{ _t(
"Your keys are being backed up (the first backup could take a few minutes).", "Your keys are being backed up (the first backup could take a few minutes).",
)}</p> ) }</p>
<DialogButtons primaryButton={_t('OK')} <DialogButtons primaryButton={_t('OK')}
onPrimaryButtonClick={this._onDone} onPrimaryButtonClick={this._onDone}
hasCancel={false} hasCancel={false}
@ -417,10 +417,10 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_renderPhaseOptOutConfirm() { _renderPhaseOptOutConfirm() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div> return <div>
{_t( { _t(
"Without setting up Secure Message Recovery, you won't be able to restore your " + "Without setting up Secure Message Recovery, you won't be able to restore your " +
"encrypted message history if you log out or use another session.", "encrypted message history if you log out or use another session.",
)} ) }
<DialogButtons primaryButton={_t('Set up Secure Message Recovery')} <DialogButtons primaryButton={_t('Set up Secure Message Recovery')}
onPrimaryButtonClick={this._onSetUpClick} onPrimaryButtonClick={this._onSetUpClick}
hasCancel={false} hasCancel={false}
@ -457,7 +457,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
if (this.state.error) { if (this.state.error) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
content = <div> content = <div>
<p>{_t("Unable to create key backup")}</p> <p>{ _t("Unable to create key backup") }</p>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')} <DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._createBackup} onPrimaryButtonClick={this._createBackup}
@ -499,7 +499,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)} hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
> >
<div> <div>
{content} { content }
</div> </div>
</BaseDialog> </BaseDialog>
); );

View file

@ -475,9 +475,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
> >
<div className="mx_CreateSecretStorageDialog_optionTitle"> <div className="mx_CreateSecretStorageDialog_optionTitle">
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"></span> <span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"></span>
{_t("Generate a Security Key")} { _t("Generate a Security Key") }
</div> </div>
<div>{_t("Well generate a Security Key for you to store somewhere safe, like a password manager or a safe.")}</div> <div>{ _t("Well generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }</div>
</StyledRadioButton> </StyledRadioButton>
); );
} }
@ -494,9 +494,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
> >
<div className="mx_CreateSecretStorageDialog_optionTitle"> <div className="mx_CreateSecretStorageDialog_optionTitle">
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"></span> <span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"></span>
{_t("Enter a Security Phrase")} { _t("Enter a Security Phrase") }
</div> </div>
<div>{_t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.")}</div> <div>{ _t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.") }</div>
</StyledRadioButton> </StyledRadioButton>
); );
} }
@ -507,13 +507,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
const optionPassphrase = setupMethods.includes("passphrase") ? this._renderOptionPassphrase() : null; const optionPassphrase = setupMethods.includes("passphrase") ? this._renderOptionPassphrase() : null;
return <form onSubmit={this._onChooseKeyPassphraseFormSubmit}> return <form onSubmit={this._onChooseKeyPassphraseFormSubmit}>
<p className="mx_CreateSecretStorageDialog_centeredBody">{_t( <p className="mx_CreateSecretStorageDialog_centeredBody">{ _t(
"Safeguard against losing access to encrypted messages & data by " + "Safeguard against losing access to encrypted messages & data by " +
"backing up encryption keys on your server.", "backing up encryption keys on your server.",
)}</p> ) }</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup"> <div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup">
{optionKey} { optionKey }
{optionPassphrase} { optionPassphrase }
</div> </div>
<DialogButtons <DialogButtons
primaryButton={_t("Continue")} primaryButton={_t("Continue")}
@ -536,7 +536,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
let nextCaption = _t("Next"); let nextCaption = _t("Next");
if (this.state.canUploadKeysWithPasswordOnly) { if (this.state.canUploadKeysWithPasswordOnly) {
authPrompt = <div> authPrompt = <div>
<div>{_t("Enter your account password to confirm the upgrade:")}</div> <div>{ _t("Enter your account password to confirm the upgrade:") }</div>
<div><Field <div><Field
type="password" type="password"
label={_t("Password")} label={_t("Password")}
@ -548,22 +548,22 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div>; </div>;
} else if (!this.state.backupSigStatus.usable) { } else if (!this.state.backupSigStatus.usable) {
authPrompt = <div> authPrompt = <div>
<div>{_t("Restore your key backup to upgrade your encryption")}</div> <div>{ _t("Restore your key backup to upgrade your encryption") }</div>
</div>; </div>;
nextCaption = _t("Restore"); nextCaption = _t("Restore");
} else { } else {
authPrompt = <p> authPrompt = <p>
{_t("You'll need to authenticate with the server to confirm the upgrade.")} { _t("You'll need to authenticate with the server to confirm the upgrade.") }
</p>; </p>;
} }
return <form onSubmit={this._onMigrateFormSubmit}> return <form onSubmit={this._onMigrateFormSubmit}>
<p>{_t( <p>{ _t(
"Upgrade this session to allow it to verify other sessions, " + "Upgrade this session to allow it to verify other sessions, " +
"granting them access to encrypted messages and marking them " + "granting them access to encrypted messages and marking them " +
"as trusted for other users.", "as trusted for other users.",
)}</p> ) }</p>
<div>{authPrompt}</div> <div>{ authPrompt }</div>
<DialogButtons <DialogButtons
primaryButton={nextCaption} primaryButton={nextCaption}
onPrimaryButtonClick={this._onMigrateFormSubmit} onPrimaryButtonClick={this._onMigrateFormSubmit}
@ -571,7 +571,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword} primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
> >
<button type="button" className="danger" onClick={this._onCancelClick}> <button type="button" className="danger" onClick={this._onCancelClick}>
{_t('Skip')} { _t('Skip') }
</button> </button>
</DialogButtons> </DialogButtons>
</form>; </form>;
@ -579,10 +579,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_renderPhasePassPhrase() { _renderPhasePassPhrase() {
return <form onSubmit={this._onPassPhraseNextClick}> return <form onSubmit={this._onPassPhraseNextClick}>
<p>{_t( <p>{ _t(
"Enter a security phrase only you know, as its used to safeguard your data. " + "Enter a security phrase only you know, as its used to safeguard your data. " +
"To be secure, you shouldnt re-use your account password.", "To be secure, you shouldnt re-use your account password.",
)}</p> ) }</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer"> <div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<PassphraseField <PassphraseField
@ -609,7 +609,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<button type="button" <button type="button"
onClick={this._onCancelClick} onClick={this._onCancelClick}
className="danger" className="danger"
>{_t("Cancel")}</button> >{ _t("Cancel") }</button>
</DialogButtons> </DialogButtons>
</form>; </form>;
} }
@ -637,18 +637,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
let passPhraseMatch = null; let passPhraseMatch = null;
if (matchText) { if (matchText) {
passPhraseMatch = <div> passPhraseMatch = <div>
<div>{matchText}</div> <div>{ matchText }</div>
<div> <div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}> <AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
{changeText} { changeText }
</AccessibleButton> </AccessibleButton>
</div> </div>
</div>; </div>;
} }
return <form onSubmit={this._onPassPhraseConfirmNextClick}> return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t( <p>{ _t(
"Enter your Security Phrase a second time to confirm it.", "Enter your Security Phrase a second time to confirm it.",
)}</p> ) }</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer"> <div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<Field <Field
type="password" type="password"
@ -660,7 +660,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
autoComplete="new-password" autoComplete="new-password"
/> />
<div className="mx_CreateSecretStorageDialog_passPhraseMatch"> <div className="mx_CreateSecretStorageDialog_passPhraseMatch">
{passPhraseMatch} { passPhraseMatch }
</div> </div>
</div> </div>
<DialogButtons <DialogButtons
@ -672,7 +672,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<button type="button" <button type="button"
onClick={this._onCancelClick} onClick={this._onCancelClick}
className="danger" className="danger"
>{_t("Skip")}</button> >{ _t("Skip") }</button>
</DialogButtons> </DialogButtons>
</form>; </form>;
} }
@ -691,35 +691,35 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div>; </div>;
} }
return <div> return <div>
<p>{_t( <p>{ _t(
"Store your Security Key somewhere safe, like a password manager or a safe, " + "Store your Security Key somewhere safe, like a password manager or a safe, " +
"as its used to safeguard your encrypted data.", "as its used to safeguard your encrypted data.",
)}</p> ) }</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer"> <div className="mx_CreateSecretStorageDialog_primaryContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer"> <div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKey"> <div className="mx_CreateSecretStorageDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._recoveryKey.encodedPrivateKey}</code> <code ref={this._collectRecoveryKeyNode}>{ this._recoveryKey.encodedPrivateKey }</code>
</div> </div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons"> <div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
<AccessibleButton kind='primary' className="mx_Dialog_primary" <AccessibleButton kind='primary' className="mx_Dialog_primary"
onClick={this._onDownloadClick} onClick={this._onDownloadClick}
disabled={this.state.phase === PHASE_STORING} disabled={this.state.phase === PHASE_STORING}
> >
{_t("Download")} { _t("Download") }
</AccessibleButton> </AccessibleButton>
<span>{_t("or")}</span> <span>{ _t("or") }</span>
<AccessibleButton <AccessibleButton
kind='primary' kind='primary'
className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn" className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
onClick={this._onCopyClick} onClick={this._onCopyClick}
disabled={this.state.phase === PHASE_STORING} disabled={this.state.phase === PHASE_STORING}
> >
{this.state.copied ? _t("Copied!") : _t("Copy")} { this.state.copied ? _t("Copied!") : _t("Copy") }
</AccessibleButton> </AccessibleButton>
</div> </div>
</div> </div>
</div> </div>
{continueButton} { continueButton }
</div>; </div>;
} }
@ -732,7 +732,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_renderPhaseLoadError() { _renderPhaseLoadError() {
return <div> return <div>
<p>{_t("Unable to query secret storage status")}</p> <p>{ _t("Unable to query secret storage status") }</p>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')} <DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._onLoadRetryClick} onPrimaryButtonClick={this._onLoadRetryClick}
@ -745,17 +745,17 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_renderPhaseSkipConfirm() { _renderPhaseSkipConfirm() {
return <div> return <div>
<p>{_t( <p>{ _t(
"If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.",
)}</p> ) }</p>
<p>{_t( <p>{ _t(
"You can also set up Secure Backup & manage your keys in Settings.", "You can also set up Secure Backup & manage your keys in Settings.",
)}</p> ) }</p>
<DialogButtons primaryButton={_t('Go back')} <DialogButtons primaryButton={_t('Go back')}
onPrimaryButtonClick={this._onGoBackClick} onPrimaryButtonClick={this._onGoBackClick}
hasCancel={false} hasCancel={false}
> >
<button type="button" className="danger" onClick={this._onCancel}>{_t('Cancel')}</button> <button type="button" className="danger" onClick={this._onCancel}>{ _t('Cancel') }</button>
</DialogButtons> </DialogButtons>
</div>; </div>;
} }
@ -787,7 +787,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
let content; let content;
if (this.state.error) { if (this.state.error) {
content = <div> content = <div>
<p>{_t("Unable to set up secret storage")}</p> <p>{ _t("Unable to set up secret storage") }</p>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')} <DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._bootstrapSecretStorage} onPrimaryButtonClick={this._bootstrapSecretStorage}
@ -857,7 +857,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
fixedWidth={false} fixedWidth={false}
> >
<div> <div>
{content} { content }
</div> </div>
</BaseDialog> </BaseDialog>
); );

View file

@ -54,28 +54,28 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent("views.elements.DialogButtons"); const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
const title = <span className="mx_KeyBackupFailedDialog_title"> const title = <span className="mx_KeyBackupFailedDialog_title">
{_t("New Recovery Method")} { _t("New Recovery Method") }
</span>; </span>;
const newMethodDetected = <p>{_t( const newMethodDetected = <p>{ _t(
"A new Security Phrase and key for Secure Messages have been detected.", "A new Security Phrase and key for Secure Messages have been detected.",
)}</p>; ) }</p>;
const hackWarning = <p className="warning">{_t( const hackWarning = <p className="warning">{ _t(
"If you didn't set the new recovery method, an " + "If you didn't set the new recovery method, an " +
"attacker may be trying to access your account. " + "attacker may be trying to access your account. " +
"Change your account password and set a new recovery " + "Change your account password and set a new recovery " +
"method immediately in Settings.", "method immediately in Settings.",
)}</p>; ) }</p>;
let content; let content;
if (MatrixClientPeg.get().getKeyBackupEnabled()) { if (MatrixClientPeg.get().getKeyBackupEnabled()) {
content = <div> content = <div>
{newMethodDetected} { newMethodDetected }
<p>{_t( <p>{ _t(
"This session is encrypting history using the new recovery method.", "This session is encrypting history using the new recovery method.",
)}</p> ) }</p>
{hackWarning} { hackWarning }
<DialogButtons <DialogButtons
primaryButton={_t("OK")} primaryButton={_t("OK")}
onPrimaryButtonClick={this.onOkClick} onPrimaryButtonClick={this.onOkClick}
@ -85,8 +85,8 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
</div>; </div>;
} else { } else {
content = <div> content = <div>
{newMethodDetected} { newMethodDetected }
{hackWarning} { hackWarning }
<DialogButtons <DialogButtons
primaryButton={_t("Set up Secure Messages")} primaryButton={_t("Set up Secure Messages")}
onPrimaryButtonClick={this.onSetupClick} onPrimaryButtonClick={this.onSetupClick}
@ -101,7 +101,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={title} title={title}
> >
{content} { content }
</BaseDialog> </BaseDialog>
); );
} }

View file

@ -46,7 +46,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent("views.elements.DialogButtons"); const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
const title = <span className="mx_KeyBackupFailedDialog_title"> const title = <span className="mx_KeyBackupFailedDialog_title">
{_t("Recovery Method Removed")} { _t("Recovery Method Removed") }
</span>; </span>;
return ( return (
@ -55,21 +55,21 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
title={title} title={title}
> >
<div> <div>
<p>{_t( <p>{ _t(
"This session has detected that your Security Phrase and key " + "This session has detected that your Security Phrase and key " +
"for Secure Messages have been removed.", "for Secure Messages have been removed.",
)}</p> ) }</p>
<p>{_t( <p>{ _t(
"If you did this accidentally, you can setup Secure Messages on " + "If you did this accidentally, you can setup Secure Messages on " +
"this session which will re-encrypt this session's message " + "this session which will re-encrypt this session's message " +
"history with a new recovery method.", "history with a new recovery method.",
)}</p> ) }</p>
<p className="warning">{_t( <p className="warning">{ _t(
"If you didn't remove the recovery method, an " + "If you didn't remove the recovery method, an " +
"attacker may be trying to access your account. " + "attacker may be trying to access your account. " +
"Change your account password and set a new recovery " + "Change your account password and set a new recovery " +
"method immediately in Settings.", "method immediately in Settings.",
)}</p> ) }</p>
<DialogButtons <DialogButtons
primaryButton={_t("Set up Secure Messages")} primaryButton={_t("Set up Secure Messages")}
onPrimaryButtonClick={this.onSetupClick} onPrimaryButtonClick={this.onSetupClick}

View file

@ -27,8 +27,8 @@ import EmojiProvider from './EmojiProvider';
import NotifProvider from './NotifProvider'; import NotifProvider from './NotifProvider';
import { timeout } from "../utils/promise"; import { timeout } from "../utils/promise";
import AutocompleteProvider, { ICommand } from "./AutocompleteProvider"; import AutocompleteProvider, { ICommand } from "./AutocompleteProvider";
import SettingsStore from "../settings/SettingsStore";
import SpaceProvider from "./SpaceProvider"; import SpaceProvider from "./SpaceProvider";
import SpaceStore from "../stores/SpaceStore";
export interface ISelectionRange { export interface ISelectionRange {
beginning?: boolean; // whether the selection is in the first block of the editor or not beginning?: boolean; // whether the selection is in the first block of the editor or not
@ -58,8 +58,7 @@ const PROVIDERS = [
DuckDuckGoProvider, DuckDuckGoProvider,
]; ];
// as the spaces feature is device configurable only, and toggling it refreshes the page, we can do this here if (SpaceStore.spacesEnabled) {
if (SettingsStore.getValue("feature_spaces")) {
PROVIDERS.push(SpaceProvider); PROVIDERS.push(SpaceProvider);
} else { } else {
PROVIDERS.push(CommunityProvider); PROVIDERS.push(CommunityProvider);

View file

@ -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;
} }

View file

@ -28,7 +28,7 @@ import { PillCompletion } from './Components';
import { makeRoomPermalink } from "../utils/permalinks/Permalinks"; import { makeRoomPermalink } from "../utils/permalinks/Permalinks";
import { ICompletion, ISelectionRange } from "./Autocompleter"; import { ICompletion, ISelectionRange } from "./Autocompleter";
import RoomAvatar from '../components/views/avatars/RoomAvatar'; import RoomAvatar from '../components/views/avatars/RoomAvatar';
import SettingsStore from "../settings/SettingsStore"; import SpaceStore from "../stores/SpaceStore";
const ROOM_REGEX = /\B#\S*/g; const ROOM_REGEX = /\B#\S*/g;
@ -59,7 +59,8 @@ export default class RoomProvider extends AutocompleteProvider {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
let rooms = cli.getVisibleRooms(); let rooms = cli.getVisibleRooms();
if (SettingsStore.getValue("feature_spaces")) { // if spaces are enabled then filter them out here as they get their own autocomplete provider
if (SpaceStore.spacesEnabled) {
rooms = rooms.filter(r => !r.isSpaceRoom()); rooms = rooms.filter(r => !r.isSpaceRoom());
} }

View file

@ -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 = {};

View 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();
}
}

View file

@ -56,7 +56,7 @@ class CustomRoomTagPanel extends React.Component {
return (<div className={classes}> return (<div className={classes}>
<div className="mx_CustomRoomTagPanel_divider" /> <div className="mx_CustomRoomTagPanel_divider" />
<AutoHideScrollbar className="mx_CustomRoomTagPanel_scroller"> <AutoHideScrollbar className="mx_CustomRoomTagPanel_scroller">
{tags} { tags }
</AutoHideScrollbar> </AutoHideScrollbar>
</div>); </div>);
} }
@ -84,7 +84,7 @@ class CustomRoomTagTile extends React.Component {
"mx_TagTile_badge": true, "mx_TagTile_badge": true,
"mx_TagTile_badgeHighlight": badgeNotifState.hasMentions, "mx_TagTile_badgeHighlight": badgeNotifState.hasMentions,
}); });
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badgeNotifState.count)}</div>); badgeElement = (<div className={badgeClasses}>{ FormattingUtils.formatCount(badgeNotifState.count) }</div>);
} }
return ( return (

View file

@ -125,11 +125,11 @@ export default class EmbeddedPage extends React.PureComponent {
if (this.props.scrollbar) { if (this.props.scrollbar) {
return <AutoHideScrollbar className={classes}> return <AutoHideScrollbar className={classes}>
{content} { content }
</AutoHideScrollbar>; </AutoHideScrollbar>;
} else { } else {
return <div className={classes}> return <div className={classes}>
{content} { content }
</div>; </div>;
} }
} }

View file

@ -241,8 +241,8 @@ class FilePanel extends React.Component<IProps, IState> {
// wrap a TimelinePanel with the jump-to-event bits turned off. // wrap a TimelinePanel with the jump-to-event bits turned off.
const emptyState = (<div className="mx_RightPanel_empty mx_FilePanel_empty"> const emptyState = (<div className="mx_RightPanel_empty mx_FilePanel_empty">
<h2>{_t('No files visible in this room')}</h2> <h2>{ _t('No files visible in this room') }</h2>
<p>{_t('Attach files from chat or just drag and drop them anywhere in a room.')}</p> <p>{ _t('Attach files from chat or just drag and drop them anywhere in a room.') }</p>
</div>); </div>);
const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId); const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
@ -262,7 +262,7 @@ class FilePanel extends React.Component<IProps, IState> {
manageReadReceipts={false} manageReadReceipts={false}
manageReadMarkers={false} manageReadMarkers={false}
timelineSet={this.state.timelineSet} timelineSet={this.state.timelineSet}
showUrlPreview = {false} showUrlPreview={false}
onPaginationRequest={this.onPaginationRequest} onPaginationRequest={this.onPaginationRequest}
tileShape={TileShape.FileGrid} tileShape={TileShape.FileGrid}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}

View file

@ -28,8 +28,8 @@ export default class GenericErrorPage extends React.PureComponent {
render() { render() {
return <div className='mx_GenericErrorPage'> return <div className='mx_GenericErrorPage'>
<div className='mx_GenericErrorPage_box'> <div className='mx_GenericErrorPage_box'>
<h1>{this.props.title}</h1> <h1>{ this.props.title }</h1>
<p>{this.props.message}</p> <p>{ this.props.message }</p>
</div> </div>
</div>; </div>;
} }

View file

@ -819,12 +819,12 @@ export default class GroupView extends React.Component {
let hostingSignup = null; let hostingSignup = null;
if (hostingSignupLink && this.state.isUserPrivileged) { if (hostingSignupLink && this.state.isUserPrivileged) {
hostingSignup = <div className="mx_GroupView_hostingSignup"> hostingSignup = <div className="mx_GroupView_hostingSignup">
{_t( { _t(
"Want more than a community? <a>Get your own server</a>", {}, "Want more than a community? <a>Get your own server</a>", {},
{ {
a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{sub}</a>, a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{ sub }</a>,
}, },
)} ) }
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener"> <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
<img src={require("../../../res/img/external-link.svg")} width="11" height="10" alt='' /> <img src={require("../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a> </a>

View file

@ -54,7 +54,7 @@ export default class InteractiveAuthComponent extends React.Component {
// * emailSid {string} If email auth was performed, the sid of // * emailSid {string} If email auth was performed, the sid of
// the auth session. // the auth session.
// * clientSecret {string} The client secret used in auth // * clientSecret {string} The client secret used in auth
// sessions with the ID server. // sessions with the identity server.
onAuthFinished: PropTypes.func.isRequired, onAuthFinished: PropTypes.func.isRequired,
// Inputs provided by the user to the auth process // Inputs provided by the user to the auth process

View file

@ -429,7 +429,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
onSelectRoom={this.selectRoom} onSelectRoom={this.selectRoom}
/> />
{dialPadButton} { dialPadButton }
<AccessibleTooltipButton <AccessibleTooltipButton
className={classNames("mx_LeftPanel_exploreButton", { className={classNames("mx_LeftPanel_exploreButton", {
@ -448,7 +448,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
leftLeftPanel = ( leftLeftPanel = (
<div className="mx_LeftPanel_GroupFilterPanelContainer"> <div className="mx_LeftPanel_GroupFilterPanelContainer">
<GroupFilterPanel /> <GroupFilterPanel />
{SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null} { SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null }
</div> </div>
); );
} }
@ -476,11 +476,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
return ( return (
<div className={containerClasses} ref={this.ref}> <div className={containerClasses} ref={this.ref}>
{leftLeftPanel} { leftLeftPanel }
<aside className="mx_LeftPanel_roomListContainer"> <aside className="mx_LeftPanel_roomListContainer">
{this.renderHeader()} { this.renderHeader() }
{this.renderSearchDialExplore()} { this.renderSearchDialExplore() }
{this.renderBreadcrumbs()} { this.renderBreadcrumbs() }
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} /> <RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
<div className="mx_LeftPanel_roomListWrapper"> <div className="mx_LeftPanel_roomListWrapper">
<div <div
@ -490,7 +490,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
// overflow:scroll;, so force it out of tab order. // overflow:scroll;, so force it out of tab order.
tabIndex={-1} tabIndex={-1}
> >
{roomList} { roomList }
</div> </div>
</div> </div>
{ !this.props.isMinimized && <LeftPanelWidget /> } { !this.props.isMinimized && <LeftPanelWidget /> }

View file

@ -125,15 +125,15 @@ const LeftPanelWidget: React.FC = () => {
<span>{ WidgetUtils.getWidgetName(app) }</span> <span>{ WidgetUtils.getWidgetName(app) }</span>
</AccessibleButton> </AccessibleButton>
{/* Code for the maximise button for once we have full screen widgets */} { /* Code for the maximise button for once we have full screen widgets */ }
{/*<AccessibleTooltipButton { /*<AccessibleTooltipButton
tabIndex={tabIndex} tabIndex={tabIndex}
onClick={() => { onClick={() => {
}} }}
className="mx_LeftPanelWidget_maximizeButton" className="mx_LeftPanelWidget_maximizeButton"
tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip" tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip"
title={_t("Maximize")} title={_t("Maximize")}
/>*/} />*/ }
</div> </div>
</div> </div>

View file

@ -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';
@ -63,6 +63,7 @@ import ToastContainer from './ToastContainer';
import MyGroups from "./MyGroups"; import MyGroups from "./MyGroups";
import UserView from "./UserView"; import UserView from "./UserView";
import GroupView from "./GroupView"; import GroupView from "./GroupView";
import SpaceStore from "../../stores/SpaceStore";
// We need to fetch each pinned message individually (if we don't already have it) // We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity. // so each pinned message may trigger a request. Limit the number per room for sanity.
@ -78,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;
@ -139,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>;
@ -180,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);
@ -199,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);
@ -220,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 => {
@ -266,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;
@ -274,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" });
} }
@ -306,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();
} }
}; };
@ -325,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;
@ -345,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 [];
@ -377,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,
@ -386,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
@ -424,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);
@ -449,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:
@ -564,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);
} }
@ -624,14 +615,14 @@ 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}
> >
<ToastContainer /> <ToastContainer />
<div ref={this._resizeContainer} className={bodyClasses}> <div ref={this._resizeContainer} className={bodyClasses}>
{ SettingsStore.getValue("feature_spaces") ? <SpacePanel /> : null } { SpaceStore.spacesEnabled ? <SpacePanel /> : null }
<LeftPanel <LeftPanel
isMinimized={this.props.collapseLhs || false} isMinimized={this.props.collapseLhs || false}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
@ -643,7 +634,7 @@ class LoggedInView extends React.Component<IProps, IState> {
<CallContainer /> <CallContainer />
<NonUrgentToastContainer /> <NonUrgentToastContainer />
<HostSignupContainer /> <HostSignupContainer />
{audioFeedArraysForCalls} { audioFeedArraysForCalls }
</MatrixClientContext.Provider> </MatrixClientContext.Provider>
); );
} }

View file

@ -19,7 +19,7 @@ import { createClient } from "matrix-js-sdk/src/matrix";
import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { sleep, defer, IDeferred } from "matrix-js-sdk/src/utils"; import { sleep, defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils";
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss // focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
import 'focus-visible'; import 'focus-visible';
@ -105,6 +105,8 @@ import VerificationRequestToast from '../views/toasts/VerificationRequestToast';
import PerformanceMonitor, { PerformanceEntryNames } from "../../performance"; import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
import UIStore, { UI_EVENTS } from "../../stores/UIStore"; import UIStore, { UI_EVENTS } from "../../stores/UIStore";
import SoftLogout from './auth/SoftLogout'; import SoftLogout from './auth/SoftLogout';
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
import { copyPlaintext } from "../../utils/strings";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
@ -153,7 +155,7 @@ const ONBOARDING_FLOW_STARTERS = [
interface IScreen { interface IScreen {
screen: string; screen: string;
params?: object; params?: QueryDict;
} }
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@ -183,9 +185,9 @@ interface IProps { // TODO type things better
onNewScreen: (screen: string, replaceLast: boolean) => void; onNewScreen: (screen: string, replaceLast: boolean) => void;
enableGuest?: boolean; enableGuest?: boolean;
// the queryParams extracted from the [real] query-string of the URI // the queryParams extracted from the [real] query-string of the URI
realQueryParams?: Record<string, string>; realQueryParams?: QueryDict;
// the initial queryParams extracted from the hash-fragment of the URI // the initial queryParams extracted from the hash-fragment of the URI
startingFragmentQueryParams?: Record<string, string>; startingFragmentQueryParams?: QueryDict;
// called when we have completed a token login // called when we have completed a token login
onTokenLoginCompleted?: () => void; onTokenLoginCompleted?: () => void;
// Represents the screen to display as a result of parsing the initial window.location // Represents the screen to display as a result of parsing the initial window.location
@ -193,7 +195,7 @@ interface IProps { // TODO type things better
// displayname, if any, to set on the device when logging in/registering. // displayname, if any, to set on the device when logging in/registering.
defaultDeviceDisplayName?: string; defaultDeviceDisplayName?: string;
// A function that makes a registration URL // A function that makes a registration URL
makeRegistrationUrl: (object) => string; makeRegistrationUrl: (params: QueryDict) => string;
} }
interface IState { interface IState {
@ -251,7 +253,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private pageChanging: boolean; private pageChanging: boolean;
private tokenLogin?: boolean; private tokenLogin?: boolean;
private accountPassword?: string; private accountPassword?: string;
private accountPasswordTimer?: NodeJS.Timeout; private accountPasswordTimer?: number;
private focusComposer: boolean; private focusComposer: boolean;
private subTitleStatus: string; private subTitleStatus: string;
private prevWindowWidth: number; private prevWindowWidth: number;
@ -296,7 +298,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (this.screenAfterLogin.screen.startsWith("room/") && params['signurl'] && params['email']) { if (this.screenAfterLogin.screen.startsWith("room/") && params['signurl'] && params['email']) {
// probably a threepid invite - try to store it // probably a threepid invite - try to store it
const roomId = this.screenAfterLogin.screen.substring("room/".length); const roomId = this.screenAfterLogin.screen.substring("room/".length);
ThreepidInviteStore.instance.storeInvite(roomId, params as IThreepidInviteWireFormat); ThreepidInviteStore.instance.storeInvite(roomId, params as unknown as IThreepidInviteWireFormat);
} }
} }
@ -429,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();
@ -561,7 +563,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
switch (payload.action) { switch (payload.action) {
case 'MatrixActions.accountData': case 'MatrixActions.accountData':
// XXX: This is a collection of several hacks to solve a minor problem. We want to // XXX: This is a collection of several hacks to solve a minor problem. We want to
// update our local state when the ID server changes, but don't want to put that in // update our local state when the identity server changes, but don't want to put that in
// the js-sdk as we'd be then dictating how all consumers need to behave. However, // the js-sdk as we'd be then dictating how all consumers need to behave. However,
// this component is already bloated and we probably don't want this tiny logic in // this component is already bloated and we probably don't want this tiny logic in
// here, but there's no better place in the react-sdk for it. Additionally, we're // here, but there's no better place in the react-sdk for it. Additionally, we're
@ -627,6 +629,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case 'forget_room': case 'forget_room':
this.forgetRoom(payload.room_id); this.forgetRoom(payload.room_id);
break; break;
case 'copy_room':
this.copyRoom(payload.room_id);
break;
case 'reject_invite': case 'reject_invite':
Modal.createTrackedDialog('Reject invitation', '', QuestionDialog, { Modal.createTrackedDialog('Reject invitation', '', QuestionDialog, {
title: _t('Reject invitation'), title: _t('Reject invitation'),
@ -1099,7 +1104,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private leaveRoomWarnings(roomId: string) { private leaveRoomWarnings(roomId: string) {
const roomToLeave = MatrixClientPeg.get().getRoom(roomId); const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom(); const isSpace = SpaceStore.spacesEnabled && roomToLeave?.isSpaceRoom();
// Show a warning if there are additional complications. // Show a warning if there are additional complications.
const warnings = []; const warnings = [];
@ -1107,7 +1112,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (memberCount === 1) { if (memberCount === 1) {
warnings.push(( warnings.push((
<span className="warning" key="only_member_warning"> <span className="warning" key="only_member_warning">
{' '/* Whitespace, otherwise the sentences get smashed together */ } { ' '/* Whitespace, otherwise the sentences get smashed together */ }
{ _t("You are the only person here. " + { _t("You are the only person here. " +
"If you leave, no one will be able to join in the future, including you.") } "If you leave, no one will be able to join in the future, including you.") }
</span> </span>
@ -1122,7 +1127,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (rule !== "public") { if (rule !== "public") {
warnings.push(( warnings.push((
<span className="warning" key="non_public_warning"> <span className="warning" key="non_public_warning">
{' '/* Whitespace, otherwise the sentences get smashed together */ } { ' '/* Whitespace, otherwise the sentences get smashed together */ }
{ isSpace { isSpace
? _t("This space is not public. You will not be able to rejoin without an invite.") ? _t("This space is not public. You will not be able to rejoin without an invite.")
: _t("This room is not public. You will not be able to rejoin without an invite.") } : _t("This room is not public. You will not be able to rejoin without an invite.") }
@ -1137,7 +1142,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const roomToLeave = MatrixClientPeg.get().getRoom(roomId); const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
const warnings = this.leaveRoomWarnings(roomId); const warnings = this.leaveRoomWarnings(roomId);
const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom(); const isSpace = SpaceStore.spacesEnabled && roomToLeave?.isSpaceRoom();
Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, { Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, {
title: isSpace ? _t("Leave space") : _t("Leave room"), title: isSpace ? _t("Leave space") : _t("Leave room"),
description: ( description: (
@ -1150,7 +1155,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
: _t( : _t(
"Are you sure you want to leave the room '%(roomName)s'?", "Are you sure you want to leave the room '%(roomName)s'?",
{ roomName: roomToLeave.name }, { roomName: roomToLeave.name },
)} ) }
{ warnings } { warnings }
</span> </span>
), ),
@ -1193,6 +1198,17 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}); });
} }
private async copyRoom(roomId: string) {
const roomLink = makeRoomPermalink(roomId);
const success = await copyPlaintext(roomLink);
if (!success) {
Modal.createTrackedDialog("Unable to copy room link", "", ErrorDialog, {
title: _t("Unable to copy room link"),
description: _t("Unable to copy a link to the room to the clipboard."),
});
}
}
/** /**
* Starts a chat with the welcome user, if the user doesn't already have one * Starts a chat with the welcome user, if the user doesn't already have one
* @returns {string} The room ID of the new room, or null if no room was created * @returns {string} The room ID of the new room, or null if no room was created
@ -1687,7 +1703,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const type = screen === "start_sso" ? "sso" : "cas"; const type = screen === "start_sso" ? "sso" : "cas";
PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin()); PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin());
} else if (screen === 'groups') { } else if (screen === 'groups') {
if (SettingsStore.getValue("feature_spaces")) { if (SpaceStore.spacesEnabled) {
dis.dispatch({ action: "view_home_page" }); dis.dispatch({ action: "view_home_page" });
return; return;
} }
@ -1774,7 +1790,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
subAction: params.action, subAction: params.action,
}); });
} else if (screen.indexOf('group/') === 0) { } else if (screen.indexOf('group/') === 0) {
if (SettingsStore.getValue("feature_spaces")) { if (SpaceStore.spacesEnabled) {
dis.dispatch({ action: "view_home_page" }); dis.dispatch({ action: "view_home_page" });
return; return;
} }
@ -1848,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");
}; };
@ -1936,7 +1945,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.setState({ serverConfig }); this.setState({ serverConfig });
}; };
private makeRegistrationUrl = (params: {[key: string]: string}) => { private makeRegistrationUrl = (params: QueryDict) => {
if (this.props.startingFragmentQueryParams.referrer) { if (this.props.startingFragmentQueryParams.referrer) {
params.referrer = this.props.startingFragmentQueryParams.referrer; params.referrer = this.props.startingFragmentQueryParams.referrer;
} }
@ -2027,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}
/> />
@ -2037,15 +2045,15 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
let errorBox; let errorBox;
if (this.state.syncError && !isStoreError) { if (this.state.syncError && !isStoreError) {
errorBox = <div className="mx_MatrixChat_syncError"> errorBox = <div className="mx_MatrixChat_syncError">
{messageForSyncError(this.state.syncError)} { messageForSyncError(this.state.syncError) }
</div>; </div>;
} }
view = ( view = (
<div className="mx_MatrixChat_splash"> <div className="mx_MatrixChat_splash">
{errorBox} { errorBox }
<Spinner /> <Spinner />
<a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}> <a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}>
{_t('Logout')} { _t('Logout') }
</a> </a>
</div> </div>
); );
@ -2091,7 +2099,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined} onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined}
onServerConfigChange={this.onServerConfigChange} onServerConfigChange={this.onServerConfigChange}
fragmentAfterLogin={fragmentAfterLogin} fragmentAfterLogin={fragmentAfterLogin}
defaultUsername={this.props.startingFragmentQueryParams.defaultUsername} defaultUsername={this.props.startingFragmentQueryParams.defaultUsername as string}
{...this.getServerProperties()} {...this.getServerProperties()}
/> />
); );
@ -2108,7 +2116,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
return <ErrorBoundary> return <ErrorBoundary>
{view} { view }
</ErrorBoundary>; </ErrorBoundary>;
} }
} }

View file

@ -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';
@ -59,7 +60,11 @@ const groupedEvents = [
// 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
@ -79,7 +84,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;
} }
@ -233,6 +238,11 @@ 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>();
private membersCount = 0;
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
@ -244,7 +254,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 =
@ -252,11 +263,14 @@ export default class MessagePanel extends React.Component<IProps, IState> {
} }
componentDidMount() { componentDidMount() {
this.calculateRoomMembersCount();
this.props.room?.on("RoomState.members", this.calculateRoomMembersCount);
this.isMounted = true; this.isMounted = true;
} }
componentWillUnmount() { componentWillUnmount() {
this.isMounted = false; this.isMounted = false;
this.props.room?.off("RoomState.members", this.calculateRoomMembersCount);
SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef);
} }
@ -270,6 +284,10 @@ export default class MessagePanel extends React.Component<IProps, IState> {
} }
} }
private calculateRoomMembersCount = (): void => {
this.membersCount = this.props.room?.getMembers().length || 0;
};
private onShowTypingNotificationsChange = (): void => { private onShowTypingNotificationsChange = (): void => {
this.setState({ this.setState({
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
@ -404,17 +422,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 (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) { 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
} }
@ -572,9 +594,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
@ -649,12 +685,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);
@ -685,6 +724,7 @@ 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}>
@ -707,7 +747,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}
@ -715,6 +755,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.membersCount <= 2 && this.props.layout === Layout.Bubble}
/> />
</TileErrorBoundary>, </TileErrorBoundary>,
); );
@ -951,7 +993,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;
} }
@ -1205,10 +1247,10 @@ class MemberGrouper extends BaseGrouper {
return groupedEvents.includes(ev.getType() as EventType); return groupedEvents.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(),

View file

@ -121,7 +121,7 @@ export default class MyGroups extends React.Component {
) } ) }
</div> </div>
</div> </div>
{/*<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard"> { /*<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard">
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onJoinGroupClick}> <AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onJoinGroupClick}>
<img src={require("../../../res/img/icons-create-room.svg")} width="50" height="50" /> <img src={require("../../../res/img/icons-create-room.svg")} width="50" height="50" />
</AccessibleButton> </AccessibleButton>
@ -137,7 +137,7 @@ export default class MyGroups extends React.Component {
{ 'i': (sub) => <i>{ sub }</i> }) { 'i': (sub) => <i>{ sub }</i> })
} }
</div> </div>
</div>*/} </div>*/ }
</div> </div>
<BetaCard featureId="feature_spaces" title={_t("Communities are changing to Spaces")} /> <BetaCard featureId="feature_spaces" title={_t("Communities are changing to Spaces")} />
<div className="mx_MyGroups_content"> <div className="mx_MyGroups_content">

View file

@ -51,14 +51,14 @@ export default class NonUrgentToastContainer extends React.PureComponent<IProps,
const toasts = this.state.toasts.map((t, i) => { const toasts = this.state.toasts.map((t, i) => {
return ( return (
<div className="mx_NonUrgentToastContainer_toast" key={`toast-${i}`}> <div className="mx_NonUrgentToastContainer_toast" key={`toast-${i}`}>
{React.createElement(t, {})} { React.createElement(t, {}) }
</div> </div>
); );
}); });
return ( return (
<div className="mx_NonUrgentToastContainer" role="alert"> <div className="mx_NonUrgentToastContainer" role="alert">
{toasts} { toasts }
</div> </div>
); );
} }

View file

@ -35,8 +35,8 @@ interface IProps {
export default class NotificationPanel extends React.PureComponent<IProps> { export default class NotificationPanel extends React.PureComponent<IProps> {
render() { render() {
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty"> const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
<h2>{_t('Youre all caught up')}</h2> <h2>{ _t('Youre all caught up') }</h2>
<p>{_t('You have no visible notifications.')}</p> <p>{ _t('You have no visible notifications.') }</p>
</div>); </div>);
let content; let content;

View file

@ -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";
@ -48,6 +49,7 @@ import NotificationPanel from "./NotificationPanel";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import SpaceStore from "../../stores/SpaceStore";
interface IProps { interface IProps {
room?: Room; // if showing panels for a given room, this is set room?: Room; // if showing panels for a given room, this is set
@ -107,7 +109,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
return RightPanelPhases.GroupMemberList; return RightPanelPhases.GroupMemberList;
} }
return rps.groupPanelPhase; return rps.groupPanelPhase;
} else if (SettingsStore.getValue("feature_spaces") && this.props.room?.isSpaceRoom() } else if (SpaceStore.spacesEnabled && this.props.room?.isSpaceRoom()
&& !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase) && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)
) { ) {
return RightPanelPhases.SpaceMemberList; return RightPanelPhases.SpaceMemberList;
@ -151,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);
@ -173,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;
} }

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