mirror of https://github.com/vector-im/riot-web
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/17282
Conflicts: package.json yarn.lockpull/21833/head
commit
66fce64ee0
|
@ -18,7 +18,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
overrides: [{
|
overrides: [{
|
||||||
"files": ["src/**/*.{ts,tsx}"],
|
"files": ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"],
|
||||||
"extends": ["matrix-org/ts"],
|
"extends": ["matrix-org/ts"],
|
||||||
"rules": {
|
"rules": {
|
||||||
// We're okay being explicit at the moment
|
// We're okay being explicit at the moment
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
name: Develop jobs
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [develop]
|
||||||
|
jobs:
|
||||||
|
end-to-end:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: vectorim/element-web-ci-e2etests-env:latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: End-to-End tests
|
||||||
|
run: ./scripts/ci/end-to-end-tests.sh
|
||||||
|
- name: Archive logs
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
test/end-to-end-tests/logs/**/*
|
||||||
|
test/end-to-end-tests/synapse/installations/consent/homeserver.log
|
||||||
|
retention-days: 14
|
||||||
|
- name: Archive performance benchmark
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: performance-entries.json
|
||||||
|
path: test/end-to-end-tests/performance-entries.json
|
106
CHANGELOG.md
106
CHANGELOG.md
|
@ -1,3 +1,109 @@
|
||||||
|
Changes in [3.23.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0) (2021-06-07)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0-rc.1...v3.23.0)
|
||||||
|
|
||||||
|
* Upgrade to JS SDK 11.2.0
|
||||||
|
* [Release] Fix notif panel timestamp padding
|
||||||
|
[\#6158](https://github.com/matrix-org/matrix-react-sdk/pull/6158)
|
||||||
|
|
||||||
|
Changes in [3.23.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0-rc.1) (2021-06-01)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0...v3.23.0-rc.1)
|
||||||
|
|
||||||
|
* Upgrade to JS SDK 11.2.0-rc.1
|
||||||
|
* Translations update from Weblate
|
||||||
|
[\#6128](https://github.com/matrix-org/matrix-react-sdk/pull/6128)
|
||||||
|
* Fix all DMs wrongly appearing in room list when `m.direct` is changed
|
||||||
|
[\#6122](https://github.com/matrix-org/matrix-react-sdk/pull/6122)
|
||||||
|
* Update way of checking for registration disabled
|
||||||
|
[\#6123](https://github.com/matrix-org/matrix-react-sdk/pull/6123)
|
||||||
|
* Fix the ability to remove avatar from a space via settings
|
||||||
|
[\#6126](https://github.com/matrix-org/matrix-react-sdk/pull/6126)
|
||||||
|
* Switch to stable endpoint/fields for MSC2858
|
||||||
|
[\#6125](https://github.com/matrix-org/matrix-react-sdk/pull/6125)
|
||||||
|
* Clear stored editor state when canceling editing using a shortcut
|
||||||
|
[\#6117](https://github.com/matrix-org/matrix-react-sdk/pull/6117)
|
||||||
|
* Respect newlines in space topics
|
||||||
|
[\#6124](https://github.com/matrix-org/matrix-react-sdk/pull/6124)
|
||||||
|
* Add url param `defaultUsername` to prefill the login username field
|
||||||
|
[\#5674](https://github.com/matrix-org/matrix-react-sdk/pull/5674)
|
||||||
|
* Bump ws from 7.4.2 to 7.4.6
|
||||||
|
[\#6115](https://github.com/matrix-org/matrix-react-sdk/pull/6115)
|
||||||
|
* Sticky headers repositioning without layout trashing
|
||||||
|
[\#6110](https://github.com/matrix-org/matrix-react-sdk/pull/6110)
|
||||||
|
* Handle user_busy in voip calls
|
||||||
|
[\#6112](https://github.com/matrix-org/matrix-react-sdk/pull/6112)
|
||||||
|
* Avoid showing warning modals from the invite dialog after it unmounts
|
||||||
|
[\#6105](https://github.com/matrix-org/matrix-react-sdk/pull/6105)
|
||||||
|
* Fix misleading child counts in spaces
|
||||||
|
[\#6109](https://github.com/matrix-org/matrix-react-sdk/pull/6109)
|
||||||
|
* Close creation menu when expanding space panel via expand hierarchy
|
||||||
|
[\#6090](https://github.com/matrix-org/matrix-react-sdk/pull/6090)
|
||||||
|
* Prevent having duplicates in pending room state
|
||||||
|
[\#6108](https://github.com/matrix-org/matrix-react-sdk/pull/6108)
|
||||||
|
* Update reactions row on event decryption
|
||||||
|
[\#6106](https://github.com/matrix-org/matrix-react-sdk/pull/6106)
|
||||||
|
* Destroy playback instance on voice message unmount
|
||||||
|
[\#6101](https://github.com/matrix-org/matrix-react-sdk/pull/6101)
|
||||||
|
* Fix message preview not up to date
|
||||||
|
[\#6102](https://github.com/matrix-org/matrix-react-sdk/pull/6102)
|
||||||
|
* Convert some Flow typed files to TS (round 2)
|
||||||
|
[\#6076](https://github.com/matrix-org/matrix-react-sdk/pull/6076)
|
||||||
|
* Remove unused middlePanelResized event listener
|
||||||
|
[\#6086](https://github.com/matrix-org/matrix-react-sdk/pull/6086)
|
||||||
|
* Fix accessing currentState on an invalid joinedRoom
|
||||||
|
[\#6100](https://github.com/matrix-org/matrix-react-sdk/pull/6100)
|
||||||
|
* Remove Promise allSettled polyfill as js-sdk uses it directly
|
||||||
|
[\#6097](https://github.com/matrix-org/matrix-react-sdk/pull/6097)
|
||||||
|
* Prevent DecoratedRoomAvatar to update its state for the same value
|
||||||
|
[\#6099](https://github.com/matrix-org/matrix-react-sdk/pull/6099)
|
||||||
|
* Skip generatePreview if event is not part of the live timeline
|
||||||
|
[\#6098](https://github.com/matrix-org/matrix-react-sdk/pull/6098)
|
||||||
|
* fix sticky headers when results num get displayed
|
||||||
|
[\#6095](https://github.com/matrix-org/matrix-react-sdk/pull/6095)
|
||||||
|
* Improve addEventsToTimeline performance scoping WhoIsTypingTile::setState
|
||||||
|
[\#6094](https://github.com/matrix-org/matrix-react-sdk/pull/6094)
|
||||||
|
* Safeguards to prevent layout trashing for window dimensions
|
||||||
|
[\#6092](https://github.com/matrix-org/matrix-react-sdk/pull/6092)
|
||||||
|
* Use local room state to render space hierarchy if the room is known
|
||||||
|
[\#6089](https://github.com/matrix-org/matrix-react-sdk/pull/6089)
|
||||||
|
* Add spinner in UserMenu to list pending long running actions
|
||||||
|
[\#6085](https://github.com/matrix-org/matrix-react-sdk/pull/6085)
|
||||||
|
* Stop overscroll in Firefox Nightly for macOS
|
||||||
|
[\#6093](https://github.com/matrix-org/matrix-react-sdk/pull/6093)
|
||||||
|
* Move SettingsStore watchers/monitors over to ES6 maps for performance
|
||||||
|
[\#6063](https://github.com/matrix-org/matrix-react-sdk/pull/6063)
|
||||||
|
* Bump libolm version.
|
||||||
|
[\#6080](https://github.com/matrix-org/matrix-react-sdk/pull/6080)
|
||||||
|
* Improve styling of the message action bar
|
||||||
|
[\#6066](https://github.com/matrix-org/matrix-react-sdk/pull/6066)
|
||||||
|
* Improve explore rooms when no results are found
|
||||||
|
[\#6070](https://github.com/matrix-org/matrix-react-sdk/pull/6070)
|
||||||
|
* Remove logo spinner
|
||||||
|
[\#6078](https://github.com/matrix-org/matrix-react-sdk/pull/6078)
|
||||||
|
* Fix add reaction prompt showing even when user is not joined to room
|
||||||
|
[\#6073](https://github.com/matrix-org/matrix-react-sdk/pull/6073)
|
||||||
|
* Vectorize spinners
|
||||||
|
[\#5680](https://github.com/matrix-org/matrix-react-sdk/pull/5680)
|
||||||
|
* Fix handling of via servers for suggested rooms
|
||||||
|
[\#6077](https://github.com/matrix-org/matrix-react-sdk/pull/6077)
|
||||||
|
* Upgrade showChatEffects to room-level setting exposure
|
||||||
|
[\#6075](https://github.com/matrix-org/matrix-react-sdk/pull/6075)
|
||||||
|
* Delete RoomView dead code
|
||||||
|
[\#6071](https://github.com/matrix-org/matrix-react-sdk/pull/6071)
|
||||||
|
* Reduce noise in tests
|
||||||
|
[\#6074](https://github.com/matrix-org/matrix-react-sdk/pull/6074)
|
||||||
|
* Fix room name issues in right panel summary card
|
||||||
|
[\#6069](https://github.com/matrix-org/matrix-react-sdk/pull/6069)
|
||||||
|
* Cache normalized room name
|
||||||
|
[\#6072](https://github.com/matrix-org/matrix-react-sdk/pull/6072)
|
||||||
|
* Update MemberList to reflect changes for invite permission change
|
||||||
|
[\#6061](https://github.com/matrix-org/matrix-react-sdk/pull/6061)
|
||||||
|
* Delete RoomView dead code
|
||||||
|
[\#6065](https://github.com/matrix-org/matrix-react-sdk/pull/6065)
|
||||||
|
* Show subspace rooms count even if it is 0 for consistency
|
||||||
|
[\#6067](https://github.com/matrix-org/matrix-react-sdk/pull/6067)
|
||||||
|
|
||||||
Changes in [3.22.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0) (2021-05-24)
|
Changes in [3.22.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0) (2021-05-24)
|
||||||
=====================================================================================================
|
=====================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0-rc.1...v3.22.0)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0-rc.1...v3.22.0)
|
||||||
|
|
13
package.json
13
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.22.0",
|
"version": "3.23.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -55,7 +55,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"await-lock": "^2.1.0",
|
"await-lock": "^2.1.0",
|
||||||
"blueimp-canvas-to-blob": "^3.28.0",
|
|
||||||
"browser-encrypt-attachment": "^0.3.0",
|
"browser-encrypt-attachment": "^0.3.0",
|
||||||
"browser-request": "^0.3.3",
|
"browser-request": "^0.3.3",
|
||||||
"cheerio": "^1.0.0-rc.9",
|
"cheerio": "^1.0.0-rc.9",
|
||||||
|
@ -88,18 +87,16 @@
|
||||||
"png-chunks-extract": "^1.0.0",
|
"png-chunks-extract": "^1.0.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"qs": "^6.9.6",
|
|
||||||
"re-resizable": "^6.9.0",
|
"re-resizable": "^6.9.0",
|
||||||
"react": "^16.14.0",
|
"react": "^17.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^17.0.2",
|
||||||
"react-focus-lock": "^2.5.0",
|
"react-focus-lock": "^2.5.0",
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"rfc4648": "^1.4.0",
|
"rfc4648": "^1.4.0",
|
||||||
"sanitize-html": "^2.3.2",
|
"sanitize-html": "^2.3.2",
|
||||||
"tar-js": "^0.3.0",
|
"tar-js": "^0.3.0",
|
||||||
"text-encoding-utf-8": "^1.0.2",
|
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
"what-input": "^5.2.10",
|
"what-input": "^5.2.10",
|
||||||
"zxcvbn": "^4.4.2"
|
"zxcvbn": "^4.4.2"
|
||||||
|
@ -148,7 +145,7 @@
|
||||||
"chokidar": "^3.5.1",
|
"chokidar": "^3.5.1",
|
||||||
"concurrently": "^5.3.0",
|
"concurrently": "^5.3.0",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
"enzyme-adapter-react-16": "^1.15.6",
|
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
|
||||||
"eslint": "7.18.0",
|
"eslint": "7.18.0",
|
||||||
"eslint-config-matrix-org": "^0.2.0",
|
"eslint-config-matrix-org": "^0.2.0",
|
||||||
"eslint-plugin-babel": "^5.3.1",
|
"eslint-plugin-babel": "^5.3.1",
|
||||||
|
@ -163,7 +160,7 @@
|
||||||
"matrix-mock-request": "^1.2.3",
|
"matrix-mock-request": "^1.2.3",
|
||||||
"matrix-react-test-utils": "^0.2.2",
|
"matrix-react-test-utils": "^0.2.2",
|
||||||
"matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
|
"matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
|
||||||
"react-test-renderer": "^16.14.0",
|
"react-test-renderer": "^17.0.2",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"stylelint": "^13.9.0",
|
"stylelint": "^13.9.0",
|
||||||
"stylelint-config-standard": "^20.0.0",
|
"stylelint-config-standard": "^20.0.0",
|
||||||
|
|
|
@ -291,6 +291,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
||||||
|
|
||||||
.mx_Dialog_staticWrapper .mx_Dialog {
|
.mx_Dialog_staticWrapper .mx_Dialog {
|
||||||
z-index: 4010;
|
z-index: 4010;
|
||||||
|
contain: content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Dialog_background {
|
.mx_Dialog_background {
|
||||||
|
|
|
@ -76,6 +76,7 @@
|
||||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||||
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
|
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
|
||||||
@import "./views/dialogs/_FeedbackDialog.scss";
|
@import "./views/dialogs/_FeedbackDialog.scss";
|
||||||
|
@import "./views/dialogs/_ForwardDialog.scss";
|
||||||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||||
@import "./views/dialogs/_HostSignupDialog.scss";
|
@import "./views/dialogs/_HostSignupDialog.scss";
|
||||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||||
|
@ -179,6 +180,7 @@
|
||||||
@import "./views/messages/_common_CryptoEvent.scss";
|
@import "./views/messages/_common_CryptoEvent.scss";
|
||||||
@import "./views/right_panel/_BaseCard.scss";
|
@import "./views/right_panel/_BaseCard.scss";
|
||||||
@import "./views/right_panel/_EncryptionInfo.scss";
|
@import "./views/right_panel/_EncryptionInfo.scss";
|
||||||
|
@import "./views/right_panel/_PinnedMessagesCard.scss";
|
||||||
@import "./views/right_panel/_RoomSummaryCard.scss";
|
@import "./views/right_panel/_RoomSummaryCard.scss";
|
||||||
@import "./views/right_panel/_UserInfo.scss";
|
@import "./views/right_panel/_UserInfo.scss";
|
||||||
@import "./views/right_panel/_VerificationPanel.scss";
|
@import "./views/right_panel/_VerificationPanel.scss";
|
||||||
|
@ -203,7 +205,6 @@
|
||||||
@import "./views/rooms/_NewRoomIntro.scss";
|
@import "./views/rooms/_NewRoomIntro.scss";
|
||||||
@import "./views/rooms/_NotificationBadge.scss";
|
@import "./views/rooms/_NotificationBadge.scss";
|
||||||
@import "./views/rooms/_PinnedEventTile.scss";
|
@import "./views/rooms/_PinnedEventTile.scss";
|
||||||
@import "./views/rooms/_PinnedEventsPanel.scss";
|
|
||||||
@import "./views/rooms/_PresenceLabel.scss";
|
@import "./views/rooms/_PresenceLabel.scss";
|
||||||
@import "./views/rooms/_ReplyPreview.scss";
|
@import "./views/rooms/_ReplyPreview.scss";
|
||||||
@import "./views/rooms/_RoomBreadcrumbs.scss";
|
@import "./views/rooms/_RoomBreadcrumbs.scss";
|
||||||
|
|
|
@ -38,6 +38,7 @@ limitations under the License.
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
z-index: 5001;
|
z-index: 5001;
|
||||||
|
contain: content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ContextualMenu_right {
|
.mx_ContextualMenu_right {
|
||||||
|
|
|
@ -25,6 +25,7 @@ $roomListCollapsedWidth: 68px;
|
||||||
|
|
||||||
// Create a row-based flexbox for the GroupFilterPanel and the room list
|
// Create a row-based flexbox for the GroupFilterPanel and the room list
|
||||||
display: flex;
|
display: flex;
|
||||||
|
contain: content;
|
||||||
|
|
||||||
.mx_LeftPanel_GroupFilterPanelContainer {
|
.mx_LeftPanel_GroupFilterPanelContainer {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
@ -70,6 +71,7 @@ $roomListCollapsedWidth: 68px;
|
||||||
// aligned correctly. This is also a row-based flexbox.
|
// aligned correctly. This is also a row-based flexbox.
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
contain: content;
|
||||||
|
|
||||||
&.mx_IndicatorScrollbar_leftOverflow {
|
&.mx_IndicatorScrollbar_leftOverflow {
|
||||||
mask-image: linear-gradient(90deg, transparent, black 5%);
|
mask-image: linear-gradient(90deg, transparent, black 5%);
|
||||||
|
|
|
@ -82,7 +82,6 @@ limitations under the License.
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
display: inline;
|
display: inline;
|
||||||
padding-left: 0px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_NotificationPanel .mx_EventTile_senderDetails {
|
.mx_NotificationPanel .mx_EventTile_senderDetails {
|
||||||
|
@ -103,6 +102,7 @@ limitations under the License.
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
position: initial;
|
position: initial;
|
||||||
display: inline;
|
display: inline;
|
||||||
|
padding-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_NotificationPanel .mx_EventTile_line {
|
.mx_NotificationPanel .mx_EventTile_line {
|
||||||
|
|
|
@ -25,6 +25,7 @@ limitations under the License.
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
contain: strict;
|
||||||
|
|
||||||
.mx_RoomView_MessageList {
|
.mx_RoomView_MessageList {
|
||||||
padding: 14px 18px; // top and bottom is 4px smaller to balance with the padding set above
|
padding: 14px 18px; // top and bottom is 4px smaller to balance with the padding set above
|
||||||
|
@ -98,6 +99,48 @@ limitations under the License.
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$dot-size: 8px;
|
||||||
|
$pulse-color: $pinned-unread-color;
|
||||||
|
|
||||||
|
.mx_RightPanel_pinnedMessagesButton {
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/pin.svg');
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RightPanel_pinnedMessagesButton_unreadIndicator {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
margin: 4px;
|
||||||
|
width: $dot-size;
|
||||||
|
height: $dot-size;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: scale(1);
|
||||||
|
background: rgba($pulse-color, 1);
|
||||||
|
box-shadow: 0 0 0 0 rgba($pulse-color, 1);
|
||||||
|
animation: mx_RightPanel_indicator_pulse 2s infinite;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mx_RightPanel_indicator_pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
box-shadow: 0 0 0 0 rgba($pulse-color, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 10px rgba($pulse-color, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
box-shadow: 0 0 0 0 rgba($pulse-color, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RightPanel_headerButton_highlight {
|
.mx_RightPanel_headerButton_highlight {
|
||||||
&::before {
|
&::before {
|
||||||
background-color: $accent-color !important;
|
background-color: $accent-color !important;
|
||||||
|
|
|
@ -152,6 +152,7 @@ limitations under the License.
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
contain: content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomView_statusArea {
|
.mx_RoomView_statusArea {
|
||||||
|
@ -237,6 +238,7 @@ hr.mx_RoomView_myReadMarker {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
will-change: width;
|
||||||
transition: width 400ms easeinsine 1s, opacity 400ms easeinsine 1s;
|
transition: width 400ms easeinsine 1s, opacity 400ms easeinsine 1s;
|
||||||
width: 99%;
|
width: 99%;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
|
@ -21,5 +21,8 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: 50px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -365,6 +365,45 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SpaceRoomView_betaWarning {
|
||||||
|
padding: 12px 12px 12px 54px;
|
||||||
|
position: relative;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
width: 432px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: $info-plinth-bg-color;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
> h3 {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
content: '';
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
left: 14px;
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_SpaceRoomView_inviteTeammates {
|
.mx_SpaceRoomView_inviteTeammates {
|
||||||
// XXX remove this when spaces leaves Beta
|
// XXX remove this when spaces leaves Beta
|
||||||
.mx_SpaceRoomView_inviteTeammates_betaDisclaimer {
|
.mx_SpaceRoomView_inviteTeammates_betaDisclaimer {
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_DecoratedRoomAvatar, .mx_ExtraTile {
|
.mx_DecoratedRoomAvatar, .mx_ExtraTile {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
contain: content;
|
||||||
|
|
||||||
&.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar {
|
&.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar {
|
||||||
mask-image: url('$(res)/img/element-icons/roomlist/decorated-avatar-mask.svg');
|
mask-image: url('$(res)/img/element-icons/roomlist/decorated-avatar-mask.svg');
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Robin Townsend <robin@robin.town>
|
||||||
|
|
||||||
|
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_ForwardDialog {
|
||||||
|
width: 520px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
min-height: 0;
|
||||||
|
height: 80vh;
|
||||||
|
|
||||||
|
> h3 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
font-size: $font-12px;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
line-height: $font-15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .mx_ForwardDialog_preview {
|
||||||
|
max-height: 30%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: scroll;
|
||||||
|
|
||||||
|
div {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_msgOption {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When forwarding messages from encrypted rooms, EventTile will complain
|
||||||
|
// that our preview is unencrypted, which doesn't actually matter
|
||||||
|
.mx_EventTile_e2eIcon_unencrypted {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We also hide download links to not encourage users to try interacting
|
||||||
|
.mx_MFileBody_download {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> hr {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid $input-border-color;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .mx_ForwardList {
|
||||||
|
display: contents;
|
||||||
|
|
||||||
|
.mx_SearchBox {
|
||||||
|
// To match the space around the title
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_content {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_noResults {
|
||||||
|
display: block;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_results {
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_entry {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 32px;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $groupFilterPanel-bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_roomButton {
|
||||||
|
display: flex;
|
||||||
|
margin-right: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.mx_DecoratedRoomAvatar {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_entry_name {
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: 30px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_sendButton {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:not(.mx_ForwardList_canSend) .mx_ForwardList_sendLabel {
|
||||||
|
// Hide the "Send" label while preserving button size
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ForwardList_sendIcon, .mx_NotificationBadge {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_NotificationBadge {
|
||||||
|
// Match the failed to send indicator's color with the disabled button
|
||||||
|
background-color: $button-danger-disabled-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_ForwardList_sending .mx_ForwardList_sendIcon {
|
||||||
|
background-color: $button-primary-bg-color;
|
||||||
|
mask-image: url('$(res)/img/element-icons/circle-sending.svg');
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_ForwardList_sent .mx_ForwardList_sendIcon {
|
||||||
|
background-color: $button-primary-bg-color;
|
||||||
|
mask-image: url('$(res)/img/element-icons/circle-sent.svg');
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,7 +50,8 @@ limitations under the License.
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
display: inherit;
|
display: inherit;
|
||||||
}
|
}
|
||||||
.mx_ShareDialog_matrixto_copy > div {
|
.mx_ShareDialog_matrixto_copy::after {
|
||||||
|
content: "";
|
||||||
mask-image: url($copy-button-url);
|
mask-image: url($copy-button-url);
|
||||||
background-color: $message-action-bar-fg-color;
|
background-color: $message-action-bar-fg-color;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
|
|
|
@ -22,6 +22,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ImageView_image_wrapper {
|
.mx_ImageView_image_wrapper {
|
||||||
|
pointer-events: initial;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -30,7 +31,6 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ImageView_image {
|
.mx_ImageView_image {
|
||||||
pointer-events: all;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ImageView_info_wrapper {
|
.mx_ImageView_info_wrapper {
|
||||||
pointer-events: all;
|
pointer-events: initial;
|
||||||
padding-left: 32px;
|
padding-left: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -63,7 +63,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_ImageView_toolbar {
|
.mx_ImageView_toolbar {
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
pointer-events: all;
|
pointer-events: initial;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_SenderProfile_name {
|
.mx_SenderProfile_displayName {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SenderProfile_mxid {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-left: 5px;
|
||||||
|
opacity: 0.5; // Match mx_TextualEvent
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
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_PinnedMessagesCard {
|
||||||
|
padding-top: 0;
|
||||||
|
|
||||||
|
.mx_BaseCard_header {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0;
|
||||||
|
border-bottom: 1px solid $menu-border-color;
|
||||||
|
|
||||||
|
> h2 {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-18px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_BaseCard_close {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_PinnedMessagesCard_empty {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
height: max-content;
|
||||||
|
text-align: center;
|
||||||
|
margin: auto 40px;
|
||||||
|
|
||||||
|
.mx_PinnedMessagesCard_MessageActionBar {
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
height: 32px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: $primary-bg-color;
|
||||||
|
border: 1px solid $input-border-color;
|
||||||
|
padding: 1px;
|
||||||
|
width: max-content;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.mx_MessageActionBar_maskButton {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageActionBar_optionsButton {
|
||||||
|
background: $roomlist-button-bg-color;
|
||||||
|
border-radius: 6px;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background-color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> h2 {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$left-gutter: 64px;
|
$left-gutter: 64px;
|
||||||
|
$hover-select-border: 4px;
|
||||||
|
|
||||||
.mx_EventTile {
|
.mx_EventTile {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
@ -85,12 +86,11 @@ $left-gutter: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_isEditing .mx_MessageTimestamp {
|
.mx_EventTile_isEditing .mx_MessageTimestamp {
|
||||||
visibility: hidden !important;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile .mx_MessageTimestamp {
|
.mx_EventTile .mx_MessageTimestamp {
|
||||||
display: block;
|
display: block;
|
||||||
visibility: hidden;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -142,27 +142,8 @@ $left-gutter: 64px;
|
||||||
line-height: 57px !important;
|
line-height: 57px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessagePanel_alwaysShowTimestamps .mx_MessageTimestamp {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
|
.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
|
||||||
left: 3px;
|
left: calc(-$hover-select-border);
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
|
||||||
// The first set is to handle the 'group layout' (default) and the second for the IRC layout
|
|
||||||
.mx_EventTile_last > div > a > .mx_MessageTimestamp,
|
|
||||||
.mx_EventTile:hover > div > a > .mx_MessageTimestamp,
|
|
||||||
.mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp,
|
|
||||||
.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp,
|
|
||||||
.mx_IRCLayout .mx_EventTile_last > a > .mx_MessageTimestamp,
|
|
||||||
.mx_IRCLayout .mx_EventTile:hover > a > .mx_MessageTimestamp,
|
|
||||||
.mx_IRCLayout .mx_ReplyThread .mx_EventTile > a > .mx_MessageTimestamp,
|
|
||||||
.mx_IRCLayout .mx_EventTile.mx_EventTile_actionBarFocused > a > .mx_MessageTimestamp,
|
|
||||||
.mx_IRCLayout .mx_EventTile.focus-visible:focus-within > a > .mx_MessageTimestamp {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile:hover .mx_MessageActionBar,
|
.mx_EventTile:hover .mx_MessageActionBar,
|
||||||
|
@ -177,7 +158,7 @@ $left-gutter: 64px;
|
||||||
*/
|
*/
|
||||||
.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: 60px;
|
padding-left: calc($left-gutter - $hover-select-border);
|
||||||
background-color: $event-selected-color;
|
background-color: $event-selected-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,8 +171,12 @@ $left-gutter: 64px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_info .mx_EventTile_line {
|
||||||
|
padding-left: calc($left-gutter + 18px);
|
||||||
|
}
|
||||||
|
|
||||||
.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
|
.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
|
||||||
padding-left: 78px;
|
padding-left: calc($left-gutter + 18px - $hover-select-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile:hover .mx_EventTile_line,
|
.mx_EventTile:hover .mx_EventTile_line,
|
||||||
|
@ -427,7 +412,7 @@ $left-gutter: 64px;
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line,
|
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line,
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line,
|
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line,
|
||||||
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
|
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
|
||||||
padding-left: 60px;
|
padding-left: calc($left-gutter - $hover-select-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line {
|
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line {
|
||||||
|
@ -445,7 +430,7 @@ $left-gutter: 64px;
|
||||||
.mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
|
.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_unverified.mx_EventTile_info .mx_EventTile_line,
|
||||||
.mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
|
.mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
|
||||||
padding-left: 78px;
|
padding-left: calc($left-gutter + 18px - $hover-select-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* End to end encryption stuff */
|
/* End to end encryption stuff */
|
||||||
|
@ -457,7 +442,7 @@ $left-gutter: 64px;
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
.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_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
||||||
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
|
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
|
||||||
width: $MessageTimestamp_width_hover;
|
left: calc(-$hover-select-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
||||||
|
|
|
@ -24,10 +24,6 @@ $left-gutter: 64px;
|
||||||
margin-left: $left-gutter;
|
margin-left: $left-gutter;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .mx_EventTile_line {
|
|
||||||
padding-left: $left-gutter;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .mx_EventTile_avatar {
|
> .mx_EventTile_avatar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
@ -43,10 +39,6 @@ $left-gutter: 64px;
|
||||||
line-height: $font-22px;
|
line-height: $font-22px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_info .mx_EventTile_line {
|
|
||||||
padding-left: calc($left-gutter + 18px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Compact layout overrides */
|
/* Compact layout overrides */
|
||||||
|
|
|
@ -115,8 +115,7 @@ $irc-line-height: $font-18px;
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line {
|
||||||
.mx_EventTile_e2eIcon,
|
.mx_EventTile_e2eIcon,
|
||||||
.mx_TextualEvent,
|
.mx_TextualEvent,
|
||||||
.mx_MTextBody,
|
.mx_MTextBody {
|
||||||
.mx_ReplyThread_wrapper_empty {
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,16 +176,13 @@ $irc-line-height: $font-18px;
|
||||||
.mx_SenderProfile_hover {
|
.mx_SenderProfile_hover {
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
> span {
|
> .mx_SenderProfile_name {
|
||||||
display: flex;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
> .mx_SenderProfile_name {
|
min-width: var(--name-width);
|
||||||
overflow: hidden;
|
text-align: end;
|
||||||
text-overflow: ellipsis;
|
|
||||||
min-width: var(--name-width);
|
|
||||||
text-align: end;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_JumpToBottomButton_scrollDown {
|
.mx_JumpToBottomButton_scrollDown {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: block;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
border-radius: 19px;
|
border-radius: 19px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
|
@ -16,62 +16,91 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_PinnedEventTile {
|
.mx_PinnedEventTile {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
margin-bottom: 5px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 5px; // for the hover
|
padding: 0 4px 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventTile:hover {
|
display: grid;
|
||||||
background-color: $event-selected-color;
|
grid-template-areas:
|
||||||
}
|
"avatar name remove"
|
||||||
|
"content content content"
|
||||||
|
"footer footer footer";
|
||||||
|
grid-template-rows: max-content auto max-content;
|
||||||
|
grid-template-columns: 24px auto 24px;
|
||||||
|
grid-row-gap: 12px;
|
||||||
|
grid-column-gap: 8px;
|
||||||
|
|
||||||
.mx_PinnedEventTile .mx_PinnedEventTile_sender,
|
& + .mx_PinnedEventTile {
|
||||||
.mx_PinnedEventTile .mx_PinnedEventTile_timestamp {
|
padding: 12px 4px;
|
||||||
color: #868686;
|
border-top: 1px solid $menu-border-color;
|
||||||
font-size: 0.8em;
|
}
|
||||||
vertical-align: top;
|
|
||||||
display: inline-block;
|
|
||||||
padding-bottom: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventTile .mx_PinnedEventTile_timestamp {
|
.mx_PinnedEventTile_senderAvatar {
|
||||||
padding-left: 15px;
|
grid-area: avatar;
|
||||||
display: none;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventTile .mx_PinnedEventTile_senderAvatar .mx_BaseAvatar {
|
.mx_PinnedEventTile_sender {
|
||||||
float: left;
|
grid-area: name;
|
||||||
margin-right: 10px;
|
font-weight: $font-semi-bold;
|
||||||
}
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_PinnedEventTile_actions {
|
.mx_PinnedEventTile_unpinButton {
|
||||||
float: right;
|
visibility: hidden;
|
||||||
margin-right: 10px;
|
grid-area: remove;
|
||||||
display: none;
|
position: relative;
|
||||||
}
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
.mx_PinnedEventTile:hover .mx_PinnedEventTile_timestamp {
|
&:hover {
|
||||||
display: inline-block;
|
background-color: $roomheader-addroom-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_PinnedEventTile:hover .mx_PinnedEventTile_actions {
|
&::before {
|
||||||
display: block;
|
content: "";
|
||||||
}
|
position: absolute;
|
||||||
|
//top: 0;
|
||||||
|
//left: 0;
|
||||||
|
height: inherit;
|
||||||
|
width: inherit;
|
||||||
|
background: $secondary-fg-color;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: 8px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-image: url('$(res)/img/image-view/close.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_PinnedEventTile_unpinButton {
|
.mx_PinnedEventTile_message {
|
||||||
display: inline-block;
|
grid-area: content;
|
||||||
cursor: pointer;
|
}
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventTile_gotoButton {
|
.mx_PinnedEventTile_footer {
|
||||||
display: inline-block;
|
grid-area: footer;
|
||||||
font-size: 0.7em; // Smaller text to avoid conflicting with the layout
|
font-size: 10px;
|
||||||
}
|
line-height: 12px;
|
||||||
|
|
||||||
.mx_PinnedEventTile_message {
|
.mx_PinnedEventTile_timestamp {
|
||||||
margin-left: 50px;
|
font-size: inherit;
|
||||||
position: relative;
|
line-height: inherit;
|
||||||
top: 0;
|
color: $secondary-fg-color;
|
||||||
left: 0;
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_link {
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 12px;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.mx_PinnedEventTile_unpinButton {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,14 +32,14 @@ limitations under the License.
|
||||||
// first triggering the enter state with the newest breadcrumb off screen (-40px) then
|
// first triggering the enter state with the newest breadcrumb off screen (-40px) then
|
||||||
// sliding it into view.
|
// sliding it into view.
|
||||||
&.mx_RoomBreadcrumbs-enter {
|
&.mx_RoomBreadcrumbs-enter {
|
||||||
margin-left: -40px; // 32px for the avatar, 8px for the margin
|
transform: translateX(-40px); // 32px for the avatar, 8px for the margin
|
||||||
}
|
}
|
||||||
&.mx_RoomBreadcrumbs-enter-active {
|
&.mx_RoomBreadcrumbs-enter-active {
|
||||||
margin-left: 0;
|
transform: translateX(0);
|
||||||
|
|
||||||
// Timing function is as-requested by design.
|
// Timing function is as-requested by design.
|
||||||
// NOTE: The transition time MUST match the value passed to CSSTransition!
|
// NOTE: The transition time MUST match the value passed to CSSTransition!
|
||||||
transition: margin-left 640ms cubic-bezier(0.66, 0.02, 0.36, 1);
|
transition: transform 640ms cubic-bezier(0.66, 0.02, 0.36, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomBreadcrumbs_placeholder {
|
.mx_RoomBreadcrumbs_placeholder {
|
||||||
|
|
|
@ -277,24 +277,6 @@ limitations under the License.
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomHeader_pinnedButton::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/room/pin.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_pinsIndicator {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 4px;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: $pinned-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_pinsIndicatorUnread {
|
|
||||||
background-color: $pinned-unread-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 480px) {
|
@media only screen and (max-width: 480px) {
|
||||||
.mx_RoomHeader_wrapper {
|
.mx_RoomHeader_wrapper {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -198,6 +198,7 @@ limitations under the License.
|
||||||
// as the box model should be top aligned. Happens in both FF and Chromium
|
// as the box model should be top aligned. Happens in both FF and Chromium
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-self: stretch;
|
||||||
|
|
||||||
mask-image: linear-gradient(0deg, transparent, black 4px);
|
mask-image: linear-gradient(0deg, transparent, black 4px);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,10 @@ limitations under the License.
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
|
||||||
|
contain: content; // Not strict as it will break when resizing a sublist vertically
|
||||||
|
height: 40px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
// The tile is also a flexbox row itself
|
// The tile is also a flexbox row itself
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M18.5151 20.0831L15.6941 17.2621L17.2621 15.6941L20.0831 18.5151C21.5741 20.0061 22.1529 21.7793 21.9661 21.9661C21.7793 22.1529 20.0061 21.5741 18.5151 20.0831Z" fill="black"/>
|
<path d="M18.5151 20.0831L15.6941 17.2621L17.2621 15.6941L20.0831 18.5151C21.5741 20.0061 22.1529 21.7793 21.9661 21.9661C21.7793 22.1529 20.0061 21.5741 18.5151 20.0831Z" fill="#737D8C"/>
|
||||||
<path d="M7.46196 11.3821C7.07677 11.5059 5.49073 12.0989 3.63366 12.0744C1.77658 12.0499 1.67795 10.8941 2.46811 10.1039L6.28598 6.28602L9.42196 9.42203L7.46196 11.3821Z" fill="black"/>
|
<path d="M7.46196 11.3821C7.07677 11.5059 5.49073 12.0989 3.63366 12.0744C1.77658 12.0499 1.67795 10.8941 2.46811 10.1039L6.28598 6.28602L9.42196 9.42203L7.46196 11.3821Z" fill="#737D8C"/>
|
||||||
<path d="M11.3821 7.46202C11.5059 7.07682 12.0989 5.49077 12.0744 3.63368C12.0499 1.77658 10.8941 1.67795 10.1039 2.46812L6.28598 6.28602L9.42196 9.42203L11.3821 7.46202Z" fill="black"/>
|
<path d="M11.3821 7.46202C11.5059 7.07682 12.0989 5.49077 12.0744 3.63368C12.0499 1.77658 10.8941 1.67795 10.1039 2.46812L6.28598 6.28602L9.42196 9.42203L11.3821 7.46202Z" fill="#737D8C"/>
|
||||||
<path d="M7.40596 11.438L11.4379 7.40602L14.9099 10.206L10.2059 14.9101L7.40596 11.438Z" fill="black"/>
|
<path d="M7.40596 11.438L11.4379 7.40602L14.9099 10.206L10.2059 14.9101L7.40596 11.438Z" fill="#737D8C"/>
|
||||||
<path d="M11.774 11.774C9.31114 14.2369 8.61779 17.7115 9.83827 20.3213C10.3104 21.3308 11.6288 21.3273 12.4169 20.5392L20.5391 12.4169C21.3271 11.6289 21.3307 10.3104 20.3212 9.83829C17.7114 8.61779 14.2369 9.31115 11.774 11.774Z" fill="black"/>
|
<path d="M11.774 11.774C9.31114 14.2369 8.61779 17.7115 9.83827 20.3213C10.3104 21.3308 11.6288 21.3273 12.4169 20.5392L20.5391 12.4169C21.3271 11.6289 21.3307 10.3104 20.3212 9.83829C17.7114 8.61779 14.2369 9.31115 11.774 11.774Z" fill="#737D8C"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 1015 B After Width: | Height: | Size: 1.0 KiB |
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,52 +14,61 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { ComponentType } from "react";
|
||||||
|
|
||||||
import * as sdk from './index';
|
import * as sdk from './index';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
import { IDialogProps } from "./components/views/dialogs/IDialogProps";
|
||||||
|
|
||||||
|
type AsyncImport<T> = { default: T };
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
// A promise which resolves with the real component
|
||||||
|
prom: Promise<ComponentType | AsyncImport<ComponentType>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
component?: ComponentType;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrap an asynchronous loader function with a react component which shows a
|
* Wrap an asynchronous loader function with a react component which shows a
|
||||||
* spinner until the real component loads.
|
* spinner until the real component loads.
|
||||||
*/
|
*/
|
||||||
export default class AsyncWrapper extends React.Component {
|
export default class AsyncWrapper extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private unmounted = false;
|
||||||
/** A promise which resolves with the real component
|
|
||||||
*/
|
|
||||||
prom: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
public state = {
|
||||||
component: null,
|
component: null,
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._unmounted = false;
|
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
// https://github.com/vector-im/element-web/issues/3148
|
// https://github.com/vector-im/element-web/issues/3148
|
||||||
console.log('Starting load of AsyncWrapper for modal');
|
console.log('Starting load of AsyncWrapper for modal');
|
||||||
this.props.prom.then((result) => {
|
this.props.prom.then((result) => {
|
||||||
if (this._unmounted) {
|
if (this.unmounted) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Take the 'default' member if it's there, then we support
|
// Take the 'default' member if it's there, then we support
|
||||||
// passing in just an import()ed module, since ES6 async import
|
// passing in just an import()ed module, since ES6 async import
|
||||||
// always returns a module *namespace*.
|
// always returns a module *namespace*.
|
||||||
const component = result.default ? result.default : result;
|
const component = (result as AsyncImport<ComponentType>).default
|
||||||
this.setState({component});
|
? (result as AsyncImport<ComponentType>).default
|
||||||
|
: result as ComponentType;
|
||||||
|
this.setState({ component });
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
console.warn('AsyncWrapper promise failed', e);
|
console.warn('AsyncWrapper promise failed', e);
|
||||||
this.setState({error: e});
|
this.setState({ error: e });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._unmounted = true;
|
this.unmounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWrapperCancelClick = () => {
|
private onWrapperCancelClick = () => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -71,12 +79,10 @@ export default class AsyncWrapper extends React.Component {
|
||||||
} else if (this.state.error) {
|
} else if (this.state.error) {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return <BaseDialog onFinished={this.props.onFinished}
|
return <BaseDialog onFinished={this.props.onFinished} title={_t("Error")}>
|
||||||
title={_t("Error")}
|
{ _t("Unable to load! Check your network connectivity and try again.") }
|
||||||
>
|
|
||||||
{_t("Unable to load! Check your network connectivity and try again.")}
|
|
||||||
<DialogButtons primaryButton={_t("Dismiss")}
|
<DialogButtons primaryButton={_t("Dismiss")}
|
||||||
onPrimaryButtonClick={this._onWrapperCancelClick}
|
onPrimaryButtonClick={this.onWrapperCancelClick}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
/>
|
/>
|
||||||
</BaseDialog>;
|
</BaseDialog>;
|
|
@ -264,7 +264,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSupportsVirtualRooms() {
|
public getSupportsVirtualRooms() {
|
||||||
return this.supportsPstnProtocol;
|
return this.supportsSipNativeVirtual;
|
||||||
}
|
}
|
||||||
|
|
||||||
public pstnLookup(phoneNumber: string): Promise<ThirdpartyLookupResponse[]> {
|
public pstnLookup(phoneNumber: string): Promise<ThirdpartyLookupResponse[]> {
|
||||||
|
@ -521,7 +521,9 @@ export default class CallHandler extends EventEmitter {
|
||||||
let newNativeAssertedIdentity = newAssertedIdentity;
|
let newNativeAssertedIdentity = newAssertedIdentity;
|
||||||
if (newAssertedIdentity) {
|
if (newAssertedIdentity) {
|
||||||
const response = await this.sipNativeLookup(newAssertedIdentity);
|
const response = await this.sipNativeLookup(newAssertedIdentity);
|
||||||
if (response.length) newNativeAssertedIdentity = response[0].userid;
|
if (response.length && response[0].fields.lookup_success) {
|
||||||
|
newNativeAssertedIdentity = response[0].userid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
console.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`);
|
console.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`);
|
||||||
|
|
||||||
|
@ -540,6 +542,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
if (newMappedRoomId !== mappedRoomId) {
|
if (newMappedRoomId !== mappedRoomId) {
|
||||||
this.removeCallForRoom(mappedRoomId);
|
this.removeCallForRoom(mappedRoomId);
|
||||||
mappedRoomId = newMappedRoomId;
|
mappedRoomId = newMappedRoomId;
|
||||||
|
console.log("Moving call to room " + mappedRoomId);
|
||||||
this.calls.set(mappedRoomId, call);
|
this.calls.set(mappedRoomId, call);
|
||||||
this.emit(CallHandlerEvent.CallChangeRoom, call);
|
this.emit(CallHandlerEvent.CallChangeRoom, call);
|
||||||
}
|
}
|
||||||
|
@ -605,6 +608,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeCallForRoom(roomId: string) {
|
private removeCallForRoom(roomId: string) {
|
||||||
|
console.log("Removing call for room ", roomId);
|
||||||
this.calls.delete(roomId);
|
this.calls.delete(roomId);
|
||||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||||
}
|
}
|
||||||
|
@ -678,6 +682,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
|
console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
|
||||||
const call = MatrixClientPeg.get().createCall(mappedRoomId);
|
const call = MatrixClientPeg.get().createCall(mappedRoomId);
|
||||||
|
|
||||||
|
console.log("Adding call for room ", roomId);
|
||||||
this.calls.set(roomId, call);
|
this.calls.set(roomId, call);
|
||||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||||
if (transferee) {
|
if (transferee) {
|
||||||
|
@ -810,6 +815,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
||||||
|
console.log("Adding call for room ", mappedRoomId);
|
||||||
this.calls.set(mappedRoomId, call)
|
this.calls.set(mappedRoomId, call)
|
||||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||||
this.setCallListeners(call);
|
this.setCallListeners(call);
|
||||||
|
@ -862,9 +868,43 @@ export default class CallHandler extends EventEmitter {
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case Action.DialNumber:
|
||||||
|
this.dialNumber(payload.number);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async dialNumber(number: string) {
|
||||||
|
const results = await this.pstnLookup(number);
|
||||||
|
if (!results || results.length === 0 || !results[0].userid) {
|
||||||
|
Modal.createTrackedDialog('', '', ErrorDialog, {
|
||||||
|
title: _t("Unable to look up phone number"),
|
||||||
|
description: _t("There was an error looking up the phone number"),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const userId = results[0].userid;
|
||||||
|
|
||||||
|
// Now check to see if this is a virtual user, in which case we should find the
|
||||||
|
// native user
|
||||||
|
let nativeUserId;
|
||||||
|
if (this.getSupportsVirtualRooms()) {
|
||||||
|
const nativeLookupResults = await this.sipNativeLookup(userId);
|
||||||
|
const lookupSuccess = nativeLookupResults.length > 0 && nativeLookupResults[0].fields.lookup_success;
|
||||||
|
nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId;
|
||||||
|
console.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId);
|
||||||
|
} else {
|
||||||
|
nativeUserId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomId = await ensureDMExists(MatrixClientPeg.get(), nativeUserId);
|
||||||
|
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setActiveCallRoomId(activeCallRoomId: string) {
|
setActiveCallRoomId(activeCallRoomId: string) {
|
||||||
logger.info("Setting call in room " + activeCallRoomId + " active");
|
logger.info("Setting call in room " + activeCallRoomId + " active");
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,6 @@ 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";
|
||||||
|
|
||||||
// Polyfill for Canvas.toBlob API using Canvas.toDataURL
|
|
||||||
import "blueimp-canvas-to-blob";
|
|
||||||
import { Action } from "./dispatcher/actions";
|
import { Action } from "./dispatcher/actions";
|
||||||
import CountlyAnalytics from "./CountlyAnalytics";
|
import CountlyAnalytics from "./CountlyAnalytics";
|
||||||
import {
|
import {
|
||||||
|
@ -40,6 +38,7 @@ 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";
|
||||||
|
|
||||||
const MAX_WIDTH = 800;
|
const MAX_WIDTH = 800;
|
||||||
const MAX_HEIGHT = 600;
|
const MAX_HEIGHT = 600;
|
||||||
|
@ -208,12 +207,12 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let imageInfo;
|
let imageInfo;
|
||||||
return loadImageElement(imageFile).then(function(r) {
|
return loadImageElement(imageFile).then((r) => {
|
||||||
return createThumbnail(r.img, r.width, r.height, thumbnailType);
|
return createThumbnail(r.img, r.width, r.height, thumbnailType);
|
||||||
}).then(function(result) {
|
}).then((result) => {
|
||||||
imageInfo = result.info;
|
imageInfo = result.info;
|
||||||
return uploadFile(matrixClient, roomId, result.thumbnail);
|
return uploadFile(matrixClient, roomId, result.thumbnail);
|
||||||
}).then(function(result) {
|
}).then((result) => {
|
||||||
imageInfo.thumbnail_url = result.url;
|
imageInfo.thumbnail_url = result.url;
|
||||||
imageInfo.thumbnail_file = result.file;
|
imageInfo.thumbnail_file = result.file;
|
||||||
return imageInfo;
|
return imageInfo;
|
||||||
|
@ -264,12 +263,12 @@ function infoForVideoFile(matrixClient, roomId, videoFile) {
|
||||||
const thumbnailType = "image/jpeg";
|
const thumbnailType = "image/jpeg";
|
||||||
|
|
||||||
let videoInfo;
|
let videoInfo;
|
||||||
return loadVideoElement(videoFile).then(function(video) {
|
return loadVideoElement(videoFile).then((video) => {
|
||||||
return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
|
return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
|
||||||
}).then(function(result) {
|
}).then((result) => {
|
||||||
videoInfo = result.info;
|
videoInfo = result.info;
|
||||||
return uploadFile(matrixClient, roomId, result.thumbnail);
|
return uploadFile(matrixClient, roomId, result.thumbnail);
|
||||||
}).then(function(result) {
|
}).then((result) => {
|
||||||
videoInfo.thumbnail_url = result.url;
|
videoInfo.thumbnail_url = result.url;
|
||||||
videoInfo.thumbnail_file = result.file;
|
videoInfo.thumbnail_file = result.file;
|
||||||
return videoInfo;
|
return videoInfo;
|
||||||
|
@ -308,7 +307,12 @@ function readFileAsArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
|
||||||
* If the file is unencrypted then the object will have a "url" key.
|
* If the file is unencrypted then the object will have a "url" key.
|
||||||
* If the file is encrypted then the object will have a "file" key.
|
* If the file is encrypted then the object will have a "file" key.
|
||||||
*/
|
*/
|
||||||
function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blob, progressHandler?: any) {
|
function uploadFile(
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
roomId: string,
|
||||||
|
file: File | Blob,
|
||||||
|
progressHandler?: any, // TODO: Types
|
||||||
|
): Promise<{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.
|
||||||
|
@ -355,7 +359,7 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo
|
||||||
// If the attachment isn't encrypted then include the URL directly.
|
// If the attachment isn't encrypted then include the URL directly.
|
||||||
return {"url": url};
|
return {"url": url};
|
||||||
});
|
});
|
||||||
promise1.abort = () => {
|
(promise1 as any).abort = () => {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
MatrixClientPeg.get().cancelUpload(basePromise);
|
MatrixClientPeg.get().cancelUpload(basePromise);
|
||||||
};
|
};
|
||||||
|
@ -367,7 +371,7 @@ export default class ContentMessages {
|
||||||
private inprogress: IUpload[] = [];
|
private inprogress: IUpload[] = [];
|
||||||
private mediaConfig: IMediaConfig = null;
|
private mediaConfig: IMediaConfig = null;
|
||||||
|
|
||||||
sendStickerContentToRoom(url: string, roomId: string, info: string, text: string, matrixClient: MatrixClient) {
|
sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) {
|
||||||
const startTime = CountlyAnalytics.getTimestamp();
|
const startTime = CountlyAnalytics.getTimestamp();
|
||||||
const prom = MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => {
|
const prom = MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => {
|
||||||
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
||||||
|
@ -441,7 +445,7 @@ export default class ContentMessages {
|
||||||
let uploadAll = false;
|
let uploadAll = false;
|
||||||
// Promise to complete before sending next file into room, used for synchronisation of file-sending
|
// Promise to complete before sending next file into room, used for synchronisation of file-sending
|
||||||
// to match the order the files were specified in
|
// to match the order the files were specified in
|
||||||
let promBefore = Promise.resolve();
|
let promBefore: Promise<any> = Promise.resolve();
|
||||||
for (let i = 0; i < okFiles.length; ++i) {
|
for (let i = 0; i < okFiles.length; ++i) {
|
||||||
const file = okFiles[i];
|
const file = okFiles[i];
|
||||||
if (!uploadAll) {
|
if (!uploadAll) {
|
||||||
|
|
|
@ -24,13 +24,6 @@ import {sleep} from "./utils/promise";
|
||||||
import RoomViewStore from "./stores/RoomViewStore";
|
import RoomViewStore from "./stores/RoomViewStore";
|
||||||
import { Action } from "./dispatcher/actions";
|
import { Action } from "./dispatcher/actions";
|
||||||
|
|
||||||
// polyfill textencoder if necessary
|
|
||||||
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
|
|
||||||
let TextEncoder = window.TextEncoder;
|
|
||||||
if (!TextEncoder) {
|
|
||||||
TextEncoder = TextEncodingUtf8.TextEncoder;
|
|
||||||
}
|
|
||||||
|
|
||||||
const INACTIVITY_TIME = 20; // seconds
|
const INACTIVITY_TIME = 20; // seconds
|
||||||
const HEARTBEAT_INTERVAL = 5_000; // ms
|
const HEARTBEAT_INTERVAL = 5_000; // ms
|
||||||
const SESSION_UPDATE_INTERVAL = 60; // seconds
|
const SESSION_UPDATE_INTERVAL = 60; // seconds
|
||||||
|
|
|
@ -98,7 +98,7 @@ class Presence {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await MatrixClientPeg.get().setPresence(this.state);
|
await MatrixClientPeg.get().setPresence({presence: this.state});
|
||||||
console.info("Presence:", newState);
|
console.info("Presence:", newState);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to set presence:", err);
|
console.error("Failed to set presence:", err);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,35 +14,37 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||||
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
|
|
||||||
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import dis from './dispatcher/dispatcher';
|
import dis from './dispatcher/dispatcher';
|
||||||
import { EventStatus } from 'matrix-js-sdk/src/models/event';
|
|
||||||
|
|
||||||
export default class Resend {
|
export default class Resend {
|
||||||
static resendUnsentEvents(room) {
|
static resendUnsentEvents(room: Room): Promise<void[]> {
|
||||||
return Promise.all(room.getPendingEvents().filter(function(ev) {
|
return Promise.all(room.getPendingEvents().filter(function(ev: MatrixEvent) {
|
||||||
return ev.status === EventStatus.NOT_SENT;
|
return ev.status === EventStatus.NOT_SENT;
|
||||||
}).map(function(event) {
|
}).map(function(event: MatrixEvent) {
|
||||||
return Resend.resend(event);
|
return Resend.resend(event);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
static cancelUnsentEvents(room) {
|
static cancelUnsentEvents(room: Room): void {
|
||||||
room.getPendingEvents().filter(function(ev) {
|
room.getPendingEvents().filter(function(ev: MatrixEvent) {
|
||||||
return ev.status === EventStatus.NOT_SENT;
|
return ev.status === EventStatus.NOT_SENT;
|
||||||
}).forEach(function(event) {
|
}).forEach(function(event: MatrixEvent) {
|
||||||
Resend.removeFromQueue(event);
|
Resend.removeFromQueue(event);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static resend(event) {
|
static resend(event: MatrixEvent): Promise<void> {
|
||||||
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
||||||
return MatrixClientPeg.get().resendEvent(event, room).then(function(res) {
|
return MatrixClientPeg.get().resendEvent(event, room).then(function(res) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_sent',
|
action: 'message_sent',
|
||||||
event: event,
|
event: event,
|
||||||
});
|
});
|
||||||
}, function(err) {
|
}, function(err: Error) {
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
// https://github.com/vector-im/element-web/issues/3148
|
// https://github.com/vector-im/element-web/issues/3148
|
||||||
console.log('Resend got send failure: ' + err.name + '(' + err + ')');
|
console.log('Resend got send failure: ' + err.name + '(' + err + ')');
|
||||||
|
@ -55,7 +56,7 @@ export default class Resend {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static removeFromQueue(event) {
|
static removeFromQueue(event: MatrixEvent): void {
|
||||||
MatrixClientPeg.get().cancelPendingEvent(event);
|
MatrixClientPeg.get().cancelPendingEvent(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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.
|
||||||
|
@ -24,12 +24,12 @@ limitations under the License.
|
||||||
* A similar thing could also be achieved via `pushState` with a state object,
|
* A similar thing could also be achieved via `pushState` with a state object,
|
||||||
* but keeping it separate like this seems easier in case we do want to extend.
|
* but keeping it separate like this seems easier in case we do want to extend.
|
||||||
*/
|
*/
|
||||||
const aliasToIDMap = new Map();
|
const aliasToIDMap = new Map<string, string>();
|
||||||
|
|
||||||
export function storeRoomAliasInCache(alias, id) {
|
export function storeRoomAliasInCache(alias: string, id: string): void {
|
||||||
aliasToIDMap.set(alias, id);
|
aliasToIDMap.set(alias, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCachedRoomIDForAlias(alias) {
|
export function getCachedRoomIDForAlias(alias: string): string {
|
||||||
return aliasToIDMap.get(alias);
|
return aliasToIDMap.get(alias);
|
||||||
}
|
}
|
|
@ -66,7 +66,7 @@ async function serverSideSearchProcess(term, roomId = undefined) {
|
||||||
highlights: [],
|
highlights: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
return client._processRoomEventsSearch(searchResult, result.response);
|
return client.processRoomEventsSearch(searchResult, result.response);
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareEvents(a, b) {
|
function compareEvents(a, b) {
|
||||||
|
@ -131,7 +131,7 @@ async function combinedSearch(searchTerm) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = client._processRoomEventsSearch(emptyResult, response);
|
const result = client.processRoomEventsSearch(emptyResult, response);
|
||||||
|
|
||||||
// Restore our encryption info so we can properly re-verify the events.
|
// Restore our encryption info so we can properly re-verify the events.
|
||||||
restoreEncryptionInfo(result.results);
|
restoreEncryptionInfo(result.results);
|
||||||
|
@ -185,7 +185,7 @@ async function localSearchProcess(searchTerm, roomId = undefined) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const processedResult = MatrixClientPeg.get()._processRoomEventsSearch(emptyResult, response);
|
const processedResult = MatrixClientPeg.get().processRoomEventsSearch(emptyResult, response);
|
||||||
// Restore our encryption info so we can properly re-verify the events.
|
// Restore our encryption info so we can properly re-verify the events.
|
||||||
restoreEncryptionInfo(processedResult.results);
|
restoreEncryptionInfo(processedResult.results);
|
||||||
|
|
||||||
|
@ -210,7 +210,7 @@ async function localPagination(searchResult) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = MatrixClientPeg.get()._processRoomEventsSearch(searchResult, response);
|
const result = MatrixClientPeg.get().processRoomEventsSearch(searchResult, response);
|
||||||
|
|
||||||
// Restore our encryption info so we can properly re-verify the events.
|
// Restore our encryption info so we can properly re-verify the events.
|
||||||
const newSlice = result.results.slice(Math.max(result.results.length - newResultCount, 0));
|
const newSlice = result.results.slice(Math.max(result.results.length - newResultCount, 0));
|
||||||
|
@ -520,7 +520,7 @@ async function combinedPagination(searchResult) {
|
||||||
const oldResultCount = searchResult.results ? searchResult.results.length : 0;
|
const oldResultCount = searchResult.results ? searchResult.results.length : 0;
|
||||||
|
|
||||||
// Let the client process the combined result.
|
// Let the client process the combined result.
|
||||||
const result = client._processRoomEventsSearch(searchResult, response);
|
const result = client.processRoomEventsSearch(searchResult, response);
|
||||||
|
|
||||||
// Restore our encryption info so we can properly re-verify the events.
|
// Restore our encryption info so we can properly re-verify the events.
|
||||||
const newResultCount = result.results.length - oldResultCount;
|
const newResultCount = result.results.length - oldResultCount;
|
||||||
|
|
|
@ -271,7 +271,7 @@ async function onSecretRequested(
|
||||||
}
|
}
|
||||||
return key && encodeBase64(key);
|
return key && encodeBase64(key);
|
||||||
} else if (name === "m.megolm_backup.v1") {
|
} else if (name === "m.megolm_backup.v1") {
|
||||||
const key = await client._crypto.getSessionBackupPrivateKey();
|
const key = await client.crypto.getSessionBackupPrivateKey();
|
||||||
if (!key) {
|
if (!key) {
|
||||||
console.log(
|
console.log(
|
||||||
`session backup key requested by ${deviceId}, but not found in cache`,
|
`session backup key requested by ${deviceId}, but not found in cache`,
|
||||||
|
|
|
@ -103,7 +103,7 @@ export async function startTermsFlow(
|
||||||
|
|
||||||
// fetch the set of agreed policy URLs from account data
|
// fetch the set of agreed policy URLs from account data
|
||||||
const currentAcceptedTerms = await MatrixClientPeg.get().getAccountData('m.accepted_terms');
|
const currentAcceptedTerms = await MatrixClientPeg.get().getAccountData('m.accepted_terms');
|
||||||
let agreedUrlSet;
|
let agreedUrlSet: Set<string>;
|
||||||
if (!currentAcceptedTerms || !currentAcceptedTerms.getContent() || !currentAcceptedTerms.getContent().accepted) {
|
if (!currentAcceptedTerms || !currentAcceptedTerms.getContent() || !currentAcceptedTerms.getContent().accepted) {
|
||||||
agreedUrlSet = new Set();
|
agreedUrlSet = new Set();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -21,153 +21,161 @@ import SettingsStore from "./settings/SettingsStore";
|
||||||
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
|
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
|
||||||
import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
|
import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
|
||||||
|
|
||||||
function textForMemberEvent(ev) {
|
// These functions are frequently used just to check whether an event has
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
function textForMemberEvent(ev): () => 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();
|
||||||
const prevContent = ev.getPrevContent();
|
const prevContent = ev.getPrevContent();
|
||||||
const content = ev.getContent();
|
const content = ev.getContent();
|
||||||
|
|
||||||
const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : '';
|
const getReason = () => content.reason ? (_t('Reason') + ': ' + content.reason) : '';
|
||||||
switch (content.membership) {
|
switch (content.membership) {
|
||||||
case 'invite': {
|
case 'invite': {
|
||||||
const threePidContent = content.third_party_invite;
|
const threePidContent = content.third_party_invite;
|
||||||
if (threePidContent) {
|
if (threePidContent) {
|
||||||
if (threePidContent.display_name) {
|
if (threePidContent.display_name) {
|
||||||
return _t('%(targetName)s accepted the invitation for %(displayName)s.', {
|
return () => _t('%(targetName)s accepted the invitation for %(displayName)s.', {
|
||||||
targetName,
|
targetName,
|
||||||
displayName: threePidContent.display_name,
|
displayName: threePidContent.display_name,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return _t('%(targetName)s accepted an invitation.', {targetName});
|
return () => _t('%(targetName)s accepted an invitation.', {targetName});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
|
return () => _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'ban':
|
case 'ban':
|
||||||
return _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + reason;
|
return () => _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
|
||||||
case 'join':
|
case 'join':
|
||||||
if (prevContent && prevContent.membership === 'join') {
|
if (prevContent && prevContent.membership === 'join') {
|
||||||
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
||||||
return _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
|
return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
|
||||||
oldDisplayName: prevContent.displayname,
|
oldDisplayName: prevContent.displayname,
|
||||||
displayName: content.displayname,
|
displayName: content.displayname,
|
||||||
});
|
});
|
||||||
} else if (!prevContent.displayname && content.displayname) {
|
} else if (!prevContent.displayname && content.displayname) {
|
||||||
return _t('%(senderName)s set their display name to %(displayName)s.', {
|
return () => _t('%(senderName)s set their display name to %(displayName)s.', {
|
||||||
senderName: ev.getSender(),
|
senderName: ev.getSender(),
|
||||||
displayName: content.displayname,
|
displayName: content.displayname,
|
||||||
});
|
});
|
||||||
} else if (prevContent.displayname && !content.displayname) {
|
} else if (prevContent.displayname && !content.displayname) {
|
||||||
return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
|
return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
|
||||||
senderName,
|
senderName,
|
||||||
oldDisplayName: prevContent.displayname,
|
oldDisplayName: prevContent.displayname,
|
||||||
});
|
});
|
||||||
} else if (prevContent.avatar_url && !content.avatar_url) {
|
} else if (prevContent.avatar_url && !content.avatar_url) {
|
||||||
return _t('%(senderName)s removed their profile picture.', {senderName});
|
return () => _t('%(senderName)s removed their profile picture.', {senderName});
|
||||||
} else if (prevContent.avatar_url && content.avatar_url &&
|
} else if (prevContent.avatar_url && content.avatar_url &&
|
||||||
prevContent.avatar_url !== content.avatar_url) {
|
prevContent.avatar_url !== content.avatar_url) {
|
||||||
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 (SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
||||||
// This is a null rejoin, it will only be visible if the Labs option is enabled
|
// This is a null rejoin, it will only be visible if the Labs option is enabled
|
||||||
return _t("%(senderName)s made no change.", {senderName});
|
return () => _t("%(senderName)s made no change.", {senderName});
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
||||||
return _t('%(targetName)s joined the room.', {targetName});
|
return () => _t('%(targetName)s joined the room.', {targetName});
|
||||||
}
|
}
|
||||||
case 'leave':
|
case 'leave':
|
||||||
if (ev.getSender() === ev.getStateKey()) {
|
if (ev.getSender() === ev.getStateKey()) {
|
||||||
if (prevContent.membership === "invite") {
|
if (prevContent.membership === "invite") {
|
||||||
return _t('%(targetName)s rejected the invitation.', {targetName});
|
return () => _t('%(targetName)s rejected the invitation.', {targetName});
|
||||||
} else {
|
} else {
|
||||||
return _t('%(targetName)s left the room.', {targetName});
|
return () => _t('%(targetName)s left the room.', {targetName});
|
||||||
}
|
}
|
||||||
} else if (prevContent.membership === "ban") {
|
} else if (prevContent.membership === "ban") {
|
||||||
return _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
|
return () => _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
|
||||||
} else if (prevContent.membership === "invite") {
|
} else if (prevContent.membership === "invite") {
|
||||||
return _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
|
return () => _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
|
||||||
senderName,
|
senderName,
|
||||||
targetName,
|
targetName,
|
||||||
}) + ' ' + reason;
|
}) + ' ' + getReason();
|
||||||
} else if (prevContent.membership === "join") {
|
} else if (prevContent.membership === "join") {
|
||||||
return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason;
|
return () => _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForTopicEvent(ev) {
|
function textForTopicEvent(ev): () => 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,
|
||||||
topic: ev.getContent().topic,
|
topic: ev.getContent().topic,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForRoomNameEvent(ev) {
|
function textForRoomNameEvent(ev): () => 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) {
|
||||||
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
|
return () => _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
|
||||||
}
|
}
|
||||||
if (ev.getPrevContent().name) {
|
if (ev.getPrevContent().name) {
|
||||||
return _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', {
|
return () => _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', {
|
||||||
senderDisplayName,
|
senderDisplayName,
|
||||||
oldRoomName: ev.getPrevContent().name,
|
oldRoomName: ev.getPrevContent().name,
|
||||||
newRoomName: ev.getContent().name,
|
newRoomName: ev.getContent().name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
|
return () => _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
|
||||||
senderDisplayName,
|
senderDisplayName,
|
||||||
roomName: ev.getContent().name,
|
roomName: ev.getContent().name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForTombstoneEvent(ev) {
|
function textForTombstoneEvent(ev): () => 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) {
|
function textForJoinRulesEvent(ev): () => 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":
|
||||||
return _t('%(senderDisplayName)s made the room public to whoever knows the link.', {senderDisplayName});
|
return () => _t('%(senderDisplayName)s made the room public to whoever knows the link.', {
|
||||||
|
senderDisplayName,
|
||||||
|
});
|
||||||
case "invite":
|
case "invite":
|
||||||
return _t('%(senderDisplayName)s made the room invite only.', {senderDisplayName});
|
return () => _t('%(senderDisplayName)s made the room invite only.', {
|
||||||
|
senderDisplayName,
|
||||||
|
});
|
||||||
default:
|
default:
|
||||||
// The spec supports "knock" and "private", however nothing implements these.
|
// The spec supports "knock" and "private", however nothing implements these.
|
||||||
return _t('%(senderDisplayName)s changed the join rule to %(rule)s', {
|
return () => _t('%(senderDisplayName)s changed the join rule to %(rule)s', {
|
||||||
senderDisplayName,
|
senderDisplayName,
|
||||||
rule: ev.getContent().join_rule,
|
rule: ev.getContent().join_rule,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForGuestAccessEvent(ev) {
|
function textForGuestAccessEvent(ev): () => 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":
|
||||||
return _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName});
|
return () => _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName});
|
||||||
case "forbidden":
|
case "forbidden":
|
||||||
return _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName});
|
return () => _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName});
|
||||||
default:
|
default:
|
||||||
// There's no other options we can expect, however just for safety's sake we'll do this.
|
// There's no other options we can expect, however just for safety's sake we'll do this.
|
||||||
return _t('%(senderDisplayName)s changed guest access to %(rule)s', {
|
return () => _t('%(senderDisplayName)s changed guest access to %(rule)s', {
|
||||||
senderDisplayName,
|
senderDisplayName,
|
||||||
rule: ev.getContent().guest_access,
|
rule: ev.getContent().guest_access,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForRelatedGroupsEvent(ev) {
|
function textForRelatedGroupsEvent(ev): () => 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 || [];
|
||||||
|
@ -175,17 +183,17 @@ function textForRelatedGroupsEvent(ev) {
|
||||||
const removed = prevGroups.filter((g) => !groups.includes(g));
|
const removed = prevGroups.filter((g) => !groups.includes(g));
|
||||||
|
|
||||||
if (added.length && !removed.length) {
|
if (added.length && !removed.length) {
|
||||||
return _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', {
|
return () => _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', {
|
||||||
senderDisplayName,
|
senderDisplayName,
|
||||||
groups: added.join(', '),
|
groups: added.join(', '),
|
||||||
});
|
});
|
||||||
} else if (!added.length && removed.length) {
|
} else if (!added.length && removed.length) {
|
||||||
return _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', {
|
return () => _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', {
|
||||||
senderDisplayName,
|
senderDisplayName,
|
||||||
groups: removed.join(', '),
|
groups: removed.join(', '),
|
||||||
});
|
});
|
||||||
} else if (added.length && removed.length) {
|
} else if (added.length && removed.length) {
|
||||||
return _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' +
|
return () => _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' +
|
||||||
'%(oldGroups)s in this room.', {
|
'%(oldGroups)s in this room.', {
|
||||||
senderDisplayName,
|
senderDisplayName,
|
||||||
newGroups: added.join(', '),
|
newGroups: added.join(', '),
|
||||||
|
@ -193,11 +201,11 @@ function textForRelatedGroupsEvent(ev) {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Don't bother rendering this change (because there were no changes)
|
// Don't bother rendering this change (because there were no changes)
|
||||||
return '';
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForServerACLEvent(ev) {
|
function textForServerACLEvent(ev): () => 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();
|
||||||
|
@ -207,11 +215,11 @@ function textForServerACLEvent(ev) {
|
||||||
allow_ip_literals: !(prevContent.allow_ip_literals === false),
|
allow_ip_literals: !(prevContent.allow_ip_literals === false),
|
||||||
};
|
};
|
||||||
|
|
||||||
let text = "";
|
let getText = null;
|
||||||
if (prev.deny.length === 0 && prev.allow.length === 0) {
|
if (prev.deny.length === 0 && prev.allow.length === 0) {
|
||||||
text = _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName});
|
getText = () => _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName});
|
||||||
} else {
|
} else {
|
||||||
text = _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName});
|
getText = () => _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(current.allow)) {
|
if (!Array.isArray(current.allow)) {
|
||||||
|
@ -220,24 +228,27 @@ function textForServerACLEvent(ev) {
|
||||||
|
|
||||||
// If we know for sure everyone is banned, mark the room as obliterated
|
// If we know for sure everyone is banned, mark the room as obliterated
|
||||||
if (current.allow.length === 0) {
|
if (current.allow.length === 0) {
|
||||||
return text + " " + _t("🎉 All servers are banned from participating! This room can no longer be used.");
|
return () => getText() + " " +
|
||||||
|
_t("🎉 All servers are banned from participating! This room can no longer be used.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return text;
|
return getText;
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForMessageEvent(ev) {
|
function textForMessageEvent(ev): () => string | null {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
return () => {
|
||||||
let message = senderDisplayName + ': ' + ev.getContent().body;
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
if (ev.getContent().msgtype === "m.emote") {
|
let message = senderDisplayName + ': ' + ev.getContent().body;
|
||||||
message = "* " + senderDisplayName + " " + message;
|
if (ev.getContent().msgtype === "m.emote") {
|
||||||
} else if (ev.getContent().msgtype === "m.image") {
|
message = "* " + senderDisplayName + " " + message;
|
||||||
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
} else if (ev.getContent().msgtype === "m.image") {
|
||||||
}
|
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
||||||
return message;
|
}
|
||||||
|
return message;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCanonicalAliasEvent(ev) {
|
function textForCanonicalAliasEvent(ev): () => 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 || [];
|
||||||
|
@ -248,96 +259,100 @@ function textForCanonicalAliasEvent(ev) {
|
||||||
|
|
||||||
if (!removedAltAliases.length && !addedAltAliases.length) {
|
if (!removedAltAliases.length && !addedAltAliases.length) {
|
||||||
if (newAlias) {
|
if (newAlias) {
|
||||||
return _t('%(senderName)s set the main address for this room to %(address)s.', {
|
return () => _t('%(senderName)s set the main address for this room to %(address)s.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
address: ev.getContent().alias,
|
address: ev.getContent().alias,
|
||||||
});
|
});
|
||||||
} else if (oldAlias) {
|
} else if (oldAlias) {
|
||||||
return _t('%(senderName)s removed the main address for this room.', {
|
return () => _t('%(senderName)s removed the main address for this room.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (newAlias === oldAlias) {
|
} else if (newAlias === oldAlias) {
|
||||||
if (addedAltAliases.length && !removedAltAliases.length) {
|
if (addedAltAliases.length && !removedAltAliases.length) {
|
||||||
return _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', {
|
return () => _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
addresses: addedAltAliases.join(", "),
|
addresses: addedAltAliases.join(", "),
|
||||||
count: addedAltAliases.length,
|
count: addedAltAliases.length,
|
||||||
});
|
});
|
||||||
} if (removedAltAliases.length && !addedAltAliases.length) {
|
} if (removedAltAliases.length && !addedAltAliases.length) {
|
||||||
return _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', {
|
return () => _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
addresses: removedAltAliases.join(", "),
|
addresses: removedAltAliases.join(", "),
|
||||||
count: removedAltAliases.length,
|
count: removedAltAliases.length,
|
||||||
});
|
});
|
||||||
} if (removedAltAliases.length && addedAltAliases.length) {
|
} if (removedAltAliases.length && addedAltAliases.length) {
|
||||||
return _t('%(senderName)s changed the alternative addresses for this room.', {
|
return () => _t('%(senderName)s changed the alternative addresses for this room.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// both alias and alt_aliases where modified
|
// both alias and alt_aliases where modified
|
||||||
return _t('%(senderName)s changed the main and alternative addresses for this room.', {
|
return () => _t('%(senderName)s changed the main and alternative addresses for this room.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// in case there is no difference between the two events,
|
// in case there is no difference between the two events,
|
||||||
// say something as we can't simply hide the tile from here
|
// say something as we can't simply hide the tile from here
|
||||||
return _t('%(senderName)s changed the addresses for this room.', {
|
return () => _t('%(senderName)s changed the addresses for this room.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallAnswerEvent(event) {
|
function textForCallAnswerEvent(event): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
return () => {
|
||||||
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
|
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
||||||
|
return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallHangupEvent(event) {
|
function textForCallHangupEvent(event): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
||||||
const eventContent = event.getContent();
|
const eventContent = event.getContent();
|
||||||
let reason = "";
|
let getReason = () => "";
|
||||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
reason = _t('(not supported by this browser)');
|
getReason = () => _t('(not supported by this browser)');
|
||||||
} else if (eventContent.reason) {
|
} else if (eventContent.reason) {
|
||||||
if (eventContent.reason === "ice_failed") {
|
if (eventContent.reason === "ice_failed") {
|
||||||
// We couldn't establish a connection at all
|
// We couldn't establish a connection at all
|
||||||
reason = _t('(could not connect media)');
|
getReason = () => _t('(could not connect media)');
|
||||||
} else if (eventContent.reason === "ice_timeout") {
|
} else if (eventContent.reason === "ice_timeout") {
|
||||||
// We established a connection but it died
|
// We established a connection but it died
|
||||||
reason = _t('(connection failed)');
|
getReason = () => _t('(connection failed)');
|
||||||
} else if (eventContent.reason === "user_media_failed") {
|
} else if (eventContent.reason === "user_media_failed") {
|
||||||
// The other side couldn't open capture devices
|
// The other side couldn't open capture devices
|
||||||
reason = _t("(their device couldn't start the camera / microphone)");
|
getReason = () => _t("(their device couldn't start the camera / microphone)");
|
||||||
} else if (eventContent.reason === "unknown_error") {
|
} else if (eventContent.reason === "unknown_error") {
|
||||||
// An error code the other side doesn't have a way to express
|
// 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,
|
// (as opposed to an error code they gave but we don't know about,
|
||||||
// in which case we show the error code)
|
// in which case we show the error code)
|
||||||
reason = _t("(an error occurred)");
|
getReason = () => _t("(an error occurred)");
|
||||||
} else if (eventContent.reason === "invite_timeout") {
|
} else if (eventContent.reason === "invite_timeout") {
|
||||||
reason = _t('(no answer)');
|
getReason = () => _t('(no answer)');
|
||||||
} else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") {
|
} else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") {
|
||||||
// workaround for https://github.com/vector-im/element-web/issues/5178
|
// workaround for https://github.com/vector-im/element-web/issues/5178
|
||||||
// it seems Android randomly sets a reason of "user hangup" which is
|
// it seems Android randomly sets a reason of "user hangup" which is
|
||||||
// interpreted as an error code :(
|
// interpreted as an error code :(
|
||||||
// https://github.com/vector-im/riot-android/issues/2623
|
// https://github.com/vector-im/riot-android/issues/2623
|
||||||
// Also the correct hangup code as of VoIP v1 (with underscore)
|
// Also the correct hangup code as of VoIP v1 (with underscore)
|
||||||
reason = '';
|
getReason = () => '';
|
||||||
} else {
|
} else {
|
||||||
reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
|
getReason = () => _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return _t('%(senderName)s ended the call.', {senderName}) + ' ' + reason;
|
return () => _t('%(senderName)s ended the call.', {senderName: getSenderName()}) + ' ' + getReason();
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallRejectEvent(event) {
|
function textForCallRejectEvent(event): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
return () => {
|
||||||
return _t('%(senderName)s declined the call.', {senderName});
|
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
|
return _t('%(senderName)s declined the call.', {senderName});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallInviteEvent(event) {
|
function textForCallInviteEvent(event): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
||||||
// FIXME: Find a better way to determine this from the event?
|
// FIXME: Find a better way to determine this from the event?
|
||||||
let isVoice = true;
|
let isVoice = true;
|
||||||
if (event.getContent().offer && event.getContent().offer.sdp &&
|
if (event.getContent().offer && event.getContent().offer.sdp &&
|
||||||
|
@ -350,48 +365,55 @@ function textForCallInviteEvent(event) {
|
||||||
// can have a hard time translating those strings. In an effort to make translations easier
|
// 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.
|
// and more accurate, we break out the string-based variables to a couple booleans.
|
||||||
if (isVoice && isSupported) {
|
if (isVoice && isSupported) {
|
||||||
return _t("%(senderName)s placed a voice call.", {senderName});
|
return () => _t("%(senderName)s placed a voice call.", {
|
||||||
|
senderName: getSenderName(),
|
||||||
|
});
|
||||||
} else if (isVoice && !isSupported) {
|
} else if (isVoice && !isSupported) {
|
||||||
return _t("%(senderName)s placed a voice call. (not supported by this browser)", {senderName});
|
return () => _t("%(senderName)s placed a voice call. (not supported by this browser)", {
|
||||||
|
senderName: getSenderName(),
|
||||||
|
});
|
||||||
} else if (!isVoice && isSupported) {
|
} else if (!isVoice && isSupported) {
|
||||||
return _t("%(senderName)s placed a video call.", {senderName});
|
return () => _t("%(senderName)s placed a video call.", {
|
||||||
|
senderName: getSenderName(),
|
||||||
|
});
|
||||||
} else if (!isVoice && !isSupported) {
|
} else if (!isVoice && !isSupported) {
|
||||||
return _t("%(senderName)s placed a video call. (not supported by this browser)", {senderName});
|
return () => _t("%(senderName)s placed a video call. (not supported by this browser)", {
|
||||||
|
senderName: getSenderName(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForThreePidInviteEvent(event) {
|
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)) {
|
||||||
const targetDisplayName = event.getPrevContent().display_name || _t("Someone");
|
return () => _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', {
|
||||||
return _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', {
|
|
||||||
senderName,
|
senderName,
|
||||||
targetDisplayName,
|
targetDisplayName: event.getPrevContent().display_name || _t("Someone"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
|
return () => _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
|
||||||
senderName,
|
senderName,
|
||||||
targetDisplayName: event.getContent().display_name,
|
targetDisplayName: event.getContent().display_name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForHistoryVisibilityEvent(event) {
|
function textForHistoryVisibilityEvent(event): () => 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':
|
||||||
return _t('%(senderName)s made future room history visible to all room members, '
|
return () => _t('%(senderName)s made future room history visible to all room members, '
|
||||||
+ 'from the point they are invited.', {senderName});
|
+ 'from the point they are invited.', {senderName});
|
||||||
case 'joined':
|
case 'joined':
|
||||||
return _t('%(senderName)s made future room history visible to all room members, '
|
return () => _t('%(senderName)s made future room history visible to all room members, '
|
||||||
+ 'from the point they joined.', {senderName});
|
+ 'from the point they joined.', {senderName});
|
||||||
case 'shared':
|
case 'shared':
|
||||||
return _t('%(senderName)s made future room history visible to all room members.', {senderName});
|
return () => _t('%(senderName)s made future room history visible to all room members.', {senderName});
|
||||||
case 'world_readable':
|
case 'world_readable':
|
||||||
return _t('%(senderName)s made future room history visible to anyone.', {senderName});
|
return () => _t('%(senderName)s made future room history visible to anyone.', {senderName});
|
||||||
default:
|
default:
|
||||||
return _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
|
return () => _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
|
||||||
senderName,
|
senderName,
|
||||||
visibility: event.getContent().history_visibility,
|
visibility: event.getContent().history_visibility,
|
||||||
});
|
});
|
||||||
|
@ -399,11 +421,11 @@ function textForHistoryVisibilityEvent(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
function textForPowerEvent(event): () => 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 '';
|
return null;
|
||||||
}
|
}
|
||||||
const userDefault = event.getContent().users_default || 0;
|
const userDefault = event.getContent().users_default || 0;
|
||||||
// Construct set of userIds
|
// Construct set of userIds
|
||||||
|
@ -418,38 +440,38 @@ function textForPowerEvent(event) {
|
||||||
if (users.indexOf(userId) === -1) users.push(userId);
|
if (users.indexOf(userId) === -1) users.push(userId);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const diff = [];
|
const diffs = [];
|
||||||
// XXX: This is also surely broken for i18n
|
|
||||||
users.forEach((userId) => {
|
users.forEach((userId) => {
|
||||||
// Previous power level
|
// Previous power level
|
||||||
const from = event.getPrevContent().users[userId];
|
const from = event.getPrevContent().users[userId];
|
||||||
// Current power level
|
// Current power level
|
||||||
const to = event.getContent().users[userId];
|
const to = event.getContent().users[userId];
|
||||||
if (to !== from) {
|
if (to !== from) {
|
||||||
diff.push(
|
diffs.push({ userId, from, to });
|
||||||
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
|
|
||||||
userId,
|
|
||||||
fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
|
|
||||||
toPowerLevel: Roles.textualPowerLevel(to, userDefault),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!diff.length) {
|
if (!diffs.length) {
|
||||||
return '';
|
return null;
|
||||||
}
|
}
|
||||||
return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
|
// XXX: This is also surely broken for i18n
|
||||||
|
return () => _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
|
||||||
senderName,
|
senderName,
|
||||||
powerLevelDiffText: diff.join(", "),
|
powerLevelDiffText: diffs.map(diff =>
|
||||||
|
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
|
||||||
|
userId: diff.userId,
|
||||||
|
fromPowerLevel: Roles.textualPowerLevel(diff.from, userDefault),
|
||||||
|
toPowerLevel: Roles.textualPowerLevel(diff.to, userDefault),
|
||||||
|
}),
|
||||||
|
).join(", "),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForPinnedEvent(event) {
|
function textForPinnedEvent(event): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
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) {
|
function textForWidgetEvent(event): () => 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() || {};
|
||||||
|
@ -464,27 +486,27 @@ function textForWidgetEvent(event) {
|
||||||
// equivalent to that condition.
|
// equivalent to that condition.
|
||||||
if (url) {
|
if (url) {
|
||||||
if (prevUrl) {
|
if (prevUrl) {
|
||||||
return _t('%(widgetName)s widget modified by %(senderName)s', {
|
return () => _t('%(widgetName)s widget modified by %(senderName)s', {
|
||||||
widgetName, senderName,
|
widgetName, senderName,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return _t('%(widgetName)s widget added by %(senderName)s', {
|
return () => _t('%(widgetName)s widget added by %(senderName)s', {
|
||||||
widgetName, senderName,
|
widgetName, senderName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return _t('%(widgetName)s widget removed by %(senderName)s', {
|
return () => _t('%(widgetName)s widget removed by %(senderName)s', {
|
||||||
widgetName, senderName,
|
widgetName, senderName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForWidgetLayoutEvent(event) {
|
function textForWidgetLayoutEvent(event): () => 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) {
|
function textForMjolnirEvent(event): () => 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();
|
||||||
|
@ -492,74 +514,74 @@ function textForMjolnirEvent(event) {
|
||||||
// Rule removed
|
// Rule removed
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s removed the rule banning users matching %(glob)s",
|
return () => _t("%(senderName)s removed the rule banning users matching %(glob)s",
|
||||||
{senderName, glob: prevEntity});
|
{senderName, glob: prevEntity});
|
||||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s removed the rule banning rooms matching %(glob)s",
|
return () => _t("%(senderName)s removed the rule banning rooms matching %(glob)s",
|
||||||
{senderName, glob: prevEntity});
|
{senderName, glob: prevEntity});
|
||||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s removed the rule banning servers matching %(glob)s",
|
return () => _t("%(senderName)s removed the rule banning servers matching %(glob)s",
|
||||||
{senderName, glob: prevEntity});
|
{senderName, glob: prevEntity});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown type. We'll say something, but we shouldn't end up here.
|
// Unknown type. We'll say something, but we shouldn't end up here.
|
||||||
return _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity});
|
return () => _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalid rule
|
// Invalid rule
|
||||||
if (!recommendation || !reason) return _t(`%(senderName)s updated an invalid ban rule`, {senderName});
|
if (!recommendation || !reason) return () => _t(`%(senderName)s updated an invalid ban rule`, {senderName});
|
||||||
|
|
||||||
// Rule updated
|
// Rule updated
|
||||||
if (entity === prevEntity) {
|
if (entity === prevEntity) {
|
||||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{senderName, glob: entity, reason});
|
||||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{senderName, glob: entity, reason});
|
||||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{senderName, glob: entity, reason});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown type. We'll say something but we shouldn't end up here.
|
// Unknown type. We'll say something but we shouldn't end up here.
|
||||||
return _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{senderName, glob: entity, reason});
|
||||||
}
|
}
|
||||||
|
|
||||||
// New rule
|
// New rule
|
||||||
if (!prevEntity) {
|
if (!prevEntity) {
|
||||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{senderName, glob: entity, reason});
|
||||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{senderName, glob: entity, reason});
|
||||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{senderName, glob: entity, reason});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown type. We'll say something but we shouldn't end up here.
|
// Unknown type. We'll say something but we shouldn't end up here.
|
||||||
return _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{senderName, glob: entity, reason});
|
||||||
}
|
}
|
||||||
|
|
||||||
// else the entity !== prevEntity - count as a removal & add
|
// else the entity !== prevEntity - count as a removal & add
|
||||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t(
|
return () => _t(
|
||||||
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
|
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
|
||||||
"%(newGlob)s for %(reason)s",
|
"%(newGlob)s for %(reason)s",
|
||||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
||||||
);
|
);
|
||||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t(
|
return () => _t(
|
||||||
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
|
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
|
||||||
"%(newGlob)s for %(reason)s",
|
"%(newGlob)s for %(reason)s",
|
||||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
||||||
);
|
);
|
||||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||||
return _t(
|
return () => _t(
|
||||||
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
|
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
|
||||||
"%(newGlob)s for %(reason)s",
|
"%(newGlob)s for %(reason)s",
|
||||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
||||||
|
@ -567,11 +589,15 @@ function textForMjolnirEvent(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown type. We'll say something but we shouldn't end up here.
|
// Unknown type. We'll say something but we shouldn't end up here.
|
||||||
return _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
|
return () => _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
|
||||||
"for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason});
|
"for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason});
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlers = {
|
interface IHandlers {
|
||||||
|
[type: string]: (ev: any) => (() => string | null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers: IHandlers = {
|
||||||
'm.room.message': textForMessageEvent,
|
'm.room.message': textForMessageEvent,
|
||||||
'm.call.invite': textForCallInviteEvent,
|
'm.call.invite': textForCallInviteEvent,
|
||||||
'm.call.answer': textForCallAnswerEvent,
|
'm.call.answer': textForCallAnswerEvent,
|
||||||
|
@ -579,7 +605,7 @@ const handlers = {
|
||||||
'm.call.reject': textForCallRejectEvent,
|
'm.call.reject': textForCallRejectEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateHandlers = {
|
const stateHandlers: IHandlers = {
|
||||||
'm.room.canonical_alias': textForCanonicalAliasEvent,
|
'm.room.canonical_alias': textForCanonicalAliasEvent,
|
||||||
'm.room.name': textForRoomNameEvent,
|
'm.room.name': textForRoomNameEvent,
|
||||||
'm.room.topic': textForTopicEvent,
|
'm.room.topic': textForTopicEvent,
|
||||||
|
@ -604,8 +630,12 @@ for (const evType of ALL_RULE_TYPES) {
|
||||||
stateHandlers[evType] = textForMjolnirEvent;
|
stateHandlers[evType] = textForMjolnirEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function textForEvent(ev) {
|
export function hasText(ev): boolean {
|
||||||
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||||
if (handler) return handler(ev);
|
return Boolean(handler?.(ev));
|
||||||
return '';
|
}
|
||||||
|
|
||||||
|
export function textForEvent(ev): string {
|
||||||
|
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||||
|
return handler?.(ev)?.() || '';
|
||||||
}
|
}
|
|
@ -33,7 +33,7 @@ export default class VoipUserMapper {
|
||||||
|
|
||||||
private async userToVirtualUser(userId: string): Promise<string> {
|
private async userToVirtualUser(userId: string): Promise<string> {
|
||||||
const results = await CallHandler.sharedInstance().sipVirtualLookup(userId);
|
const results = await CallHandler.sharedInstance().sipVirtualLookup(userId);
|
||||||
if (results.length === 0) return null;
|
if (results.length === 0 || !results[0].fields.lookup_success) return null;
|
||||||
return results[0].userid;
|
return results[0].userid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,14 +82,14 @@ export default class VoipUserMapper {
|
||||||
return Boolean(claimedNativeRoomId);
|
return Boolean(claimedNativeRoomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onNewInvitedRoom(invitedRoom: Room) {
|
public async onNewInvitedRoom(invitedRoom: Room): Promise<void> {
|
||||||
if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return;
|
if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return;
|
||||||
|
|
||||||
const inviterId = invitedRoom.getDMInviter();
|
const inviterId = invitedRoom.getDMInviter();
|
||||||
console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
|
console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
|
||||||
const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);
|
const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);
|
||||||
if (result.length === 0) {
|
if (result.length === 0) {
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result[0].fields.is_virtual) {
|
if (result[0].fields.is_virtual) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017, 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,7 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import dis from '../dispatcher/dispatcher';
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
|
||||||
|
|
||||||
|
import dis from "../dispatcher/dispatcher";
|
||||||
|
import {ActionPayload} from "../dispatcher/payloads";
|
||||||
|
|
||||||
// TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events
|
// TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events
|
||||||
// become dispatches in the same place.
|
// become dispatches in the same place.
|
||||||
|
@ -27,7 +33,7 @@ import dis from '../dispatcher/dispatcher';
|
||||||
* @param {string} prevState the previous sync state.
|
* @param {string} prevState the previous sync state.
|
||||||
* @returns {Object} an action of type MatrixActions.sync.
|
* @returns {Object} an action of type MatrixActions.sync.
|
||||||
*/
|
*/
|
||||||
function createSyncAction(matrixClient, state, prevState) {
|
function createSyncAction(matrixClient: MatrixClient, state: string, prevState: string): ActionPayload {
|
||||||
return {
|
return {
|
||||||
action: 'MatrixActions.sync',
|
action: 'MatrixActions.sync',
|
||||||
state,
|
state,
|
||||||
|
@ -53,7 +59,7 @@ function createSyncAction(matrixClient, state, prevState) {
|
||||||
* @param {MatrixEvent} accountDataEvent the account data event.
|
* @param {MatrixEvent} accountDataEvent the account data event.
|
||||||
* @returns {AccountDataAction} an action of type MatrixActions.accountData.
|
* @returns {AccountDataAction} an action of type MatrixActions.accountData.
|
||||||
*/
|
*/
|
||||||
function createAccountDataAction(matrixClient, accountDataEvent) {
|
function createAccountDataAction(matrixClient: MatrixClient, accountDataEvent: MatrixEvent): ActionPayload {
|
||||||
return {
|
return {
|
||||||
action: 'MatrixActions.accountData',
|
action: 'MatrixActions.accountData',
|
||||||
event: accountDataEvent,
|
event: accountDataEvent,
|
||||||
|
@ -81,7 +87,11 @@ function createAccountDataAction(matrixClient, accountDataEvent) {
|
||||||
* @param {Room} room the room where account data was changed
|
* @param {Room} room the room where account data was changed
|
||||||
* @returns {RoomAccountDataAction} an action of type MatrixActions.Room.accountData.
|
* @returns {RoomAccountDataAction} an action of type MatrixActions.Room.accountData.
|
||||||
*/
|
*/
|
||||||
function createRoomAccountDataAction(matrixClient, accountDataEvent, room) {
|
function createRoomAccountDataAction(
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
accountDataEvent: MatrixEvent,
|
||||||
|
room: Room,
|
||||||
|
): ActionPayload {
|
||||||
return {
|
return {
|
||||||
action: 'MatrixActions.Room.accountData',
|
action: 'MatrixActions.Room.accountData',
|
||||||
event: accountDataEvent,
|
event: accountDataEvent,
|
||||||
|
@ -106,7 +116,7 @@ function createRoomAccountDataAction(matrixClient, accountDataEvent, room) {
|
||||||
* @param {Room} room the Room that was stored.
|
* @param {Room} room the Room that was stored.
|
||||||
* @returns {RoomAction} an action of type `MatrixActions.Room`.
|
* @returns {RoomAction} an action of type `MatrixActions.Room`.
|
||||||
*/
|
*/
|
||||||
function createRoomAction(matrixClient, room) {
|
function createRoomAction(matrixClient: MatrixClient, room: Room): ActionPayload {
|
||||||
return { action: 'MatrixActions.Room', room };
|
return { action: 'MatrixActions.Room', room };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +137,7 @@ function createRoomAction(matrixClient, room) {
|
||||||
* @param {Room} room the Room whose tags were changed.
|
* @param {Room} room the Room whose tags were changed.
|
||||||
* @returns {RoomTagsAction} an action of type `MatrixActions.Room.tags`.
|
* @returns {RoomTagsAction} an action of type `MatrixActions.Room.tags`.
|
||||||
*/
|
*/
|
||||||
function createRoomTagsAction(matrixClient, roomTagsEvent, room) {
|
function createRoomTagsAction(matrixClient: MatrixClient, roomTagsEvent: MatrixEvent, room: Room): ActionPayload {
|
||||||
return { action: 'MatrixActions.Room.tags', room };
|
return { action: 'MatrixActions.Room.tags', room };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +150,7 @@ function createRoomTagsAction(matrixClient, roomTagsEvent, room) {
|
||||||
* @param {Room} room the room the receipt happened in.
|
* @param {Room} room the room the receipt happened in.
|
||||||
* @returns {Object} an action of type MatrixActions.Room.receipt.
|
* @returns {Object} an action of type MatrixActions.Room.receipt.
|
||||||
*/
|
*/
|
||||||
function createRoomReceiptAction(matrixClient, event, room) {
|
function createRoomReceiptAction(matrixClient: MatrixClient, event: MatrixEvent, room: Room): ActionPayload {
|
||||||
return {
|
return {
|
||||||
action: 'MatrixActions.Room.receipt',
|
action: 'MatrixActions.Room.receipt',
|
||||||
event,
|
event,
|
||||||
|
@ -178,7 +188,17 @@ function createRoomReceiptAction(matrixClient, event, room) {
|
||||||
* @param {EventTimeline} data.timeline the timeline being altered.
|
* @param {EventTimeline} data.timeline the timeline being altered.
|
||||||
* @returns {RoomTimelineAction} an action of type `MatrixActions.Room.timeline`.
|
* @returns {RoomTimelineAction} an action of type `MatrixActions.Room.timeline`.
|
||||||
*/
|
*/
|
||||||
function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTimeline, removed, data) {
|
function createRoomTimelineAction(
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
timelineEvent: MatrixEvent,
|
||||||
|
room: Room,
|
||||||
|
toStartOfTimeline: boolean,
|
||||||
|
removed: boolean,
|
||||||
|
data: {
|
||||||
|
liveEvent: boolean;
|
||||||
|
timeline: EventTimeline;
|
||||||
|
},
|
||||||
|
): ActionPayload {
|
||||||
return {
|
return {
|
||||||
action: 'MatrixActions.Room.timeline',
|
action: 'MatrixActions.Room.timeline',
|
||||||
event: timelineEvent,
|
event: timelineEvent,
|
||||||
|
@ -208,8 +228,13 @@ function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTi
|
||||||
* @param {string} oldMembership the previous membership, can be null.
|
* @param {string} oldMembership the previous membership, can be null.
|
||||||
* @returns {RoomMembershipAction} an action of type `MatrixActions.Room.myMembership`.
|
* @returns {RoomMembershipAction} an action of type `MatrixActions.Room.myMembership`.
|
||||||
*/
|
*/
|
||||||
function createSelfMembershipAction(matrixClient, room, membership, oldMembership) {
|
function createSelfMembershipAction(
|
||||||
return { action: 'MatrixActions.Room.myMembership', room, membership, oldMembership};
|
matrixClient: MatrixClient,
|
||||||
|
room: Room,
|
||||||
|
membership: string,
|
||||||
|
oldMembership: string,
|
||||||
|
): ActionPayload {
|
||||||
|
return { action: 'MatrixActions.Room.myMembership', room, membership, oldMembership };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -228,61 +253,65 @@ function createSelfMembershipAction(matrixClient, room, membership, oldMembershi
|
||||||
* @param {MatrixEvent} event the matrix event that was decrypted.
|
* @param {MatrixEvent} event the matrix event that was decrypted.
|
||||||
* @returns {EventDecryptedAction} an action of type `MatrixActions.Event.decrypted`.
|
* @returns {EventDecryptedAction} an action of type `MatrixActions.Event.decrypted`.
|
||||||
*/
|
*/
|
||||||
function createEventDecryptedAction(matrixClient, event) {
|
function createEventDecryptedAction(matrixClient: MatrixClient, event: MatrixEvent): ActionPayload {
|
||||||
return { action: 'MatrixActions.Event.decrypted', event };
|
return { action: 'MatrixActions.Event.decrypted', event };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Listener = () => void;
|
||||||
|
type ActionCreator = (matrixClient: MatrixClient, ...args: any) => ActionPayload;
|
||||||
|
|
||||||
|
// A list of callbacks to call to unregister all listeners added
|
||||||
|
let matrixClientListenersStop: Listener[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start listening to events of type eventName on matrixClient and when they are emitted,
|
||||||
|
* dispatch an action created by the actionCreator function.
|
||||||
|
* @param {MatrixClient} matrixClient a MatrixClient to register a listener with.
|
||||||
|
* @param {string} eventName the event to listen to on MatrixClient.
|
||||||
|
* @param {function} actionCreator a function that should return an action to dispatch
|
||||||
|
* when given the MatrixClient as an argument as well as
|
||||||
|
* arguments emitted in the MatrixClient event.
|
||||||
|
*/
|
||||||
|
function addMatrixClientListener(matrixClient: MatrixClient, eventName: string, actionCreator: ActionCreator): void {
|
||||||
|
const listener: Listener = (...args) => {
|
||||||
|
const payload = actionCreator(matrixClient, ...args);
|
||||||
|
if (payload) {
|
||||||
|
dis.dispatch(payload, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
matrixClient.on(eventName, listener);
|
||||||
|
matrixClientListenersStop.push(() => {
|
||||||
|
matrixClient.removeListener(eventName, listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This object is responsible for dispatching actions when certain events are emitted by
|
* This object is responsible for dispatching actions when certain events are emitted by
|
||||||
* the given MatrixClient.
|
* the given MatrixClient.
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
// A list of callbacks to call to unregister all listeners added
|
|
||||||
_matrixClientListenersStop: [],
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start listening to certain events from the MatrixClient and dispatch actions when
|
* Start listening to certain events from the MatrixClient and dispatch actions when
|
||||||
* they are emitted.
|
* they are emitted.
|
||||||
* @param {MatrixClient} matrixClient the MatrixClient to listen to events from
|
* @param {MatrixClient} matrixClient the MatrixClient to listen to events from
|
||||||
*/
|
*/
|
||||||
start(matrixClient) {
|
start(matrixClient: MatrixClient) {
|
||||||
this._addMatrixClientListener(matrixClient, 'sync', createSyncAction);
|
addMatrixClientListener(matrixClient, 'sync', createSyncAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
|
addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction);
|
addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'Room', createRoomAction);
|
addMatrixClientListener(matrixClient, 'Room', createRoomAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
|
addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'Room.receipt', createRoomReceiptAction);
|
addMatrixClientListener(matrixClient, 'Room.receipt', createRoomReceiptAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction);
|
addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'Room.myMembership', createSelfMembershipAction);
|
addMatrixClientListener(matrixClient, 'Room.myMembership', createSelfMembershipAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction);
|
addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction);
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start listening to events of type eventName on matrixClient and when they are emitted,
|
|
||||||
* dispatch an action created by the actionCreator function.
|
|
||||||
* @param {MatrixClient} matrixClient a MatrixClient to register a listener with.
|
|
||||||
* @param {string} eventName the event to listen to on MatrixClient.
|
|
||||||
* @param {function} actionCreator a function that should return an action to dispatch
|
|
||||||
* when given the MatrixClient as an argument as well as
|
|
||||||
* arguments emitted in the MatrixClient event.
|
|
||||||
*/
|
|
||||||
_addMatrixClientListener(matrixClient, eventName, actionCreator) {
|
|
||||||
const listener = (...args) => {
|
|
||||||
const payload = actionCreator(matrixClient, ...args);
|
|
||||||
if (payload) {
|
|
||||||
dis.dispatch(payload, true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
matrixClient.on(eventName, listener);
|
|
||||||
this._matrixClientListenersStop.push(() => {
|
|
||||||
matrixClient.removeListener(eventName, listener);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop listening to events.
|
* Stop listening to events.
|
||||||
*/
|
*/
|
||||||
stop() {
|
stop() {
|
||||||
this._matrixClientListenersStop.forEach((stopListener) => stopListener());
|
matrixClientListenersStop.forEach((stopListener) => stopListener());
|
||||||
|
matrixClientListenersStop = [];
|
||||||
},
|
},
|
||||||
};
|
};
|
|
@ -24,13 +24,16 @@ import { HostSignupStore } from "../../stores/HostSignupStore";
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface IProps {}
|
interface IProps {
|
||||||
|
onClick?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
interface IState {}
|
interface IState {}
|
||||||
|
|
||||||
@replaceableComponent("structures.HostSignupAction")
|
@replaceableComponent("structures.HostSignupAction")
|
||||||
export default class HostSignupAction extends React.PureComponent<IProps, IState> {
|
export default class HostSignupAction extends React.PureComponent<IProps, IState> {
|
||||||
private openDialog = async () => {
|
private openDialog = async () => {
|
||||||
|
this.props.onClick?.();
|
||||||
await HostSignupStore.instance.setHostSignupActive(true);
|
await HostSignupStore.instance.setHostSignupActive(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -439,6 +439,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
activeSpace={this.state.activeSpace}
|
activeSpace={this.state.activeSpace}
|
||||||
|
onResize={this.refreshStickyHeaders}
|
||||||
onListCollapse={this.refreshStickyHeaders}
|
onListCollapse={this.refreshStickyHeaders}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
|
|
|
@ -355,7 +355,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
|
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
|
||||||
for (const eventId of pinnedEventIds) {
|
for (const eventId of pinnedEventIds) {
|
||||||
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
|
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
|
||||||
const event = timeline.getEvents().find(ev => ev.getId() === eventId);
|
const event = timeline.getEvents().find(ev => ev.getId() === eventId);
|
||||||
if (event) events.push(event);
|
if (event) events.push(event);
|
||||||
}
|
}
|
||||||
|
|
|
@ -378,7 +378,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
this.onLoggedIn();
|
this.onLoggedIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
const promisesList = [this.firstSyncPromise.promise];
|
const promisesList: Promise<any>[] = [this.firstSyncPromise.promise];
|
||||||
if (cryptoEnabled) {
|
if (cryptoEnabled) {
|
||||||
// wait for the client to finish downloading cross-signing keys for us so we
|
// wait for the client to finish downloading cross-signing keys for us so we
|
||||||
// know whether or not we have keys set up on this account
|
// know whether or not we have keys set up on this account
|
||||||
|
|
|
@ -19,17 +19,17 @@ limitations under the License.
|
||||||
import React, {createRef} from 'react';
|
import React, {createRef} from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
|
||||||
import shouldHideEvent from '../../shouldHideEvent';
|
import shouldHideEvent from '../../shouldHideEvent';
|
||||||
import {wantsDateSeparator} from '../../DateUtils';
|
import {wantsDateSeparator} from '../../DateUtils';
|
||||||
import * as sdk from '../../index';
|
import * as sdk from '../../index';
|
||||||
|
|
||||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||||
import SettingsStore from '../../settings/SettingsStore';
|
import SettingsStore from '../../settings/SettingsStore';
|
||||||
|
import RoomContext from "../../contexts/RoomContext";
|
||||||
import {Layout, LayoutPropType} from "../../settings/Layout";
|
import {Layout, LayoutPropType} from "../../settings/Layout";
|
||||||
import {_t} from "../../languageHandler";
|
import {_t} from "../../languageHandler";
|
||||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||||
import {textForEvent} from "../../TextForEvent";
|
import {hasText} from "../../TextForEvent";
|
||||||
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
|
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
|
||||||
import DMRoomMap from "../../utils/DMRoomMap";
|
import DMRoomMap from "../../utils/DMRoomMap";
|
||||||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||||
|
@ -121,6 +121,9 @@ export default class MessagePanel extends React.Component {
|
||||||
// callback which is called when the panel is scrolled.
|
// callback which is called when the panel is scrolled.
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
|
|
||||||
|
// callback which is called when the user interacts with the room timeline
|
||||||
|
onUserScroll: PropTypes.func,
|
||||||
|
|
||||||
// callback which is called when more content is needed.
|
// callback which is called when more content is needed.
|
||||||
onFillRequest: PropTypes.func,
|
onFillRequest: PropTypes.func,
|
||||||
|
|
||||||
|
@ -149,6 +152,8 @@ export default class MessagePanel extends React.Component {
|
||||||
enableFlair: PropTypes.bool,
|
enableFlair: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static contextType = RoomContext;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -378,7 +383,7 @@ export default class MessagePanel extends React.Component {
|
||||||
// Always show highlighted event
|
// Always show highlighted event
|
||||||
if (this.props.highlightedEventId === mxEv.getId()) return true;
|
if (this.props.highlightedEventId === mxEv.getId()) return true;
|
||||||
|
|
||||||
return !shouldHideEvent(mxEv);
|
return !shouldHideEvent(mxEv, this.context);
|
||||||
}
|
}
|
||||||
|
|
||||||
_readMarkerForEvent(eventId, isLastEvent) {
|
_readMarkerForEvent(eventId, isLastEvent) {
|
||||||
|
@ -613,10 +618,6 @@ export default class MessagePanel extends React.Component {
|
||||||
const eventId = mxEv.getId();
|
const eventId = mxEv.getId();
|
||||||
const highlight = (eventId === this.props.highlightedEventId);
|
const highlight = (eventId === this.props.highlightedEventId);
|
||||||
|
|
||||||
// we can't use local echoes as scroll tokens, because their event IDs change.
|
|
||||||
// Local echos have a send "status".
|
|
||||||
const scrollToken = mxEv.status ? undefined : eventId;
|
|
||||||
|
|
||||||
const readReceipts = this._readReceiptsByEvent[eventId];
|
const readReceipts = this._readReceiptsByEvent[eventId];
|
||||||
|
|
||||||
let isLastSuccessful = false;
|
let isLastSuccessful = false;
|
||||||
|
@ -645,39 +646,36 @@ export default class MessagePanel extends React.Component {
|
||||||
|
|
||||||
// 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(
|
||||||
<li
|
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
|
||||||
key={mxEv.getTxnId() || eventId}
|
<EventTile
|
||||||
ref={this._collectEventNode.bind(this, eventId)}
|
as="li"
|
||||||
data-scroll-tokens={scrollToken}
|
ref={this._collectEventNode.bind(this, eventId)}
|
||||||
>
|
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
|
||||||
<TileErrorBoundary mxEvent={mxEv}>
|
mxEvent={mxEv}
|
||||||
<EventTile
|
continuation={continuation}
|
||||||
mxEvent={mxEv}
|
isRedacted={mxEv.isRedacted()}
|
||||||
continuation={continuation}
|
replacingEventId={mxEv.replacingEventId()}
|
||||||
isRedacted={mxEv.isRedacted()}
|
editState={isEditing && this.props.editState}
|
||||||
replacingEventId={mxEv.replacingEventId()}
|
onHeightChanged={this._onHeightChanged}
|
||||||
editState={isEditing && this.props.editState}
|
readReceipts={readReceipts}
|
||||||
onHeightChanged={this._onHeightChanged}
|
readReceiptMap={this._readReceiptMap}
|
||||||
readReceipts={readReceipts}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
readReceiptMap={this._readReceiptMap}
|
checkUnmounting={this._isUnmounting}
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
eventSendStatus={mxEv.getAssociatedStatus()}
|
||||||
checkUnmounting={this._isUnmounting}
|
tileShape={this.props.tileShape}
|
||||||
eventSendStatus={mxEv.getAssociatedStatus()}
|
isTwelveHour={this.props.isTwelveHour}
|
||||||
tileShape={this.props.tileShape}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
isTwelveHour={this.props.isTwelveHour}
|
last={last}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
lastInSection={willWantDateSeparator}
|
||||||
last={last}
|
lastSuccessful={isLastSuccessful}
|
||||||
lastInSection={willWantDateSeparator}
|
isSelectedEvent={highlight}
|
||||||
lastSuccessful={isLastSuccessful}
|
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||||
isSelectedEvent={highlight}
|
showReactions={this.props.showReactions}
|
||||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
layout={this.props.layout}
|
||||||
showReactions={this.props.showReactions}
|
enableFlair={this.props.enableFlair}
|
||||||
layout={this.props.layout}
|
showReadReceipts={this.props.showReadReceipts}
|
||||||
enableFlair={this.props.enableFlair}
|
/>
|
||||||
showReadReceipts={this.props.showReadReceipts}
|
</TileErrorBoundary>,
|
||||||
/>
|
|
||||||
</TileErrorBoundary>
|
|
||||||
</li>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
@ -779,7 +777,7 @@ export default class MessagePanel extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_collectEventNode = (eventId, node) => {
|
_collectEventNode = (eventId, node) => {
|
||||||
this.eventNodes[eventId] = node;
|
this.eventNodes[eventId] = node?.ref?.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
// once dynamic content in the events load, make the scrollPanel check the
|
// once dynamic content in the events load, make the scrollPanel check the
|
||||||
|
@ -853,13 +851,6 @@ export default class MessagePanel extends React.Component {
|
||||||
|
|
||||||
const style = this.props.hidden ? { display: 'none' } : {};
|
const style = this.props.hidden ? { display: 'none' } : {};
|
||||||
|
|
||||||
const className = classNames(
|
|
||||||
this.props.className,
|
|
||||||
{
|
|
||||||
"mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let whoIsTyping;
|
let whoIsTyping;
|
||||||
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
|
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
|
||||||
whoIsTyping = (<WhoIsTypingTile
|
whoIsTyping = (<WhoIsTypingTile
|
||||||
|
@ -883,8 +874,9 @@ export default class MessagePanel extends React.Component {
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ScrollPanel
|
<ScrollPanel
|
||||||
ref={this._scrollPanel}
|
ref={this._scrollPanel}
|
||||||
className={className}
|
className={this.props.className}
|
||||||
onScroll={this.props.onScroll}
|
onScroll={this.props.onScroll}
|
||||||
|
onUserScroll={this.props.onUserScroll}
|
||||||
onResize={this.onResize}
|
onResize={this.onResize}
|
||||||
onFillRequest={this.props.onFillRequest}
|
onFillRequest={this.props.onFillRequest}
|
||||||
onUnfillRequest={this.props.onUnfillRequest}
|
onUnfillRequest={this.props.onUnfillRequest}
|
||||||
|
@ -1175,11 +1167,8 @@ class MemberGrouper {
|
||||||
|
|
||||||
add(ev) {
|
add(ev) {
|
||||||
if (ev.getType() === 'm.room.member') {
|
if (ev.getType() === 'm.room.member') {
|
||||||
// We'll just double check that it's worth our time to do so, through an
|
// We can ignore any events that don't actually have a message to display
|
||||||
// ugly hack. If textForEvent returns something, we should group it for
|
if (!hasText(ev)) return;
|
||||||
// rendering but if it doesn't then we'll exclude it.
|
|
||||||
const renderText = textForEvent(ev);
|
|
||||||
if (!renderText || renderText.trim().length === 0) return; // quietly ignore
|
|
||||||
}
|
}
|
||||||
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
|
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
|
||||||
ev.getId(),
|
ev.getId(),
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016, 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 New Vector Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,29 +14,25 @@ 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 PropTypes from "prop-types";
|
|
||||||
|
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import * as sdk from "../../index";
|
|
||||||
import BaseCard from "../views/right_panel/BaseCard";
|
import BaseCard from "../views/right_panel/BaseCard";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import TimelinePanel from "./TimelinePanel";
|
||||||
|
import Spinner from "../views/elements/Spinner";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Component which shows the global notification list using a TimelinePanel
|
* Component which shows the global notification list using a TimelinePanel
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("structures.NotificationPanel")
|
@replaceableComponent("structures.NotificationPanel")
|
||||||
class NotificationPanel extends React.Component {
|
export default class NotificationPanel extends React.PureComponent<IProps> {
|
||||||
static propTypes = {
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
|
||||||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
|
||||||
|
|
||||||
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
||||||
<h2>{_t('You’re all caught up')}</h2>
|
<h2>{_t('You’re all caught up')}</h2>
|
||||||
<p>{_t('You have no visible notifications.')}</p>
|
<p>{_t('You have no visible notifications.')}</p>
|
||||||
|
@ -47,6 +41,7 @@ class NotificationPanel extends React.Component {
|
||||||
let content;
|
let content;
|
||||||
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
||||||
if (timelineSet) {
|
if (timelineSet) {
|
||||||
|
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||||
content = (
|
content = (
|
||||||
<TimelinePanel
|
<TimelinePanel
|
||||||
manageReadReceipts={false}
|
manageReadReceipts={false}
|
||||||
|
@ -55,11 +50,12 @@ class NotificationPanel extends React.Component {
|
||||||
showUrlPreview={false}
|
showUrlPreview={false}
|
||||||
tileShape="notif"
|
tileShape="notif"
|
||||||
empty={emptyState}
|
empty={emptyState}
|
||||||
|
alwaysShowTimestamps={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error("No notifTimelineSet available!");
|
console.error("No notifTimelineSet available!");
|
||||||
content = <Loader />;
|
content = <Spinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
|
return <BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
|
||||||
|
@ -67,5 +63,3 @@ class NotificationPanel extends React.Component {
|
||||||
</BaseCard>;
|
</BaseCard>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NotificationPanel;
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
Copyright 2015 - 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,70 +16,92 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
|
|
||||||
import * as sdk from '../../index';
|
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import RateLimitedFunc from '../../ratelimitedfunc';
|
import RateLimitedFunc from '../../ratelimitedfunc';
|
||||||
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
|
||||||
import GroupStore from '../../stores/GroupStore';
|
import GroupStore from '../../stores/GroupStore';
|
||||||
import {
|
import {
|
||||||
RightPanelPhases,
|
|
||||||
RIGHT_PANEL_PHASES_NO_ARGS,
|
RIGHT_PANEL_PHASES_NO_ARGS,
|
||||||
RIGHT_PANEL_SPACE_PHASES,
|
RIGHT_PANEL_SPACE_PHASES,
|
||||||
|
RightPanelPhases,
|
||||||
} from "../../stores/RightPanelStorePhases";
|
} from "../../stores/RightPanelStorePhases";
|
||||||
import RightPanelStore from "../../stores/RightPanelStore";
|
import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import {Action} from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
||||||
import WidgetCard from "../views/right_panel/WidgetCard";
|
import WidgetCard from "../views/right_panel/WidgetCard";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
|
import MemberList from "../views/rooms/MemberList";
|
||||||
|
import GroupMemberList from "../views/groups/GroupMemberList";
|
||||||
|
import GroupRoomList from "../views/groups/GroupRoomList";
|
||||||
|
import GroupRoomInfo from "../views/groups/GroupRoomInfo";
|
||||||
|
import UserInfo from "../views/right_panel/UserInfo";
|
||||||
|
import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo";
|
||||||
|
import FilePanel from "./FilePanel";
|
||||||
|
import NotificationPanel from "./NotificationPanel";
|
||||||
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
|
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
room?: Room; // if showing panels for a given room, this is set
|
||||||
|
groupId?: string; // if showing panels for a given group, this is set
|
||||||
|
user?: User; // used if we know the user ahead of opening the panel
|
||||||
|
resizeNotifier: ResizeNotifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
phase: RightPanelPhases;
|
||||||
|
isUserPrivilegedInGroup?: boolean;
|
||||||
|
member?: RoomMember;
|
||||||
|
verificationRequest?: VerificationRequest;
|
||||||
|
verificationRequestPromise?: Promise<VerificationRequest>;
|
||||||
|
space?: Room;
|
||||||
|
widgetId?: string;
|
||||||
|
groupRoomId?: string;
|
||||||
|
groupId?: string;
|
||||||
|
event: MatrixEvent;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.RightPanel")
|
@replaceableComponent("structures.RightPanel")
|
||||||
export default class RightPanel extends React.Component {
|
export default class RightPanel extends React.Component<IProps, IState> {
|
||||||
static get propTypes() {
|
|
||||||
return {
|
|
||||||
room: PropTypes.instanceOf(Room), // if showing panels for a given room, this is set
|
|
||||||
groupId: PropTypes.string, // if showing panels for a given group, this is set
|
|
||||||
user: PropTypes.object, // used if we know the user ahead of opening the panel
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
|
private readonly delayedUpdate: RateLimitedFunc;
|
||||||
|
private dispatcherRef: string;
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.state = {
|
this.state = {
|
||||||
...RightPanelStore.getSharedInstance().roomPanelPhaseParams,
|
...RightPanelStore.getSharedInstance().roomPanelPhaseParams,
|
||||||
phase: this._getPhaseFromProps(),
|
phase: this.getPhaseFromProps(),
|
||||||
isUserPrivilegedInGroup: null,
|
isUserPrivilegedInGroup: null,
|
||||||
member: this._getUserForPanel(),
|
member: this.getUserForPanel(),
|
||||||
};
|
};
|
||||||
this.onAction = this.onAction.bind(this);
|
|
||||||
this.onRoomStateMember = this.onRoomStateMember.bind(this);
|
|
||||||
this.onGroupStoreUpdated = this.onGroupStoreUpdated.bind(this);
|
|
||||||
this.onInviteToGroupButtonClick = this.onInviteToGroupButtonClick.bind(this);
|
|
||||||
this.onAddRoomToGroupButtonClick = this.onAddRoomToGroupButtonClick.bind(this);
|
|
||||||
|
|
||||||
this._delayedUpdate = new RateLimitedFunc(() => {
|
this.delayedUpdate = new RateLimitedFunc(() => {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to split out the logic for _getPhaseFromProps() and the constructor
|
// Helper function to split out the logic for getPhaseFromProps() and the constructor
|
||||||
// as both are called at the same time in the constructor.
|
// as both are called at the same time in the constructor.
|
||||||
_getUserForPanel() {
|
private getUserForPanel() {
|
||||||
if (this.state && this.state.member) return this.state.member;
|
if (this.state && this.state.member) return this.state.member;
|
||||||
const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams;
|
const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams;
|
||||||
return this.props.user || lastParams['member'];
|
return this.props.user || lastParams['member'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// gets the current phase from the props and also maybe the store
|
// gets the current phase from the props and also maybe the store
|
||||||
_getPhaseFromProps() {
|
private getPhaseFromProps() {
|
||||||
const rps = RightPanelStore.getSharedInstance();
|
const rps = RightPanelStore.getSharedInstance();
|
||||||
const userForPanel = this._getUserForPanel();
|
const userForPanel = this.getUserForPanel();
|
||||||
if (this.props.groupId) {
|
if (this.props.groupId) {
|
||||||
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
|
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
|
||||||
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList});
|
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList});
|
||||||
|
@ -118,7 +140,7 @@ export default class RightPanel extends React.Component {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
const cli = this.context;
|
const cli = this.context;
|
||||||
cli.on("RoomState.members", this.onRoomStateMember);
|
cli.on("RoomState.members", this.onRoomStateMember);
|
||||||
this._initGroupStore(this.props.groupId);
|
this.initGroupStore(this.props.groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -126,61 +148,47 @@ export default class RightPanel extends React.Component {
|
||||||
if (this.context) {
|
if (this.context) {
|
||||||
this.context.removeListener("RoomState.members", this.onRoomStateMember);
|
this.context.removeListener("RoomState.members", this.onRoomStateMember);
|
||||||
}
|
}
|
||||||
this._unregisterGroupStore(this.props.groupId);
|
this.unregisterGroupStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 camelcase
|
||||||
if (newProps.groupId !== this.props.groupId) {
|
if (newProps.groupId !== this.props.groupId) {
|
||||||
this._unregisterGroupStore(this.props.groupId);
|
this.unregisterGroupStore();
|
||||||
this._initGroupStore(newProps.groupId);
|
this.initGroupStore(newProps.groupId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_initGroupStore(groupId) {
|
private initGroupStore(groupId: string) {
|
||||||
if (!groupId) return;
|
if (!groupId) return;
|
||||||
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
|
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
_unregisterGroupStore() {
|
private unregisterGroupStore() {
|
||||||
GroupStore.unregisterListener(this.onGroupStoreUpdated);
|
GroupStore.unregisterListener(this.onGroupStoreUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupStoreUpdated() {
|
private onGroupStoreUpdated = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
|
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onInviteToGroupButtonClick() {
|
private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => {
|
||||||
showGroupInviteDialog(this.props.groupId).then(() => {
|
|
||||||
this.setState({
|
|
||||||
phase: RightPanelPhases.GroupMemberList,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onAddRoomToGroupButtonClick() {
|
|
||||||
showGroupAddRoomDialog(this.props.groupId).then(() => {
|
|
||||||
this.forceUpdate();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onRoomStateMember(ev, state, member) {
|
|
||||||
if (!this.props.room || member.roomId !== this.props.room.roomId) {
|
if (!this.props.room || member.roomId !== this.props.room.roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// redraw the badge on the membership list
|
// redraw the badge on the membership list
|
||||||
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) {
|
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) {
|
||||||
this._delayedUpdate();
|
this.delayedUpdate();
|
||||||
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId &&
|
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId &&
|
||||||
member.userId === this.state.member.userId) {
|
member.userId === this.state.member.userId) {
|
||||||
// refresh the member info (e.g. new power level)
|
// refresh the member info (e.g. new power level)
|
||||||
this._delayedUpdate();
|
this.delayedUpdate();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
onAction(payload) {
|
private onAction = (payload: ActionPayload) => {
|
||||||
if (payload.action === Action.AfterRightPanelPhaseChange) {
|
if (payload.action === Action.AfterRightPanelPhaseChange) {
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: payload.phase,
|
phase: payload.phase,
|
||||||
|
@ -194,9 +202,9 @@ export default class RightPanel extends React.Component {
|
||||||
space: payload.space,
|
space: payload.space,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
onClose = () => {
|
private onClose = () => {
|
||||||
// XXX: There are three different ways of 'closing' this panel depending on what state
|
// XXX: There are three different ways of 'closing' this panel depending on what state
|
||||||
// things are in... this knows far more than it should do about the state of the rest
|
// things are in... this knows far more than it should do about the state of the rest
|
||||||
// of the app and is generally a bit silly.
|
// of the app and is generally a bit silly.
|
||||||
|
@ -224,16 +232,6 @@ export default class RightPanel extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const MemberList = sdk.getComponent('rooms.MemberList');
|
|
||||||
const UserInfo = sdk.getComponent('right_panel.UserInfo');
|
|
||||||
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
|
|
||||||
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
|
||||||
const FilePanel = sdk.getComponent('structures.FilePanel');
|
|
||||||
|
|
||||||
const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
|
|
||||||
const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
|
|
||||||
const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
|
|
||||||
|
|
||||||
let panel = <div />;
|
let panel = <div />;
|
||||||
const roomId = this.props.room ? this.props.room.roomId : undefined;
|
const roomId = this.props.room ? this.props.room.roomId : undefined;
|
||||||
|
|
||||||
|
@ -285,6 +283,7 @@ export default class RightPanel extends React.Component {
|
||||||
user={this.state.member}
|
user={this.state.member}
|
||||||
groupId={this.props.groupId}
|
groupId={this.props.groupId}
|
||||||
key={this.state.member.userId}
|
key={this.state.member.userId}
|
||||||
|
phase={this.state.phase}
|
||||||
onClose={this.onClose} />;
|
onClose={this.onClose} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -299,6 +298,12 @@ export default class RightPanel extends React.Component {
|
||||||
panel = <NotificationPanel onClose={this.onClose} />;
|
panel = <NotificationPanel onClose={this.onClose} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case RightPanelPhases.PinnedMessages:
|
||||||
|
if (SettingsStore.getValue("feature_pinning")) {
|
||||||
|
panel = <PinnedMessagesCard room={this.props.room} onClose={this.onClose} />;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case RightPanelPhases.FilePanel:
|
case RightPanelPhases.FilePanel:
|
||||||
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
|
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
|
||||||
break;
|
break;
|
|
@ -46,7 +46,7 @@ import RoomViewStore from '../../stores/RoomViewStore';
|
||||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import {Layout} from "../../settings/Layout";
|
import { Layout } from "../../settings/Layout";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
import RightPanelStore from "../../stores/RightPanelStore";
|
import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||||
|
@ -54,16 +54,13 @@ import RoomContext from "../../contexts/RoomContext";
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { SettingLevel } from "../../settings/SettingLevel";
|
|
||||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||||
import ScrollPanel from "./ScrollPanel";
|
import ScrollPanel from "./ScrollPanel";
|
||||||
import TimelinePanel from "./TimelinePanel";
|
import TimelinePanel from "./TimelinePanel";
|
||||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||||
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
||||||
import ForwardMessage from "../views/rooms/ForwardMessage";
|
|
||||||
import SearchBar from "../views/rooms/SearchBar";
|
import SearchBar from "../views/rooms/SearchBar";
|
||||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||||
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
|
|
||||||
import AuxPanel from "../views/rooms/AuxPanel";
|
import AuxPanel from "../views/rooms/AuxPanel";
|
||||||
import RoomHeader from "../views/rooms/RoomHeader";
|
import RoomHeader from "../views/rooms/RoomHeader";
|
||||||
import { XOR } from "../../@types/common";
|
import { XOR } from "../../@types/common";
|
||||||
|
@ -82,7 +79,8 @@ import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager';
|
||||||
import { objectHasDiff } from "../../utils/objects";
|
import { objectHasDiff } from "../../utils/objects";
|
||||||
import SpaceRoomView from "./SpaceRoomView";
|
import SpaceRoomView from "./SpaceRoomView";
|
||||||
import { IOpts } from "../../createRoom";
|
import { IOpts } from "../../createRoom";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import { omit } from 'lodash';
|
||||||
import UIStore from "../../stores/UIStore";
|
import UIStore from "../../stores/UIStore";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
|
@ -137,7 +135,6 @@ export interface IState {
|
||||||
// Whether to highlight the event scrolled to
|
// Whether to highlight the event scrolled to
|
||||||
isInitialEventHighlighted?: boolean;
|
isInitialEventHighlighted?: boolean;
|
||||||
replyToEvent?: MatrixEvent;
|
replyToEvent?: MatrixEvent;
|
||||||
forwardingEvent?: MatrixEvent;
|
|
||||||
numUnreadMessages: number;
|
numUnreadMessages: number;
|
||||||
draggingFile: boolean;
|
draggingFile: boolean;
|
||||||
searching: boolean;
|
searching: boolean;
|
||||||
|
@ -156,8 +153,6 @@ export interface IState {
|
||||||
canPeek: boolean;
|
canPeek: boolean;
|
||||||
showApps: boolean;
|
showApps: boolean;
|
||||||
isPeeking: boolean;
|
isPeeking: boolean;
|
||||||
showingPinned: boolean;
|
|
||||||
showReadReceipts: boolean;
|
|
||||||
showRightPanel: boolean;
|
showRightPanel: boolean;
|
||||||
// error object, as from the matrix client/server API
|
// error object, as from the matrix client/server API
|
||||||
// If we failed to load information about the room,
|
// If we failed to load information about the room,
|
||||||
|
@ -176,6 +171,7 @@ export interface IState {
|
||||||
statusBarVisible: boolean;
|
statusBarVisible: boolean;
|
||||||
// We load this later by asking the js-sdk to suggest a version for us.
|
// We load this later by asking the js-sdk to suggest a version for us.
|
||||||
// This object is the result of Room#getRecommendedVersion()
|
// This object is the result of Room#getRecommendedVersion()
|
||||||
|
|
||||||
upgradeRecommendation?: {
|
upgradeRecommendation?: {
|
||||||
version: string;
|
version: string;
|
||||||
needsUpgrade: boolean;
|
needsUpgrade: boolean;
|
||||||
|
@ -184,6 +180,12 @@ export interface IState {
|
||||||
canReact: boolean;
|
canReact: boolean;
|
||||||
canReply: boolean;
|
canReply: boolean;
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
|
lowBandwidth: boolean;
|
||||||
|
showReadReceipts: boolean;
|
||||||
|
showRedactions: boolean;
|
||||||
|
showJoinLeaves: boolean;
|
||||||
|
showAvatarChanges: boolean;
|
||||||
|
showDisplaynameChanges: boolean;
|
||||||
matrixClientIsReady: boolean;
|
matrixClientIsReady: boolean;
|
||||||
showUrlPreview?: boolean;
|
showUrlPreview?: boolean;
|
||||||
e2eStatus?: E2EStatus;
|
e2eStatus?: E2EStatus;
|
||||||
|
@ -201,8 +203,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
private readonly dispatcherRef: string;
|
private readonly dispatcherRef: string;
|
||||||
private readonly roomStoreToken: EventSubscription;
|
private readonly roomStoreToken: EventSubscription;
|
||||||
private readonly rightPanelStoreToken: EventSubscription;
|
private readonly rightPanelStoreToken: EventSubscription;
|
||||||
private readonly showReadReceiptsWatchRef: string;
|
private settingWatchers: string[];
|
||||||
private readonly layoutWatcherRef: string;
|
|
||||||
|
|
||||||
private unmounted = false;
|
private unmounted = false;
|
||||||
private permalinkCreators: Record<string, RoomPermalinkCreator> = {};
|
private permalinkCreators: Record<string, RoomPermalinkCreator> = {};
|
||||||
|
@ -233,8 +234,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
canPeek: false,
|
canPeek: false,
|
||||||
showApps: false,
|
showApps: false,
|
||||||
isPeeking: false,
|
isPeeking: false,
|
||||||
showingPinned: false,
|
|
||||||
showReadReceipts: true,
|
|
||||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||||
joining: false,
|
joining: false,
|
||||||
atEndOfLiveTimeline: true,
|
atEndOfLiveTimeline: true,
|
||||||
|
@ -244,6 +243,12 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
canReact: false,
|
canReact: false,
|
||||||
canReply: false,
|
canReply: false,
|
||||||
layout: SettingsStore.getValue("layout"),
|
layout: SettingsStore.getValue("layout"),
|
||||||
|
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
|
||||||
|
showReadReceipts: true,
|
||||||
|
showRedactions: true,
|
||||||
|
showJoinLeaves: true,
|
||||||
|
showAvatarChanges: true,
|
||||||
|
showDisplaynameChanges: true,
|
||||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||||
dragCounter: 0,
|
dragCounter: 0,
|
||||||
};
|
};
|
||||||
|
@ -270,9 +275,14 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
||||||
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||||
|
|
||||||
this.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null,
|
this.settingWatchers = [
|
||||||
this.onReadReceiptsChange);
|
SettingsStore.watchSetting("layout", null, () =>
|
||||||
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, this.onLayoutChange);
|
this.setState({ layout: SettingsStore.getValue("layout") }),
|
||||||
|
),
|
||||||
|
SettingsStore.watchSetting("lowBandwidth", null, () =>
|
||||||
|
this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
|
||||||
|
),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWidgetStoreUpdate = () => {
|
private onWidgetStoreUpdate = () => {
|
||||||
|
@ -325,14 +335,45 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
initialEventId: RoomViewStore.getInitialEventId(),
|
initialEventId: RoomViewStore.getInitialEventId(),
|
||||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||||
replyToEvent: RoomViewStore.getQuotingEvent(),
|
replyToEvent: RoomViewStore.getQuotingEvent(),
|
||||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
|
||||||
// we should only peek once we have a ready client
|
// we should only peek once we have a ready client
|
||||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
||||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
|
|
||||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||||
|
showRedactions: SettingsStore.getValue("showRedactions", roomId),
|
||||||
|
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
|
||||||
|
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
|
||||||
|
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
|
||||||
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
|
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add watchers for each of the settings we just looked up
|
||||||
|
this.settingWatchers = this.settingWatchers.concat([
|
||||||
|
SettingsStore.watchSetting("showReadReceipts", null, () =>
|
||||||
|
this.setState({
|
||||||
|
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
SettingsStore.watchSetting("showRedactions", null, () =>
|
||||||
|
this.setState({
|
||||||
|
showRedactions: SettingsStore.getValue("showRedactions", roomId),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
SettingsStore.watchSetting("showJoinLeaves", null, () =>
|
||||||
|
this.setState({
|
||||||
|
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
SettingsStore.watchSetting("showAvatarChanges", null, () =>
|
||||||
|
this.setState({
|
||||||
|
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
SettingsStore.watchSetting("showDisplaynameChanges", null, () =>
|
||||||
|
this.setState({
|
||||||
|
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
|
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
|
||||||
// Stop peeking because we have joined this room now
|
// Stop peeking because we have joined this room now
|
||||||
this.context.stopPeeking();
|
this.context.stopPeeking();
|
||||||
|
@ -529,7 +570,20 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
return (objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, nextState));
|
const hasPropsDiff = objectHasDiff(this.props, nextProps);
|
||||||
|
|
||||||
|
// React only shallow comparison and we only want to trigger
|
||||||
|
// a component re-render if a room requires an upgrade
|
||||||
|
const newUpgradeRecommendation = nextState.upgradeRecommendation || {}
|
||||||
|
|
||||||
|
const state = omit(this.state, ['upgradeRecommendation']);
|
||||||
|
const newState = omit(nextState, ['upgradeRecommendation'])
|
||||||
|
|
||||||
|
const hasStateDiff =
|
||||||
|
objectHasDiff(state, newState) ||
|
||||||
|
(newUpgradeRecommendation.needsUpgrade === true)
|
||||||
|
|
||||||
|
return hasPropsDiff || hasStateDiff;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
|
@ -628,10 +682,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.showReadReceiptsWatchRef) {
|
|
||||||
SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
// cancel any pending calls to the rate_limited_funcs
|
// cancel any pending calls to the rate_limited_funcs
|
||||||
this.updateRoomMembers.cancelPendingCall();
|
this.updateRoomMembers.cancelPendingCall();
|
||||||
|
|
||||||
|
@ -639,7 +689,20 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
// console.log("Tinter.tint from RoomView.unmount");
|
// console.log("Tinter.tint from RoomView.unmount");
|
||||||
// Tinter.tint(); // reset colourscheme
|
// Tinter.tint(); // reset colourscheme
|
||||||
|
|
||||||
SettingsStore.unwatchSetting(this.layoutWatcherRef);
|
for (const watcher of this.settingWatchers) {
|
||||||
|
SettingsStore.unwatchSetting(watcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onUserScroll = () => {
|
||||||
|
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: this.state.room.roomId,
|
||||||
|
event_id: this.state.initialEventId,
|
||||||
|
highlighted: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onLayoutChange = () => {
|
private onLayoutChange = () => {
|
||||||
|
@ -798,7 +861,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
// update unread count when scrolled up
|
// update unread count when scrolled up
|
||||||
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
|
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
|
||||||
// no change
|
// no change
|
||||||
} else if (!shouldHideEvent(ev)) {
|
} else if (!shouldHideEvent(ev, this.state)) {
|
||||||
this.setState((state, props) => {
|
this.setState((state, props) => {
|
||||||
return {numUnreadMessages: state.numUnreadMessages + 1};
|
return {numUnreadMessages: state.numUnreadMessages + 1};
|
||||||
});
|
});
|
||||||
|
@ -812,7 +875,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onEvent = (ev) => {
|
private onEvent = (ev) => {
|
||||||
if (ev.isBeingDecrypted() || ev.isDecryptionFailure() || ev.shouldAttemptDecryption()) return;
|
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
|
||||||
this.handleEffects(ev);
|
this.handleEffects(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1377,13 +1440,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onPinnedClick = () => {
|
|
||||||
const nowShowingPinned = !this.state.showingPinned;
|
|
||||||
const roomId = this.state.room.roomId;
|
|
||||||
this.setState({showingPinned: nowShowingPinned, searching: false});
|
|
||||||
SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onCallPlaced = (type: PlaceCallType) => {
|
private onCallPlaced = (type: PlaceCallType) => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'place_call',
|
action: 'place_call',
|
||||||
|
@ -1396,18 +1452,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
dis.dispatch({ action: "open_room_settings" });
|
dis.dispatch({ action: "open_room_settings" });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onCancelClick = () => {
|
|
||||||
console.log("updateTint from onCancelClick");
|
|
||||||
this.updateTint();
|
|
||||||
if (this.state.forwardingEvent) {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'forward_event',
|
|
||||||
event: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
dis.fire(Action.FocusComposer);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onAppsClick = () => {
|
private onAppsClick = () => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "appsDrawer",
|
action: "appsDrawer",
|
||||||
|
@ -1500,7 +1544,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
private onSearchClick = () => {
|
private onSearchClick = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
searching: !this.state.searching,
|
searching: !this.state.searching,
|
||||||
showingPinned: false,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1513,8 +1556,19 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// jump down to the bottom of this room, where new events are arriving
|
// jump down to the bottom of this room, where new events are arriving
|
||||||
private jumpToLiveTimeline = () => {
|
private jumpToLiveTimeline = () => {
|
||||||
this.messagePanel.jumpToLiveTimeline();
|
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
||||||
dis.fire(Action.FocusComposer);
|
// If we were viewing a highlighted event, firing view_room without
|
||||||
|
// an event will take care of both clearing the URL fragment and
|
||||||
|
// jumping to the bottom
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: this.state.room.roomId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Otherwise we have to jump manually
|
||||||
|
this.messagePanel.jumpToLiveTimeline();
|
||||||
|
dis.fire(Action.FocusComposer);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// jump up to wherever our read marker is
|
// jump up to wherever our read marker is
|
||||||
|
@ -1615,6 +1669,24 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* called by the parent component when PageUp/Down/etc is pressed.
|
||||||
|
*
|
||||||
|
* We pass it down to the scroll panel.
|
||||||
|
*/
|
||||||
|
private handleScrollKey = ev => {
|
||||||
|
let panel;
|
||||||
|
if (this.searchResultsPanel.current) {
|
||||||
|
panel = this.searchResultsPanel.current;
|
||||||
|
} else if (this.messagePanel) {
|
||||||
|
panel = this.messagePanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (panel) {
|
||||||
|
panel.handleScrollKey(ev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get any current call for this room
|
* get any current call for this room
|
||||||
*/
|
*/
|
||||||
|
@ -1813,11 +1885,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
let aux = null;
|
let aux = null;
|
||||||
let previewBar;
|
let previewBar;
|
||||||
let hideCancel = false;
|
if (this.state.searching) {
|
||||||
if (this.state.forwardingEvent) {
|
|
||||||
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
|
|
||||||
} else if (this.state.searching) {
|
|
||||||
hideCancel = true; // has own cancel
|
|
||||||
aux = <SearchBar
|
aux = <SearchBar
|
||||||
searchInProgress={this.state.searchInProgress}
|
searchInProgress={this.state.searchInProgress}
|
||||||
onCancelClick={this.onCancelSearchClick}
|
onCancelClick={this.onCancelSearchClick}
|
||||||
|
@ -1826,10 +1894,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
/>;
|
/>;
|
||||||
} else if (showRoomUpgradeBar) {
|
} else if (showRoomUpgradeBar) {
|
||||||
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
||||||
hideCancel = true;
|
|
||||||
} else if (this.state.showingPinned) {
|
|
||||||
hideCancel = true; // has own cancel
|
|
||||||
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
|
|
||||||
} else if (myMembership !== "join") {
|
} else if (myMembership !== "join") {
|
||||||
// We do have a room object for this room, but we're not currently in it.
|
// We do have a room object for this room, but we're not currently in it.
|
||||||
// We may have a 3rd party invite to it.
|
// We may have a 3rd party invite to it.
|
||||||
|
@ -1838,7 +1902,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
inviterName = this.props.oobData.inviterName;
|
inviterName = this.props.oobData.inviterName;
|
||||||
}
|
}
|
||||||
const invitedEmail = this.props.threepidInvite?.toEmail;
|
const invitedEmail = this.props.threepidInvite?.toEmail;
|
||||||
hideCancel = true;
|
|
||||||
previewBar = (
|
previewBar = (
|
||||||
<RoomPreviewBar
|
<RoomPreviewBar
|
||||||
onJoinClick={this.onJoinButtonClicked}
|
onJoinClick={this.onJoinButtonClicked}
|
||||||
|
@ -1956,11 +2019,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
hideMessagePanel = true;
|
hideMessagePanel = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldHighlight = this.state.isInitialEventHighlighted;
|
|
||||||
let highlightedEventId = null;
|
let highlightedEventId = null;
|
||||||
if (this.state.forwardingEvent) {
|
if (this.state.isInitialEventHighlighted) {
|
||||||
highlightedEventId = this.state.forwardingEvent.getId();
|
|
||||||
} else if (shouldHighlight) {
|
|
||||||
highlightedEventId = this.state.initialEventId;
|
highlightedEventId = this.state.initialEventId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1985,6 +2045,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
eventId={this.state.initialEventId}
|
eventId={this.state.initialEventId}
|
||||||
eventPixelOffset={this.state.initialEventPixelOffset}
|
eventPixelOffset={this.state.initialEventPixelOffset}
|
||||||
onScroll={this.onMessageListScroll}
|
onScroll={this.onMessageListScroll}
|
||||||
|
onUserScroll={this.onUserScroll}
|
||||||
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
|
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
|
||||||
showUrlPreview = {this.state.showUrlPreview}
|
showUrlPreview = {this.state.showUrlPreview}
|
||||||
className={messagePanelClassNames}
|
className={messagePanelClassNames}
|
||||||
|
@ -2011,6 +2072,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
highlight={this.state.room.getUnreadNotificationCount('highlight') > 0}
|
highlight={this.state.room.getUnreadNotificationCount('highlight') > 0}
|
||||||
numUnreadMessages={this.state.numUnreadMessages}
|
numUnreadMessages={this.state.numUnreadMessages}
|
||||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||||
|
roomId={this.state.roomId}
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2047,8 +2109,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
inRoom={myMembership === 'join'}
|
inRoom={myMembership === 'join'}
|
||||||
onSearchClick={this.onSearchClick}
|
onSearchClick={this.onSearchClick}
|
||||||
onSettingsClick={this.onSettingsClick}
|
onSettingsClick={this.onSettingsClick}
|
||||||
onPinnedClick={this.onPinnedClick}
|
|
||||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
|
||||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||||
e2eStatus={this.state.e2eStatus}
|
e2eStatus={this.state.e2eStatus}
|
||||||
|
|
|
@ -133,6 +133,10 @@ export default class ScrollPanel extends React.Component {
|
||||||
*/
|
*/
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
|
|
||||||
|
/* onUserScroll: callback which is called when the user interacts with the room timeline
|
||||||
|
*/
|
||||||
|
onUserScroll: PropTypes.func,
|
||||||
|
|
||||||
/* className: classnames to add to the top-level div
|
/* className: classnames to add to the top-level div
|
||||||
*/
|
*/
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
@ -535,21 +539,29 @@ export default class ScrollPanel extends React.Component {
|
||||||
* @param {object} ev the keyboard event
|
* @param {object} ev the keyboard event
|
||||||
*/
|
*/
|
||||||
handleScrollKey = ev => {
|
handleScrollKey = ev => {
|
||||||
|
let isScrolling = false;
|
||||||
const roomAction = getKeyBindingsManager().getRoomAction(ev);
|
const roomAction = getKeyBindingsManager().getRoomAction(ev);
|
||||||
switch (roomAction) {
|
switch (roomAction) {
|
||||||
case RoomAction.ScrollUp:
|
case RoomAction.ScrollUp:
|
||||||
this.scrollRelative(-1);
|
this.scrollRelative(-1);
|
||||||
|
isScrolling = true;
|
||||||
break;
|
break;
|
||||||
case RoomAction.RoomScrollDown:
|
case RoomAction.RoomScrollDown:
|
||||||
this.scrollRelative(1);
|
this.scrollRelative(1);
|
||||||
|
isScrolling = true;
|
||||||
break;
|
break;
|
||||||
case RoomAction.JumpToFirstMessage:
|
case RoomAction.JumpToFirstMessage:
|
||||||
this.scrollToTop();
|
this.scrollToTop();
|
||||||
|
isScrolling = true;
|
||||||
break;
|
break;
|
||||||
case RoomAction.JumpToLatestMessage:
|
case RoomAction.JumpToLatestMessage:
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
|
isScrolling = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (isScrolling && this.props.onUserScroll) {
|
||||||
|
this.props.onUserScroll(ev);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Scroll the panel to bring the DOM node with the scroll token
|
/* Scroll the panel to bring the DOM node with the scroll token
|
||||||
|
@ -888,9 +900,8 @@ export default class ScrollPanel extends React.Component {
|
||||||
<AutoHideScrollbar
|
<AutoHideScrollbar
|
||||||
wrappedRef={this._collectScroll}
|
wrappedRef={this._collectScroll}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
className={`mx_ScrollPanel ${this.props.className}`}
|
onWheel={this.props.onUserScroll}
|
||||||
style={this.props.style}
|
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
|
||||||
>
|
|
||||||
{ this.props.fixedChildren }
|
{ this.props.fixedChildren }
|
||||||
<div className="mx_RoomView_messageListWrapper">
|
<div className="mx_RoomView_messageListWrapper">
|
||||||
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
|
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
|
||||||
|
|
|
@ -28,7 +28,7 @@ import RoomTopic from "../views/elements/RoomTopic";
|
||||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||||
import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
|
import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
|
||||||
import {useRoomMembers} from "../../hooks/useRoomMembers";
|
import {useRoomMembers} from "../../hooks/useRoomMembers";
|
||||||
import createRoom, {IOpts, Preset} from "../../createRoom";
|
import createRoom, {IOpts} from "../../createRoom";
|
||||||
import Field from "../views/elements/Field";
|
import Field from "../views/elements/Field";
|
||||||
import {useEventEmitter} from "../../hooks/useEventEmitter";
|
import {useEventEmitter} from "../../hooks/useEventEmitter";
|
||||||
import withValidation from "../views/elements/Validation";
|
import withValidation from "../views/elements/Validation";
|
||||||
|
@ -65,6 +65,7 @@ import dis from "../../dispatcher/dispatcher";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
|
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
|
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -587,6 +588,10 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
|
||||||
<h3>{ _t("Me and my teammates") }</h3>
|
<h3>{ _t("Me and my teammates") }</h3>
|
||||||
<div>{ _t("A private space for you and your teammates") }</div>
|
<div>{ _t("A private space for you and your teammates") }</div>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
<div className="mx_SpaceRoomView_betaWarning">
|
||||||
|
<h3>{ _t("Teammates might not be able to view or join any private rooms you make.") }</h3>
|
||||||
|
<p>{ _t("We're working on this as part of the beta, but just want to let you know.") }</p>
|
||||||
|
</div>
|
||||||
<SpaceFeedbackPrompt />
|
<SpaceFeedbackPrompt />
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline";
|
||||||
import {TimelineWindow} from "matrix-js-sdk/src/timeline-window";
|
import {TimelineWindow} from "matrix-js-sdk/src/timeline-window";
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||||
|
import RoomContext from "../../contexts/RoomContext";
|
||||||
import UserActivity from "../../UserActivity";
|
import UserActivity from "../../UserActivity";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
|
@ -36,7 +37,6 @@ import shouldHideEvent from '../../shouldHideEvent';
|
||||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||||
import {UIFeature} from "../../settings/UIFeature";
|
import {UIFeature} from "../../settings/UIFeature";
|
||||||
import {objectHasDiff} from "../../utils/objects";
|
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
import { arrayFastClone } from "../../utils/arrays";
|
import { arrayFastClone } from "../../utils/arrays";
|
||||||
|
|
||||||
|
@ -94,6 +94,9 @@ class TimelinePanel extends React.Component {
|
||||||
// callback which is called when the panel is scrolled.
|
// callback which is called when the panel is scrolled.
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
|
|
||||||
|
// callback which is called when the user interacts with the room timeline
|
||||||
|
onUserScroll: PropTypes.func,
|
||||||
|
|
||||||
// callback which is called when the read-up-to mark is updated.
|
// callback which is called when the read-up-to mark is updated.
|
||||||
onReadMarkerUpdated: PropTypes.func,
|
onReadMarkerUpdated: PropTypes.func,
|
||||||
|
|
||||||
|
@ -118,8 +121,13 @@ class TimelinePanel extends React.Component {
|
||||||
|
|
||||||
// which layout to use
|
// which layout to use
|
||||||
layout: LayoutPropType,
|
layout: LayoutPropType,
|
||||||
|
|
||||||
|
// whether to always show timestamps for an event
|
||||||
|
alwaysShowTimestamps: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static contextType = RoomContext;
|
||||||
|
|
||||||
// a map from room id to read marker event timestamp
|
// a map from room id to read marker event timestamp
|
||||||
static roomReadMarkerTsMap = {};
|
static roomReadMarkerTsMap = {};
|
||||||
|
|
||||||
|
@ -258,37 +266,15 @@ class TimelinePanel extends React.Component {
|
||||||
console.warn("Replacing timelineSet on a TimelinePanel - confusion may ensue");
|
console.warn("Replacing timelineSet on a TimelinePanel - confusion may ensue");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newProps.eventId != this.props.eventId) {
|
const differentEventId = newProps.eventId != this.props.eventId;
|
||||||
|
const differentHighlightedEventId = newProps.highlightedEventId != this.props.highlightedEventId;
|
||||||
|
if (differentEventId || differentHighlightedEventId) {
|
||||||
console.log("TimelinePanel switching to eventId " + newProps.eventId +
|
console.log("TimelinePanel switching to eventId " + newProps.eventId +
|
||||||
" (was " + this.props.eventId + ")");
|
" (was " + this.props.eventId + ")");
|
||||||
return this._initTimeline(newProps);
|
return this._initTimeline(newProps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
if (objectHasDiff(this.props, nextProps)) {
|
|
||||||
if (DEBUG) {
|
|
||||||
console.group("Timeline.shouldComponentUpdate: props change");
|
|
||||||
console.log("props before:", this.props);
|
|
||||||
console.log("props after:", nextProps);
|
|
||||||
console.groupEnd();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (objectHasDiff(this.state, nextState)) {
|
|
||||||
if (DEBUG) {
|
|
||||||
console.group("Timeline.shouldComponentUpdate: state change");
|
|
||||||
console.log("state before:", this.state);
|
|
||||||
console.log("state after:", nextState);
|
|
||||||
console.groupEnd();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
// set a boolean to say we've been unmounted, which any pending
|
// set a boolean to say we've been unmounted, which any pending
|
||||||
// promises can use to throw away their results.
|
// promises can use to throw away their results.
|
||||||
|
@ -1305,7 +1291,7 @@ class TimelinePanel extends React.Component {
|
||||||
|
|
||||||
const shouldIgnore = !!ev.status || // local echo
|
const shouldIgnore = !!ev.status || // local echo
|
||||||
(ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message
|
(ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message
|
||||||
const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev);
|
const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev, this.context);
|
||||||
|
|
||||||
if (isWithoutTile || !node) {
|
if (isWithoutTile || !node) {
|
||||||
// don't start counting if the event should be ignored,
|
// don't start counting if the event should be ignored,
|
||||||
|
@ -1456,10 +1442,11 @@ class TimelinePanel extends React.Component {
|
||||||
ourUserId={MatrixClientPeg.get().credentials.userId}
|
ourUserId={MatrixClientPeg.get().credentials.userId}
|
||||||
stickyBottom={stickyBottom}
|
stickyBottom={stickyBottom}
|
||||||
onScroll={this.onMessageListScroll}
|
onScroll={this.onMessageListScroll}
|
||||||
|
onUserScroll={this.props.onUserScroll}
|
||||||
onFillRequest={this.onMessageListFillRequest}
|
onFillRequest={this.onMessageListFillRequest}
|
||||||
onUnfillRequest={this.onMessageListUnfillRequest}
|
onUnfillRequest={this.onMessageListUnfillRequest}
|
||||||
isTwelveHour={this.state.isTwelveHour}
|
isTwelveHour={this.state.isTwelveHour}
|
||||||
alwaysShowTimestamps={this.state.alwaysShowTimestamps}
|
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps}
|
||||||
className={this.props.className}
|
className={this.props.className}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
|
|
|
@ -55,6 +55,7 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
||||||
const totalCount = this.state.toasts.length;
|
const totalCount = this.state.toasts.length;
|
||||||
const isStacked = totalCount > 1;
|
const isStacked = totalCount > 1;
|
||||||
let toast;
|
let toast;
|
||||||
|
let containerClasses;
|
||||||
if (totalCount !== 0) {
|
if (totalCount !== 0) {
|
||||||
const topToast = this.state.toasts[0];
|
const topToast = this.state.toasts[0];
|
||||||
const {title, icon, key, component, className, props} = topToast;
|
const {title, icon, key, component, className, props} = topToast;
|
||||||
|
@ -79,16 +80,17 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
|
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
|
||||||
</div>);
|
</div>);
|
||||||
|
|
||||||
|
containerClasses = classNames("mx_ToastContainer", {
|
||||||
|
"mx_ToastContainer_stacked": isStacked,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
return toast
|
||||||
const containerClasses = classNames("mx_ToastContainer", {
|
? (
|
||||||
"mx_ToastContainer_stacked": isStacked,
|
<div className={containerClasses} role="alert">
|
||||||
});
|
{toast}
|
||||||
|
</div>
|
||||||
return (
|
)
|
||||||
<div className={containerClasses} role="alert">
|
: null;
|
||||||
{toast}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -366,9 +366,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
const mxDomain = MatrixClientPeg.get().getDomain();
|
const mxDomain = MatrixClientPeg.get().getDomain();
|
||||||
const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`)));
|
const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`)));
|
||||||
if (!hostSignupConfig.domains || validDomains.length > 0) {
|
if (!hostSignupConfig.domains || validDomains.length > 0) {
|
||||||
topSection = <div onClick={this.onCloseMenu}>
|
topSection = <HostSignupAction onClick={this.onCloseMenu} />;
|
||||||
<HostSignupAction />
|
|
||||||
</div>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ interface IProps {
|
||||||
is_url?: string;
|
is_url?: string;
|
||||||
session_id: string;
|
session_id: string;
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
}): void;
|
}): string;
|
||||||
// registration shouldn't know or care how login is done.
|
// registration shouldn't know or care how login is done.
|
||||||
onLoginClick(): void;
|
onLoginClick(): void;
|
||||||
onServerConfigChange(config: ValidatedServerConfig): void;
|
onServerConfigChange(config: ValidatedServerConfig): void;
|
||||||
|
|
|
@ -22,6 +22,7 @@ import classNames from 'classnames';
|
||||||
import * as AvatarLogic from '../../../Avatar';
|
import * as AvatarLogic from '../../../Avatar';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import RoomContext from "../../../contexts/RoomContext";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||||
import {toPx} from "../../../utils/units";
|
import {toPx} from "../../../utils/units";
|
||||||
|
@ -44,12 +45,12 @@ interface IProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculateUrls = (url, urls) => {
|
const calculateUrls = (url, urls, lowBandwidth) => {
|
||||||
// work out the full set of urls to try to load. This is formed like so:
|
// work out the full set of urls to try to load. This is formed like so:
|
||||||
// imageUrls: [ props.url, ...props.urls ]
|
// imageUrls: [ props.url, ...props.urls ]
|
||||||
|
|
||||||
let _urls = [];
|
let _urls = [];
|
||||||
if (!SettingsStore.getValue("lowBandwidth")) {
|
if (!lowBandwidth) {
|
||||||
_urls = urls || [];
|
_urls = urls || [];
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
|
@ -63,7 +64,13 @@ const calculateUrls = (url, urls) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const useImageUrl = ({url, urls}): [string, () => void] => {
|
const useImageUrl = ({url, urls}): [string, () => void] => {
|
||||||
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls));
|
// Since this is a hot code path and the settings store can be slow, we
|
||||||
|
// use the cached lowBandwidth value from the room context if it exists
|
||||||
|
const roomContext = useContext(RoomContext);
|
||||||
|
const lowBandwidth = roomContext ?
|
||||||
|
roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth");
|
||||||
|
|
||||||
|
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls, lowBandwidth));
|
||||||
const [urlsIndex, setIndex] = useState<number>(0);
|
const [urlsIndex, setIndex] = useState<number>(0);
|
||||||
|
|
||||||
const onError = useCallback(() => {
|
const onError = useCallback(() => {
|
||||||
|
@ -71,7 +78,7 @@ const useImageUrl = ({url, urls}): [string, () => void] => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUrls(calculateUrls(url, urls));
|
setUrls(calculateUrls(url, urls, lowBandwidth));
|
||||||
setIndex(0);
|
setIndex(0);
|
||||||
}, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,9 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
import { EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||||
|
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -28,9 +28,11 @@ import Resend from '../../../Resend';
|
||||||
import SettingsStore from '../../../settings/SettingsStore';
|
import SettingsStore from '../../../settings/SettingsStore';
|
||||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||||
import { isContentActionable } from '../../../utils/EventUtils';
|
import { isContentActionable } from '../../../utils/EventUtils';
|
||||||
import {MenuItem} from "../../structures/ContextMenu";
|
import { MenuItem } from "../../structures/ContextMenu";
|
||||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
|
||||||
|
import ForwardDialog from "../dialogs/ForwardDialog";
|
||||||
|
|
||||||
export function canCancel(eventStatus) {
|
export function canCancel(eventStatus) {
|
||||||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||||
|
@ -82,7 +84,7 @@ export default class MessageContextMenu extends React.Component {
|
||||||
const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId)
|
const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId)
|
||||||
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl
|
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl
|
||||||
&& this.props.mxEvent.getType() !== EventType.RoomEncryption;
|
&& this.props.mxEvent.getType() !== EventType.RoomEncryption;
|
||||||
let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli);
|
let canPin = room.currentState.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli);
|
||||||
|
|
||||||
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
|
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
|
||||||
if (!SettingsStore.getValue("feature_pinning")) canPin = false;
|
if (!SettingsStore.getValue("feature_pinning")) canPin = false;
|
||||||
|
@ -92,7 +94,7 @@ export default class MessageContextMenu extends React.Component {
|
||||||
|
|
||||||
_isPinned() {
|
_isPinned() {
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||||
const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', '');
|
const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, '');
|
||||||
if (!pinnedEvent) return false;
|
if (!pinnedEvent) return false;
|
||||||
const content = pinnedEvent.getContent();
|
const content = pinnedEvent.getContent();
|
||||||
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
|
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
|
||||||
|
@ -156,34 +158,32 @@ export default class MessageContextMenu extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
onForwardClick = () => {
|
onForwardClick = () => {
|
||||||
if (this.props.onCloseDialog) this.props.onCloseDialog();
|
Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
|
||||||
dis.dispatch({
|
matrixClient: MatrixClientPeg.get(),
|
||||||
action: 'forward_event',
|
|
||||||
event: this.props.mxEvent,
|
event: this.props.mxEvent,
|
||||||
|
permalinkCreator: this.props.permalinkCreator,
|
||||||
});
|
});
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
onPinClick = () => {
|
onPinClick = () => {
|
||||||
MatrixClientPeg.get().getStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', '')
|
const cli = MatrixClientPeg.get();
|
||||||
.catch((e) => {
|
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||||
// Intercept the Event Not Found error and fall through the promise chain with no event.
|
const eventId = this.props.mxEvent.getId();
|
||||||
if (e.errcode === "M_NOT_FOUND") return null;
|
|
||||||
throw e;
|
|
||||||
})
|
|
||||||
.then((event) => {
|
|
||||||
const eventIds = (event ? event.pinned : []) || [];
|
|
||||||
if (!eventIds.includes(this.props.mxEvent.getId())) {
|
|
||||||
// Not pinned - add
|
|
||||||
eventIds.push(this.props.mxEvent.getId());
|
|
||||||
} else {
|
|
||||||
// Pinned - remove
|
|
||||||
eventIds.splice(eventIds.indexOf(this.props.mxEvent.getId()), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.pinned || [];
|
||||||
cli.sendStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', {pinned: eventIds}, '');
|
if (pinnedIds.includes(eventId)) {
|
||||||
|
pinnedIds.splice(pinnedIds.indexOf(eventId), 1);
|
||||||
|
} else {
|
||||||
|
pinnedIds.push(eventId);
|
||||||
|
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
||||||
|
event_ids: [
|
||||||
|
...room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids,
|
||||||
|
eventId,
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
cli.sendStateEvent(this.props.mxEvent.getRoomId(), EventType.RoomPinnedEvents, { pinned: pinnedIds }, "");
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,8 @@ interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
|
||||||
showUnpin?: boolean;
|
showUnpin?: boolean;
|
||||||
// override delete handler
|
// override delete handler
|
||||||
onDeleteClick?(): void;
|
onDeleteClick?(): void;
|
||||||
|
// override edit handler
|
||||||
|
onEditClick?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WidgetContextMenu: React.FC<IProps> = ({
|
const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
|
@ -47,6 +49,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
app,
|
app,
|
||||||
userWidget,
|
userWidget,
|
||||||
onDeleteClick,
|
onDeleteClick,
|
||||||
|
onEditClick,
|
||||||
showUnpin,
|
showUnpin,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -89,12 +92,16 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
|
|
||||||
let editButton;
|
let editButton;
|
||||||
if (canModify && WidgetUtils.isManagedByManager(app)) {
|
if (canModify && WidgetUtils.isManagedByManager(app)) {
|
||||||
const onEditClick = () => {
|
const _onEditClick = () => {
|
||||||
WidgetUtils.editWidget(room, app);
|
if (onEditClick) {
|
||||||
|
onEditClick();
|
||||||
|
} else {
|
||||||
|
WidgetUtils.editWidget(room, app);
|
||||||
|
}
|
||||||
onFinished();
|
onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
editButton = <IconizedContextMenuOption onClick={onEditClick} label={_t("Edit")} />;
|
editButton = <IconizedContextMenuOption onClick={_onEditClick} label={_t("Edit")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let snapshotButton;
|
let snapshotButton;
|
||||||
|
@ -116,24 +123,29 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
||||||
|
|
||||||
let deleteButton;
|
let deleteButton;
|
||||||
if (onDeleteClick || canModify) {
|
if (onDeleteClick || canModify) {
|
||||||
const onDeleteClickDefault = () => {
|
const _onDeleteClick = () => {
|
||||||
// Show delete confirmation dialog
|
if (onDeleteClick) {
|
||||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
onDeleteClick();
|
||||||
title: _t("Delete Widget"),
|
} else {
|
||||||
description: _t(
|
// Show delete confirmation dialog
|
||||||
"Deleting a widget removes it for all users in this room." +
|
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||||
" Are you sure you want to delete this widget?"),
|
title: _t("Delete Widget"),
|
||||||
button: _t("Delete widget"),
|
description: _t(
|
||||||
onFinished: (confirmed) => {
|
"Deleting a widget removes it for all users in this room." +
|
||||||
if (!confirmed) return;
|
" Are you sure you want to delete this widget?"),
|
||||||
WidgetUtils.setRoomWidget(roomId, app.id);
|
button: _t("Delete widget"),
|
||||||
},
|
onFinished: (confirmed) => {
|
||||||
});
|
if (!confirmed) return;
|
||||||
|
WidgetUtils.setRoomWidget(roomId, app.id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onFinished();
|
onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteButton = <IconizedContextMenuOption
|
deleteButton = <IconizedContextMenuOption
|
||||||
onClick={onDeleteClick || onDeleteClickDefault}
|
onClick={_onDeleteClick}
|
||||||
label={userWidget ? _t("Remove") : _t("Remove for everyone")}
|
label={userWidget ? _t("Remove") : _t("Remove for everyone")}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,22 +15,23 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {ChangeEvent, createRef, KeyboardEvent, SyntheticEvent} from "react";
|
import React, { ChangeEvent, createRef, KeyboardEvent, SyntheticEvent } from "react";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import withValidation, {IFieldState} from '../elements/Validation';
|
import withValidation, { IFieldState } from '../elements/Validation';
|
||||||
import {_t} from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import {Key} from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import {IOpts, Preset, privateShouldBeEncrypted, Visibility} from "../../../createRoom";
|
import { IOpts, privateShouldBeEncrypted } from "../../../createRoom";
|
||||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import RoomAliasField from "../elements/RoomAliasField";
|
import RoomAliasField from "../elements/RoomAliasField";
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
import BaseDialog from "../dialogs/BaseDialog";
|
import BaseDialog from "../dialogs/BaseDialog";
|
||||||
|
import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
defaultPublic?: boolean;
|
defaultPublic?: boolean;
|
||||||
|
@ -72,7 +73,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||||
canChangeEncryption: true,
|
canChangeEncryption: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
MatrixClientPeg.get().doesServerForceEncryptionForPreset("private")
|
MatrixClientPeg.get().doesServerForceEncryptionForPreset(Preset.PrivateChat)
|
||||||
.then(isForced => this.setState({ canChangeEncryption: !isForced }));
|
.then(isForced => this.setState({ canChangeEncryption: !isForced }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -766,7 +766,7 @@ class VerificationExplorer extends React.PureComponent<IExplorerProps> {
|
||||||
render() {
|
render() {
|
||||||
const cli = this.context;
|
const cli = this.context;
|
||||||
const room = this.props.room;
|
const room = this.props.room;
|
||||||
const inRoomChannel = cli._crypto._inRoomVerificationRequests;
|
const inRoomChannel = cli.crypto._inRoomVerificationRequests;
|
||||||
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
|
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
|
||||||
|
|
||||||
return (<div>
|
return (<div>
|
||||||
|
|
|
@ -0,0 +1,247 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Robin Townsend <robin@robin.town>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {useMemo, useState, useEffect} from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||||
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
|
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
|
import {_t} from "../../../languageHandler";
|
||||||
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
import {useSettingValue, useFeatureEnabled} from "../../../hooks/useSettings";
|
||||||
|
import {UIFeature} from "../../../settings/UIFeature";
|
||||||
|
import {Layout} from "../../../settings/Layout";
|
||||||
|
import {IDialogProps} from "./IDialogProps";
|
||||||
|
import BaseDialog from "./BaseDialog";
|
||||||
|
import {avatarUrlForUser} from "../../../Avatar";
|
||||||
|
import EventTile from "../rooms/EventTile";
|
||||||
|
import SearchBox from "../../structures/SearchBox";
|
||||||
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
|
import {Alignment} from '../elements/Tooltip';
|
||||||
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||||
|
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
|
||||||
|
import NotificationBadge from "../rooms/NotificationBadge";
|
||||||
|
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||||
|
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||||
|
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||||
|
|
||||||
|
const AVATAR_SIZE = 30;
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
matrixClient: MatrixClient;
|
||||||
|
// The event to forward
|
||||||
|
event: MatrixEvent;
|
||||||
|
// We need a permalink creator for the source room to pass through to EventTile
|
||||||
|
// in case the event is a reply (even though the user can't get at the link)
|
||||||
|
permalinkCreator: RoomPermalinkCreator;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IEntryProps {
|
||||||
|
room: Room;
|
||||||
|
event: MatrixEvent;
|
||||||
|
matrixClient: MatrixClient;
|
||||||
|
onFinished(success: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SendState {
|
||||||
|
CanSend,
|
||||||
|
Sending,
|
||||||
|
Sent,
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Entry: React.FC<IEntryProps> = ({ room, event, matrixClient: cli, onFinished }) => {
|
||||||
|
const [sendState, setSendState] = useState<SendState>(SendState.CanSend);
|
||||||
|
|
||||||
|
const jumpToRoom = () => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: "view_room",
|
||||||
|
room_id: room.roomId,
|
||||||
|
});
|
||||||
|
onFinished(true);
|
||||||
|
};
|
||||||
|
const send = async () => {
|
||||||
|
setSendState(SendState.Sending);
|
||||||
|
try {
|
||||||
|
await cli.sendEvent(room.roomId, event.getType(), event.getContent());
|
||||||
|
setSendState(SendState.Sent);
|
||||||
|
} catch (e) {
|
||||||
|
setSendState(SendState.Failed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let className;
|
||||||
|
let disabled = false;
|
||||||
|
let title;
|
||||||
|
let icon;
|
||||||
|
if (sendState === SendState.CanSend) {
|
||||||
|
className = "mx_ForwardList_canSend";
|
||||||
|
if (room.maySendMessage()) {
|
||||||
|
title = _t("Send");
|
||||||
|
} else {
|
||||||
|
disabled = true;
|
||||||
|
title = _t("You don't have permission to do this");
|
||||||
|
}
|
||||||
|
} else if (sendState === SendState.Sending) {
|
||||||
|
className = "mx_ForwardList_sending";
|
||||||
|
disabled = true;
|
||||||
|
title = _t("Sending");
|
||||||
|
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
|
||||||
|
} else if (sendState === SendState.Sent) {
|
||||||
|
className = "mx_ForwardList_sent";
|
||||||
|
disabled = true;
|
||||||
|
title = _t("Sent");
|
||||||
|
icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
|
||||||
|
} else {
|
||||||
|
className = "mx_ForwardList_sendFailed";
|
||||||
|
disabled = true;
|
||||||
|
title = _t("Failed to send");
|
||||||
|
icon = <NotificationBadge
|
||||||
|
notification={StaticNotificationState.RED_EXCLAMATION}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_ForwardList_entry">
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_ForwardList_roomButton"
|
||||||
|
onClick={jumpToRoom}
|
||||||
|
title={_t("Open link")}
|
||||||
|
yOffset={-20}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
>
|
||||||
|
<DecoratedRoomAvatar room={room} avatarSize={32} />
|
||||||
|
<span className="mx_ForwardList_entry_name">{ room.name }</span>
|
||||||
|
</AccessibleTooltipButton>
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"}
|
||||||
|
className={`mx_ForwardList_sendButton ${className}`}
|
||||||
|
onClick={send}
|
||||||
|
disabled={disabled}
|
||||||
|
title={title}
|
||||||
|
yOffset={-20}
|
||||||
|
alignment={Alignment.Top}
|
||||||
|
>
|
||||||
|
<div className="mx_ForwardList_sendLabel">{ _t("Send") }</div>
|
||||||
|
{ icon }
|
||||||
|
</AccessibleTooltipButton>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCreator, onFinished }) => {
|
||||||
|
const userId = cli.getUserId();
|
||||||
|
const [profileInfo, setProfileInfo] = useState<any>({});
|
||||||
|
useEffect(() => {
|
||||||
|
cli.getProfileInfo(userId).then(info => setProfileInfo(info));
|
||||||
|
}, [cli, userId]);
|
||||||
|
|
||||||
|
// For the message preview we fake the sender as ourselves
|
||||||
|
const mockEvent = new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: userId,
|
||||||
|
content: event.getContent(),
|
||||||
|
unsigned: {
|
||||||
|
age: 97,
|
||||||
|
},
|
||||||
|
event_id: "$9999999999999999999999999999999999999999999",
|
||||||
|
room_id: event.getRoomId(),
|
||||||
|
});
|
||||||
|
mockEvent.sender = {
|
||||||
|
name: profileInfo.displayname || userId,
|
||||||
|
userId,
|
||||||
|
getAvatarUrl: (..._) => {
|
||||||
|
return avatarUrlForUser(
|
||||||
|
{ avatarUrl: profileInfo.avatar_url },
|
||||||
|
AVATAR_SIZE, AVATAR_SIZE, "crop",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getMxcAvatarUrl: () => profileInfo.avatar_url,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const lcQuery = query.toLowerCase();
|
||||||
|
|
||||||
|
const spacesEnabled = useFeatureEnabled("feature_spaces");
|
||||||
|
const flairEnabled = useFeatureEnabled(UIFeature.Flair);
|
||||||
|
const previewLayout = useSettingValue<Layout>("layout");
|
||||||
|
|
||||||
|
let rooms = useMemo(() => sortRooms(
|
||||||
|
cli.getVisibleRooms().filter(
|
||||||
|
room => room.getMyMembership() === "join" &&
|
||||||
|
!(spacesEnabled && room.isSpaceRoom()),
|
||||||
|
),
|
||||||
|
), [cli, spacesEnabled]);
|
||||||
|
|
||||||
|
if (lcQuery) {
|
||||||
|
rooms = new QueryMatcher<Room>(rooms, {
|
||||||
|
keys: ["name"],
|
||||||
|
funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
|
||||||
|
shouldMatchWordsOnly: false,
|
||||||
|
}).match(lcQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BaseDialog
|
||||||
|
title={_t("Forward message")}
|
||||||
|
className="mx_ForwardDialog"
|
||||||
|
contentId="mx_ForwardList"
|
||||||
|
onFinished={onFinished}
|
||||||
|
fixedWidth={false}
|
||||||
|
>
|
||||||
|
<h3>{ _t("Message preview") }</h3>
|
||||||
|
<div className={classnames("mx_ForwardDialog_preview", {
|
||||||
|
"mx_IRCLayout": previewLayout == Layout.IRC,
|
||||||
|
"mx_GroupLayout": previewLayout == Layout.Group,
|
||||||
|
})}>
|
||||||
|
<EventTile
|
||||||
|
mxEvent={mockEvent}
|
||||||
|
layout={previewLayout}
|
||||||
|
enableFlair={flairEnabled}
|
||||||
|
permalinkCreator={permalinkCreator}
|
||||||
|
as="div"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div className="mx_ForwardList" id="mx_ForwardList">
|
||||||
|
<SearchBox
|
||||||
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
|
placeholder={_t("Search for rooms or people")}
|
||||||
|
onSearch={setQuery}
|
||||||
|
autoComplete={true}
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
<AutoHideScrollbar className="mx_ForwardList_content">
|
||||||
|
{ rooms.length > 0 ? (
|
||||||
|
<div className="mx_ForwardList_results">
|
||||||
|
{ rooms.map(room =>
|
||||||
|
<Entry
|
||||||
|
key={room.roomId}
|
||||||
|
room={room}
|
||||||
|
event={event}
|
||||||
|
matrixClient={cli}
|
||||||
|
onFinished={onFinished}
|
||||||
|
/>,
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
) : <span className="mx_ForwardList_noResults">
|
||||||
|
{ _t("No results") }
|
||||||
|
</span> }
|
||||||
|
</AutoHideScrollbar>
|
||||||
|
</div>
|
||||||
|
</BaseDialog>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ForwardDialog;
|
|
@ -15,5 +15,5 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface IDialogProps {
|
export interface IDialogProps {
|
||||||
onFinished: (bool) => void;
|
onFinished(...args: any): void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,6 @@ import Modal from "../../../Modal";
|
||||||
import {humanizeTime} from "../../../utils/humanize";
|
import {humanizeTime} from "../../../utils/humanize";
|
||||||
import createRoom, {
|
import createRoom, {
|
||||||
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
|
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
|
||||||
IInvite3PID,
|
|
||||||
} from "../../../createRoom";
|
} from "../../../createRoom";
|
||||||
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
||||||
import {Key} from "../../../Keyboard";
|
import {Key} from "../../../Keyboard";
|
||||||
|
@ -50,6 +49,7 @@ import {getAddressType} from "../../../UserAddress";
|
||||||
import BaseAvatar from '../avatars/BaseAvatar';
|
import BaseAvatar from '../avatars/BaseAvatar';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import { compare } from '../../../utils/strings';
|
import { compare } from '../../../utils/strings';
|
||||||
|
import { IInvite3PID } from "matrix-js-sdk/src/@types/requests";
|
||||||
|
|
||||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
|
@ -159,7 +159,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
|
||||||
stickyBottom={false}
|
stickyBottom={false}
|
||||||
startAtBottom={false}
|
startAtBottom={false}
|
||||||
>
|
>
|
||||||
<ul className="mx_MessageEditHistoryDialog_edits mx_MessagePanel_alwaysShowTimestamps">{this._renderEdits()}</ul>
|
<ul className="mx_MessageEditHistoryDialog_edits">{this._renderEdits()}</ul>
|
||||||
</ScrollPanel>);
|
</ScrollPanel>);
|
||||||
}
|
}
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import TabbedView, {Tab} from "../../structures/TabbedView";
|
import TabbedView, {Tab} from "../../structures/TabbedView";
|
||||||
import {_t, _td} from "../../../languageHandler";
|
import {_t, _td} from "../../../languageHandler";
|
||||||
import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
|
import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
|
||||||
|
@ -39,31 +38,36 @@ export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB";
|
||||||
export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB";
|
export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB";
|
||||||
export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB";
|
export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
roomId: string;
|
||||||
|
onFinished: (success: boolean) => void;
|
||||||
|
initialTabId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.RoomSettingsDialog")
|
@replaceableComponent("views.dialogs.RoomSettingsDialog")
|
||||||
export default class RoomSettingsDialog extends React.Component {
|
export default class RoomSettingsDialog extends React.Component<IProps> {
|
||||||
static propTypes = {
|
private dispatcherRef: string;
|
||||||
roomId: PropTypes.string.isRequired,
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount() {
|
||||||
this._dispatcherRef = dis.register(this._onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
if (this._dispatcherRef) dis.unregister(this._dispatcherRef);
|
if (this.dispatcherRef) {
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAction = (payload) => {
|
private onAction = (payload): void => {
|
||||||
// When view changes below us, close the room settings
|
// When view changes below us, close the room settings
|
||||||
// whilst the modal is open this can only be triggered when someone hits Leave Room
|
// whilst the modal is open this can only be triggered when someone hits Leave Room
|
||||||
if (payload.action === 'view_home_page') {
|
if (payload.action === 'view_home_page') {
|
||||||
this.props.onFinished();
|
this.props.onFinished(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_getTabs() {
|
private getTabs(): Tab[] {
|
||||||
const tabs = [];
|
const tabs: Tab[] = [];
|
||||||
|
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
ROOM_GENERAL_TAB,
|
ROOM_GENERAL_TAB,
|
||||||
|
@ -123,7 +127,10 @@ export default class RoomSettingsDialog extends React.Component {
|
||||||
title={_t("Room Settings - %(roomName)s", {roomName})}
|
title={_t("Room Settings - %(roomName)s", {roomName})}
|
||||||
>
|
>
|
||||||
<div className='mx_SettingsDialog_content'>
|
<div className='mx_SettingsDialog_content'>
|
||||||
<TabbedView tabs={this._getTabs()} />
|
<TabbedView
|
||||||
|
tabs={this.getTabs()}
|
||||||
|
initialTabId={this.props.initialTabId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
|
@ -19,7 +19,7 @@ import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import AccessibleButton from "./AccessibleButton";
|
import AccessibleButton from "./AccessibleButton";
|
||||||
import Tooltip from './Tooltip';
|
import Tooltip, {Alignment} from './Tooltip';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||||
|
@ -28,6 +28,7 @@ interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||||
tooltipClassName?: string;
|
tooltipClassName?: string;
|
||||||
forceHide?: boolean;
|
forceHide?: boolean;
|
||||||
yOffset?: number;
|
yOffset?: number;
|
||||||
|
alignment?: Alignment;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -66,14 +67,15 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, ...props} = this.props;
|
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, ...props} = this.props;
|
||||||
|
|
||||||
const tip = this.state.hover ? <Tooltip
|
const tip = this.state.hover ? <Tooltip
|
||||||
className="mx_AccessibleTooltipButton_container"
|
className="mx_AccessibleTooltipButton_container"
|
||||||
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
|
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
|
||||||
label={tooltip || title}
|
label={tooltip || title}
|
||||||
yOffset={yOffset}
|
yOffset={yOffset}
|
||||||
/> : <div />;
|
alignment={alignment}
|
||||||
|
/> : null;
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -47,9 +47,14 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
// The key used for PersistedElement
|
// The key used for PersistedElement
|
||||||
this._persistKey = getPersistKey(this.props.app.id);
|
this._persistKey = getPersistKey(this.props.app.id);
|
||||||
this._sgWidget = new StopGapWidget(this.props);
|
try {
|
||||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
this._sgWidget = new StopGapWidget(this.props);
|
||||||
this._sgWidget.on("ready", this._onWidgetReady);
|
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||||
|
this._sgWidget.on("ready", this._onWidgetReady);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to construct widget", e);
|
||||||
|
this._sgWidget = null;
|
||||||
|
}
|
||||||
this.iframe = null; // ref to the iframe (callback style)
|
this.iframe = null; // ref to the iframe (callback style)
|
||||||
|
|
||||||
this.state = this._getNewState(props);
|
this.state = this._getNewState(props);
|
||||||
|
@ -97,7 +102,7 @@ export default class AppTile extends React.Component {
|
||||||
// Force the widget to be non-persistent (able to be deleted/forgotten)
|
// Force the widget to be non-persistent (able to be deleted/forgotten)
|
||||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
PersistedElement.destroyElement(this._persistKey);
|
||||||
this._sgWidget.stop();
|
if (this._sgWidget) this._sgWidget.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ hasPermissionToLoad });
|
this.setState({ hasPermissionToLoad });
|
||||||
|
@ -117,7 +122,7 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// Only fetch IM token on mount if we're showing and have permission to load
|
// Only fetch IM token on mount if we're showing and have permission to load
|
||||||
if (this.state.hasPermissionToLoad) {
|
if (this._sgWidget && this.state.hasPermissionToLoad) {
|
||||||
this._startWidget();
|
this._startWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,10 +151,15 @@ export default class AppTile extends React.Component {
|
||||||
if (this._sgWidget) {
|
if (this._sgWidget) {
|
||||||
this._sgWidget.stop();
|
this._sgWidget.stop();
|
||||||
}
|
}
|
||||||
this._sgWidget = new StopGapWidget(newProps);
|
try {
|
||||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
this._sgWidget = new StopGapWidget(newProps);
|
||||||
this._sgWidget.on("ready", this._onWidgetReady);
|
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||||
this._startWidget();
|
this._sgWidget.on("ready", this._onWidgetReady);
|
||||||
|
this._startWidget();
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to construct widget", e);
|
||||||
|
this._sgWidget = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_startWidget() {
|
_startWidget() {
|
||||||
|
@ -161,7 +171,7 @@ export default class AppTile extends React.Component {
|
||||||
_iframeRefChange = (ref) => {
|
_iframeRefChange = (ref) => {
|
||||||
this.iframe = ref;
|
this.iframe = ref;
|
||||||
if (ref) {
|
if (ref) {
|
||||||
this._sgWidget.start(ref);
|
if (this._sgWidget) this._sgWidget.start(ref);
|
||||||
} else {
|
} else {
|
||||||
this._resetWidget(this.props);
|
this._resetWidget(this.props);
|
||||||
}
|
}
|
||||||
|
@ -209,7 +219,7 @@ export default class AppTile extends React.Component {
|
||||||
// Delete the widget from the persisted store for good measure.
|
// Delete the widget from the persisted store for good measure.
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
PersistedElement.destroyElement(this._persistKey);
|
||||||
|
|
||||||
this._sgWidget.stop({forceDestroy: true});
|
if (this._sgWidget) this._sgWidget.stop({forceDestroy: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWidgetPrepared = () => {
|
_onWidgetPrepared = () => {
|
||||||
|
@ -340,7 +350,13 @@ export default class AppTile extends React.Component {
|
||||||
<Spinner message={_t("Loading...")} />
|
<Spinner message={_t("Loading...")} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
if (!this.state.hasPermissionToLoad) {
|
if (this._sgWidget === null) {
|
||||||
|
appTileBody = (
|
||||||
|
<div className={appTileBodyClass} style={appTileBodyStyles}>
|
||||||
|
<AppWarning errorMsg={_t("Error loading Widget")} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (!this.state.hasPermissionToLoad) {
|
||||||
// only possible for room widgets, can assert this.props.room here
|
// only possible for room widgets, can assert this.props.room here
|
||||||
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||||
appTileBody = (
|
appTileBody = (
|
||||||
|
@ -364,7 +380,7 @@ export default class AppTile extends React.Component {
|
||||||
if (this.isMixedContent()) {
|
if (this.isMixedContent()) {
|
||||||
appTileBody = (
|
appTileBody = (
|
||||||
<div className={appTileBodyClass} style={appTileBodyStyles}>
|
<div className={appTileBodyClass} style={appTileBodyStyles}>
|
||||||
<AppWarning errorMsg="Error - Mixed content" />
|
<AppWarning errorMsg={_t("Error - Mixed content")} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -417,6 +433,8 @@ export default class AppTile extends React.Component {
|
||||||
onFinished={this._closeContextMenu}
|
onFinished={this._closeContextMenu}
|
||||||
showUnpin={!this.props.userWidget}
|
showUnpin={!this.props.userWidget}
|
||||||
userWidget={this.props.userWidget}
|
userWidget={this.props.userWidget}
|
||||||
|
onEditClick={this.props.onEditClick}
|
||||||
|
onDeleteClick={this.props.onDeleteClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,9 +63,9 @@ const EventListSummary: React.FC<IProps> = ({
|
||||||
// If we are only given few events then just pass them through
|
// If we are only given few events then just pass them through
|
||||||
if (events.length < threshold) {
|
if (events.length < threshold) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
|
<li className="mx_EventListSummary" data-scroll-tokens={eventIds}>
|
||||||
{ children }
|
{ children }
|
||||||
</div>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -128,6 +128,7 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
||||||
mxEvent={event}
|
mxEvent={event}
|
||||||
layout={this.props.layout}
|
layout={this.props.layout}
|
||||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||||
|
as="div"
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,7 +116,7 @@ export default class Flair extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.profiles.length === 0) {
|
if (this.state.profiles.length === 0) {
|
||||||
return <span className="mx_Flair" />;
|
return null;
|
||||||
}
|
}
|
||||||
const avatars = this.state.profiles.map((profile, index) => {
|
const avatars = this.state.profiles.map((profile, index) => {
|
||||||
return <FlairAvatar key={index} groupProfile={profile} />;
|
return <FlairAvatar key={index} groupProfile={profile} />;
|
||||||
|
|
|
@ -19,20 +19,20 @@ limitations under the License.
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||||
import {Key} from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import FocusLock from "react-focus-lock";
|
import FocusLock from "react-focus-lock";
|
||||||
import MemberAvatar from "../avatars/MemberAvatar";
|
import MemberAvatar from "../avatars/MemberAvatar";
|
||||||
import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||||
import MessageContextMenu from "../context_menus/MessageContextMenu";
|
import MessageContextMenu from "../context_menus/MessageContextMenu";
|
||||||
import {aboveLeftOf, ContextMenu} from '../../structures/ContextMenu';
|
import { aboveLeftOf, ContextMenu } from '../../structures/ContextMenu';
|
||||||
import MessageTimestamp from "../messages/MessageTimestamp";
|
import MessageTimestamp from "../messages/MessageTimestamp";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {formatFullDate} from "../../../DateUtils";
|
import { formatFullDate } from "../../../DateUtils";
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"
|
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"
|
||||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import {normalizeWheelEvent} from "../../../utils/Mouse";
|
import { normalizeWheelEvent } from "../../../utils/Mouse";
|
||||||
|
|
||||||
// Max scale to keep gaps around the image
|
// Max scale to keep gaps around the image
|
||||||
const MAX_SCALE = 0.95;
|
const MAX_SCALE = 0.95;
|
||||||
|
@ -95,8 +95,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private initX = 0;
|
private initX = 0;
|
||||||
private initY = 0;
|
private initY = 0;
|
||||||
private lastX = 0;
|
|
||||||
private lastY = 0;
|
|
||||||
private previousX = 0;
|
private previousX = 0;
|
||||||
private previousY = 0;
|
private previousY = 0;
|
||||||
|
|
||||||
|
@ -105,23 +103,35 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
// needs to be passive in order to work with Chromium
|
// needs to be passive in order to work with Chromium
|
||||||
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
|
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
|
||||||
// We want to recalculate zoom whenever the window's size changes
|
// We want to recalculate zoom whenever the window's size changes
|
||||||
window.addEventListener("resize", this.calculateZoom);
|
window.addEventListener("resize", this.recalculateZoom);
|
||||||
// After the image loads for the first time we want to calculate the zoom
|
// After the image loads for the first time we want to calculate the zoom
|
||||||
this.image.current.addEventListener("load", this.calculateZoom);
|
this.image.current.addEventListener("load", this.recalculateZoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.focusLock.current.removeEventListener('wheel', this.onWheel);
|
this.focusLock.current.removeEventListener('wheel', this.onWheel);
|
||||||
window.removeEventListener("resize", this.calculateZoom);
|
window.removeEventListener("resize", this.recalculateZoom);
|
||||||
this.image.current.removeEventListener("load", this.calculateZoom);
|
this.image.current.removeEventListener("load", this.recalculateZoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateZoom = () => {
|
private recalculateZoom = () => {
|
||||||
|
this.setZoomAndRotation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setZoomAndRotation = (inputRotation?: number) => {
|
||||||
const image = this.image.current;
|
const image = this.image.current;
|
||||||
const imageWrapper = this.imageWrapper.current;
|
const imageWrapper = this.imageWrapper.current;
|
||||||
|
|
||||||
const zoomX = imageWrapper.clientWidth / image.naturalWidth;
|
const rotation = inputRotation || this.state.rotation;
|
||||||
const zoomY = imageWrapper.clientHeight / image.naturalHeight;
|
|
||||||
|
const imageIsNotFlipped = rotation % 180 === 0;
|
||||||
|
|
||||||
|
// If the image is rotated take it into account
|
||||||
|
const width = imageIsNotFlipped ? image.naturalWidth : image.naturalHeight;
|
||||||
|
const height = imageIsNotFlipped ? image.naturalHeight : image.naturalWidth;
|
||||||
|
|
||||||
|
const zoomX = imageWrapper.clientWidth / width;
|
||||||
|
const zoomY = imageWrapper.clientHeight / height;
|
||||||
|
|
||||||
// If the image is smaller in both dimensions set its the zoom to 1 to
|
// If the image is smaller in both dimensions set its the zoom to 1 to
|
||||||
// display it in its original size
|
// display it in its original size
|
||||||
|
@ -130,6 +140,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
minZoom: 1,
|
minZoom: 1,
|
||||||
maxZoom: 1,
|
maxZoom: 1,
|
||||||
|
rotation: rotation,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -138,10 +149,14 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
// image by default
|
// image by default
|
||||||
const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE;
|
const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE;
|
||||||
|
|
||||||
if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom});
|
// If zoom is smaller than minZoom don't go below that value
|
||||||
|
const zoom = (this.state.zoom <= this.state.minZoom) ? minZoom : this.state.zoom;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
minZoom: minZoom,
|
minZoom: minZoom,
|
||||||
maxZoom: 1,
|
maxZoom: 1,
|
||||||
|
rotation: rotation,
|
||||||
|
zoom: zoom,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +172,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (newZoom >= this.state.maxZoom) {
|
if (newZoom >= this.state.maxZoom) {
|
||||||
this.setState({zoom: this.state.maxZoom});
|
this.setState({ zoom: this.state.maxZoom });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,7 +185,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
const {deltaY} = normalizeWheelEvent(ev);
|
const { deltaY } = normalizeWheelEvent(ev);
|
||||||
this.zoom(-(deltaY * ZOOM_COEFFICIENT));
|
this.zoom(-(deltaY * ZOOM_COEFFICIENT));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -192,14 +207,12 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onRotateCounterClockwiseClick = () => {
|
private onRotateCounterClockwiseClick = () => {
|
||||||
const cur = this.state.rotation;
|
const cur = this.state.rotation;
|
||||||
const rotationDegrees = cur - 90;
|
this.setZoomAndRotation(cur - 90);
|
||||||
this.setState({ rotation: rotationDegrees });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRotateClockwiseClick = () => {
|
private onRotateClockwiseClick = () => {
|
||||||
const cur = this.state.rotation;
|
const cur = this.state.rotation;
|
||||||
const rotationDegrees = cur + 90;
|
this.setZoomAndRotation(cur + 90);
|
||||||
this.setState({ rotation: rotationDegrees });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onDownloadClick = () => {
|
private onDownloadClick = () => {
|
||||||
|
@ -246,15 +259,15 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// Zoom in if we are completely zoomed out
|
// Zoom in if we are completely zoomed out
|
||||||
if (this.state.zoom === this.state.minZoom) {
|
if (this.state.zoom === this.state.minZoom) {
|
||||||
this.setState({zoom: this.state.maxZoom});
|
this.setState({ zoom: this.state.maxZoom });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({moving: true});
|
this.setState({ moving: true });
|
||||||
this.previousX = this.state.translationX;
|
this.previousX = this.state.translationX;
|
||||||
this.previousY = this.state.translationY;
|
this.previousY = this.state.translationY;
|
||||||
this.initX = ev.pageX - this.lastX;
|
this.initX = ev.pageX - this.state.translationX;
|
||||||
this.initY = ev.pageY - this.lastY;
|
this.initY = ev.pageY - this.state.translationY;
|
||||||
};
|
};
|
||||||
|
|
||||||
private onMoving = (ev: React.MouseEvent) => {
|
private onMoving = (ev: React.MouseEvent) => {
|
||||||
|
@ -263,11 +276,9 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
if (!this.state.moving) return;
|
if (!this.state.moving) return;
|
||||||
|
|
||||||
this.lastX = ev.pageX - this.initX;
|
|
||||||
this.lastY = ev.pageY - this.initY;
|
|
||||||
this.setState({
|
this.setState({
|
||||||
translationX: this.lastX,
|
translationX: ev.pageX - this.initX,
|
||||||
translationY: this.lastY,
|
translationY: ev.pageY - this.initY,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -283,8 +294,10 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
translationX: 0,
|
translationX: 0,
|
||||||
translationY: 0,
|
translationY: 0,
|
||||||
});
|
});
|
||||||
|
this.initX = 0;
|
||||||
|
this.initY = 0;
|
||||||
}
|
}
|
||||||
this.setState({moving: false});
|
this.setState({ moving: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderContextMenu() {
|
private renderContextMenu() {
|
||||||
|
@ -355,7 +368,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||||
const sender = (
|
const sender = (
|
||||||
<div className="mx_ImageView_info_sender">
|
<div className="mx_ImageView_info_sender">
|
||||||
{senderName}
|
{ senderName }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
const messageTimestamp = (
|
const messageTimestamp = (
|
||||||
|
@ -382,10 +395,10 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
info = (
|
info = (
|
||||||
<div className="mx_ImageView_info_wrapper">
|
<div className="mx_ImageView_info_wrapper">
|
||||||
{avatar}
|
{ avatar }
|
||||||
<div className="mx_ImageView_info">
|
<div className="mx_ImageView_info">
|
||||||
{sender}
|
{ sender }
|
||||||
{messageTimestamp}
|
{ messageTimestamp }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -425,7 +438,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className="mx_ImageView_button mx_ImageView_button_zoomIn"
|
className="mx_ImageView_button mx_ImageView_button_zoomIn"
|
||||||
title={_t("Zoom in")}
|
title={_t("Zoom in")}
|
||||||
onClick={ this.onZoomInClick }>
|
onClick={this.onZoomInClick}>
|
||||||
</AccessibleTooltipButton>
|
</AccessibleTooltipButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -441,7 +454,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
ref={this.focusLock}
|
ref={this.focusLock}
|
||||||
>
|
>
|
||||||
<div className="mx_ImageView_panel">
|
<div className="mx_ImageView_panel">
|
||||||
{info}
|
{ info }
|
||||||
<div className="mx_ImageView_toolbar">
|
<div className="mx_ImageView_toolbar">
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
|
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
|
||||||
|
@ -453,25 +466,30 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
title={_t("Rotate Right")}
|
title={_t("Rotate Right")}
|
||||||
onClick={this.onRotateClockwiseClick}>
|
onClick={this.onRotateClockwiseClick}>
|
||||||
</AccessibleTooltipButton>
|
</AccessibleTooltipButton>
|
||||||
{zoomOutButton}
|
{ zoomOutButton }
|
||||||
{zoomInButton}
|
{ zoomInButton }
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className="mx_ImageView_button mx_ImageView_button_download"
|
className="mx_ImageView_button mx_ImageView_button_download"
|
||||||
title={_t("Download")}
|
title={_t("Download")}
|
||||||
onClick={ this.onDownloadClick }>
|
onClick={ this.onDownloadClick }>
|
||||||
</AccessibleTooltipButton>
|
</AccessibleTooltipButton>
|
||||||
{contextMenuButton}
|
{ contextMenuButton }
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className="mx_ImageView_button mx_ImageView_button_close"
|
className="mx_ImageView_button mx_ImageView_button_close"
|
||||||
title={_t("Close")}
|
title={_t("Close")}
|
||||||
onClick={ this.props.onFinished }>
|
onClick={ this.props.onFinished }>
|
||||||
</AccessibleTooltipButton>
|
</AccessibleTooltipButton>
|
||||||
{this.renderContextMenu()}
|
{ this.renderContextMenu() }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="mx_ImageView_image_wrapper"
|
className="mx_ImageView_image_wrapper"
|
||||||
ref={this.imageWrapper}>
|
ref={this.imageWrapper}
|
||||||
|
onMouseDown={this.props.onFinished}
|
||||||
|
onMouseMove={this.onMoving}
|
||||||
|
onMouseUp={this.onEndMoving}
|
||||||
|
onMouseLeave={this.onEndMoving}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={this.props.src}
|
src={this.props.src}
|
||||||
title={this.props.name}
|
title={this.props.name}
|
||||||
|
@ -480,9 +498,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
className="mx_ImageView_image"
|
className="mx_ImageView_image"
|
||||||
draggable={true}
|
draggable={true}
|
||||||
onMouseDown={this.onStartMoving}
|
onMouseDown={this.onStartMoving}
|
||||||
onMouseMove={this.onMoving}
|
|
||||||
onMouseUp={this.onEndMoving}
|
|
||||||
onMouseLeave={this.onEndMoving}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FocusLock>
|
</FocusLock>
|
||||||
|
|
|
@ -46,6 +46,8 @@ export default class ReplyThread extends React.Component {
|
||||||
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
||||||
// Specifies which layout to use.
|
// Specifies which layout to use.
|
||||||
layout: LayoutPropType,
|
layout: LayoutPropType,
|
||||||
|
// Whether to always show a timestamp
|
||||||
|
alwaysShowTimestamps: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
@ -212,9 +214,9 @@ export default class ReplyThread extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) {
|
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout, alwaysShowTimestamps) {
|
||||||
if (!ReplyThread.getParentEventId(parentEv)) {
|
if (!ReplyThread.getParentEventId(parentEv)) {
|
||||||
return <div className="mx_ReplyThread_wrapper_empty" />;
|
return null;
|
||||||
}
|
}
|
||||||
return <ReplyThread
|
return <ReplyThread
|
||||||
parentEv={parentEv}
|
parentEv={parentEv}
|
||||||
|
@ -222,6 +224,7 @@ export default class ReplyThread extends React.Component {
|
||||||
ref={ref}
|
ref={ref}
|
||||||
permalinkCreator={permalinkCreator}
|
permalinkCreator={permalinkCreator}
|
||||||
layout={layout}
|
layout={layout}
|
||||||
|
alwaysShowTimestamps={alwaysShowTimestamps}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,36 +272,27 @@ export default class ReplyThread extends React.Component {
|
||||||
const {parentEv} = this.props;
|
const {parentEv} = this.props;
|
||||||
// at time of making this component we checked that props.parentEv has a parentEventId
|
// at time of making this component we checked that props.parentEv has a parentEventId
|
||||||
const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv));
|
const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv));
|
||||||
|
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
|
|
||||||
if (ev) {
|
if (ev) {
|
||||||
|
const loadedEv = await this.getNextEvent(ev);
|
||||||
this.setState({
|
this.setState({
|
||||||
events: [ev],
|
events: [ev],
|
||||||
}, this.loadNextEvent);
|
loadedEv,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({err: true});
|
this.setState({err: true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadNextEvent() {
|
async getNextEvent(ev) {
|
||||||
if (this.unmounted) return;
|
try {
|
||||||
const ev = this.state.events[0];
|
const inReplyToEventId = ReplyThread.getParentEventId(ev);
|
||||||
const inReplyToEventId = ReplyThread.getParentEventId(ev);
|
return await this.getEvent(inReplyToEventId);
|
||||||
|
} catch (e) {
|
||||||
if (!inReplyToEventId) {
|
return null;
|
||||||
this.setState({
|
|
||||||
loading: false,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadedEv = await this.getEvent(inReplyToEventId);
|
|
||||||
if (this.unmounted) return;
|
|
||||||
|
|
||||||
if (loadedEv) {
|
|
||||||
this.setState({loadedEv});
|
|
||||||
} else {
|
|
||||||
this.setState({err: true});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,13 +320,18 @@ export default class ReplyThread extends React.Component {
|
||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
onQuoteClick() {
|
async onQuoteClick() {
|
||||||
const events = [this.state.loadedEv, ...this.state.events];
|
const events = [this.state.loadedEv, ...this.state.events];
|
||||||
|
|
||||||
|
let loadedEv = null;
|
||||||
|
if (events.length > 0) {
|
||||||
|
loadedEv = await this.getNextEvent(events[0]);
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loadedEv: null,
|
loadedEv,
|
||||||
events,
|
events,
|
||||||
}, this.loadNextEvent);
|
});
|
||||||
|
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusComposer);
|
||||||
}
|
}
|
||||||
|
@ -390,6 +389,7 @@ export default class ReplyThread extends React.Component {
|
||||||
isRedacted={ev.isRedacted()}
|
isRedacted={ev.isRedacted()}
|
||||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||||
layout={this.props.layout}
|
layout={this.props.layout}
|
||||||
|
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
|
||||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||||
replacingEventId={ev.replacingEventId()}
|
replacingEventId={ev.replacingEventId()}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {forwardRef, ReactNode} from "react";
|
import React, {forwardRef, ReactNode, ReactChildren} from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
className: string;
|
className: string;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle?: ReactNode;
|
subtitle?: ReactNode;
|
||||||
|
children?: ReactChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EventTileBubble = forwardRef<HTMLDivElement, IProps>(({ className, title, subtitle, children }, ref) => {
|
const EventTileBubble = forwardRef<HTMLDivElement, IProps>(({ className, title, subtitle, children }, ref) => {
|
||||||
|
|
|
@ -15,37 +15,47 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import Flair from '../elements/Flair.js';
|
import Flair from '../elements/Flair.js';
|
||||||
import FlairStore from '../../../stores/FlairStore';
|
import FlairStore from '../../../stores/FlairStore';
|
||||||
import {getUserNameColorClass} from '../../../utils/FormattingUtils';
|
import {getUserNameColorClass} from '../../../utils/FormattingUtils';
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import MatrixEvent from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
mxEvent: MatrixEvent;
|
||||||
|
onClick(): void;
|
||||||
|
enableFlair: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
userGroups;
|
||||||
|
relatedGroups;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.messages.SenderProfile")
|
@replaceableComponent("views.messages.SenderProfile")
|
||||||
export default class SenderProfile extends React.Component {
|
export default class SenderProfile extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
|
||||||
mxEvent: PropTypes.object.isRequired, // event whose sender we're showing
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
private unmounted: boolean;
|
||||||
|
|
||||||
state = {
|
constructor(props: IProps) {
|
||||||
userGroups: null,
|
super(props)
|
||||||
relatedGroups: [],
|
const senderId = this.props.mxEvent.getSender();
|
||||||
};
|
|
||||||
|
this.state = {
|
||||||
|
userGroups: FlairStore.cachedPublicisedGroups(senderId) || [],
|
||||||
|
relatedGroups: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.unmounted = false;
|
this.unmounted = false;
|
||||||
this._updateRelatedGroups();
|
this._updateRelatedGroups();
|
||||||
|
|
||||||
FlairStore.getPublicisedGroupsCached(
|
if (this.state.userGroups.length === 0) {
|
||||||
this.context, this.props.mxEvent.getSender(),
|
this.getPublicisedGroups();
|
||||||
).then((userGroups) => {
|
}
|
||||||
if (this.unmounted) return;
|
|
||||||
this.setState({userGroups});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.context.on('RoomState.events', this.onRoomStateEvents);
|
this.context.on('RoomState.events', this.onRoomStateEvents);
|
||||||
}
|
}
|
||||||
|
@ -55,6 +65,15 @@ export default class SenderProfile extends React.Component {
|
||||||
this.context.removeListener('RoomState.events', this.onRoomStateEvents);
|
this.context.removeListener('RoomState.events', this.onRoomStateEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPublicisedGroups() {
|
||||||
|
if (!this.unmounted) {
|
||||||
|
const userGroups = await FlairStore.getPublicisedGroupsCached(
|
||||||
|
this.context, this.props.mxEvent.getSender(),
|
||||||
|
);
|
||||||
|
this.setState({userGroups});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onRoomStateEvents = event => {
|
onRoomStateEvents = event => {
|
||||||
if (event.getType() === 'm.room.related_groups' &&
|
if (event.getType() === 'm.room.related_groups' &&
|
||||||
event.getRoomId() === this.props.mxEvent.getRoomId()
|
event.getRoomId() === this.props.mxEvent.getRoomId()
|
||||||
|
@ -89,14 +108,26 @@ export default class SenderProfile extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const {mxEvent} = this.props;
|
const {mxEvent} = this.props;
|
||||||
const colorClass = getUserNameColorClass(mxEvent.getSender());
|
const colorClass = getUserNameColorClass(mxEvent.getSender());
|
||||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
|
||||||
const {msgtype} = mxEvent.getContent();
|
const {msgtype} = mxEvent.getContent();
|
||||||
|
|
||||||
|
const disambiguate = mxEvent.sender?.disambiguate;
|
||||||
|
const displayName = mxEvent.sender?.rawDisplayName || mxEvent.getSender() || "";
|
||||||
|
const mxid = mxEvent.sender?.userId || mxEvent.getSender() || "";
|
||||||
|
|
||||||
if (msgtype === 'm.emote') {
|
if (msgtype === 'm.emote') {
|
||||||
return <span />; // emote message must include the name so don't duplicate it
|
return null; // emote message must include the name so don't duplicate it
|
||||||
}
|
}
|
||||||
|
|
||||||
let flair = <div />;
|
let mxidElement;
|
||||||
|
if (disambiguate) {
|
||||||
|
mxidElement = (
|
||||||
|
<span className="mx_SenderProfile_mxid">
|
||||||
|
{ mxid }
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let flair;
|
||||||
if (this.props.enableFlair) {
|
if (this.props.enableFlair) {
|
||||||
const displayedGroups = this._getDisplayedGroups(
|
const displayedGroups = this._getDisplayedGroups(
|
||||||
this.state.userGroups, this.state.relatedGroups,
|
this.state.userGroups, this.state.relatedGroups,
|
||||||
|
@ -108,21 +139,13 @@ export default class SenderProfile extends React.Component {
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameElem = name || '';
|
|
||||||
|
|
||||||
// Name + flair
|
|
||||||
const nameFlair = <span>
|
|
||||||
<span className={`mx_SenderProfile_name ${colorClass}`}>
|
|
||||||
{ nameElem }
|
|
||||||
</span>
|
|
||||||
{ flair }
|
|
||||||
</span>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
|
<div className="mx_SenderProfile mx_SenderProfile_hover" dir="auto" onClick={this.props.onClick}>
|
||||||
<div className="mx_SenderProfile_hover">
|
<span className={`mx_SenderProfile_displayName ${colorClass}`}>
|
||||||
{ nameFlair }
|
{ displayName }
|
||||||
</div>
|
</span>
|
||||||
|
{ mxidElement }
|
||||||
|
{ flair }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -278,15 +278,15 @@ export default class TextualBody extends React.Component {
|
||||||
// pass only the first child which is the event tile otherwise this recurses on edited events
|
// pass only the first child which is the event tile otherwise this recurses on edited events
|
||||||
let links = this.findLinks([this._content.current]);
|
let links = this.findLinks([this._content.current]);
|
||||||
if (links.length) {
|
if (links.length) {
|
||||||
// de-dup the links (but preserve ordering)
|
// de-duplicate the links after stripping hashes as they don't affect the preview
|
||||||
const seen = new Set();
|
// using a set here maintains the order
|
||||||
links = links.filter((link) => {
|
links = Array.from(new Set(links.map(link => {
|
||||||
if (seen.has(link)) return false;
|
const url = new URL(link);
|
||||||
seen.add(link);
|
url.hash = "";
|
||||||
return true;
|
return url.toString();
|
||||||
});
|
})));
|
||||||
|
|
||||||
this.setState({ links: links });
|
this.setState({ links });
|
||||||
|
|
||||||
// lazy-load the hidden state of the preview widget from localstorage
|
// lazy-load the hidden state of the preview widget from localstorage
|
||||||
if (global.localStorage) {
|
if (global.localStorage) {
|
||||||
|
|
|
@ -21,12 +21,12 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import HeaderButton from './HeaderButton';
|
import HeaderButton from './HeaderButton';
|
||||||
import HeaderButtons, {HeaderKind} from './HeaderButtons';
|
import HeaderButtons, { HeaderKind } from './HeaderButtons';
|
||||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import {ActionPayload} from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
|
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
const GROUP_PHASES = [
|
const GROUP_PHASES = [
|
||||||
RightPanelPhases.GroupMemberInfo,
|
RightPanelPhases.GroupMemberInfo,
|
||||||
|
@ -84,19 +84,21 @@ export default class GroupHeaderButtons extends HeaderButtons {
|
||||||
};
|
};
|
||||||
|
|
||||||
renderButtons() {
|
renderButtons() {
|
||||||
return [
|
return <>
|
||||||
<HeaderButton key="groupMembersButton" name="groupMembersButton"
|
<HeaderButton
|
||||||
|
name="groupMembersButton"
|
||||||
title={_t('Members')}
|
title={_t('Members')}
|
||||||
isHighlighted={this.isPhase(GROUP_PHASES)}
|
isHighlighted={this.isPhase(GROUP_PHASES)}
|
||||||
onClick={this.onMembersClicked}
|
onClick={this.onMembersClicked}
|
||||||
analytics={['Right Panel', 'Group Member List Button', 'click']}
|
analytics={['Right Panel', 'Group Member List Button', 'click']}
|
||||||
/>,
|
/>
|
||||||
<HeaderButton key="roomsButton" name="roomsButton"
|
<HeaderButton
|
||||||
|
name="roomsButton"
|
||||||
title={_t('Rooms')}
|
title={_t('Rooms')}
|
||||||
isHighlighted={this.isPhase(ROOM_PHASES)}
|
isHighlighted={this.isPhase(ROOM_PHASES)}
|
||||||
onClick={this.onRoomsClicked}
|
onClick={this.onRoomsClicked}
|
||||||
analytics={['Right Panel', 'Group Room List Button', 'click']}
|
analytics={['Right Panel', 'Group Room List Button', 'click']}
|
||||||
/>,
|
/>
|
||||||
];
|
</>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,15 +22,13 @@ import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Analytics from '../../../Analytics';
|
import Analytics from '../../../Analytics';
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// Whether this button is highlighted
|
// Whether this button is highlighted
|
||||||
isHighlighted: boolean;
|
isHighlighted: boolean;
|
||||||
// click handler
|
// click handler
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
// The badge to display above the icon
|
|
||||||
badge?: React.ReactNode;
|
|
||||||
// The parameters to track the click event
|
// The parameters to track the click event
|
||||||
analytics: Parameters<typeof Analytics.trackEvent>;
|
analytics: Parameters<typeof Analytics.trackEvent>;
|
||||||
|
|
||||||
|
@ -40,31 +38,29 @@ interface IProps {
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: replace this, the composer buttons and the right panel buttons with a unified
|
// TODO: replace this, the composer buttons and the right panel buttons with a unified representation
|
||||||
// representation
|
|
||||||
@replaceableComponent("views.right_panel.HeaderButton")
|
@replaceableComponent("views.right_panel.HeaderButton")
|
||||||
export default class HeaderButton extends React.Component<IProps> {
|
export default class HeaderButton extends React.Component<IProps> {
|
||||||
constructor(props: IProps) {
|
private onClick = () => {
|
||||||
super(props);
|
|
||||||
this.onClick = this.onClick.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onClick() {
|
|
||||||
Analytics.trackEvent(...this.props.analytics);
|
Analytics.trackEvent(...this.props.analytics);
|
||||||
this.props.onClick();
|
this.props.onClick();
|
||||||
}
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const {isHighlighted, onClick, analytics, name, title, ...props} = this.props;
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
mx_RightPanel_headerButton: true,
|
mx_RightPanel_headerButton: true,
|
||||||
mx_RightPanel_headerButton_highlight: this.props.isHighlighted,
|
mx_RightPanel_headerButton_highlight: isHighlighted,
|
||||||
[`mx_RightPanel_${this.props.name}`]: true,
|
[`mx_RightPanel_${name}`]: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <AccessibleTooltipButton
|
return <AccessibleTooltipButton
|
||||||
aria-selected={this.props.isHighlighted}
|
{...props}
|
||||||
|
aria-selected={isHighlighted}
|
||||||
role="tab"
|
role="tab"
|
||||||
title={this.props.title}
|
title={title}
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -21,14 +21,14 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import RightPanelStore from "../../../stores/RightPanelStore";
|
import RightPanelStore from "../../../stores/RightPanelStore";
|
||||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||||
import {Action} from '../../../dispatcher/actions';
|
import { Action } from '../../../dispatcher/actions';
|
||||||
import {
|
import {
|
||||||
SetRightPanelPhasePayload,
|
SetRightPanelPhasePayload,
|
||||||
SetRightPanelPhaseRefireParams,
|
SetRightPanelPhaseRefireParams,
|
||||||
} from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
|
} from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
|
||||||
import {EventSubscription} from "fbemitter";
|
import type { EventSubscription } from "fbemitter";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
export enum HeaderKind {
|
export enum HeaderKind {
|
||||||
Room = "room",
|
Room = "room",
|
||||||
|
@ -43,11 +43,11 @@ interface IState {
|
||||||
interface IProps {}
|
interface IProps {}
|
||||||
|
|
||||||
@replaceableComponent("views.right_panel.HeaderButtons")
|
@replaceableComponent("views.right_panel.HeaderButtons")
|
||||||
export default abstract class HeaderButtons extends React.Component<IProps, IState> {
|
export default abstract class HeaderButtons<P = {}> extends React.Component<IProps & P, IState> {
|
||||||
private storeToken: EventSubscription;
|
private storeToken: EventSubscription;
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
|
|
||||||
constructor(props: IProps, kind: HeaderKind) {
|
constructor(props: IProps & P, kind: HeaderKind) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const rps = RightPanelStore.getSharedInstance();
|
const rps = RightPanelStore.getSharedInstance();
|
||||||
|
@ -95,7 +95,7 @@ export default abstract class HeaderButtons extends React.Component<IProps, ISta
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: Make renderButtons a prop
|
// XXX: Make renderButtons a prop
|
||||||
public abstract renderButtons(): JSX.Element[];
|
public abstract renderButtons(): JSX.Element;
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return <div className="mx_HeaderButtons">
|
return <div className="mx_HeaderButtons">
|
||||||
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {useCallback, useContext, useEffect, useState} from "react";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||||
|
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import BaseCard from "./BaseCard";
|
||||||
|
import Spinner from "../elements/Spinner";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||||
|
import PinningUtils from "../../../utils/PinningUtils";
|
||||||
|
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||||
|
import PinnedEventTile from "../rooms/PinnedEventTile";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
room: Room;
|
||||||
|
onClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePinnedEvents = (room: Room): string[] => {
|
||||||
|
const [pinnedEvents, setPinnedEvents] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const update = useCallback((ev?: MatrixEvent) => {
|
||||||
|
if (!room) return;
|
||||||
|
if (ev && ev.getType() !== EventType.RoomPinnedEvents) return;
|
||||||
|
setPinnedEvents(room.currentState.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent()?.pinned || []);
|
||||||
|
}, [room]);
|
||||||
|
|
||||||
|
useEventEmitter(room?.currentState, "RoomState.events", update);
|
||||||
|
useEffect(() => {
|
||||||
|
update();
|
||||||
|
return () => {
|
||||||
|
setPinnedEvents([]);
|
||||||
|
};
|
||||||
|
}, [update]);
|
||||||
|
return pinnedEvents;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReadPinsEventId = "im.vector.room.read_pins";
|
||||||
|
|
||||||
|
export const useReadPinnedEvents = (room: Room): Set<string> => {
|
||||||
|
const [readPinnedEvents, setReadPinnedEvents] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const update = useCallback((ev?: MatrixEvent) => {
|
||||||
|
if (!room) return;
|
||||||
|
if (ev && ev.getType() !== ReadPinsEventId) return;
|
||||||
|
const readPins = room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids;
|
||||||
|
setReadPinnedEvents(new Set(readPins || []));
|
||||||
|
}, [room]);
|
||||||
|
|
||||||
|
useEventEmitter(room, "Room.accountData", update);
|
||||||
|
useEffect(() => {
|
||||||
|
update();
|
||||||
|
return () => {
|
||||||
|
setReadPinnedEvents(new Set());
|
||||||
|
};
|
||||||
|
}, [update]);
|
||||||
|
return readPinnedEvents;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useRoomState = <T extends any>(room: Room, mapper: (state: RoomState) => T): T => {
|
||||||
|
const [value, setValue] = useState<T>(room ? mapper(room.currentState) : undefined);
|
||||||
|
|
||||||
|
const update = useCallback(() => {
|
||||||
|
if (!room) return;
|
||||||
|
setValue(mapper(room.currentState));
|
||||||
|
}, [room, mapper]);
|
||||||
|
|
||||||
|
useEventEmitter(room?.currentState, "RoomState.events", update);
|
||||||
|
useEffect(() => {
|
||||||
|
update();
|
||||||
|
return () => {
|
||||||
|
setValue(undefined);
|
||||||
|
};
|
||||||
|
}, [update]);
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PinnedMessagesCard = ({ room, onClose }: IProps) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli));
|
||||||
|
const pinnedEventIds = usePinnedEvents(room);
|
||||||
|
const readPinnedEvents = useReadPinnedEvents(room);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newlyRead = pinnedEventIds.filter(id => !readPinnedEvents.has(id));
|
||||||
|
if (newlyRead.length > 0) {
|
||||||
|
// clear out any read pinned events which no longer are pinned
|
||||||
|
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
||||||
|
event_ids: pinnedEventIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [cli, room.roomId, pinnedEventIds, readPinnedEvents]);
|
||||||
|
|
||||||
|
const pinnedEvents = useAsyncMemo(() => {
|
||||||
|
const promises = pinnedEventIds.map(async eventId => {
|
||||||
|
const timelineSet = room.getUnfilteredTimelineSet();
|
||||||
|
const localEvent = timelineSet?.getTimelineForEvent(eventId)?.getEvents().find(e => e.getId() === eventId);
|
||||||
|
if (localEvent) return localEvent;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const evJson = await cli.fetchRoomEvent(room.roomId, eventId);
|
||||||
|
const event = new MatrixEvent(evJson);
|
||||||
|
if (event.isEncrypted()) {
|
||||||
|
await cli.decryptEventIfNeeded(event); // TODO await?
|
||||||
|
}
|
||||||
|
if (event && PinningUtils.isPinnable(event)) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error looking up pinned event " + eventId + " in room " + room.roomId);
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}, [cli, room, pinnedEventIds], null);
|
||||||
|
|
||||||
|
let content;
|
||||||
|
if (!pinnedEvents) {
|
||||||
|
content = <Spinner />;
|
||||||
|
} else if (pinnedEvents.length > 0) {
|
||||||
|
let onUnpinClicked;
|
||||||
|
if (canUnpin) {
|
||||||
|
onUnpinClicked = async (event: MatrixEvent) => {
|
||||||
|
const pinnedEvents = room.currentState.getStateEvents(EventType.RoomPinnedEvents, "");
|
||||||
|
if (pinnedEvents?.getContent()?.pinned) {
|
||||||
|
const pinned = pinnedEvents.getContent().pinned;
|
||||||
|
const index = pinned.indexOf(event.getId());
|
||||||
|
if (index !== -1) {
|
||||||
|
pinned.splice(index, 1);
|
||||||
|
await cli.sendStateEvent(room.roomId, EventType.RoomPinnedEvents, { pinned }, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// show them in reverse, with latest pinned at the top
|
||||||
|
content = pinnedEvents.filter(Boolean).reverse().map(ev => (
|
||||||
|
<PinnedEventTile key={ev.getId()} room={room} event={ev} onUnpinClicked={() => onUnpinClicked(ev)} />
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
content = <div className="mx_PinnedMessagesCard_empty">
|
||||||
|
<div>
|
||||||
|
{ /* XXX: We reuse the classes for simplicity, but deliberately not the components for non-interactivity. */ }
|
||||||
|
<div className="mx_PinnedMessagesCard_MessageActionBar">
|
||||||
|
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton" />
|
||||||
|
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton" />
|
||||||
|
<div className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>{ _t("Nothing pinned, yet") }</h2>
|
||||||
|
{ _t("If you have permissions, open the menu on any message and select " +
|
||||||
|
"<b>Pin</b> to stick them here.", {}, {
|
||||||
|
b: sub => <b>{ sub }</b>,
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BaseCard
|
||||||
|
header={<h2>{ _t("Pinned messages") }</h2>}
|
||||||
|
className="mx_PinnedMessagesCard"
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
{ content }
|
||||||
|
</BaseCard>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PinnedMessagesCard;
|
|
@ -18,15 +18,19 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import {_t} from '../../../languageHandler';
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
import HeaderButton from './HeaderButton';
|
import HeaderButton from './HeaderButton';
|
||||||
import HeaderButtons, {HeaderKind} from './HeaderButtons';
|
import HeaderButtons, { HeaderKind } from './HeaderButtons';
|
||||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import {ActionPayload} from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import RightPanelStore from "../../../stores/RightPanelStore";
|
import RightPanelStore from "../../../stores/RightPanelStore";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { useSettingValue } from "../../../hooks/useSettings";
|
||||||
|
import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard';
|
||||||
|
|
||||||
const ROOM_INFO_PHASES = [
|
const ROOM_INFO_PHASES = [
|
||||||
RightPanelPhases.RoomSummary,
|
RightPanelPhases.RoomSummary,
|
||||||
|
@ -38,9 +42,35 @@ const ROOM_INFO_PHASES = [
|
||||||
RightPanelPhases.Room3pidMemberInfo,
|
RightPanelPhases.Room3pidMemberInfo,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => {
|
||||||
|
const pinningEnabled = useSettingValue("feature_pinning");
|
||||||
|
const pinnedEvents = usePinnedEvents(pinningEnabled && room);
|
||||||
|
const readPinnedEvents = useReadPinnedEvents(pinningEnabled && room);
|
||||||
|
if (!pinningEnabled) return null;
|
||||||
|
|
||||||
|
let unreadIndicator;
|
||||||
|
if (pinnedEvents.some(id => !readPinnedEvents.has(id))) {
|
||||||
|
unreadIndicator = <div className="mx_RightPanel_pinnedMessagesButton_unreadIndicator" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <HeaderButton
|
||||||
|
name="pinnedMessagesButton"
|
||||||
|
title={_t("Pinned messages")}
|
||||||
|
isHighlighted={isHighlighted}
|
||||||
|
onClick={onClick}
|
||||||
|
analytics={["Right Panel", "Pinned Messages Button", "click"]}
|
||||||
|
>
|
||||||
|
{ unreadIndicator }
|
||||||
|
</HeaderButton>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
room?: Room;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.right_panel.RoomHeaderButtons")
|
@replaceableComponent("views.right_panel.RoomHeaderButtons")
|
||||||
export default class RoomHeaderButtons extends HeaderButtons {
|
export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props, HeaderKind.Room);
|
super(props, HeaderKind.Room);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,24 +110,32 @@ export default class RoomHeaderButtons extends HeaderButtons {
|
||||||
this.setPhase(RightPanelPhases.NotificationPanel);
|
this.setPhase(RightPanelPhases.NotificationPanel);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onPinnedMessagesClicked = () => {
|
||||||
|
// This toggles for us, if needed
|
||||||
|
this.setPhase(RightPanelPhases.PinnedMessages);
|
||||||
|
};
|
||||||
|
|
||||||
public renderButtons() {
|
public renderButtons() {
|
||||||
return [
|
return <>
|
||||||
|
<PinnedMessagesHeaderButton
|
||||||
|
room={this.props.room}
|
||||||
|
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
|
||||||
|
onClick={this.onPinnedMessagesClicked}
|
||||||
|
/>
|
||||||
<HeaderButton
|
<HeaderButton
|
||||||
key="notifsButton"
|
|
||||||
name="notifsButton"
|
name="notifsButton"
|
||||||
title={_t('Notifications')}
|
title={_t('Notifications')}
|
||||||
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
|
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
|
||||||
onClick={this.onNotificationsClicked}
|
onClick={this.onNotificationsClicked}
|
||||||
analytics={['Right Panel', 'Notification List Button', 'click']}
|
analytics={['Right Panel', 'Notification List Button', 'click']}
|
||||||
/>,
|
/>
|
||||||
<HeaderButton
|
<HeaderButton
|
||||||
key="roomSummaryButton"
|
|
||||||
name="roomSummaryButton"
|
name="roomSummaryButton"
|
||||||
title={_t('Room Info')}
|
title={_t('Room Info')}
|
||||||
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
|
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
|
||||||
onClick={this.onRoomSummaryClicked}
|
onClick={this.onRoomSummaryClicked}
|
||||||
analytics={['Right Panel', 'Room Summary Button', 'click']}
|
analytics={['Right Panel', 'Room Summary Button', 'click']}
|
||||||
/>,
|
/>
|
||||||
];
|
</>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,18 +17,19 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {MatrixClient} from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import {RoomMember} from 'matrix-js-sdk/src/models/room-member';
|
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 {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
|
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||||
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
|
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
|
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
|
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import {_t} from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import createRoom, { findDMForUser, privateShouldBeEncrypted } from '../../../createRoom';
|
import createRoom, { findDMForUser, privateShouldBeEncrypted } from '../../../createRoom';
|
||||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
@ -39,18 +40,18 @@ import MultiInviter from "../../../utils/MultiInviter";
|
||||||
import GroupStore from "../../../stores/GroupStore";
|
import GroupStore from "../../../stores/GroupStore";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
import E2EIcon from "../rooms/E2EIcon";
|
import E2EIcon from "../rooms/E2EIcon";
|
||||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||||
import {textualPowerLevel} from '../../../Roles';
|
import { textualPowerLevel } from '../../../Roles';
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||||
import EncryptionPanel from "./EncryptionPanel";
|
import EncryptionPanel from "./EncryptionPanel";
|
||||||
import {useAsyncMemo} from '../../../hooks/useAsyncMemo';
|
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
|
||||||
import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification';
|
import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification';
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
|
import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
|
||||||
import {useIsEncrypted} from "../../../hooks/useIsEncrypted";
|
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
|
||||||
import BaseCard from "./BaseCard";
|
import BaseCard from "./BaseCard";
|
||||||
import {E2EStatus} from "../../../utils/ShieldUtils";
|
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||||
import ImageView from "../elements/ImageView";
|
import ImageView from "../elements/ImageView";
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import PowerSelector from "../elements/PowerSelector";
|
import PowerSelector from "../elements/PowerSelector";
|
||||||
|
@ -65,7 +66,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
import {mediaFromMxc} from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
import UIStore from "../../../stores/UIStore";
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
|
||||||
export interface IDevice {
|
export interface IDevice {
|
||||||
|
@ -514,9 +515,6 @@ export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
||||||
} else {
|
} else {
|
||||||
setPowerLevels({});
|
setPowerLevels({});
|
||||||
}
|
}
|
||||||
return () => {
|
|
||||||
setPowerLevels({});
|
|
||||||
};
|
|
||||||
}, [room]);
|
}, [room]);
|
||||||
|
|
||||||
useEventEmitter(cli, "RoomState.events", update);
|
useEventEmitter(cli, "RoomState.events", update);
|
||||||
|
@ -1530,21 +1528,16 @@ interface IProps {
|
||||||
user: Member;
|
user: Member;
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
room?: Room;
|
room?: Room;
|
||||||
phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo | RightPanelPhases.SpaceMemberInfo;
|
phase: RightPanelPhases.RoomMemberInfo
|
||||||
|
| RightPanelPhases.GroupMemberInfo
|
||||||
|
| RightPanelPhases.SpaceMemberInfo
|
||||||
|
| RightPanelPhases.EncryptionPanel;
|
||||||
onClose(): void;
|
onClose(): void;
|
||||||
|
verificationRequest?: VerificationRequest;
|
||||||
|
verificationRequestPromise?: Promise<VerificationRequest>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPropsWithEncryptionPanel extends React.ComponentProps<typeof EncryptionPanel> {
|
const UserInfo: React.FC<IProps> = ({
|
||||||
user: Member;
|
|
||||||
groupId: void;
|
|
||||||
room: Room;
|
|
||||||
phase: RightPanelPhases.EncryptionPanel;
|
|
||||||
onClose(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = IProps | IPropsWithEncryptionPanel;
|
|
||||||
|
|
||||||
const UserInfo: React.FC<Props> = ({
|
|
||||||
user,
|
user,
|
||||||
groupId,
|
groupId,
|
||||||
room,
|
room,
|
||||||
|
|
|
@ -25,7 +25,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import * as TextForEvent from "../../../TextForEvent";
|
import { hasText } from "../../../TextForEvent";
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
@ -277,6 +277,12 @@ interface IProps {
|
||||||
|
|
||||||
// Helper to build permalinks for the room
|
// Helper to build permalinks for the room
|
||||||
permalinkCreator?: RoomPermalinkCreator;
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
|
|
||||||
|
// Symbol of the root node
|
||||||
|
as?: string
|
||||||
|
|
||||||
|
// whether or not to always show timestamps
|
||||||
|
alwaysShowTimestamps?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -291,12 +297,15 @@ interface IState {
|
||||||
previouslyRequestedKeys: boolean;
|
previouslyRequestedKeys: boolean;
|
||||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||||
reactions: Relations;
|
reactions: Relations;
|
||||||
|
|
||||||
|
hover: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.EventTile")
|
@replaceableComponent("views.rooms.EventTile")
|
||||||
export default class EventTile extends React.Component<IProps, IState> {
|
export default class EventTile extends React.Component<IProps, IState> {
|
||||||
private suppressReadReceiptAnimation: boolean;
|
private suppressReadReceiptAnimation: boolean;
|
||||||
private isListeningForReceipts: boolean;
|
private isListeningForReceipts: boolean;
|
||||||
|
private ref: React.RefObject<unknown>;
|
||||||
private tile = React.createRef();
|
private tile = React.createRef();
|
||||||
private replyThread = React.createRef();
|
private replyThread = React.createRef();
|
||||||
|
|
||||||
|
@ -322,6 +331,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
previouslyRequestedKeys: false,
|
previouslyRequestedKeys: false,
|
||||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||||
reactions: this.getReactions(),
|
reactions: this.getReactions(),
|
||||||
|
|
||||||
|
hover: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// don't do RR animations until we are mounted
|
// don't do RR animations until we are mounted
|
||||||
|
@ -333,6 +344,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
// to determine if we've already subscribed and use a combination of other flags to find
|
// to determine if we've already subscribed and use a combination of other flags to find
|
||||||
// out if we should even be subscribed at all.
|
// out if we should even be subscribed at all.
|
||||||
this.isListeningForReceipts = false;
|
this.isListeningForReceipts = false;
|
||||||
|
|
||||||
|
this.ref = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -631,7 +644,18 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// return early if there are no read receipts
|
// return early if there are no read receipts
|
||||||
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
|
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
|
||||||
return (<span className="mx_EventTile_readAvatars" />);
|
// We currently must include `mx_EventTile_readAvatars` in the DOM
|
||||||
|
// of all events, as it is the positioned parent of the animated
|
||||||
|
// read receipts. We can't let it unmount when a receipt moves
|
||||||
|
// events, so for now we mount it for all events. Without it, the
|
||||||
|
// animation will start from the top of the timeline (because it
|
||||||
|
// lost its container).
|
||||||
|
// See also https://github.com/vector-im/element-web/issues/17561
|
||||||
|
return (
|
||||||
|
<div className="mx_EventTile_msgOption">
|
||||||
|
<span className="mx_EventTile_readAvatars" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
|
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
|
||||||
|
@ -639,7 +663,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
const receiptOffset = 15;
|
const receiptOffset = 15;
|
||||||
let left = 0;
|
let left = 0;
|
||||||
|
|
||||||
const receipts = this.props.readReceipts || [];
|
const receipts = this.props.readReceipts;
|
||||||
|
|
||||||
for (let i = 0; i < receipts.length; ++i) {
|
for (let i = 0; i < receipts.length; ++i) {
|
||||||
const receipt = receipts[i];
|
const receipt = receipts[i];
|
||||||
|
|
||||||
|
@ -690,10 +715,14 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <span className="mx_EventTile_readAvatars">
|
return (
|
||||||
{ remText }
|
<div className="mx_EventTile_msgOption">
|
||||||
{ avatars }
|
<span className="mx_EventTile_readAvatars">
|
||||||
</span>;
|
{ remText }
|
||||||
|
{ avatars }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
onSenderProfileClick = event => {
|
onSenderProfileClick = event => {
|
||||||
|
@ -884,6 +913,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we can't use local echoes as scroll tokens, because their event IDs change.
|
||||||
|
// Local echos have a send "status".
|
||||||
|
const scrollToken = this.props.mxEvent.status
|
||||||
|
? undefined
|
||||||
|
: this.props.mxEvent.getId();
|
||||||
|
|
||||||
let avatar;
|
let avatar;
|
||||||
let sender;
|
let sender;
|
||||||
let avatarSize;
|
let avatarSize;
|
||||||
|
@ -953,7 +988,9 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
onFocusChange={this.onActionBarFocusChange}
|
onFocusChange={this.onActionBarFocusChange}
|
||||||
/> : undefined;
|
/> : undefined;
|
||||||
|
|
||||||
const timestamp = this.props.mxEvent.getTs() ?
|
const showTimestamp = this.props.mxEvent.getTs() &&
|
||||||
|
(this.props.alwaysShowTimestamps || this.props.last || this.state.hover || this.state.actionBarFocused);
|
||||||
|
const timestamp = showTimestamp ?
|
||||||
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
||||||
|
|
||||||
const keyRequestHelpText =
|
const keyRequestHelpText =
|
||||||
|
@ -1016,68 +1053,71 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
let msgOption;
|
let msgOption;
|
||||||
if (this.props.showReadReceipts) {
|
if (this.props.showReadReceipts) {
|
||||||
const readAvatars = this.getReadAvatars();
|
const readAvatars = this.getReadAvatars();
|
||||||
msgOption = (
|
msgOption = readAvatars;
|
||||||
<div className="mx_EventTile_msgOption">
|
|
||||||
{ readAvatars }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this.props.tileShape) {
|
switch (this.props.tileShape) {
|
||||||
case 'notif': {
|
case 'notif': {
|
||||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||||
return (
|
return React.createElement(this.props.as || "li", {
|
||||||
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
"className": classes,
|
||||||
<div className="mx_EventTile_roomName">
|
"aria-live": ariaLive,
|
||||||
<RoomAvatar room={room} width={28} height={28} />
|
"aria-atomic": true,
|
||||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
"data-scroll-tokens": scrollToken,
|
||||||
{ room ? room.name : '' }
|
}, [
|
||||||
</a>
|
<div className="mx_EventTile_roomName" key="mx_EventTile_roomName">
|
||||||
</div>
|
<RoomAvatar room={room} width={28} height={28} />
|
||||||
<div className="mx_EventTile_senderDetails">
|
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||||
{ avatar }
|
{ room ? room.name : '' }
|
||||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
</a>
|
||||||
{ sender }
|
</div>,
|
||||||
{ timestamp }
|
<div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
|
||||||
</a>
|
{ avatar }
|
||||||
</div>
|
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||||
<div className="mx_EventTile_line">
|
{ sender }
|
||||||
<EventTileType ref={this.tile}
|
{ timestamp }
|
||||||
mxEvent={this.props.mxEvent}
|
</a>
|
||||||
highlights={this.props.highlights}
|
</div>,
|
||||||
highlightLink={this.props.highlightLink}
|
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
<EventTileType ref={this.tile}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
mxEvent={this.props.mxEvent}
|
||||||
/>
|
highlights={this.props.highlights}
|
||||||
</div>
|
highlightLink={this.props.highlightLink}
|
||||||
</div>
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
);
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
|
/>
|
||||||
|
</div>,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
case 'file_grid': {
|
case 'file_grid': {
|
||||||
return (
|
return React.createElement(this.props.as || "li", {
|
||||||
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
"className": classes,
|
||||||
<div className="mx_EventTile_line">
|
"aria-live": ariaLive,
|
||||||
<EventTileType ref={this.tile}
|
"aria-atomic": true,
|
||||||
mxEvent={this.props.mxEvent}
|
"data-scroll-tokens": scrollToken,
|
||||||
highlights={this.props.highlights}
|
}, [
|
||||||
highlightLink={this.props.highlightLink}
|
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
<EventTileType ref={this.tile}
|
||||||
tileShape={this.props.tileShape}
|
mxEvent={this.props.mxEvent}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
highlights={this.props.highlights}
|
||||||
/>
|
highlightLink={this.props.highlightLink}
|
||||||
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
|
tileShape={this.props.tileShape}
|
||||||
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
|
/>
|
||||||
|
</div>,
|
||||||
|
<a
|
||||||
|
className="mx_EventTile_senderDetailsLink"
|
||||||
|
key="mx_EventTile_senderDetailsLink"
|
||||||
|
href={permalink}
|
||||||
|
onClick={this.onPermalinkClicked}
|
||||||
|
>
|
||||||
|
<div className="mx_EventTile_senderDetails">
|
||||||
|
{ sender }
|
||||||
|
{ timestamp }
|
||||||
</div>
|
</div>
|
||||||
<a
|
</a>,
|
||||||
className="mx_EventTile_senderDetailsLink"
|
]);
|
||||||
href={permalink}
|
|
||||||
onClick={this.onPermalinkClicked}
|
|
||||||
>
|
|
||||||
<div className="mx_EventTile_senderDetails">
|
|
||||||
{ sender }
|
|
||||||
{ timestamp }
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'reply':
|
case 'reply':
|
||||||
|
@ -1089,29 +1129,34 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
this.props.onHeightChanged,
|
this.props.onHeightChanged,
|
||||||
this.props.permalinkCreator,
|
this.props.permalinkCreator,
|
||||||
this.replyThread,
|
this.replyThread,
|
||||||
|
null,
|
||||||
|
this.props.alwaysShowTimestamps || this.state.hover,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return React.createElement(this.props.as || "li", {
|
||||||
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
"className": classes,
|
||||||
{ ircTimestamp }
|
"aria-live": ariaLive,
|
||||||
{ avatar }
|
"aria-atomic": true,
|
||||||
{ sender }
|
"data-scroll-tokens": scrollToken,
|
||||||
{ ircPadlock }
|
}, [
|
||||||
<div className="mx_EventTile_reply">
|
ircTimestamp,
|
||||||
{ groupTimestamp }
|
avatar,
|
||||||
{ groupPadlock }
|
sender,
|
||||||
{ thread }
|
ircPadlock,
|
||||||
<EventTileType ref={this.tile}
|
<div className="mx_EventTile_reply" key="mx_EventTile_reply">
|
||||||
mxEvent={this.props.mxEvent}
|
{ groupTimestamp }
|
||||||
highlights={this.props.highlights}
|
{ groupPadlock }
|
||||||
highlightLink={this.props.highlightLink}
|
{ thread }
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
<EventTileType ref={this.tile}
|
||||||
replacingEventId={this.props.replacingEventId}
|
mxEvent={this.props.mxEvent}
|
||||||
showUrlPreview={false}
|
highlights={this.props.highlights}
|
||||||
/>
|
highlightLink={this.props.highlightLink}
|
||||||
</div>
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
</div>
|
replacingEventId={this.props.replacingEventId}
|
||||||
);
|
showUrlPreview={false}
|
||||||
|
/>
|
||||||
|
</div>,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
const thread = ReplyThread.makeThread(
|
const thread = ReplyThread.makeThread(
|
||||||
|
@ -1120,15 +1165,25 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
this.props.permalinkCreator,
|
this.props.permalinkCreator,
|
||||||
this.replyThread,
|
this.replyThread,
|
||||||
this.props.layout,
|
this.props.layout,
|
||||||
|
this.props.alwaysShowTimestamps || this.state.hover,
|
||||||
);
|
);
|
||||||
|
|
||||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||||
return (
|
return (
|
||||||
<div className={classes} tabIndex={-1} aria-live={ariaLive} aria-atomic="true">
|
React.createElement(this.props.as || "li", {
|
||||||
{ ircTimestamp }
|
"ref": this.ref,
|
||||||
{ sender }
|
"className": classes,
|
||||||
{ ircPadlock }
|
"tabIndex": -1,
|
||||||
<div className="mx_EventTile_line">
|
"aria-live": ariaLive,
|
||||||
|
"aria-atomic": "true",
|
||||||
|
"data-scroll-tokens": scrollToken,
|
||||||
|
"onMouseEnter": () => this.setState({ hover: true }),
|
||||||
|
"onMouseLeave": () => this.setState({ hover: false }),
|
||||||
|
}, [
|
||||||
|
ircTimestamp,
|
||||||
|
sender,
|
||||||
|
ircPadlock,
|
||||||
|
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||||
{ groupTimestamp }
|
{ groupTimestamp }
|
||||||
{ groupPadlock }
|
{ groupPadlock }
|
||||||
{ thread }
|
{ thread }
|
||||||
|
@ -1145,16 +1200,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
{ keyRequestInfo }
|
{ keyRequestInfo }
|
||||||
{ reactionsRow }
|
{ reactionsRow }
|
||||||
{ actionBar }
|
{ actionBar }
|
||||||
</div>
|
</div>,
|
||||||
{msgOption}
|
msgOption,
|
||||||
{
|
avatar,
|
||||||
// The avatar goes after the event tile as it's absolutely positioned to be over the
|
|
||||||
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
|
])
|
||||||
// the need for further z-indexing chaos)
|
)
|
||||||
}
|
|
||||||
{ avatar }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1176,7 +1227,7 @@ export function haveTileForEvent(e) {
|
||||||
const handler = getHandlerTile(e);
|
const handler = getHandlerTile(e);
|
||||||
if (handler === undefined) return false;
|
if (handler === undefined) return false;
|
||||||
if (handler === 'messages.TextualEvent') {
|
if (handler === 'messages.TextualEvent') {
|
||||||
return TextForEvent.textForEvent(e) !== '';
|
return hasText(e);
|
||||||
} else if (handler === 'messages.RoomCreate') {
|
} else if (handler === 'messages.RoomCreate') {
|
||||||
return Boolean(e.getContent()['predecessor']);
|
return Boolean(e.getContent()['predecessor']);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1316,11 +1367,15 @@ class SentReceipt extends React.PureComponent<ISentReceiptProps, ISentReceiptSta
|
||||||
tooltip = <Tooltip className="mx_EventTile_readAvatars_receiptTooltip" label={label} yOffset={20} />;
|
tooltip = <Tooltip className="mx_EventTile_readAvatars_receiptTooltip" label={label} yOffset={20} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <span className="mx_EventTile_readAvatars">
|
return (
|
||||||
<span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
|
<div className="mx_EventTile_msgOption">
|
||||||
{nonCssBadge}
|
<span className="mx_EventTile_readAvatars">
|
||||||
{tooltip}
|
<span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
|
||||||
</span>
|
{nonCssBadge}
|
||||||
</span>;
|
{tooltip}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2017 Michael Telatynski
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import {Key} from '../../../Keyboard';
|
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.ForwardMessage")
|
|
||||||
export default class ForwardMessage extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onCancelClick: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
document.addEventListener('keydown', this._onKeyDown);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
document.removeEventListener('keydown', this._onKeyDown);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onKeyDown = ev => {
|
|
||||||
switch (ev.key) {
|
|
||||||
case Key.ESCAPE:
|
|
||||||
this.props.onCancelClick();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="mx_ForwardMessage">
|
|
||||||
<h1>{ _t('Please select the destination room for this message') }</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -31,6 +31,17 @@ import dis from "../../../dispatcher/dispatcher";
|
||||||
import SpaceStore from "../../../stores/SpaceStore";
|
import SpaceStore from "../../../stores/SpaceStore";
|
||||||
import {showSpaceInvite} from "../../../utils/space";
|
import {showSpaceInvite} from "../../../utils/space";
|
||||||
|
|
||||||
|
import { privateShouldBeEncrypted } from "../../../createRoom";
|
||||||
|
|
||||||
|
import EventTileBubble from "../messages/EventTileBubble";
|
||||||
|
import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog";
|
||||||
|
|
||||||
|
function hasExpectedEncryptionSettings(room): boolean {
|
||||||
|
const isEncrypted: boolean = room._client?.isRoomEncrypted(room.roomId);
|
||||||
|
const isPublic: boolean = room.getJoinRule() === "public";
|
||||||
|
return isPublic || !privateShouldBeEncrypted() || isEncrypted;
|
||||||
|
}
|
||||||
|
|
||||||
const NewRoomIntro = () => {
|
const NewRoomIntro = () => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const {room, roomId} = useContext(RoomContext);
|
const {room, roomId} = useContext(RoomContext);
|
||||||
|
@ -166,7 +177,31 @@ const NewRoomIntro = () => {
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openRoomSettings(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
dis.dispatch({
|
||||||
|
action: "open_room_settings",
|
||||||
|
initial_tab_id: ROOM_SECURITY_TAB,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sub2 = _t(
|
||||||
|
"Your private messages are normally encrypted, but this room isn't. "+
|
||||||
|
"Usually this is due to an unsupported device or method being used, " +
|
||||||
|
"like email invites. <a>Enable encryption in settings.</a>", {},
|
||||||
|
{ a: sub => <a onClick={openRoomSettings} href="#">{sub}</a> },
|
||||||
|
);
|
||||||
|
|
||||||
return <div className="mx_NewRoomIntro">
|
return <div className="mx_NewRoomIntro">
|
||||||
|
|
||||||
|
{ !hasExpectedEncryptionSettings(room) && (
|
||||||
|
<EventTileBubble
|
||||||
|
className="mx_cryptoEvent mx_cryptoEvent_icon_warning"
|
||||||
|
title={_t("End-to-end encryption isn't enabled")}
|
||||||
|
subtitle={sub2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{ body }
|
{ body }
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,111 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2017 Travis Ralston
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
import MessageEvent from "../messages/MessageEvent";
|
|
||||||
import MemberAvatar from "../avatars/MemberAvatar";
|
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import {formatFullDate} from '../../../DateUtils';
|
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.PinnedEventTile")
|
|
||||||
export default class PinnedEventTile extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
mxRoom: PropTypes.object.isRequired,
|
|
||||||
mxEvent: PropTypes.object.isRequired,
|
|
||||||
onUnpinned: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
onTileClicked = () => {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
event_id: this.props.mxEvent.getId(),
|
|
||||||
highlighted: true,
|
|
||||||
room_id: this.props.mxEvent.getRoomId(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnpinClicked = () => {
|
|
||||||
const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", "");
|
|
||||||
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
|
|
||||||
// Nothing to do: already unpinned
|
|
||||||
if (this.props.onUnpinned) this.props.onUnpinned();
|
|
||||||
} else {
|
|
||||||
const pinned = pinnedEvents.getContent().pinned;
|
|
||||||
const index = pinned.indexOf(this.props.mxEvent.getId());
|
|
||||||
if (index !== -1) {
|
|
||||||
pinned.splice(index, 1);
|
|
||||||
MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '')
|
|
||||||
.then(() => {
|
|
||||||
if (this.props.onUnpinned) this.props.onUnpinned();
|
|
||||||
});
|
|
||||||
} else if (this.props.onUnpinned) this.props.onUnpinned();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_canUnpin() {
|
|
||||||
return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const sender = this.props.mxEvent.getSender();
|
|
||||||
// Get the latest sender profile rather than historical
|
|
||||||
const senderProfile = this.props.mxRoom.getMember(sender);
|
|
||||||
const avatarSize = 40;
|
|
||||||
|
|
||||||
let unpinButton = null;
|
|
||||||
if (this._canUnpin()) {
|
|
||||||
unpinButton = (
|
|
||||||
<AccessibleButton onClick={this.onUnpinClicked} className="mx_PinnedEventTile_unpinButton">
|
|
||||||
<img src={require("../../../../res/img/cancel-red.svg")} width="8" height="8" alt={_t('Unpin Message')} title={_t('Unpin Message')} />
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_PinnedEventTile">
|
|
||||||
<div className="mx_PinnedEventTile_actions">
|
|
||||||
<AccessibleButton className="mx_PinnedEventTile_gotoButton mx_textButton" onClick={this.onTileClicked}>
|
|
||||||
{ _t("Jump to message") }
|
|
||||||
</AccessibleButton>
|
|
||||||
{ unpinButton }
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className="mx_PinnedEventTile_senderAvatar">
|
|
||||||
<MemberAvatar member={senderProfile} width={avatarSize} height={avatarSize} fallbackUserId={sender} />
|
|
||||||
</span>
|
|
||||||
<span className="mx_PinnedEventTile_sender">
|
|
||||||
{ senderProfile ? senderProfile.name : sender }
|
|
||||||
</span>
|
|
||||||
<span className="mx_PinnedEventTile_timestamp">
|
|
||||||
{ formatFullDate(new Date(this.props.mxEvent.getTs())) }
|
|
||||||
</span>
|
|
||||||
<div className="mx_PinnedEventTile_message">
|
|
||||||
<MessageEvent
|
|
||||||
mxEvent={this.props.mxEvent}
|
|
||||||
className="mx_PinnedEventTile_body"
|
|
||||||
maxImageHeight={150}
|
|
||||||
onHeightChanged={() => {}} // we need to give this, apparently
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Travis Ralston
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import MessageEvent from "../messages/MessageEvent";
|
||||||
|
import MemberAvatar from "../avatars/MemberAvatar";
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import { formatDate } from '../../../DateUtils';
|
||||||
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
|
||||||
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
room: Room;
|
||||||
|
event: MatrixEvent;
|
||||||
|
onUnpinClicked?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AVATAR_SIZE = 24;
|
||||||
|
|
||||||
|
@replaceableComponent("views.rooms.PinnedEventTile")
|
||||||
|
export default class PinnedEventTile extends React.Component<IProps> {
|
||||||
|
public static contextType = MatrixClientContext;
|
||||||
|
|
||||||
|
private onTileClicked = () => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
event_id: this.props.event.getId(),
|
||||||
|
highlighted: true,
|
||||||
|
room_id: this.props.event.getRoomId(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const sender = this.props.event.getSender();
|
||||||
|
const senderProfile = this.props.room.getMember(sender);
|
||||||
|
|
||||||
|
let unpinButton = null;
|
||||||
|
if (this.props.onUnpinClicked) {
|
||||||
|
unpinButton = (
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
onClick={this.props.onUnpinClicked}
|
||||||
|
className="mx_PinnedEventTile_unpinButton"
|
||||||
|
title={_t("Unpin")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_PinnedEventTile">
|
||||||
|
<MemberAvatar
|
||||||
|
className="mx_PinnedEventTile_senderAvatar"
|
||||||
|
member={senderProfile}
|
||||||
|
width={AVATAR_SIZE}
|
||||||
|
height={AVATAR_SIZE}
|
||||||
|
fallbackUserId={sender}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className={"mx_PinnedEventTile_sender " + getUserNameColorClass(sender)}>
|
||||||
|
{ senderProfile?.name || sender }
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{ unpinButton }
|
||||||
|
|
||||||
|
<div className="mx_PinnedEventTile_message">
|
||||||
|
<MessageEvent
|
||||||
|
mxEvent={this.props.event}
|
||||||
|
className="mx_PinnedEventTile_body"
|
||||||
|
maxImageHeight={150}
|
||||||
|
onHeightChanged={() => {}} // we need to give this, apparently
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx_PinnedEventTile_footer">
|
||||||
|
<span className="mx_PinnedEventTile_timestamp">
|
||||||
|
{ formatDate(new Date(this.props.event.getTs())) }
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<AccessibleButton onClick={this.onTileClicked} kind="link">
|
||||||
|
{ _t("View message") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,145 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2017 Travis Ralston
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
import PinnedEventTile from "./PinnedEventTile";
|
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import PinningUtils from "../../../utils/PinningUtils";
|
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.PinnedEventsPanel")
|
|
||||||
export default class PinnedEventsPanel extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
// The Room from the js-sdk we're going to show pinned events for
|
|
||||||
room: PropTypes.object.isRequired,
|
|
||||||
|
|
||||||
onCancelClick: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
loading: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this._updatePinnedMessages();
|
|
||||||
MatrixClientPeg.get().on("RoomState.events", this._onStateEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (MatrixClientPeg.get()) {
|
|
||||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onStateEvent = ev => {
|
|
||||||
if (ev.getRoomId() === this.props.room.roomId && ev.getType() === "m.room.pinned_events") {
|
|
||||||
this._updatePinnedMessages();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_updatePinnedMessages = () => {
|
|
||||||
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
|
|
||||||
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
|
|
||||||
this.setState({ loading: false, pinned: [] });
|
|
||||||
} else {
|
|
||||||
const promises = [];
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
pinnedEvents.getContent().pinned.map((eventId) => {
|
|
||||||
promises.push(cli.getEventTimeline(this.props.room.getUnfilteredTimelineSet(), eventId, 0).then(
|
|
||||||
(timeline) => {
|
|
||||||
const event = timeline.getEvents().find((e) => e.getId() === eventId);
|
|
||||||
return {eventId, timeline, event};
|
|
||||||
}).catch((err) => {
|
|
||||||
console.error("Error looking up pinned event " + eventId + " in room " + this.props.room.roomId);
|
|
||||||
console.error(err);
|
|
||||||
return null; // return lack of context to avoid unhandled errors
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all(promises).then((contexts) => {
|
|
||||||
// Filter out the messages before we try to render them
|
|
||||||
const pinned = contexts.filter((context) => PinningUtils.isPinnable(context.event));
|
|
||||||
|
|
||||||
this.setState({ loading: false, pinned });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this._updateReadState();
|
|
||||||
};
|
|
||||||
|
|
||||||
_updateReadState() {
|
|
||||||
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
|
|
||||||
if (!pinnedEvents) return; // nothing to read
|
|
||||||
|
|
||||||
let readStateEvents = [];
|
|
||||||
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
|
|
||||||
if (readPinsEvent && readPinsEvent.getContent()) {
|
|
||||||
readStateEvents = readPinsEvent.getContent().event_ids || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!readStateEvents.includes(pinnedEvents.getId())) {
|
|
||||||
readStateEvents.push(pinnedEvents.getId());
|
|
||||||
|
|
||||||
// Only keep the last 10 event IDs to avoid infinite growth
|
|
||||||
readStateEvents = readStateEvents.reverse().splice(0, 10).reverse();
|
|
||||||
|
|
||||||
MatrixClientPeg.get().setRoomAccountData(this.props.room.roomId, "im.vector.room.read_pins", {
|
|
||||||
event_ids: readStateEvents,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_getPinnedTiles() {
|
|
||||||
if (this.state.pinned.length === 0) {
|
|
||||||
return (<div>{ _t("No pinned messages.") }</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.state.pinned.map((context) => {
|
|
||||||
return (
|
|
||||||
<PinnedEventTile
|
|
||||||
key={context.event.getId()}
|
|
||||||
mxRoom={this.props.room}
|
|
||||||
mxEvent={context.event}
|
|
||||||
onUnpinned={this._updatePinnedMessages}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let tiles = <div>{ _t("Loading...") }</div>;
|
|
||||||
if (this.state && !this.state.loading) {
|
|
||||||
tiles = this._getPinnedTiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_PinnedEventsPanel">
|
|
||||||
<div className="mx_PinnedEventsPanel_body">
|
|
||||||
<AccessibleButton className="mx_PinnedEventsPanel_cancel" onClick={this.props.onCancelClick}>
|
|
||||||
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18" />
|
|
||||||
</AccessibleButton>
|
|
||||||
<h3 className="mx_PinnedEventsPanel_header">{ _t("Pinned Messages") }</h3>
|
|
||||||
{ tiles }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -89,12 +89,13 @@ export default class ReplyPreview extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_ReplyPreview_clear" />
|
<div className="mx_ReplyPreview_clear" />
|
||||||
<EventTile
|
<EventTile
|
||||||
last={true}
|
alwaysShowTimestamps={true}
|
||||||
tileShape="reply_preview"
|
tileShape="reply_preview"
|
||||||
mxEvent={this.state.event}
|
mxEvent={this.state.event}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||||
|
as="div"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -19,10 +19,9 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||||
|
|
||||||
import {CancelButton} from './SimpleRoomHeader';
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||||
import E2EIcon from './E2EIcon';
|
import E2EIcon from './E2EIcon';
|
||||||
|
@ -30,8 +29,8 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import RoomTopic from "../elements/RoomTopic";
|
import RoomTopic from "../elements/RoomTopic";
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
import {PlaceCallType} from "../../../CallHandler";
|
import { PlaceCallType } from "../../../CallHandler";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.RoomHeader")
|
@replaceableComponent("views.rooms.RoomHeader")
|
||||||
export default class RoomHeader extends React.Component {
|
export default class RoomHeader extends React.Component {
|
||||||
|
@ -40,10 +39,8 @@ export default class RoomHeader extends React.Component {
|
||||||
oobData: PropTypes.object,
|
oobData: PropTypes.object,
|
||||||
inRoom: PropTypes.bool,
|
inRoom: PropTypes.bool,
|
||||||
onSettingsClick: PropTypes.func,
|
onSettingsClick: PropTypes.func,
|
||||||
onPinnedClick: PropTypes.func,
|
|
||||||
onSearchClick: PropTypes.func,
|
onSearchClick: PropTypes.func,
|
||||||
onLeaveClick: PropTypes.func,
|
onLeaveClick: PropTypes.func,
|
||||||
onCancelClick: PropTypes.func,
|
|
||||||
e2eStatus: PropTypes.string,
|
e2eStatus: PropTypes.string,
|
||||||
onAppsClick: PropTypes.func,
|
onAppsClick: PropTypes.func,
|
||||||
appsShown: PropTypes.bool,
|
appsShown: PropTypes.bool,
|
||||||
|
@ -53,20 +50,17 @@ export default class RoomHeader extends React.Component {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
editing: false,
|
editing: false,
|
||||||
inRoom: false,
|
inRoom: false,
|
||||||
onCancelClick: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
cli.on("RoomState.events", this._onRoomStateEvents);
|
cli.on("RoomState.events", this._onRoomStateEvents);
|
||||||
cli.on("Room.accountData", this._onRoomAccountData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.removeListener("RoomState.events", this._onRoomStateEvents);
|
cli.removeListener("RoomState.events", this._onRoomStateEvents);
|
||||||
cli.removeListener("Room.accountData", this._onRoomAccountData);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,52 +73,13 @@ export default class RoomHeader extends React.Component {
|
||||||
this._rateLimitedUpdate();
|
this._rateLimitedUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onRoomAccountData = (event, room) => {
|
|
||||||
if (!this.props.room || room.roomId !== this.props.room.roomId) return;
|
|
||||||
if (event.getType() !== "im.vector.room.read_pins") return;
|
|
||||||
|
|
||||||
this._rateLimitedUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
_rateLimitedUpdate = new RateLimitedFunc(function() {
|
_rateLimitedUpdate = new RateLimitedFunc(function() {
|
||||||
/* eslint-disable babel/no-invalid-this */
|
/* eslint-disable babel/no-invalid-this */
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
_hasUnreadPins() {
|
|
||||||
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
|
||||||
if (!currentPinEvent) return false;
|
|
||||||
if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) {
|
|
||||||
return false; // no pins == nothing to read
|
|
||||||
}
|
|
||||||
|
|
||||||
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
|
|
||||||
if (readPinsEvent && readPinsEvent.getContent()) {
|
|
||||||
const readStateEvents = readPinsEvent.getContent().event_ids || [];
|
|
||||||
if (readStateEvents) {
|
|
||||||
return !readStateEvents.includes(currentPinEvent.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// There's pins, and we haven't read any of them
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_hasPins() {
|
|
||||||
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
|
||||||
if (!currentPinEvent) return false;
|
|
||||||
|
|
||||||
return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let searchStatus = null;
|
let searchStatus = null;
|
||||||
let cancelButton = null;
|
|
||||||
let pinnedEventsButton = null;
|
|
||||||
|
|
||||||
if (this.props.onCancelClick) {
|
|
||||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't display the search count until the search completes and
|
// don't display the search count until the search completes and
|
||||||
// gives us a valid (possibly zero) searchCount.
|
// gives us a valid (possibly zero) searchCount.
|
||||||
|
@ -181,24 +136,6 @@ export default class RoomHeader extends React.Component {
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) {
|
|
||||||
let pinsIndicator = null;
|
|
||||||
if (this._hasUnreadPins()) {
|
|
||||||
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);
|
|
||||||
} else if (this._hasPins()) {
|
|
||||||
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator" />);
|
|
||||||
}
|
|
||||||
|
|
||||||
pinnedEventsButton =
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className="mx_RoomHeader_button mx_RoomHeader_pinnedButton"
|
|
||||||
onClick={this.props.onPinnedClick}
|
|
||||||
title={_t("Pinned Messages")}
|
|
||||||
>
|
|
||||||
{ pinsIndicator }
|
|
||||||
</AccessibleTooltipButton>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let forgetButton;
|
let forgetButton;
|
||||||
if (this.props.onForgetClick) {
|
if (this.props.onForgetClick) {
|
||||||
forgetButton =
|
forgetButton =
|
||||||
|
@ -248,7 +185,6 @@ export default class RoomHeader extends React.Component {
|
||||||
<div className="mx_RoomHeader_buttons">
|
<div className="mx_RoomHeader_buttons">
|
||||||
{ videoCallButton }
|
{ videoCallButton }
|
||||||
{ voiceCallButton }
|
{ voiceCallButton }
|
||||||
{ pinnedEventsButton }
|
|
||||||
{ forgetButton }
|
{ forgetButton }
|
||||||
{ appsButton }
|
{ appsButton }
|
||||||
{ searchButton }
|
{ searchButton }
|
||||||
|
@ -263,9 +199,8 @@ export default class RoomHeader extends React.Component {
|
||||||
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
|
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
|
||||||
{ name }
|
{ name }
|
||||||
{ topicElement }
|
{ topicElement }
|
||||||
{ cancelButton }
|
|
||||||
{ rightRow }
|
{ rightRow }
|
||||||
<RoomHeaderButtons />
|
<RoomHeaderButtons room={this.props.room} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -55,6 +55,7 @@ interface IProps {
|
||||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||||
onFocus: (ev: React.FocusEvent) => void;
|
onFocus: (ev: React.FocusEvent) => void;
|
||||||
onBlur: (ev: React.FocusEvent) => void;
|
onBlur: (ev: React.FocusEvent) => void;
|
||||||
|
onResize: () => void;
|
||||||
onListCollapse?: (isExpanded: boolean) => void;
|
onListCollapse?: (isExpanded: boolean) => void;
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
|
@ -404,7 +405,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
const newSublists = objectWithOnly(newLists, newListIds);
|
const newSublists = objectWithOnly(newLists, newListIds);
|
||||||
const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
|
const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
|
||||||
|
|
||||||
this.setState({sublists, isNameFiltering});
|
this.setState({sublists, isNameFiltering}, () => {
|
||||||
|
this.props.onResize();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -454,8 +454,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
const sublist = possibleSticky.parentElement.parentElement;
|
const sublist = possibleSticky.parentElement.parentElement;
|
||||||
const list = sublist.parentElement.parentElement;
|
const list = sublist.parentElement.parentElement;
|
||||||
// the scrollTop is capped at the height of the header in LeftPanel, the top header is always sticky
|
// the scrollTop is capped at the height of the header in LeftPanel, the top header is always sticky
|
||||||
const isAtTop = list.scrollTop <= HEADER_HEIGHT;
|
const listScrollTop = Math.round(list.scrollTop);
|
||||||
const isAtBottom = list.scrollTop >= list.scrollHeight - list.offsetHeight;
|
const isAtTop = listScrollTop <= Math.round(HEADER_HEIGHT);
|
||||||
|
const isAtBottom = listScrollTop >= Math.round(list.scrollHeight - list.offsetHeight);
|
||||||
const isStickyTop = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyTop');
|
const isStickyTop = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyTop');
|
||||||
const isStickyBottom = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyBottom');
|
const isStickyBottom = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyBottom');
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,7 @@ export default class SearchResultTile extends React.Component {
|
||||||
|
|
||||||
const ts1 = mxEv.getTs();
|
const ts1 = mxEv.getTs();
|
||||||
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
|
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
|
||||||
|
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
|
||||||
|
|
||||||
const timeline = result.context.getTimeline();
|
const timeline = result.context.getTimeline();
|
||||||
for (let j = 0; j < timeline.length; j++) {
|
for (let j = 0; j < timeline.length; j++) {
|
||||||
|
@ -67,6 +68,7 @@ export default class SearchResultTile extends React.Component {
|
||||||
highlightLink={this.props.resultLink}
|
highlightLink={this.props.resultLink}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||||
|
alwaysShowTimestamps={alwaysShowTimestamps}
|
||||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
|
@ -16,23 +16,9 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
// cancel button which is shared between room header and simple room header
|
|
||||||
export function CancelButton(props) {
|
|
||||||
const {onClick} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AccessibleButton className='mx_RoomHeader_cancelButton' onClick={onClick}>
|
|
||||||
<img src={require("../../../../res/img/cancel.svg")} className='mx_filterFlipColor'
|
|
||||||
width="18" height="18" alt={_t("Cancel")} />
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A stripped-down room header used for things like the user settings
|
* A stripped-down room header used for things like the user settings
|
||||||
* and room directory.
|
* and room directory.
|
||||||
|
@ -41,18 +27,13 @@ export function CancelButton(props) {
|
||||||
export default class SimpleRoomHeader extends React.Component {
|
export default class SimpleRoomHeader extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
onCancelClick: PropTypes.func,
|
|
||||||
|
|
||||||
// `src` to a TintableSvg. Optional.
|
// `src` to a TintableSvg. Optional.
|
||||||
icon: PropTypes.string,
|
icon: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let cancelButton;
|
|
||||||
let icon;
|
let icon;
|
||||||
if (this.props.onCancelClick) {
|
|
||||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
|
||||||
}
|
|
||||||
if (this.props.icon) {
|
if (this.props.icon) {
|
||||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||||
icon = <TintableSvg
|
icon = <TintableSvg
|
||||||
|
@ -62,13 +43,10 @@ export default class SimpleRoomHeader extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomHeader" >
|
<div className="mx_RoomHeader mx_RoomHeader_wrapper" >
|
||||||
<div className="mx_RoomHeader_wrapper">
|
<div className="mx_RoomHeader_simpleHeader">
|
||||||
<div className="mx_RoomHeader_simpleHeader">
|
{ icon }
|
||||||
{ icon }
|
{ this.props.title }
|
||||||
{ this.props.title }
|
|
||||||
{ cancelButton }
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -367,7 +367,7 @@ export default class Stickerpicker extends React.PureComponent {
|
||||||
/**
|
/**
|
||||||
* Launch the integration manager on the stickers integration page
|
* Launch the integration manager on the stickers integration page
|
||||||
*/
|
*/
|
||||||
_launchManageIntegrations() {
|
_launchManageIntegrations = () => {
|
||||||
// TODO: Open the right integration manager for the widget
|
// TODO: Open the right integration manager for the widget
|
||||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||||
IntegrationManagers.sharedInstance().openAll(
|
IntegrationManagers.sharedInstance().openAll(
|
||||||
|
@ -382,7 +382,7 @@ export default class Stickerpicker extends React.PureComponent {
|
||||||
this.state.widgetId,
|
this.state.widgetId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let stickerPicker;
|
let stickerPicker;
|
||||||
|
@ -401,7 +401,7 @@ export default class Stickerpicker extends React.PureComponent {
|
||||||
key="controls_hide_stickers"
|
key="controls_hide_stickers"
|
||||||
className={className}
|
className={className}
|
||||||
onClick={this._onHideStickersClick}
|
onClick={this._onHideStickersClick}
|
||||||
active={this.state.showStickers}
|
active={this.state.showStickers.toString()}
|
||||||
title={_t("Hide Stickers")}
|
title={_t("Hide Stickers")}
|
||||||
>
|
>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
|
|
|
@ -215,7 +215,7 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
|
||||||
this.props.whoIsTypingLimit,
|
this.props.whoIsTypingLimit,
|
||||||
);
|
);
|
||||||
if (!typingString) {
|
if (!typingString) {
|
||||||
return (<div className="mx_WhoIsTypingTile_empty" />);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -79,8 +79,8 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||||
async _getUpdatedStatus() {
|
async _getUpdatedStatus() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const pkCache = cli.getCrossSigningCacheCallbacks();
|
const pkCache = cli.getCrossSigningCacheCallbacks();
|
||||||
const crossSigning = cli._crypto._crossSigningInfo;
|
const crossSigning = cli.crypto._crossSigningInfo;
|
||||||
const secretStorage = cli._crypto._secretStorage;
|
const secretStorage = cli.crypto._secretStorage;
|
||||||
const crossSigningPublicKeysOnDevice = crossSigning.getId();
|
const crossSigningPublicKeysOnDevice = crossSigning.getId();
|
||||||
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
|
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
|
||||||
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
|
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
|
||||||
|
|
|
@ -131,10 +131,10 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
|
|
||||||
async _getUpdatedDiagnostics() {
|
async _getUpdatedDiagnostics() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const secretStorage = cli._crypto._secretStorage;
|
const secretStorage = cli.crypto._secretStorage;
|
||||||
|
|
||||||
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
|
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
|
||||||
const backupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey();
|
const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey();
|
||||||
const backupKeyCached = !!(backupKeyFromCache);
|
const backupKeyCached = !!(backupKeyFromCache);
|
||||||
const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array;
|
const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array;
|
||||||
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
||||||
|
|
|
@ -22,7 +22,7 @@ import FocusLock from "react-focus-lock";
|
||||||
import {_t} from "../../../languageHandler";
|
import {_t} from "../../../languageHandler";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import {ChevronFace, ContextMenu} from "../../structures/ContextMenu";
|
import {ChevronFace, ContextMenu} from "../../structures/ContextMenu";
|
||||||
import createRoom, {IStateEvent, Preset} from "../../../createRoom";
|
import createRoom from "../../../createRoom";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {SpaceAvatar} from "./SpaceBasicSettings";
|
import {SpaceAvatar} from "./SpaceBasicSettings";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
@ -33,6 +33,8 @@ import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog";
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import withValidation from "../elements/Validation";
|
import withValidation from "../elements/Validation";
|
||||||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||||
|
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
|
||||||
|
|
||||||
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
|
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -81,7 +83,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: IStateEvent[] = [
|
const initialState: ICreateRoomStateEvent[] = [
|
||||||
{
|
{
|
||||||
type: EventType.RoomHistoryVisibility,
|
type: EventType.RoomHistoryVisibility,
|
||||||
content: {
|
content: {
|
||||||
|
|
|
@ -15,17 +15,14 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ensureDMExists } from "../../../createRoom";
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import DialPad from './DialPad';
|
import DialPad from './DialPad';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import Modal from "../../../Modal";
|
|
||||||
import ErrorDialog from "../../views/dialogs/ErrorDialog";
|
|
||||||
import CallHandler from "../../../CallHandler";
|
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import { DialNumberPayload } from "../../../dispatcher/payloads/DialNumberPayload";
|
||||||
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onFinished: (boolean) => void;
|
onFinished: (boolean) => void;
|
||||||
|
@ -67,21 +64,11 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
onDialPress = async () => {
|
onDialPress = async () => {
|
||||||
const results = await CallHandler.sharedInstance().pstnLookup(this.state.value);
|
const payload: DialNumberPayload = {
|
||||||
if (!results || results.length === 0 || !results[0].userid) {
|
action: Action.DialNumber,
|
||||||
Modal.createTrackedDialog('', '', ErrorDialog, {
|
number: this.state.value,
|
||||||
title: _t("Unable to look up phone number"),
|
};
|
||||||
description: _t("There was an error looking up the phone number"),
|
dis.dispatch(payload);
|
||||||
});
|
|
||||||
}
|
|
||||||
const userId = results[0].userid;
|
|
||||||
|
|
||||||
const roomId = await ensureDMExists(MatrixClientPeg.get(), userId);
|
|
||||||
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_id: roomId,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue