Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/17365
This commit is contained in:
commit
f34a2e61bc
95 changed files with 2180 additions and 1311 deletions
|
@ -18,7 +18,7 @@ module.exports = {
|
|||
},
|
||||
|
||||
overrides: [{
|
||||
"files": ["src/**/*.{ts,tsx}"],
|
||||
"files": ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"],
|
||||
"extends": ["matrix-org/ts"],
|
||||
"rules": {
|
||||
// We're okay being explicit at the moment
|
||||
|
|
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)
|
||||
=====================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0-rc.1...v3.22.0)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.22.0",
|
||||
"version": "3.23.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
|
|
@ -76,6 +76,7 @@
|
|||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
|
||||
@import "./views/dialogs/_FeedbackDialog.scss";
|
||||
@import "./views/dialogs/_ForwardDialog.scss";
|
||||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||
@import "./views/dialogs/_HostSignupDialog.scss";
|
||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||
|
@ -179,6 +180,7 @@
|
|||
@import "./views/messages/_common_CryptoEvent.scss";
|
||||
@import "./views/right_panel/_BaseCard.scss";
|
||||
@import "./views/right_panel/_EncryptionInfo.scss";
|
||||
@import "./views/right_panel/_PinnedMessagesCard.scss";
|
||||
@import "./views/right_panel/_RoomSummaryCard.scss";
|
||||
@import "./views/right_panel/_UserInfo.scss";
|
||||
@import "./views/right_panel/_VerificationPanel.scss";
|
||||
|
@ -203,7 +205,6 @@
|
|||
@import "./views/rooms/_NewRoomIntro.scss";
|
||||
@import "./views/rooms/_NotificationBadge.scss";
|
||||
@import "./views/rooms/_PinnedEventTile.scss";
|
||||
@import "./views/rooms/_PinnedEventsPanel.scss";
|
||||
@import "./views/rooms/_PresenceLabel.scss";
|
||||
@import "./views/rooms/_ReplyPreview.scss";
|
||||
@import "./views/rooms/_RoomBreadcrumbs.scss";
|
||||
|
|
|
@ -82,7 +82,6 @@ limitations under the License.
|
|||
color: $primary-fg-color;
|
||||
font-size: $font-12px;
|
||||
display: inline;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.mx_NotificationPanel .mx_EventTile_senderDetails {
|
||||
|
@ -103,6 +102,7 @@ limitations under the License.
|
|||
visibility: visible;
|
||||
position: initial;
|
||||
display: inline;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.mx_NotificationPanel .mx_EventTile_line {
|
||||
|
|
|
@ -99,6 +99,48 @@ limitations under the License.
|
|||
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 {
|
||||
&::before {
|
||||
background-color: $accent-color !important;
|
||||
|
|
|
@ -222,7 +222,6 @@ limitations under the License.
|
|||
|
||||
.mx_RoomView_MessageList li {
|
||||
clear: both;
|
||||
contain: content;
|
||||
}
|
||||
|
||||
li.mx_RoomView_myReadMarker_container {
|
||||
|
|
|
@ -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 {
|
||||
// XXX remove this when spaces leaves Beta
|
||||
.mx_SpaceRoomView_inviteTeammates_betaDisclaimer {
|
||||
|
|
159
res/css/views/dialogs/_ForwardDialog.scss
Normal file
159
res/css/views/dialogs/_ForwardDialog.scss
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_ImageView_image_wrapper {
|
||||
pointer-events: initial;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
@ -30,7 +31,6 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_ImageView_image {
|
||||
pointer-events: all;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_ImageView_info_wrapper {
|
||||
pointer-events: all;
|
||||
pointer-events: initial;
|
||||
padding-left: 32px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -63,7 +63,7 @@ limitations under the License.
|
|||
|
||||
.mx_ImageView_toolbar {
|
||||
padding-right: 16px;
|
||||
pointer-events: all;
|
||||
pointer-events: initial;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
90
res/css/views/right_panel/_PinnedMessagesCard.scss
Normal file
90
res/css/views/right_panel/_PinnedMessagesCard.scss
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -85,12 +85,11 @@ $left-gutter: 64px;
|
|||
}
|
||||
|
||||
.mx_EventTile_isEditing .mx_MessageTimestamp {
|
||||
visibility: hidden !important;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.mx_EventTile .mx_MessageTimestamp {
|
||||
display: block;
|
||||
visibility: hidden;
|
||||
white-space: nowrap;
|
||||
left: 0px;
|
||||
text-align: center;
|
||||
|
@ -142,29 +141,11 @@ $left-gutter: 64px;
|
|||
line-height: 57px !important;
|
||||
}
|
||||
|
||||
.mx_MessagePanel_alwaysShowTimestamps .mx_MessageTimestamp {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
|
||||
left: 3px;
|
||||
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.mx_EventTile_actionBarFocused .mx_MessageActionBar,
|
||||
[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar,
|
||||
|
|
|
@ -16,62 +16,91 @@ limitations under the License.
|
|||
|
||||
.mx_PinnedEventTile {
|
||||
min-height: 40px;
|
||||
margin-bottom: 5px;
|
||||
width: 100%;
|
||||
border-radius: 5px; // for the hover
|
||||
}
|
||||
padding: 0 4px 12px;
|
||||
|
||||
.mx_PinnedEventTile:hover {
|
||||
background-color: $event-selected-color;
|
||||
}
|
||||
display: grid;
|
||||
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_timestamp {
|
||||
color: #868686;
|
||||
font-size: 0.8em;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
& + .mx_PinnedEventTile {
|
||||
padding: 12px 4px;
|
||||
border-top: 1px solid $menu-border-color;
|
||||
}
|
||||
|
||||
.mx_PinnedEventTile .mx_PinnedEventTile_timestamp {
|
||||
padding-left: 15px;
|
||||
display: none;
|
||||
}
|
||||
.mx_PinnedEventTile_senderAvatar {
|
||||
grid-area: avatar;
|
||||
}
|
||||
|
||||
.mx_PinnedEventTile .mx_PinnedEventTile_senderAvatar .mx_BaseAvatar {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.mx_PinnedEventTile_sender {
|
||||
grid-area: name;
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mx_PinnedEventTile_actions {
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
display: none;
|
||||
}
|
||||
.mx_PinnedEventTile_unpinButton {
|
||||
visibility: hidden;
|
||||
grid-area: remove;
|
||||
position: relative;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 8px;
|
||||
|
||||
.mx_PinnedEventTile:hover .mx_PinnedEventTile_timestamp {
|
||||
display: inline-block;
|
||||
}
|
||||
&:hover {
|
||||
background-color: $roomheader-addroom-bg-color;
|
||||
}
|
||||
|
||||
.mx_PinnedEventTile:hover .mx_PinnedEventTile_actions {
|
||||
display: block;
|
||||
}
|
||||
&::before {
|
||||
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 {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.mx_PinnedEventTile_message {
|
||||
grid-area: content;
|
||||
}
|
||||
|
||||
.mx_PinnedEventTile_gotoButton {
|
||||
display: inline-block;
|
||||
font-size: 0.7em; // Smaller text to avoid conflicting with the layout
|
||||
}
|
||||
.mx_PinnedEventTile_footer {
|
||||
grid-area: footer;
|
||||
font-size: 10px;
|
||||
line-height: 12px;
|
||||
|
||||
.mx_PinnedEventTile_message {
|
||||
margin-left: 50px;
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
.mx_PinnedEventTile_timestamp {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
color: $secondary-fg-color;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_link {
|
||||
padding: 0;
|
||||
margin-left: 12px;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.mx_PinnedEventTile_unpinButton {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -277,24 +277,6 @@ limitations under the License.
|
|||
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) {
|
||||
.mx_RoomHeader_wrapper {
|
||||
padding: 0;
|
||||
|
|
|
@ -19,7 +19,7 @@ limitations under the License.
|
|||
margin-bottom: 4px;
|
||||
padding: 4px;
|
||||
|
||||
contain: strict;
|
||||
contain: content; // Not strict as it will break when resizing a sublist vertically
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<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="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="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="M7.40596 11.438L11.4379 7.40602L14.9099 10.206L10.2059 14.9101L7.40596 11.438Z" 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="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="#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="#737D8C"/>
|
||||
<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="#737D8C"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1,015 B After Width: | Height: | Size: 1 KiB |
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 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");
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { ComponentType } from "react";
|
||||
|
||||
import * as sdk from './index';
|
||||
import PropTypes from 'prop-types';
|
||||
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
|
||||
* spinner until the real component loads.
|
||||
*/
|
||||
export default class AsyncWrapper extends React.Component {
|
||||
static propTypes = {
|
||||
/** A promise which resolves with the real component
|
||||
*/
|
||||
prom: PropTypes.object.isRequired,
|
||||
};
|
||||
export default class AsyncWrapper extends React.Component<IProps, IState> {
|
||||
private unmounted = false;
|
||||
|
||||
state = {
|
||||
public state = {
|
||||
component: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this._unmounted = false;
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/element-web/issues/3148
|
||||
console.log('Starting load of AsyncWrapper for modal');
|
||||
this.props.prom.then((result) => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
if (this.unmounted) return;
|
||||
|
||||
// Take the 'default' member if it's there, then we support
|
||||
// passing in just an import()ed module, since ES6 async import
|
||||
// always returns a module *namespace*.
|
||||
const component = result.default ? result.default : result;
|
||||
this.setState({component});
|
||||
const component = (result as AsyncImport<ComponentType>).default
|
||||
? (result as AsyncImport<ComponentType>).default
|
||||
: result as ComponentType;
|
||||
this.setState({ component });
|
||||
}).catch((e) => {
|
||||
console.warn('AsyncWrapper promise failed', e);
|
||||
this.setState({error: e});
|
||||
this.setState({ error: e });
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
_onWrapperCancelClick = () => {
|
||||
private onWrapperCancelClick = () => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
|
@ -71,12 +79,10 @@ export default class AsyncWrapper extends React.Component {
|
|||
} else if (this.state.error) {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <BaseDialog onFinished={this.props.onFinished}
|
||||
title={_t("Error")}
|
||||
>
|
||||
{_t("Unable to load! Check your network connectivity and try again.")}
|
||||
return <BaseDialog onFinished={this.props.onFinished} title={_t("Error")}>
|
||||
{ _t("Unable to load! Check your network connectivity and try again.") }
|
||||
<DialogButtons primaryButton={_t("Dismiss")}
|
||||
onPrimaryButtonClick={this._onWrapperCancelClick}
|
||||
onPrimaryButtonClick={this.onWrapperCancelClick}
|
||||
hasCancel={false}
|
||||
/>
|
||||
</BaseDialog>;
|
|
@ -264,7 +264,7 @@ export default class CallHandler extends EventEmitter {
|
|||
}
|
||||
|
||||
public getSupportsVirtualRooms() {
|
||||
return this.supportsPstnProtocol;
|
||||
return this.supportsSipNativeVirtual;
|
||||
}
|
||||
|
||||
public pstnLookup(phoneNumber: string): Promise<ThirdpartyLookupResponse[]> {
|
||||
|
@ -521,7 +521,9 @@ export default class CallHandler extends EventEmitter {
|
|||
let newNativeAssertedIdentity = newAssertedIdentity;
|
||||
if (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}`);
|
||||
|
||||
|
@ -862,9 +864,43 @@ export default class CallHandler extends EventEmitter {
|
|||
});
|
||||
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) {
|
||||
logger.info("Setting call in room " + activeCallRoomId + " active");
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ import {
|
|||
UploadStartedPayload,
|
||||
} from "./dispatcher/payloads/UploadPayload";
|
||||
import {IUpload} from "./models/IUpload";
|
||||
import { IImageInfo } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
const MAX_WIDTH = 800;
|
||||
const MAX_HEIGHT = 600;
|
||||
|
@ -208,12 +209,12 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
|
|||
}
|
||||
|
||||
let imageInfo;
|
||||
return loadImageElement(imageFile).then(function(r) {
|
||||
return loadImageElement(imageFile).then((r) => {
|
||||
return createThumbnail(r.img, r.width, r.height, thumbnailType);
|
||||
}).then(function(result) {
|
||||
}).then((result) => {
|
||||
imageInfo = result.info;
|
||||
return uploadFile(matrixClient, roomId, result.thumbnail);
|
||||
}).then(function(result) {
|
||||
}).then((result) => {
|
||||
imageInfo.thumbnail_url = result.url;
|
||||
imageInfo.thumbnail_file = result.file;
|
||||
return imageInfo;
|
||||
|
@ -264,12 +265,12 @@ function infoForVideoFile(matrixClient, roomId, videoFile) {
|
|||
const thumbnailType = "image/jpeg";
|
||||
|
||||
let videoInfo;
|
||||
return loadVideoElement(videoFile).then(function(video) {
|
||||
return loadVideoElement(videoFile).then((video) => {
|
||||
return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
|
||||
}).then(function(result) {
|
||||
}).then((result) => {
|
||||
videoInfo = result.info;
|
||||
return uploadFile(matrixClient, roomId, result.thumbnail);
|
||||
}).then(function(result) {
|
||||
}).then((result) => {
|
||||
videoInfo.thumbnail_url = result.url;
|
||||
videoInfo.thumbnail_file = result.file;
|
||||
return videoInfo;
|
||||
|
@ -308,7 +309,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 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;
|
||||
if (matrixClient.isRoomEncrypted(roomId)) {
|
||||
// If the room is encrypted then encrypt the file before uploading it.
|
||||
|
@ -355,7 +361,7 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo
|
|||
// If the attachment isn't encrypted then include the URL directly.
|
||||
return {"url": url};
|
||||
});
|
||||
promise1.abort = () => {
|
||||
(promise1 as any).abort = () => {
|
||||
canceled = true;
|
||||
MatrixClientPeg.get().cancelUpload(basePromise);
|
||||
};
|
||||
|
@ -367,7 +373,7 @@ export default class ContentMessages {
|
|||
private inprogress: IUpload[] = [];
|
||||
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 prom = MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => {
|
||||
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
||||
|
@ -441,7 +447,7 @@ export default class ContentMessages {
|
|||
let uploadAll = false;
|
||||
// Promise to complete before sending next file into room, used for synchronisation of file-sending
|
||||
// to match the order the files were specified in
|
||||
let promBefore = Promise.resolve();
|
||||
let promBefore: Promise<any> = Promise.resolve();
|
||||
for (let i = 0; i < okFiles.length; ++i) {
|
||||
const file = okFiles[i];
|
||||
if (!uploadAll) {
|
||||
|
|
|
@ -98,7 +98,7 @@ class Presence {
|
|||
}
|
||||
|
||||
try {
|
||||
await MatrixClientPeg.get().setPresence(this.state);
|
||||
await MatrixClientPeg.get().setPresence({presence: this.state});
|
||||
console.info("Presence:", newState);
|
||||
} catch (err) {
|
||||
console.error("Failed to set presence:", err);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,35 +14,37 @@ See the License for the specific language governing permissions and
|
|||
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 { EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||
|
||||
export default class Resend {
|
||||
static resendUnsentEvents(room) {
|
||||
return Promise.all(room.getPendingEvents().filter(function(ev) {
|
||||
static resendUnsentEvents(room: Room): Promise<void[]> {
|
||||
return Promise.all(room.getPendingEvents().filter(function(ev: MatrixEvent) {
|
||||
return ev.status === EventStatus.NOT_SENT;
|
||||
}).map(function(event) {
|
||||
}).map(function(event: MatrixEvent) {
|
||||
return Resend.resend(event);
|
||||
}));
|
||||
}
|
||||
|
||||
static cancelUnsentEvents(room) {
|
||||
room.getPendingEvents().filter(function(ev) {
|
||||
static cancelUnsentEvents(room: Room): void {
|
||||
room.getPendingEvents().filter(function(ev: MatrixEvent) {
|
||||
return ev.status === EventStatus.NOT_SENT;
|
||||
}).forEach(function(event) {
|
||||
}).forEach(function(event: MatrixEvent) {
|
||||
Resend.removeFromQueue(event);
|
||||
});
|
||||
}
|
||||
|
||||
static resend(event) {
|
||||
static resend(event: MatrixEvent): Promise<void> {
|
||||
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
||||
return MatrixClientPeg.get().resendEvent(event, room).then(function(res) {
|
||||
dis.dispatch({
|
||||
action: 'message_sent',
|
||||
event: event,
|
||||
});
|
||||
}, function(err) {
|
||||
}, function(err: Error) {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/element-web/issues/3148
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
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,
|
||||
* 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);
|
||||
}
|
||||
|
||||
export function getCachedRoomIDForAlias(alias) {
|
||||
export function getCachedRoomIDForAlias(alias: string): string {
|
||||
return aliasToIDMap.get(alias);
|
||||
}
|
|
@ -66,7 +66,7 @@ async function serverSideSearchProcess(term, roomId = undefined) {
|
|||
highlights: [],
|
||||
};
|
||||
|
||||
return client._processRoomEventsSearch(searchResult, result.response);
|
||||
return client.processRoomEventsSearch(searchResult, result.response);
|
||||
}
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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;
|
||||
|
||||
// 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.
|
||||
const newResultCount = result.results.length - oldResultCount;
|
||||
|
|
|
@ -271,7 +271,7 @@ async function onSecretRequested(
|
|||
}
|
||||
return key && encodeBase64(key);
|
||||
} else if (name === "m.megolm_backup.v1") {
|
||||
const key = await client._crypto.getSessionBackupPrivateKey();
|
||||
const key = await client.crypto.getSessionBackupPrivateKey();
|
||||
if (!key) {
|
||||
console.log(
|
||||
`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
|
||||
const currentAcceptedTerms = await MatrixClientPeg.get().getAccountData('m.accepted_terms');
|
||||
let agreedUrlSet;
|
||||
let agreedUrlSet: Set<string>;
|
||||
if (!currentAcceptedTerms || !currentAcceptedTerms.getContent() || !currentAcceptedTerms.getContent().accepted) {
|
||||
agreedUrlSet = new Set();
|
||||
} else {
|
||||
|
|
|
@ -33,7 +33,7 @@ export default class VoipUserMapper {
|
|||
|
||||
private async userToVirtualUser(userId: string): Promise<string> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -82,14 +82,14 @@ export default class VoipUserMapper {
|
|||
return Boolean(claimedNativeRoomId);
|
||||
}
|
||||
|
||||
public async onNewInvitedRoom(invitedRoom: Room) {
|
||||
public async onNewInvitedRoom(invitedRoom: Room): Promise<void> {
|
||||
if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return;
|
||||
|
||||
const inviterId = invitedRoom.getDMInviter();
|
||||
console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
|
||||
const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);
|
||||
if (result.length === 0) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
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");
|
||||
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.
|
||||
*/
|
||||
|
||||
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
|
||||
// become dispatches in the same place.
|
||||
|
@ -27,7 +33,7 @@ import dis from '../dispatcher/dispatcher';
|
|||
* @param {string} prevState the previous sync state.
|
||||
* @returns {Object} an action of type MatrixActions.sync.
|
||||
*/
|
||||
function createSyncAction(matrixClient, state, prevState) {
|
||||
function createSyncAction(matrixClient: MatrixClient, state: string, prevState: string): ActionPayload {
|
||||
return {
|
||||
action: 'MatrixActions.sync',
|
||||
state,
|
||||
|
@ -53,7 +59,7 @@ function createSyncAction(matrixClient, state, prevState) {
|
|||
* @param {MatrixEvent} accountDataEvent the account data event.
|
||||
* @returns {AccountDataAction} an action of type MatrixActions.accountData.
|
||||
*/
|
||||
function createAccountDataAction(matrixClient, accountDataEvent) {
|
||||
function createAccountDataAction(matrixClient: MatrixClient, accountDataEvent: MatrixEvent): ActionPayload {
|
||||
return {
|
||||
action: 'MatrixActions.accountData',
|
||||
event: accountDataEvent,
|
||||
|
@ -81,7 +87,11 @@ function createAccountDataAction(matrixClient, accountDataEvent) {
|
|||
* @param {Room} room the room where account data was changed
|
||||
* @returns {RoomAccountDataAction} an action of type MatrixActions.Room.accountData.
|
||||
*/
|
||||
function createRoomAccountDataAction(matrixClient, accountDataEvent, room) {
|
||||
function createRoomAccountDataAction(
|
||||
matrixClient: MatrixClient,
|
||||
accountDataEvent: MatrixEvent,
|
||||
room: Room,
|
||||
): ActionPayload {
|
||||
return {
|
||||
action: 'MatrixActions.Room.accountData',
|
||||
event: accountDataEvent,
|
||||
|
@ -106,7 +116,7 @@ function createRoomAccountDataAction(matrixClient, accountDataEvent, room) {
|
|||
* @param {Room} room the Room that was stored.
|
||||
* @returns {RoomAction} an action of type `MatrixActions.Room`.
|
||||
*/
|
||||
function createRoomAction(matrixClient, room) {
|
||||
function createRoomAction(matrixClient: MatrixClient, room: Room): ActionPayload {
|
||||
return { action: 'MatrixActions.Room', room };
|
||||
}
|
||||
|
||||
|
@ -127,7 +137,7 @@ function createRoomAction(matrixClient, room) {
|
|||
* @param {Room} room the Room whose tags were changed.
|
||||
* @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 };
|
||||
}
|
||||
|
||||
|
@ -140,7 +150,7 @@ function createRoomTagsAction(matrixClient, roomTagsEvent, room) {
|
|||
* @param {Room} room the room the receipt happened in.
|
||||
* @returns {Object} an action of type MatrixActions.Room.receipt.
|
||||
*/
|
||||
function createRoomReceiptAction(matrixClient, event, room) {
|
||||
function createRoomReceiptAction(matrixClient: MatrixClient, event: MatrixEvent, room: Room): ActionPayload {
|
||||
return {
|
||||
action: 'MatrixActions.Room.receipt',
|
||||
event,
|
||||
|
@ -178,7 +188,17 @@ function createRoomReceiptAction(matrixClient, event, room) {
|
|||
* @param {EventTimeline} data.timeline the timeline being altered.
|
||||
* @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 {
|
||||
action: 'MatrixActions.Room.timeline',
|
||||
event: timelineEvent,
|
||||
|
@ -208,8 +228,13 @@ function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTi
|
|||
* @param {string} oldMembership the previous membership, can be null.
|
||||
* @returns {RoomMembershipAction} an action of type `MatrixActions.Room.myMembership`.
|
||||
*/
|
||||
function createSelfMembershipAction(matrixClient, room, membership, oldMembership) {
|
||||
return { action: 'MatrixActions.Room.myMembership', room, membership, oldMembership};
|
||||
function createSelfMembershipAction(
|
||||
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.
|
||||
* @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 };
|
||||
}
|
||||
|
||||
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
|
||||
* the given MatrixClient.
|
||||
*/
|
||||
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
|
||||
* they are emitted.
|
||||
* @param {MatrixClient} matrixClient the MatrixClient to listen to events from
|
||||
*/
|
||||
start(matrixClient) {
|
||||
this._addMatrixClientListener(matrixClient, 'sync', createSyncAction);
|
||||
this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Room', createRoomAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Room.receipt', createRoomReceiptAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Room.myMembership', createSelfMembershipAction);
|
||||
this._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);
|
||||
});
|
||||
start(matrixClient: MatrixClient) {
|
||||
addMatrixClientListener(matrixClient, 'sync', createSyncAction);
|
||||
addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
|
||||
addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction);
|
||||
addMatrixClientListener(matrixClient, 'Room', createRoomAction);
|
||||
addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
|
||||
addMatrixClientListener(matrixClient, 'Room.receipt', createRoomReceiptAction);
|
||||
addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction);
|
||||
addMatrixClientListener(matrixClient, 'Room.myMembership', createSelfMembershipAction);
|
||||
addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop listening to events.
|
||||
*/
|
||||
stop() {
|
||||
this._matrixClientListenersStop.forEach((stopListener) => stopListener());
|
||||
matrixClientListenersStop.forEach((stopListener) => stopListener());
|
||||
matrixClientListenersStop = [];
|
||||
},
|
||||
};
|
|
@ -358,7 +358,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
|
||||
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
|
||||
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);
|
||||
if (event) events.push(event);
|
||||
}
|
||||
|
|
|
@ -378,7 +378,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.onLoggedIn();
|
||||
}
|
||||
|
||||
const promisesList = [this.firstSyncPromise.promise];
|
||||
const promisesList: Promise<any>[] = [this.firstSyncPromise.promise];
|
||||
if (cryptoEnabled) {
|
||||
// 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
|
||||
|
|
|
@ -19,7 +19,6 @@ limitations under the License.
|
|||
import React, {createRef} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import {wantsDateSeparator} from '../../DateUtils';
|
||||
import * as sdk from '../../index';
|
||||
|
@ -616,10 +615,6 @@ export default class MessagePanel extends React.Component {
|
|||
const eventId = mxEv.getId();
|
||||
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];
|
||||
|
||||
let isLastSuccessful = false;
|
||||
|
@ -651,7 +646,6 @@ export default class MessagePanel extends React.Component {
|
|||
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
|
||||
<EventTile
|
||||
as="li"
|
||||
data-scroll-tokens={scrollToken}
|
||||
ref={this._collectEventNode.bind(this, eventId)}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
|
||||
mxEvent={mxEv}
|
||||
|
@ -854,13 +848,6 @@ export default class MessagePanel extends React.Component {
|
|||
|
||||
const style = this.props.hidden ? { display: 'none' } : {};
|
||||
|
||||
const className = classNames(
|
||||
this.props.className,
|
||||
{
|
||||
"mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
|
||||
},
|
||||
);
|
||||
|
||||
let whoIsTyping;
|
||||
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
|
||||
whoIsTyping = (<WhoIsTypingTile
|
||||
|
@ -884,7 +871,7 @@ export default class MessagePanel extends React.Component {
|
|||
<ErrorBoundary>
|
||||
<ScrollPanel
|
||||
ref={this._scrollPanel}
|
||||
className={className}
|
||||
className={this.props.className}
|
||||
onScroll={this.props.onScroll}
|
||||
onUserScroll={this.props.onUserScroll}
|
||||
onResize={this.onResize}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2016, 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,29 +14,25 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import React from "react";
|
||||
|
||||
import { _t } from '../../languageHandler';
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import * as sdk from "../../index";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
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
|
||||
*/
|
||||
@replaceableComponent("structures.NotificationPanel")
|
||||
class NotificationPanel extends React.Component {
|
||||
static propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default class NotificationPanel extends React.PureComponent<IProps> {
|
||||
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">
|
||||
<h2>{_t('You’re all caught up')}</h2>
|
||||
<p>{_t('You have no visible notifications.')}</p>
|
||||
|
@ -47,6 +41,7 @@ class NotificationPanel extends React.Component {
|
|||
let content;
|
||||
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
||||
if (timelineSet) {
|
||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||
content = (
|
||||
<TimelinePanel
|
||||
manageReadReceipts={false}
|
||||
|
@ -55,11 +50,12 @@ class NotificationPanel extends React.Component {
|
|||
showUrlPreview={false}
|
||||
tileShape="notif"
|
||||
empty={emptyState}
|
||||
alwaysShowTimestamps={true}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
console.error("No notifTimelineSet available!");
|
||||
content = <Loader />;
|
||||
content = <Spinner />;
|
||||
}
|
||||
|
||||
return <BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
|
||||
|
@ -67,5 +63,3 @@ class NotificationPanel extends React.Component {
|
|||
</BaseCard>;
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationPanel;
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
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");
|
||||
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 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 RateLimitedFunc from '../../ratelimitedfunc';
|
||||
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||
import GroupStore from '../../stores/GroupStore';
|
||||
import {
|
||||
RightPanelPhases,
|
||||
RIGHT_PANEL_PHASES_NO_ARGS,
|
||||
RIGHT_PANEL_SPACE_PHASES,
|
||||
RightPanelPhases,
|
||||
} from "../../stores/RightPanelStorePhases";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
||||
import WidgetCard from "../views/right_panel/WidgetCard";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
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")
|
||||
export default class RightPanel extends React.Component {
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
export default class RightPanel extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
private readonly delayedUpdate: RateLimitedFunc;
|
||||
private dispatcherRef: string;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
...RightPanelStore.getSharedInstance().roomPanelPhaseParams,
|
||||
phase: this._getPhaseFromProps(),
|
||||
phase: this.getPhaseFromProps(),
|
||||
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();
|
||||
}, 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.
|
||||
_getUserForPanel() {
|
||||
private getUserForPanel() {
|
||||
if (this.state && this.state.member) return this.state.member;
|
||||
const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams;
|
||||
return this.props.user || lastParams['member'];
|
||||
}
|
||||
|
||||
// gets the current phase from the props and also maybe the store
|
||||
_getPhaseFromProps() {
|
||||
private getPhaseFromProps() {
|
||||
const rps = RightPanelStore.getSharedInstance();
|
||||
const userForPanel = this._getUserForPanel();
|
||||
const userForPanel = this.getUserForPanel();
|
||||
if (this.props.groupId) {
|
||||
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
|
||||
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);
|
||||
const cli = this.context;
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
this._initGroupStore(this.props.groupId);
|
||||
this.initGroupStore(this.props.groupId);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -126,61 +148,47 @@ export default class RightPanel extends React.Component {
|
|||
if (this.context) {
|
||||
this.context.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
}
|
||||
this._unregisterGroupStore(this.props.groupId);
|
||||
this.unregisterGroupStore();
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
||||
if (newProps.groupId !== this.props.groupId) {
|
||||
this._unregisterGroupStore(this.props.groupId);
|
||||
this._initGroupStore(newProps.groupId);
|
||||
this.unregisterGroupStore();
|
||||
this.initGroupStore(newProps.groupId);
|
||||
}
|
||||
}
|
||||
|
||||
_initGroupStore(groupId) {
|
||||
private initGroupStore(groupId: string) {
|
||||
if (!groupId) return;
|
||||
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
|
||||
}
|
||||
|
||||
_unregisterGroupStore() {
|
||||
private unregisterGroupStore() {
|
||||
GroupStore.unregisterListener(this.onGroupStoreUpdated);
|
||||
}
|
||||
|
||||
onGroupStoreUpdated() {
|
||||
private onGroupStoreUpdated = () => {
|
||||
this.setState({
|
||||
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onInviteToGroupButtonClick() {
|
||||
showGroupInviteDialog(this.props.groupId).then(() => {
|
||||
this.setState({
|
||||
phase: RightPanelPhases.GroupMemberList,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onAddRoomToGroupButtonClick() {
|
||||
showGroupAddRoomDialog(this.props.groupId).then(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
onRoomStateMember(ev, state, member) {
|
||||
private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => {
|
||||
if (!this.props.room || member.roomId !== this.props.room.roomId) {
|
||||
return;
|
||||
}
|
||||
// redraw the badge on the membership list
|
||||
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 &&
|
||||
member.userId === this.state.member.userId) {
|
||||
// refresh the member info (e.g. new power level)
|
||||
this._delayedUpdate();
|
||||
this.delayedUpdate();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onAction(payload) {
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === Action.AfterRightPanelPhaseChange) {
|
||||
this.setState({
|
||||
phase: payload.phase,
|
||||
|
@ -194,9 +202,9 @@ export default class RightPanel extends React.Component {
|
|||
space: payload.space,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
private onClose = () => {
|
||||
// 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
|
||||
// of the app and is generally a bit silly.
|
||||
|
@ -224,16 +232,6 @@ export default class RightPanel extends React.Component {
|
|||
};
|
||||
|
||||
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 />;
|
||||
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}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.userId}
|
||||
phase={this.state.phase}
|
||||
onClose={this.onClose} />;
|
||||
break;
|
||||
|
||||
|
@ -299,6 +298,12 @@ export default class RightPanel extends React.Component {
|
|||
panel = <NotificationPanel onClose={this.onClose} />;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.PinnedMessages:
|
||||
if (SettingsStore.getValue("feature_pinning")) {
|
||||
panel = <PinnedMessagesCard room={this.props.room} onClose={this.onClose} />;
|
||||
}
|
||||
break;
|
||||
|
||||
case RightPanelPhases.FilePanel:
|
||||
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
|
||||
break;
|
|
@ -46,7 +46,7 @@ import RoomViewStore from '../../stores/RoomViewStore';
|
|||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import {Layout} from "../../settings/Layout";
|
||||
import { Layout } from "../../settings/Layout";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||
|
@ -54,16 +54,13 @@ import RoomContext from "../../contexts/RoomContext";
|
|||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
import ScrollPanel from "./ScrollPanel";
|
||||
import TimelinePanel from "./TimelinePanel";
|
||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
||||
import ForwardMessage from "../views/rooms/ForwardMessage";
|
||||
import SearchBar from "../views/rooms/SearchBar";
|
||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
|
||||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import { XOR } from "../../@types/common";
|
||||
|
@ -82,7 +79,7 @@ import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager';
|
|||
import { objectHasDiff } from "../../utils/objects";
|
||||
import SpaceRoomView from "./SpaceRoomView";
|
||||
import { IOpts } from "../../createRoom";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { omit } from 'lodash';
|
||||
import UIStore from "../../stores/UIStore";
|
||||
|
||||
|
@ -138,7 +135,6 @@ export interface IState {
|
|||
// Whether to highlight the event scrolled to
|
||||
isInitialEventHighlighted?: boolean;
|
||||
replyToEvent?: MatrixEvent;
|
||||
forwardingEvent?: MatrixEvent;
|
||||
numUnreadMessages: number;
|
||||
draggingFile: boolean;
|
||||
searching: boolean;
|
||||
|
@ -157,7 +153,6 @@ export interface IState {
|
|||
canPeek: boolean;
|
||||
showApps: boolean;
|
||||
isPeeking: boolean;
|
||||
showingPinned: boolean;
|
||||
showReadReceipts: boolean;
|
||||
showRightPanel: boolean;
|
||||
// error object, as from the matrix client/server API
|
||||
|
@ -235,7 +230,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
canPeek: false,
|
||||
showApps: false,
|
||||
isPeeking: false,
|
||||
showingPinned: false,
|
||||
showReadReceipts: true,
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||
joining: false,
|
||||
|
@ -327,10 +321,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
initialEventId: RoomViewStore.getInitialEventId(),
|
||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||
replyToEvent: RoomViewStore.getQuotingEvent(),
|
||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||
// we should only peek once we have a ready client
|
||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
|
||||
};
|
||||
|
@ -1403,13 +1395,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
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) => {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
|
@ -1422,18 +1407,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
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 = () => {
|
||||
dis.dispatch({
|
||||
action: "appsDrawer",
|
||||
|
@ -1526,7 +1499,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
private onSearchClick = () => {
|
||||
this.setState({
|
||||
searching: !this.state.searching,
|
||||
showingPinned: false,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1539,10 +1511,19 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
// jump down to the bottom of this room, where new events are arriving
|
||||
private jumpToLiveTimeline = () => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.state.room.roomId,
|
||||
});
|
||||
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
||||
// 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
|
||||
|
@ -1841,11 +1822,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
let aux = null;
|
||||
let previewBar;
|
||||
let hideCancel = false;
|
||||
if (this.state.forwardingEvent) {
|
||||
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
|
||||
} else if (this.state.searching) {
|
||||
hideCancel = true; // has own cancel
|
||||
if (this.state.searching) {
|
||||
aux = <SearchBar
|
||||
searchInProgress={this.state.searchInProgress}
|
||||
onCancelClick={this.onCancelSearchClick}
|
||||
|
@ -1854,10 +1831,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
/>;
|
||||
} else if (showRoomUpgradeBar) {
|
||||
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") {
|
||||
// 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.
|
||||
|
@ -1866,7 +1839,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
inviterName = this.props.oobData.inviterName;
|
||||
}
|
||||
const invitedEmail = this.props.threepidInvite?.toEmail;
|
||||
hideCancel = true;
|
||||
previewBar = (
|
||||
<RoomPreviewBar
|
||||
onJoinClick={this.onJoinButtonClicked}
|
||||
|
@ -1984,11 +1956,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
hideMessagePanel = true;
|
||||
}
|
||||
|
||||
const shouldHighlight = this.state.isInitialEventHighlighted;
|
||||
let highlightedEventId = null;
|
||||
if (this.state.forwardingEvent) {
|
||||
highlightedEventId = this.state.forwardingEvent.getId();
|
||||
} else if (shouldHighlight) {
|
||||
if (this.state.isInitialEventHighlighted) {
|
||||
highlightedEventId = this.state.initialEventId;
|
||||
}
|
||||
|
||||
|
@ -2077,8 +2046,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
inRoom={myMembership === 'join'}
|
||||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onPinnedClick={this.onPinnedClick}
|
||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
|
|
|
@ -587,6 +587,10 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
|
|||
<h3>{ _t("Me and my teammates") }</h3>
|
||||
<div>{ _t("A private space for you and your teammates") }</div>
|
||||
</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 />
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -120,6 +120,9 @@ class TimelinePanel extends React.Component {
|
|||
|
||||
// which layout to use
|
||||
layout: LayoutPropType,
|
||||
|
||||
// whether to always show timestamps for an event
|
||||
alwaysShowTimestamps: PropTypes.bool,
|
||||
}
|
||||
|
||||
// a map from room id to read marker event timestamp
|
||||
|
@ -1440,7 +1443,7 @@ class TimelinePanel extends React.Component {
|
|||
onFillRequest={this.onMessageListFillRequest}
|
||||
onUnfillRequest={this.onMessageListUnfillRequest}
|
||||
isTwelveHour={this.state.isTwelveHour}
|
||||
alwaysShowTimestamps={this.state.alwaysShowTimestamps}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps}
|
||||
className={this.props.className}
|
||||
tileShape={this.props.tileShape}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
|
|
|
@ -61,7 +61,7 @@ interface IProps {
|
|||
is_url?: string;
|
||||
session_id: string;
|
||||
/* eslint-enable camelcase */
|
||||
}): void;
|
||||
}): string;
|
||||
// registration shouldn't know or care how login is done.
|
||||
onLoginClick(): void;
|
||||
onServerConfigChange(config: ValidatedServerConfig): void;
|
||||
|
|
|
@ -17,9 +17,9 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
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 * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -28,9 +28,11 @@ import Resend from '../../../Resend';
|
|||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { MenuItem } from "../../structures/ContextMenu";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
|
||||
import ForwardDialog from "../dialogs/ForwardDialog";
|
||||
|
||||
export function canCancel(eventStatus) {
|
||||
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)
|
||||
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl
|
||||
&& 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
|
||||
if (!SettingsStore.getValue("feature_pinning")) canPin = false;
|
||||
|
@ -92,7 +94,7 @@ export default class MessageContextMenu extends React.Component {
|
|||
|
||||
_isPinned() {
|
||||
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;
|
||||
const content = pinnedEvent.getContent();
|
||||
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 = () => {
|
||||
if (this.props.onCloseDialog) this.props.onCloseDialog();
|
||||
dis.dispatch({
|
||||
action: 'forward_event',
|
||||
Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
event: this.props.mxEvent,
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
});
|
||||
this.closeMenu();
|
||||
};
|
||||
|
||||
onPinClick = () => {
|
||||
MatrixClientPeg.get().getStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', '')
|
||||
.catch((e) => {
|
||||
// Intercept the Event Not Found error and fall through the promise chain with no event.
|
||||
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 room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||
const eventId = this.props.mxEvent.getId();
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.sendStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', {pinned: eventIds}, '');
|
||||
const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.pinned || [];
|
||||
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();
|
||||
};
|
||||
|
||||
|
|
|
@ -40,6 +40,8 @@ interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
|
|||
showUnpin?: boolean;
|
||||
// override delete handler
|
||||
onDeleteClick?(): void;
|
||||
// override edit handler
|
||||
onEditClick?(): void;
|
||||
}
|
||||
|
||||
const WidgetContextMenu: React.FC<IProps> = ({
|
||||
|
@ -47,6 +49,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
app,
|
||||
userWidget,
|
||||
onDeleteClick,
|
||||
onEditClick,
|
||||
showUnpin,
|
||||
...props
|
||||
}) => {
|
||||
|
@ -89,12 +92,16 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
|
||||
let editButton;
|
||||
if (canModify && WidgetUtils.isManagedByManager(app)) {
|
||||
const onEditClick = () => {
|
||||
WidgetUtils.editWidget(room, app);
|
||||
const _onEditClick = () => {
|
||||
if (onEditClick) {
|
||||
onEditClick();
|
||||
} else {
|
||||
WidgetUtils.editWidget(room, app);
|
||||
}
|
||||
onFinished();
|
||||
};
|
||||
|
||||
editButton = <IconizedContextMenuOption onClick={onEditClick} label={_t("Edit")} />;
|
||||
editButton = <IconizedContextMenuOption onClick={_onEditClick} label={_t("Edit")} />;
|
||||
}
|
||||
|
||||
let snapshotButton;
|
||||
|
@ -116,24 +123,29 @@ const WidgetContextMenu: React.FC<IProps> = ({
|
|||
|
||||
let deleteButton;
|
||||
if (onDeleteClick || canModify) {
|
||||
const onDeleteClickDefault = () => {
|
||||
// Show delete confirmation dialog
|
||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||
title: _t("Delete Widget"),
|
||||
description: _t(
|
||||
"Deleting a widget removes it for all users in this room." +
|
||||
" Are you sure you want to delete this widget?"),
|
||||
button: _t("Delete widget"),
|
||||
onFinished: (confirmed) => {
|
||||
if (!confirmed) return;
|
||||
WidgetUtils.setRoomWidget(roomId, app.id);
|
||||
},
|
||||
});
|
||||
const _onDeleteClick = () => {
|
||||
if (onDeleteClick) {
|
||||
onDeleteClick();
|
||||
} else {
|
||||
// Show delete confirmation dialog
|
||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||
title: _t("Delete Widget"),
|
||||
description: _t(
|
||||
"Deleting a widget removes it for all users in this room." +
|
||||
" Are you sure you want to delete this widget?"),
|
||||
button: _t("Delete widget"),
|
||||
onFinished: (confirmed) => {
|
||||
if (!confirmed) return;
|
||||
WidgetUtils.setRoomWidget(roomId, app.id);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onFinished();
|
||||
};
|
||||
|
||||
deleteButton = <IconizedContextMenuOption
|
||||
onClick={onDeleteClick || onDeleteClickDefault}
|
||||
onClick={_onDeleteClick}
|
||||
label={userWidget ? _t("Remove") : _t("Remove for everyone")}
|
||||
/>;
|
||||
}
|
||||
|
|
|
@ -766,7 +766,7 @@ class VerificationExplorer extends React.PureComponent<IExplorerProps> {
|
|||
render() {
|
||||
const cli = this.context;
|
||||
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();
|
||||
|
||||
return (<div>
|
||||
|
|
247
src/components/views/dialogs/ForwardDialog.tsx
Normal file
247
src/components/views/dialogs/ForwardDialog.tsx
Normal file
|
@ -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 {
|
||||
onFinished: (bool) => void;
|
||||
onFinished(...args: any): void;
|
||||
}
|
||||
|
|
|
@ -159,7 +159,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
|
|||
stickyBottom={false}
|
||||
startAtBottom={false}
|
||||
>
|
||||
<ul className="mx_MessageEditHistoryDialog_edits mx_MessagePanel_alwaysShowTimestamps">{this._renderEdits()}</ul>
|
||||
<ul className="mx_MessageEditHistoryDialog_edits">{this._renderEdits()}</ul>
|
||||
</ScrollPanel>);
|
||||
}
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
|
|
@ -19,7 +19,7 @@ import React from 'react';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import Tooltip from './Tooltip';
|
||||
import Tooltip, {Alignment} from './Tooltip';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||
|
@ -28,6 +28,7 @@ interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
|||
tooltipClassName?: string;
|
||||
forceHide?: boolean;
|
||||
yOffset?: number;
|
||||
alignment?: Alignment;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -66,13 +67,14 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
|
|||
|
||||
render() {
|
||||
// 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
|
||||
className="mx_AccessibleTooltipButton_container"
|
||||
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
|
||||
label={tooltip || title}
|
||||
yOffset={yOffset}
|
||||
alignment={alignment}
|
||||
/> : null;
|
||||
return (
|
||||
<AccessibleButton
|
||||
|
|
|
@ -47,9 +47,14 @@ export default class AppTile extends React.Component {
|
|||
|
||||
// The key used for PersistedElement
|
||||
this._persistKey = getPersistKey(this.props.app.id);
|
||||
this._sgWidget = new StopGapWidget(this.props);
|
||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
try {
|
||||
this._sgWidget = new StopGapWidget(this.props);
|
||||
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.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)
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
this._sgWidget.stop();
|
||||
if (this._sgWidget) this._sgWidget.stop();
|
||||
}
|
||||
|
||||
this.setState({ hasPermissionToLoad });
|
||||
|
@ -117,7 +122,7 @@ export default class AppTile extends React.Component {
|
|||
|
||||
componentDidMount() {
|
||||
// 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();
|
||||
}
|
||||
|
||||
|
@ -146,10 +151,15 @@ export default class AppTile extends React.Component {
|
|||
if (this._sgWidget) {
|
||||
this._sgWidget.stop();
|
||||
}
|
||||
this._sgWidget = new StopGapWidget(newProps);
|
||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
this._startWidget();
|
||||
try {
|
||||
this._sgWidget = new StopGapWidget(newProps);
|
||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
||||
this._sgWidget.on("ready", this._onWidgetReady);
|
||||
this._startWidget();
|
||||
} catch (e) {
|
||||
console.log("Failed to construct widget", e);
|
||||
this._sgWidget = null;
|
||||
}
|
||||
}
|
||||
|
||||
_startWidget() {
|
||||
|
@ -161,7 +171,7 @@ export default class AppTile extends React.Component {
|
|||
_iframeRefChange = (ref) => {
|
||||
this.iframe = ref;
|
||||
if (ref) {
|
||||
this._sgWidget.start(ref);
|
||||
if (this._sgWidget) this._sgWidget.start(ref);
|
||||
} else {
|
||||
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.
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
|
||||
this._sgWidget.stop({forceDestroy: true});
|
||||
if (this._sgWidget) this._sgWidget.stop({forceDestroy: true});
|
||||
}
|
||||
|
||||
_onWidgetPrepared = () => {
|
||||
|
@ -340,7 +350,13 @@ export default class AppTile extends React.Component {
|
|||
<Spinner message={_t("Loading...")} />
|
||||
</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
|
||||
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
appTileBody = (
|
||||
|
@ -364,7 +380,7 @@ export default class AppTile extends React.Component {
|
|||
if (this.isMixedContent()) {
|
||||
appTileBody = (
|
||||
<div className={appTileBodyClass} style={appTileBodyStyles}>
|
||||
<AppWarning errorMsg="Error - Mixed content" />
|
||||
<AppWarning errorMsg={_t("Error - Mixed content")} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
@ -417,6 +433,8 @@ export default class AppTile extends React.Component {
|
|||
onFinished={this._closeContextMenu}
|
||||
showUnpin={!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 (events.length < threshold) {
|
||||
return (
|
||||
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
|
||||
<li className="mx_EventListSummary" data-scroll-tokens={eventIds}>
|
||||
{ children }
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -19,20 +19,20 @@ limitations under the License.
|
|||
import React, { createRef } from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import FocusLock from "react-focus-lock";
|
||||
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 {aboveLeftOf, ContextMenu} from '../../structures/ContextMenu';
|
||||
import { aboveLeftOf, ContextMenu } from '../../structures/ContextMenu';
|
||||
import MessageTimestamp from "../messages/MessageTimestamp";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {formatFullDate} from "../../../DateUtils";
|
||||
import { formatFullDate } from "../../../DateUtils";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {normalizeWheelEvent} from "../../../utils/Mouse";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { normalizeWheelEvent } from "../../../utils/Mouse";
|
||||
|
||||
// Max scale to keep gaps around the image
|
||||
const MAX_SCALE = 0.95;
|
||||
|
@ -95,8 +95,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
|
||||
private initX = 0;
|
||||
private initY = 0;
|
||||
private lastX = 0;
|
||||
private lastY = 0;
|
||||
private previousX = 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
|
||||
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
|
||||
// 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
|
||||
this.image.current.addEventListener("load", this.calculateZoom);
|
||||
this.image.current.addEventListener("load", this.recalculateZoom);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.focusLock.current.removeEventListener('wheel', this.onWheel);
|
||||
window.removeEventListener("resize", this.calculateZoom);
|
||||
this.image.current.removeEventListener("load", this.calculateZoom);
|
||||
window.removeEventListener("resize", this.recalculateZoom);
|
||||
this.image.current.removeEventListener("load", this.recalculateZoom);
|
||||
}
|
||||
|
||||
private calculateZoom = () => {
|
||||
private recalculateZoom = () => {
|
||||
this.setZoomAndRotation();
|
||||
}
|
||||
|
||||
private setZoomAndRotation = (inputRotation?: number) => {
|
||||
const image = this.image.current;
|
||||
const imageWrapper = this.imageWrapper.current;
|
||||
|
||||
const zoomX = imageWrapper.clientWidth / image.naturalWidth;
|
||||
const zoomY = imageWrapper.clientHeight / image.naturalHeight;
|
||||
const rotation = inputRotation || this.state.rotation;
|
||||
|
||||
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
|
||||
// display it in its original size
|
||||
|
@ -130,6 +140,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
zoom: 1,
|
||||
minZoom: 1,
|
||||
maxZoom: 1,
|
||||
rotation: rotation,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -138,10 +149,14 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
// image by default
|
||||
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({
|
||||
minZoom: minZoom,
|
||||
maxZoom: 1,
|
||||
rotation: rotation,
|
||||
zoom: zoom,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -157,7 +172,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
return;
|
||||
}
|
||||
if (newZoom >= this.state.maxZoom) {
|
||||
this.setState({zoom: this.state.maxZoom});
|
||||
this.setState({ zoom: this.state.maxZoom });
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -170,7 +185,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
const {deltaY} = normalizeWheelEvent(ev);
|
||||
const { deltaY } = normalizeWheelEvent(ev);
|
||||
this.zoom(-(deltaY * ZOOM_COEFFICIENT));
|
||||
};
|
||||
|
||||
|
@ -192,14 +207,12 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
|
||||
private onRotateCounterClockwiseClick = () => {
|
||||
const cur = this.state.rotation;
|
||||
const rotationDegrees = cur - 90;
|
||||
this.setState({ rotation: rotationDegrees });
|
||||
this.setZoomAndRotation(cur - 90);
|
||||
};
|
||||
|
||||
private onRotateClockwiseClick = () => {
|
||||
const cur = this.state.rotation;
|
||||
const rotationDegrees = cur + 90;
|
||||
this.setState({ rotation: rotationDegrees });
|
||||
this.setZoomAndRotation(cur + 90);
|
||||
};
|
||||
|
||||
private onDownloadClick = () => {
|
||||
|
@ -246,15 +259,15 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
|
||||
// Zoom in if we are completely zoomed out
|
||||
if (this.state.zoom === this.state.minZoom) {
|
||||
this.setState({zoom: this.state.maxZoom});
|
||||
this.setState({ zoom: this.state.maxZoom });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({moving: true});
|
||||
this.setState({ moving: true });
|
||||
this.previousX = this.state.translationX;
|
||||
this.previousY = this.state.translationY;
|
||||
this.initX = ev.pageX - this.lastX;
|
||||
this.initY = ev.pageY - this.lastY;
|
||||
this.initX = ev.pageX - this.state.translationX;
|
||||
this.initY = ev.pageY - this.state.translationY;
|
||||
};
|
||||
|
||||
private onMoving = (ev: React.MouseEvent) => {
|
||||
|
@ -263,11 +276,9 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
|
||||
if (!this.state.moving) return;
|
||||
|
||||
this.lastX = ev.pageX - this.initX;
|
||||
this.lastY = ev.pageY - this.initY;
|
||||
this.setState({
|
||||
translationX: this.lastX,
|
||||
translationY: this.lastY,
|
||||
translationX: ev.pageX - this.initX,
|
||||
translationY: ev.pageY - this.initY,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -283,8 +294,10 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
translationX: 0,
|
||||
translationY: 0,
|
||||
});
|
||||
this.initX = 0;
|
||||
this.initY = 0;
|
||||
}
|
||||
this.setState({moving: false});
|
||||
this.setState({ moving: false });
|
||||
};
|
||||
|
||||
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 sender = (
|
||||
<div className="mx_ImageView_info_sender">
|
||||
{senderName}
|
||||
{ senderName }
|
||||
</div>
|
||||
);
|
||||
const messageTimestamp = (
|
||||
|
@ -382,10 +395,10 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
|
||||
info = (
|
||||
<div className="mx_ImageView_info_wrapper">
|
||||
{avatar}
|
||||
{ avatar }
|
||||
<div className="mx_ImageView_info">
|
||||
{sender}
|
||||
{messageTimestamp}
|
||||
{ sender }
|
||||
{ messageTimestamp }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -425,7 +438,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_zoomIn"
|
||||
title={_t("Zoom in")}
|
||||
onClick={ this.onZoomInClick }>
|
||||
onClick={this.onZoomInClick}>
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
}
|
||||
|
@ -441,7 +454,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
ref={this.focusLock}
|
||||
>
|
||||
<div className="mx_ImageView_panel">
|
||||
{info}
|
||||
{ info }
|
||||
<div className="mx_ImageView_toolbar">
|
||||
<AccessibleTooltipButton
|
||||
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")}
|
||||
onClick={this.onRotateClockwiseClick}>
|
||||
</AccessibleTooltipButton>
|
||||
{zoomOutButton}
|
||||
{zoomInButton}
|
||||
{ zoomOutButton }
|
||||
{ zoomInButton }
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_download"
|
||||
title={_t("Download")}
|
||||
onClick={ this.onDownloadClick }>
|
||||
</AccessibleTooltipButton>
|
||||
{contextMenuButton}
|
||||
{ contextMenuButton }
|
||||
<AccessibleTooltipButton
|
||||
className="mx_ImageView_button mx_ImageView_button_close"
|
||||
title={_t("Close")}
|
||||
onClick={ this.props.onFinished }>
|
||||
</AccessibleTooltipButton>
|
||||
{this.renderContextMenu()}
|
||||
{ this.renderContextMenu() }
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
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
|
||||
src={this.props.src}
|
||||
title={this.props.name}
|
||||
|
@ -480,9 +498,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||
className="mx_ImageView_image"
|
||||
draggable={true}
|
||||
onMouseDown={this.onStartMoving}
|
||||
onMouseMove={this.onMoving}
|
||||
onMouseUp={this.onEndMoving}
|
||||
onMouseLeave={this.onEndMoving}
|
||||
/>
|
||||
</div>
|
||||
</FocusLock>
|
||||
|
|
|
@ -46,6 +46,8 @@ export default class ReplyThread extends React.Component {
|
|||
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
||||
// Specifies which layout to use.
|
||||
layout: LayoutPropType,
|
||||
// Whether to always show a timestamp
|
||||
alwaysShowTimestamps: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
@ -212,7 +214,7 @@ 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)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -222,6 +224,7 @@ export default class ReplyThread extends React.Component {
|
|||
ref={ref}
|
||||
permalinkCreator={permalinkCreator}
|
||||
layout={layout}
|
||||
alwaysShowTimestamps={alwaysShowTimestamps}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
@ -386,6 +389,7 @@ export default class ReplyThread extends React.Component {
|
|||
isRedacted={ev.isRedacted()}
|
||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||
layout={this.props.layout}
|
||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
replacingEventId={ev.replacingEventId()}
|
||||
/>
|
||||
|
|
|
@ -21,12 +21,12 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import HeaderButton from './HeaderButton';
|
||||
import HeaderButtons, {HeaderKind} from './HeaderButtons';
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {ActionPayload} from "../../../dispatcher/payloads";
|
||||
import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import HeaderButtons, { HeaderKind } from './HeaderButtons';
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
const GROUP_PHASES = [
|
||||
RightPanelPhases.GroupMemberInfo,
|
||||
|
@ -84,19 +84,21 @@ export default class GroupHeaderButtons extends HeaderButtons {
|
|||
};
|
||||
|
||||
renderButtons() {
|
||||
return [
|
||||
<HeaderButton key="groupMembersButton" name="groupMembersButton"
|
||||
return <>
|
||||
<HeaderButton
|
||||
name="groupMembersButton"
|
||||
title={_t('Members')}
|
||||
isHighlighted={this.isPhase(GROUP_PHASES)}
|
||||
onClick={this.onMembersClicked}
|
||||
analytics={['Right Panel', 'Group Member List Button', 'click']}
|
||||
/>,
|
||||
<HeaderButton key="roomsButton" name="roomsButton"
|
||||
/>
|
||||
<HeaderButton
|
||||
name="roomsButton"
|
||||
title={_t('Rooms')}
|
||||
isHighlighted={this.isPhase(ROOM_PHASES)}
|
||||
onClick={this.onRoomsClicked}
|
||||
analytics={['Right Panel', 'Group Room List Button', 'click']}
|
||||
/>,
|
||||
];
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,15 +22,13 @@ import React from 'react';
|
|||
import classNames from 'classnames';
|
||||
import Analytics from '../../../Analytics';
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
// Whether this button is highlighted
|
||||
isHighlighted: boolean;
|
||||
// click handler
|
||||
onClick: () => void;
|
||||
// The badge to display above the icon
|
||||
badge?: React.ReactNode;
|
||||
// The parameters to track the click event
|
||||
analytics: Parameters<typeof Analytics.trackEvent>;
|
||||
|
||||
|
@ -40,31 +38,29 @@ interface IProps {
|
|||
title: string;
|
||||
}
|
||||
|
||||
// TODO: replace this, the composer buttons and the right panel buttons with a unified
|
||||
// representation
|
||||
// TODO: replace this, the composer buttons and the right panel buttons with a unified representation
|
||||
@replaceableComponent("views.right_panel.HeaderButton")
|
||||
export default class HeaderButton extends React.Component<IProps> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
private onClick() {
|
||||
private onClick = () => {
|
||||
Analytics.trackEvent(...this.props.analytics);
|
||||
this.props.onClick();
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const {isHighlighted, onClick, analytics, name, title, ...props} = this.props;
|
||||
|
||||
const classes = classNames({
|
||||
mx_RightPanel_headerButton: true,
|
||||
mx_RightPanel_headerButton_highlight: this.props.isHighlighted,
|
||||
[`mx_RightPanel_${this.props.name}`]: true,
|
||||
mx_RightPanel_headerButton_highlight: isHighlighted,
|
||||
[`mx_RightPanel_${name}`]: true,
|
||||
});
|
||||
|
||||
return <AccessibleTooltipButton
|
||||
aria-selected={this.props.isHighlighted}
|
||||
{...props}
|
||||
aria-selected={isHighlighted}
|
||||
role="tab"
|
||||
title={this.props.title}
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
/>;
|
||||
|
|
|
@ -21,14 +21,14 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import RightPanelStore from "../../../stores/RightPanelStore";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {Action} from '../../../dispatcher/actions';
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import {
|
||||
SetRightPanelPhasePayload,
|
||||
SetRightPanelPhaseRefireParams,
|
||||
} from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
|
||||
import {EventSubscription} from "fbemitter";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import type { EventSubscription } from "fbemitter";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
export enum HeaderKind {
|
||||
Room = "room",
|
||||
|
@ -43,11 +43,11 @@ interface IState {
|
|||
interface IProps {}
|
||||
|
||||
@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 dispatcherRef: string;
|
||||
|
||||
constructor(props: IProps, kind: HeaderKind) {
|
||||
constructor(props: IProps & P, kind: HeaderKind) {
|
||||
super(props);
|
||||
|
||||
const rps = RightPanelStore.getSharedInstance();
|
||||
|
@ -95,7 +95,7 @@ export default abstract class HeaderButtons extends React.Component<IProps, ISta
|
|||
}
|
||||
|
||||
// XXX: Make renderButtons a prop
|
||||
public abstract renderButtons(): JSX.Element[];
|
||||
public abstract renderButtons(): JSX.Element;
|
||||
|
||||
public render() {
|
||||
return <div className="mx_HeaderButtons">
|
||||
|
|
188
src/components/views/right_panel/PinnedMessagesCard.tsx
Normal file
188
src/components/views/right_panel/PinnedMessagesCard.tsx
Normal file
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import React from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import HeaderButton from './HeaderButton';
|
||||
import HeaderButtons, {HeaderKind} from './HeaderButtons';
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {ActionPayload} from "../../../dispatcher/payloads";
|
||||
import HeaderButtons, { HeaderKind } from './HeaderButtons';
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
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 = [
|
||||
RightPanelPhases.RoomSummary,
|
||||
|
@ -38,9 +42,35 @@ const ROOM_INFO_PHASES = [
|
|||
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")
|
||||
export default class RoomHeaderButtons extends HeaderButtons {
|
||||
constructor(props) {
|
||||
export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||
constructor(props: IProps) {
|
||||
super(props, HeaderKind.Room);
|
||||
}
|
||||
|
||||
|
@ -80,24 +110,32 @@ export default class RoomHeaderButtons extends HeaderButtons {
|
|||
this.setPhase(RightPanelPhases.NotificationPanel);
|
||||
};
|
||||
|
||||
private onPinnedMessagesClicked = () => {
|
||||
// This toggles for us, if needed
|
||||
this.setPhase(RightPanelPhases.PinnedMessages);
|
||||
};
|
||||
|
||||
public renderButtons() {
|
||||
return [
|
||||
return <>
|
||||
<PinnedMessagesHeaderButton
|
||||
room={this.props.room}
|
||||
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
|
||||
onClick={this.onPinnedMessagesClicked}
|
||||
/>
|
||||
<HeaderButton
|
||||
key="notifsButton"
|
||||
name="notifsButton"
|
||||
title={_t('Notifications')}
|
||||
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
|
||||
onClick={this.onNotificationsClicked}
|
||||
analytics={['Right Panel', 'Notification List Button', 'click']}
|
||||
/>,
|
||||
/>
|
||||
<HeaderButton
|
||||
key="roomSummaryButton"
|
||||
name="roomSummaryButton"
|
||||
title={_t('Room Info')}
|
||||
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
|
||||
onClick={this.onRoomSummaryClicked}
|
||||
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.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {MatrixClient} from 'matrix-js-sdk/src/client';
|
||||
import {RoomMember} from 'matrix-js-sdk/src/models/room-member';
|
||||
import {User} from 'matrix-js-sdk/src/models/user';
|
||||
import {Room} from 'matrix-js-sdk/src/models/room';
|
||||
import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
|
||||
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||
import { User } from 'matrix-js-sdk/src/models/user';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||
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 Modal from '../../../Modal';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import createRoom, { findDMForUser, privateShouldBeEncrypted } from '../../../createRoom';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
@ -39,18 +40,18 @@ import MultiInviter from "../../../utils/MultiInviter";
|
|||
import GroupStore from "../../../stores/GroupStore";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import E2EIcon from "../rooms/E2EIcon";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {textualPowerLevel} from '../../../Roles';
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { textualPowerLevel } from '../../../Roles';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import EncryptionPanel from "./EncryptionPanel";
|
||||
import {useAsyncMemo} from '../../../hooks/useAsyncMemo';
|
||||
import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification';
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
|
||||
import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
|
||||
import {useIsEncrypted} from "../../../hooks/useIsEncrypted";
|
||||
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
|
||||
import BaseCard from "./BaseCard";
|
||||
import {E2EStatus} from "../../../utils/ShieldUtils";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
import ImageView from "../elements/ImageView";
|
||||
import Spinner from "../elements/Spinner";
|
||||
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 RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
|
||||
export interface IDevice {
|
||||
|
@ -514,9 +515,6 @@ export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
|||
} else {
|
||||
setPowerLevels({});
|
||||
}
|
||||
return () => {
|
||||
setPowerLevels({});
|
||||
};
|
||||
}, [room]);
|
||||
|
||||
useEventEmitter(cli, "RoomState.events", update);
|
||||
|
@ -1530,21 +1528,16 @@ interface IProps {
|
|||
user: Member;
|
||||
groupId?: string;
|
||||
room?: Room;
|
||||
phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo | RightPanelPhases.SpaceMemberInfo;
|
||||
phase: RightPanelPhases.RoomMemberInfo
|
||||
| RightPanelPhases.GroupMemberInfo
|
||||
| RightPanelPhases.SpaceMemberInfo
|
||||
| RightPanelPhases.EncryptionPanel;
|
||||
onClose(): void;
|
||||
verificationRequest?: VerificationRequest;
|
||||
verificationRequestPromise?: Promise<VerificationRequest>;
|
||||
}
|
||||
|
||||
interface IPropsWithEncryptionPanel extends React.ComponentProps<typeof EncryptionPanel> {
|
||||
user: Member;
|
||||
groupId: void;
|
||||
room: Room;
|
||||
phase: RightPanelPhases.EncryptionPanel;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
type Props = IProps | IPropsWithEncryptionPanel;
|
||||
|
||||
const UserInfo: React.FC<Props> = ({
|
||||
const UserInfo: React.FC<IProps> = ({
|
||||
user,
|
||||
groupId,
|
||||
room,
|
||||
|
|
|
@ -644,7 +644,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
|
||||
// return early if there are no read receipts
|
||||
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
|
||||
return (<span className="mx_EventTile_readAvatars" />);
|
||||
return null;
|
||||
}
|
||||
|
||||
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
|
||||
|
@ -906,6 +906,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
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 sender;
|
||||
let avatarSize;
|
||||
|
@ -975,7 +981,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
onFocusChange={this.onActionBarFocusChange}
|
||||
/> : undefined;
|
||||
|
||||
const showTimestamp = this.props.mxEvent.getTs() && (this.props.alwaysShowTimestamps || this.state.hover);
|
||||
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;
|
||||
|
||||
|
@ -1046,7 +1053,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
case 'notif': {
|
||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||
return (
|
||||
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
||||
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
|
||||
<div className="mx_EventTile_roomName">
|
||||
<RoomAvatar room={room} width={28} height={28} />
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
|
@ -1069,12 +1076,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
onHeightChanged={this.props.onHeightChanged}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
case 'file_grid': {
|
||||
return (
|
||||
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
||||
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
|
||||
<div className="mx_EventTile_line">
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
|
@ -1095,7 +1102,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
{ timestamp }
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1108,10 +1115,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
this.props.onHeightChanged,
|
||||
this.props.permalinkCreator,
|
||||
this.replyThread,
|
||||
null,
|
||||
this.props.alwaysShowTimestamps || this.state.hover,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
||||
<li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
|
||||
{ ircTimestamp }
|
||||
{ avatar }
|
||||
{ sender }
|
||||
|
@ -1129,7 +1138,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
showUrlPreview={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
|
@ -1139,17 +1148,18 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
this.props.permalinkCreator,
|
||||
this.replyThread,
|
||||
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
|
||||
return (
|
||||
React.createElement(this.props.as || "div", {
|
||||
React.createElement(this.props.as || "li", {
|
||||
"ref": this.ref,
|
||||
"className": classes,
|
||||
"tabIndex": -1,
|
||||
"aria-live": ariaLive,
|
||||
"aria-atomic": "true",
|
||||
"data-scroll-tokens": this.props["data-scroll-tokens"],
|
||||
"data-scroll-tokens": scrollToken,
|
||||
"onMouseEnter": () => this.setState({ hover: true }),
|
||||
"onMouseLeave": () => this.setState({ hover: false }),
|
||||
}, [
|
||||
|
@ -1340,11 +1350,15 @@ class SentReceipt extends React.PureComponent<ISentReceiptProps, ISentReceiptSta
|
|||
tooltip = <Tooltip className="mx_EventTile_readAvatars_receiptTooltip" label={label} yOffset={20} />;
|
||||
}
|
||||
|
||||
return <span className="mx_EventTile_readAvatars">
|
||||
<span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
|
||||
{nonCssBadge}
|
||||
{tooltip}
|
||||
</span>
|
||||
</span>;
|
||||
return (
|
||||
<div className="mx_EventTile_msgOption">
|
||||
<span className="mx_EventTile_readAvatars">
|
||||
<span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
|
||||
{nonCssBadge}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
104
src/components/views/rooms/PinnedEventTile.tsx
Normal file
104
src/components/views/rooms/PinnedEventTile.tsx
Normal file
|
@ -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,7 +89,7 @@ export default class ReplyPreview extends React.Component {
|
|||
</div>
|
||||
<div className="mx_ReplyPreview_clear" />
|
||||
<EventTile
|
||||
last={true}
|
||||
alwaysShowTimestamps={true}
|
||||
tileShape="reply_preview"
|
||||
mxEvent={this.state.event}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
|
|
|
@ -19,10 +19,9 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
|
||||
import {CancelButton} from './SimpleRoomHeader';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||
import E2EIcon from './E2EIcon';
|
||||
|
@ -30,8 +29,8 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
|||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import RoomTopic from "../elements/RoomTopic";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import {PlaceCallType} from "../../../CallHandler";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { PlaceCallType } from "../../../CallHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.rooms.RoomHeader")
|
||||
export default class RoomHeader extends React.Component {
|
||||
|
@ -40,10 +39,8 @@ export default class RoomHeader extends React.Component {
|
|||
oobData: PropTypes.object,
|
||||
inRoom: PropTypes.bool,
|
||||
onSettingsClick: PropTypes.func,
|
||||
onPinnedClick: PropTypes.func,
|
||||
onSearchClick: PropTypes.func,
|
||||
onLeaveClick: PropTypes.func,
|
||||
onCancelClick: PropTypes.func,
|
||||
e2eStatus: PropTypes.string,
|
||||
onAppsClick: PropTypes.func,
|
||||
appsShown: PropTypes.bool,
|
||||
|
@ -53,20 +50,17 @@ export default class RoomHeader extends React.Component {
|
|||
static defaultProps = {
|
||||
editing: false,
|
||||
inRoom: false,
|
||||
onCancelClick: null,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.events", this._onRoomStateEvents);
|
||||
cli.on("Room.accountData", this._onRoomAccountData);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
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();
|
||||
};
|
||||
|
||||
_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() {
|
||||
/* eslint-disable babel/no-invalid-this */
|
||||
this.forceUpdate();
|
||||
}, 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() {
|
||||
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
|
||||
// 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;
|
||||
if (this.props.onForgetClick) {
|
||||
forgetButton =
|
||||
|
@ -248,7 +185,6 @@ export default class RoomHeader extends React.Component {
|
|||
<div className="mx_RoomHeader_buttons">
|
||||
{ videoCallButton }
|
||||
{ voiceCallButton }
|
||||
{ pinnedEventsButton }
|
||||
{ forgetButton }
|
||||
{ appsButton }
|
||||
{ searchButton }
|
||||
|
@ -263,9 +199,8 @@ export default class RoomHeader extends React.Component {
|
|||
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
|
||||
{ name }
|
||||
{ topicElement }
|
||||
{ cancelButton }
|
||||
{ rightRow }
|
||||
<RoomHeaderButtons />
|
||||
<RoomHeaderButtons room={this.props.room} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -47,6 +47,7 @@ export default class SearchResultTile extends React.Component {
|
|||
|
||||
const ts1 = mxEv.getTs();
|
||||
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
|
||||
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
|
||||
|
||||
const timeline = result.context.getTimeline();
|
||||
for (let j = 0; j < timeline.length; j++) {
|
||||
|
@ -67,6 +68,7 @@ export default class SearchResultTile extends React.Component {
|
|||
highlightLink={this.props.resultLink}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||
alwaysShowTimestamps={alwaysShowTimestamps}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -16,23 +16,9 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
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
|
||||
* and room directory.
|
||||
|
@ -41,18 +27,13 @@ export function CancelButton(props) {
|
|||
export default class SimpleRoomHeader extends React.Component {
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
onCancelClick: PropTypes.func,
|
||||
|
||||
// `src` to a TintableSvg. Optional.
|
||||
icon: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
let cancelButton;
|
||||
let icon;
|
||||
if (this.props.onCancelClick) {
|
||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||
}
|
||||
if (this.props.icon) {
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
icon = <TintableSvg
|
||||
|
@ -66,7 +47,6 @@ export default class SimpleRoomHeader extends React.Component {
|
|||
<div className="mx_RoomHeader_simpleHeader">
|
||||
{ icon }
|
||||
{ this.props.title }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -367,7 +367,7 @@ export default class Stickerpicker extends React.PureComponent {
|
|||
/**
|
||||
* Launch the integration manager on the stickers integration page
|
||||
*/
|
||||
_launchManageIntegrations() {
|
||||
_launchManageIntegrations = () => {
|
||||
// TODO: Open the right integration manager for the widget
|
||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||
IntegrationManagers.sharedInstance().openAll(
|
||||
|
@ -382,7 +382,7 @@ export default class Stickerpicker extends React.PureComponent {
|
|||
this.state.widgetId,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
let stickerPicker;
|
||||
|
@ -401,7 +401,7 @@ export default class Stickerpicker extends React.PureComponent {
|
|||
key="controls_hide_stickers"
|
||||
className={className}
|
||||
onClick={this._onHideStickersClick}
|
||||
active={this.state.showStickers}
|
||||
active={this.state.showStickers.toString()}
|
||||
title={_t("Hide Stickers")}
|
||||
>
|
||||
</AccessibleButton>;
|
||||
|
|
|
@ -79,8 +79,8 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
async _getUpdatedStatus() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const pkCache = cli.getCrossSigningCacheCallbacks();
|
||||
const crossSigning = cli._crypto._crossSigningInfo;
|
||||
const secretStorage = cli._crypto._secretStorage;
|
||||
const crossSigning = cli.crypto._crossSigningInfo;
|
||||
const secretStorage = cli.crypto._secretStorage;
|
||||
const crossSigningPublicKeysOnDevice = crossSigning.getId();
|
||||
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
|
||||
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
|
||||
|
|
|
@ -131,10 +131,10 @@ export default class SecureBackupPanel extends React.PureComponent {
|
|||
|
||||
async _getUpdatedDiagnostics() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const secretStorage = cli._crypto._secretStorage;
|
||||
const secretStorage = cli.crypto._secretStorage;
|
||||
|
||||
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
|
||||
const backupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey();
|
||||
const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey();
|
||||
const backupKeyCached = !!(backupKeyFromCache);
|
||||
const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array;
|
||||
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
||||
|
|
|
@ -15,17 +15,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { ensureDMExists } from "../../../createRoom";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Field from "../elements/Field";
|
||||
import DialPad from './DialPad';
|
||||
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 { DialNumberPayload } from "../../../dispatcher/payloads/DialNumberPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (boolean) => void;
|
||||
|
@ -67,21 +64,11 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
onDialPress = async () => {
|
||||
const results = await CallHandler.sharedInstance().pstnLookup(this.state.value);
|
||||
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"),
|
||||
});
|
||||
}
|
||||
const userId = results[0].userid;
|
||||
|
||||
const roomId = await ensureDMExists(MatrixClientPeg.get(), userId);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
});
|
||||
const payload: DialNumberPayload = {
|
||||
action: Action.DialNumber,
|
||||
number: this.state.value,
|
||||
};
|
||||
dis.dispatch(payload);
|
||||
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ limitations under the License.
|
|||
|
||||
import { createContext } from "react";
|
||||
|
||||
import {IState} from "../components/structures/RoomView";
|
||||
import {Layout} from "../settings/Layout";
|
||||
import { IState } from "../components/structures/RoomView";
|
||||
import { Layout } from "../settings/Layout";
|
||||
|
||||
const RoomContext = createContext<IState>({
|
||||
roomLoading: true,
|
||||
|
@ -31,7 +31,6 @@ const RoomContext = createContext<IState>({
|
|||
canPeek: false,
|
||||
showApps: false,
|
||||
isPeeking: false,
|
||||
showingPinned: false,
|
||||
showReadReceipts: true,
|
||||
showRightPanel: true,
|
||||
joining: false,
|
||||
|
|
|
@ -100,6 +100,12 @@ export enum Action {
|
|||
*/
|
||||
OpenDialPad = "open_dial_pad",
|
||||
|
||||
/**
|
||||
* Dial the phone number in the payload
|
||||
* payload: DialNumberPayload
|
||||
*/
|
||||
DialNumber = "dial_number",
|
||||
|
||||
/**
|
||||
* Fired when CallHandler has checked for PSTN protocol support
|
||||
* payload: none
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
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.
|
||||
|
@ -14,24 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_PinnedEventsPanel {
|
||||
border-top: 1px solid $primary-hairline-color;
|
||||
}
|
||||
import { ActionPayload } from "../payloads";
|
||||
import { Action } from "../actions";
|
||||
|
||||
.mx_PinnedEventsPanel_body {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.mx_PinnedEventsPanel_header {
|
||||
margin: 0;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.mx_PinnedEventsPanel_cancel {
|
||||
margin: 12px;
|
||||
float: right;
|
||||
display: inline-block;
|
||||
export interface DialNumberPayload extends ActionPayload {
|
||||
action: Action.DialNumber;
|
||||
number: string;
|
||||
}
|
|
@ -14,14 +14,22 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {useState, useEffect, DependencyList} from 'react';
|
||||
import { useState, useEffect, DependencyList } from 'react';
|
||||
|
||||
type Fn<T> = () => Promise<T>;
|
||||
|
||||
export const useAsyncMemo = <T>(fn: Fn<T>, deps: DependencyList, initialValue?: T): T => {
|
||||
const [value, setValue] = useState<T>(initialValue);
|
||||
useEffect(() => {
|
||||
fn().then(setValue);
|
||||
let discard = false;
|
||||
fn().then(v => {
|
||||
if (!discard) {
|
||||
setValue(v);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
discard = true;
|
||||
};
|
||||
}, deps); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
return value;
|
||||
};
|
||||
|
|
|
@ -35,8 +35,8 @@ export const useSettingValue = <T>(settingName: string, roomId: string = null, e
|
|||
};
|
||||
|
||||
// Hook to fetch whether a feature is enabled and dynamically update when that changes
|
||||
export const useFeatureEnabled = (featureName: string, roomId: string = null) => {
|
||||
const [enabled, setEnabled] = useState(SettingsStore.getValue(featureName, roomId));
|
||||
export const useFeatureEnabled = (featureName: string, roomId: string = null): boolean => {
|
||||
const [enabled, setEnabled] = useState(SettingsStore.getValue<boolean>(featureName, roomId));
|
||||
|
||||
useEffect(() => {
|
||||
const ref = SettingsStore.watchSetting(featureName, roomId, () => {
|
||||
|
|
|
@ -63,6 +63,8 @@
|
|||
"Already in call": "Already in call",
|
||||
"You're already in a call with this person.": "You're already in a call with this person.",
|
||||
"You cannot place a call with yourself.": "You cannot place a call with yourself.",
|
||||
"Unable to look up phone number": "Unable to look up phone number",
|
||||
"There was an error looking up the phone number": "There was an error looking up the phone number",
|
||||
"Call in Progress": "Call in Progress",
|
||||
"A call is currently being placed!": "A call is currently being placed!",
|
||||
"Permission Required": "Permission Required",
|
||||
|
@ -898,8 +900,6 @@
|
|||
"Fill Screen": "Fill Screen",
|
||||
"Return to call": "Return to call",
|
||||
"%(name)s on hold": "%(name)s on hold",
|
||||
"Unable to look up phone number": "Unable to look up phone number",
|
||||
"There was an error looking up the phone number": "There was an error looking up the phone number",
|
||||
"Dial pad": "Dial pad",
|
||||
"Unknown caller": "Unknown caller",
|
||||
"Incoming voice call": "Incoming voice call",
|
||||
|
@ -1473,7 +1473,6 @@
|
|||
"Encrypting your message...": "Encrypting your message...",
|
||||
"Your message was sent": "Your message was sent",
|
||||
"Failed to send": "Failed to send",
|
||||
"Please select the destination room for this message": "Please select the destination room for this message",
|
||||
"Scroll to most recent messages": "Scroll to most recent messages",
|
||||
"Close preview": "Close preview",
|
||||
"and %(count)s others...|other": "and %(count)s others...",
|
||||
|
@ -1510,11 +1509,8 @@
|
|||
"Invite to just this room": "Invite to just this room",
|
||||
"Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.",
|
||||
"This is the start of <roomName/>.": "This is the start of <roomName/>.",
|
||||
"No pinned messages.": "No pinned messages.",
|
||||
"Loading...": "Loading...",
|
||||
"Pinned Messages": "Pinned Messages",
|
||||
"Unpin Message": "Unpin Message",
|
||||
"Jump to message": "Jump to message",
|
||||
"Unpin": "Unpin",
|
||||
"View message": "View message",
|
||||
"%(duration)ss": "%(duration)ss",
|
||||
"%(duration)sm": "%(duration)sm",
|
||||
"%(duration)sh": "%(duration)sh",
|
||||
|
@ -1720,9 +1716,11 @@
|
|||
"The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to",
|
||||
"Yours, or the other users’ internet connection": "Yours, or the other users’ internet connection",
|
||||
"Yours, or the other users’ session": "Yours, or the other users’ session",
|
||||
"Nothing pinned, yet": "Nothing pinned, yet",
|
||||
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
|
||||
"Pinned messages": "Pinned messages",
|
||||
"Room Info": "Room Info",
|
||||
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
|
||||
"Unpin": "Unpin",
|
||||
"Unpin a widget to view it in this panel": "Unpin a widget to view it in this panel",
|
||||
"Options": "Options",
|
||||
"Set my room layout for everyone": "Set my room layout for everyone",
|
||||
|
@ -1898,6 +1896,7 @@
|
|||
"Add rooms to this community": "Add rooms to this community",
|
||||
"Filter community rooms": "Filter community rooms",
|
||||
"Something went wrong when trying to get your communities.": "Something went wrong when trying to get your communities.",
|
||||
"Loading...": "Loading...",
|
||||
"Display your community flair in rooms configured to show it.": "Display your community flair in rooms configured to show it.",
|
||||
"You're not currently a member of any communities.": "You're not currently a member of any communities.",
|
||||
"Frequently Used": "Frequently Used",
|
||||
|
@ -1926,6 +1925,8 @@
|
|||
"Widgets do not use message encryption.": "Widgets do not use message encryption.",
|
||||
"Widget added by": "Widget added by",
|
||||
"This widget may use cookies.": "This widget may use cookies.",
|
||||
"Error loading Widget": "Error loading Widget",
|
||||
"Error - Mixed content": "Error - Mixed content",
|
||||
"Popout widget": "Popout widget",
|
||||
"Use the <a>Desktop app</a> to see all encrypted files": "Use the <a>Desktop app</a> to see all encrypted files",
|
||||
"Use the <a>Desktop app</a> to search encrypted messages": "Use the <a>Desktop app</a> to search encrypted messages",
|
||||
|
@ -1952,7 +1953,6 @@
|
|||
"Rotate Right": "Rotate Right",
|
||||
"Download": "Download",
|
||||
"Information": "Information",
|
||||
"View message": "View message",
|
||||
"Language Dropdown": "Language Dropdown",
|
||||
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
|
||||
"%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times",
|
||||
|
@ -2205,6 +2205,13 @@
|
|||
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.",
|
||||
"Report a bug": "Report a bug",
|
||||
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.",
|
||||
"You don't have permission to do this": "You don't have permission to do this",
|
||||
"Sending": "Sending",
|
||||
"Sent": "Sent",
|
||||
"Open link": "Open link",
|
||||
"Forward message": "Forward message",
|
||||
"Message preview": "Message preview",
|
||||
"Search for rooms or people": "Search for rooms or people",
|
||||
"Confirm abort of host creation": "Confirm abort of host creation",
|
||||
"Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.",
|
||||
"Abort": "Abort",
|
||||
|
@ -2469,6 +2476,7 @@
|
|||
"Unable to reject invite": "Unable to reject invite",
|
||||
"Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)",
|
||||
"Forward Message": "Forward Message",
|
||||
"Unpin Message": "Unpin Message",
|
||||
"Pin Message": "Pin Message",
|
||||
"Unhide Preview": "Unhide Preview",
|
||||
"Share Permalink": "Share Permalink",
|
||||
|
@ -2667,7 +2675,6 @@
|
|||
"Some of your messages have not been sent": "Some of your messages have not been sent",
|
||||
"Delete all": "Delete all",
|
||||
"Retry all": "Retry all",
|
||||
"Sending": "Sending",
|
||||
"You can select all or individual messages to retry or delete": "You can select all or individual messages to retry or delete",
|
||||
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
||||
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
||||
|
@ -2728,6 +2735,8 @@
|
|||
"A private space to organise your rooms": "A private space to organise your rooms",
|
||||
"Me and my teammates": "Me and my teammates",
|
||||
"A private space for you and your teammates": "A private space for you and your teammates",
|
||||
"Teammates might not be able to view or join any private rooms you make.": "Teammates might not be able to view or join any private rooms you make.",
|
||||
"We're working on this as part of the beta, but just want to let you know.": "We're working on this as part of the beta, but just want to let you know.",
|
||||
"Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s",
|
||||
"Inviting...": "Inviting...",
|
||||
"Invite your teammates": "Invite your teammates",
|
||||
|
|
|
@ -35,7 +35,7 @@ export interface MatrixProfile {
|
|||
export interface CrawlerCheckpoint {
|
||||
roomId: string;
|
||||
token: string;
|
||||
fullCrawl: boolean;
|
||||
fullCrawl?: boolean;
|
||||
direction: string;
|
||||
}
|
||||
|
||||
|
@ -73,14 +73,14 @@ export interface EventAndProfile {
|
|||
export interface LoadArgs {
|
||||
roomId: string;
|
||||
limit: number;
|
||||
fromEvent: string;
|
||||
direction: string;
|
||||
fromEvent?: string;
|
||||
direction?: string;
|
||||
}
|
||||
|
||||
export interface IndexStats {
|
||||
size: number;
|
||||
event_count: number;
|
||||
room_count: number;
|
||||
eventCount: number;
|
||||
roomCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,33 +14,42 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set';
|
||||
import { RoomState } from 'matrix-js-sdk/src/models/room-state';
|
||||
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
|
||||
|
||||
import PlatformPeg from "../PlatformPeg";
|
||||
import {MatrixClientPeg} from "../MatrixClientPeg";
|
||||
import {RoomMember} from 'matrix-js-sdk/src/models/room-member';
|
||||
import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
|
||||
import {sleep} from "../utils/promise";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { sleep } from "../utils/promise";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import {EventEmitter} from "events";
|
||||
import {SettingLevel} from "../settings/SettingLevel";
|
||||
import { SettingLevel } from "../settings/SettingLevel";
|
||||
import {CrawlerCheckpoint, LoadArgs, SearchArgs} from "./BaseEventIndexManager";
|
||||
|
||||
// The time in ms that the crawler will wait loop iterations if there
|
||||
// have not been any checkpoints to consume in the last iteration.
|
||||
const CRAWLER_IDLE_TIME = 5000;
|
||||
|
||||
// The maximum number of events our crawler should fetch in a single crawl.
|
||||
const EVENTS_PER_CRAWL = 100;
|
||||
|
||||
interface ICrawler {
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
/*
|
||||
* Event indexing class that wraps the platform specific event indexing.
|
||||
*/
|
||||
export default class EventIndex extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.crawlerCheckpoints = [];
|
||||
// The time in ms that the crawler will wait loop iterations if there
|
||||
// have not been any checkpoints to consume in the last iteration.
|
||||
this._crawlerIdleTime = 5000;
|
||||
// The maximum number of events our crawler should fetch in a single
|
||||
// crawl.
|
||||
this._eventsPerCrawl = 100;
|
||||
this._crawler = null;
|
||||
this._currentCheckpoint = null;
|
||||
}
|
||||
private crawlerCheckpoints: CrawlerCheckpoint[] = [];
|
||||
private crawler: ICrawler = null;
|
||||
private currentCheckpoint: CrawlerCheckpoint = null;
|
||||
|
||||
async init() {
|
||||
public async init() {
|
||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||
|
||||
this.crawlerCheckpoints = await indexManager.loadCheckpoints();
|
||||
|
@ -52,7 +61,7 @@ export default class EventIndex extends EventEmitter {
|
|||
/**
|
||||
* Register event listeners that are necessary for the event index to work.
|
||||
*/
|
||||
registerListeners() {
|
||||
public registerListeners() {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
client.on('sync', this.onSync);
|
||||
|
@ -66,7 +75,7 @@ export default class EventIndex extends EventEmitter {
|
|||
/**
|
||||
* Remove the event index specific event listeners.
|
||||
*/
|
||||
removeListeners() {
|
||||
public removeListeners() {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (client === null) return;
|
||||
|
||||
|
@ -81,7 +90,7 @@ export default class EventIndex extends EventEmitter {
|
|||
/**
|
||||
* Get crawler checkpoints for the encrypted rooms and store them in the index.
|
||||
*/
|
||||
async addInitialCheckpoints() {
|
||||
public async addInitialCheckpoints() {
|
||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||
const client = MatrixClientPeg.get();
|
||||
const rooms = client.getRooms();
|
||||
|
@ -102,14 +111,14 @@ export default class EventIndex extends EventEmitter {
|
|||
const timeline = room.getLiveTimeline();
|
||||
const token = timeline.getPaginationToken("b");
|
||||
|
||||
const backCheckpoint = {
|
||||
const backCheckpoint: CrawlerCheckpoint = {
|
||||
roomId: room.roomId,
|
||||
token: token,
|
||||
direction: "b",
|
||||
fullCrawl: true,
|
||||
};
|
||||
|
||||
const forwardCheckpoint = {
|
||||
const forwardCheckpoint: CrawlerCheckpoint = {
|
||||
roomId: room.roomId,
|
||||
token: token,
|
||||
direction: "f",
|
||||
|
@ -146,7 +155,7 @@ export default class EventIndex extends EventEmitter {
|
|||
* - Every other sync, tell the event index to commit all the queued up
|
||||
* live events
|
||||
*/
|
||||
onSync = async (state, prevState, data) => {
|
||||
private onSync = async (state: string, prevState: string, data: object) => {
|
||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||
|
||||
if (prevState === "PREPARED" && state === "SYNCING") {
|
||||
|
@ -176,7 +185,15 @@ export default class EventIndex extends EventEmitter {
|
|||
* otherwise we save their event id and wait for them in the Event.decrypted
|
||||
* listener.
|
||||
*/
|
||||
onRoomTimeline = async (ev, room, toStartOfTimeline, removed, data) => {
|
||||
private onRoomTimeline = async (
|
||||
ev: MatrixEvent,
|
||||
room: Room,
|
||||
toStartOfTimeline: boolean,
|
||||
removed: boolean,
|
||||
data: {
|
||||
liveEvent: boolean;
|
||||
},
|
||||
) => {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
// We only index encrypted rooms locally.
|
||||
|
@ -194,7 +211,7 @@ export default class EventIndex extends EventEmitter {
|
|||
await this.addLiveEventToIndex(ev);
|
||||
}
|
||||
|
||||
onRoomStateEvent = async (ev, state) => {
|
||||
private onRoomStateEvent = async (ev: MatrixEvent, state: RoomState) => {
|
||||
if (!MatrixClientPeg.get().isRoomEncrypted(state.roomId)) return;
|
||||
|
||||
if (ev.getType() === "m.room.encryption" && !await this.isRoomIndexed(state.roomId)) {
|
||||
|
@ -209,7 +226,7 @@ export default class EventIndex extends EventEmitter {
|
|||
* Checks if the event was marked for addition in the Room.timeline
|
||||
* listener, if so queues it up to be added to the index.
|
||||
*/
|
||||
onEventDecrypted = async (ev, err) => {
|
||||
private onEventDecrypted = async (ev: MatrixEvent, err: Error) => {
|
||||
// If the event isn't in our live event set, ignore it.
|
||||
if (err) return;
|
||||
await this.addLiveEventToIndex(ev);
|
||||
|
@ -220,7 +237,7 @@ export default class EventIndex extends EventEmitter {
|
|||
*
|
||||
* Removes a redacted event from our event index.
|
||||
*/
|
||||
onRedaction = async (ev, room) => {
|
||||
private onRedaction = async (ev: MatrixEvent, room: Room) => {
|
||||
// We only index encrypted rooms locally.
|
||||
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return;
|
||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||
|
@ -238,7 +255,7 @@ export default class EventIndex extends EventEmitter {
|
|||
* Listens for timeline resets that are caused by a limited timeline to
|
||||
* re-add checkpoints for rooms that need to be crawled again.
|
||||
*/
|
||||
onTimelineReset = async (room, timelineSet, resetAllTimelines) => {
|
||||
private onTimelineReset = async (room: Room, timelineSet: EventTimelineSet, resetAllTimelines: boolean) => {
|
||||
if (room === null) return;
|
||||
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return;
|
||||
|
||||
|
@ -258,7 +275,7 @@ export default class EventIndex extends EventEmitter {
|
|||
* @returns {bool} Returns true if the event can be indexed, false
|
||||
* otherwise.
|
||||
*/
|
||||
isValidEvent(ev) {
|
||||
private isValidEvent(ev: MatrixEvent) {
|
||||
const isUsefulType = ["m.room.message", "m.room.name", "m.room.topic"].includes(ev.getType());
|
||||
const validEventType = isUsefulType && !ev.isRedacted() && !ev.isDecryptionFailure();
|
||||
|
||||
|
@ -282,7 +299,7 @@ export default class EventIndex extends EventEmitter {
|
|||
return validEventType && validMsgType && hasContentValue;
|
||||
}
|
||||
|
||||
eventToJson(ev) {
|
||||
private eventToJson(ev: MatrixEvent) {
|
||||
const jsonEvent = ev.toJSON();
|
||||
const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent;
|
||||
|
||||
|
@ -314,7 +331,7 @@ export default class EventIndex extends EventEmitter {
|
|||
*
|
||||
* @param {MatrixEvent} ev The event that should be added to the index.
|
||||
*/
|
||||
async addLiveEventToIndex(ev) {
|
||||
private async addLiveEventToIndex(ev: MatrixEvent) {
|
||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||
|
||||
if (!this.isValidEvent(ev)) return;
|
||||
|
@ -333,11 +350,11 @@ export default class EventIndex extends EventEmitter {
|
|||
* Emmit that the crawler has changed the checkpoint that it's currently
|
||||
* handling.
|
||||
*/
|
||||
emitNewCheckpoint() {
|
||||
private emitNewCheckpoint() {
|
||||
this.emit("changedCheckpoint", this.currentRoom());
|
||||
}
|
||||
|
||||
async addEventsFromLiveTimeline(timeline) {
|
||||
private async addEventsFromLiveTimeline(timeline: EventTimeline) {
|
||||
const events = timeline.getEvents();
|
||||
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
|
@ -346,7 +363,7 @@ export default class EventIndex extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
async addRoomCheckpoint(roomId, fullCrawl = false) {
|
||||
private async addRoomCheckpoint(roomId: string, fullCrawl = false) {
|
||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(roomId);
|
||||
|
@ -396,16 +413,16 @@ export default class EventIndex extends EventEmitter {
|
|||
* crawl, otherwise create a new checkpoint and push it to the
|
||||
* crawlerCheckpoints queue so we go through them in a round-robin way.
|
||||
*/
|
||||
async crawlerFunc() {
|
||||
private async crawlerFunc() {
|
||||
let cancelled = false;
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||
|
||||
this._crawler = {};
|
||||
|
||||
this._crawler.cancel = () => {
|
||||
cancelled = true;
|
||||
this.crawler = {
|
||||
cancel: () => {
|
||||
cancelled = true;
|
||||
},
|
||||
};
|
||||
|
||||
let idle = false;
|
||||
|
@ -417,11 +434,11 @@ export default class EventIndex extends EventEmitter {
|
|||
sleepTime = Math.max(sleepTime, 100);
|
||||
|
||||
if (idle) {
|
||||
sleepTime = this._crawlerIdleTime;
|
||||
sleepTime = CRAWLER_IDLE_TIME;
|
||||
}
|
||||
|
||||
if (this._currentCheckpoint !== null) {
|
||||
this._currentCheckpoint = null;
|
||||
if (this.currentCheckpoint !== null) {
|
||||
this.currentCheckpoint = null;
|
||||
this.emitNewCheckpoint();
|
||||
}
|
||||
|
||||
|
@ -440,7 +457,7 @@ export default class EventIndex extends EventEmitter {
|
|||
continue;
|
||||
}
|
||||
|
||||
this._currentCheckpoint = checkpoint;
|
||||
this.currentCheckpoint = checkpoint;
|
||||
this.emitNewCheckpoint();
|
||||
|
||||
idle = false;
|
||||
|
@ -453,9 +470,12 @@ export default class EventIndex extends EventEmitter {
|
|||
let res;
|
||||
|
||||
try {
|
||||
res = await client._createMessagesRequest(
|
||||
checkpoint.roomId, checkpoint.token, this._eventsPerCrawl,
|
||||
checkpoint.direction);
|
||||
res = await client.createMessagesRequest(
|
||||
checkpoint.roomId,
|
||||
checkpoint.token,
|
||||
EVENTS_PER_CRAWL,
|
||||
checkpoint.direction,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e.httpStatus === 403) {
|
||||
console.log("EventIndex: Removing checkpoint as we don't have ",
|
||||
|
@ -612,23 +632,23 @@ export default class EventIndex extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
this._crawler = null;
|
||||
this.crawler = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the crawler background task.
|
||||
*/
|
||||
startCrawler() {
|
||||
if (this._crawler !== null) return;
|
||||
public startCrawler() {
|
||||
if (this.crawler !== null) return;
|
||||
this.crawlerFunc();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the crawler background task.
|
||||
*/
|
||||
stopCrawler() {
|
||||
if (this._crawler === null) return;
|
||||
this._crawler.cancel();
|
||||
public stopCrawler() {
|
||||
if (this.crawler === null) return;
|
||||
this.crawler.cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -637,7 +657,7 @@ export default class EventIndex extends EventEmitter {
|
|||
* This removes all the MatrixClient event listeners, stops the crawler
|
||||
* task, and closes the index.
|
||||
*/
|
||||
async close() {
|
||||
public async close() {
|
||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||
this.removeListeners();
|
||||
this.stopCrawler();
|
||||
|
@ -654,7 +674,7 @@ export default class EventIndex extends EventEmitter {
|
|||
* @return {Promise<[SearchResult]>} A promise that will resolve to an array
|
||||
* of search results once the search is done.
|
||||
*/
|
||||
async search(searchArgs) {
|
||||
public async search(searchArgs: SearchArgs) {
|
||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||
return indexManager.searchEventIndex(searchArgs);
|
||||
}
|
||||
|
@ -680,11 +700,16 @@ export default class EventIndex extends EventEmitter {
|
|||
* @returns {Promise<MatrixEvent[]>} Resolves to an array of events that
|
||||
* contain URLs.
|
||||
*/
|
||||
async loadFileEvents(room, limit = 10, fromEvent = null, direction = EventTimeline.BACKWARDS) {
|
||||
public async loadFileEvents(
|
||||
room: Room,
|
||||
limit = 10,
|
||||
fromEvent: string = null,
|
||||
direction: string = EventTimeline.BACKWARDS,
|
||||
) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||
|
||||
const loadArgs = {
|
||||
const loadArgs: LoadArgs = {
|
||||
roomId: room.roomId,
|
||||
limit: limit,
|
||||
};
|
||||
|
@ -772,13 +797,13 @@ export default class EventIndex extends EventEmitter {
|
|||
* @returns {Promise<boolean>} Resolves to true if events were added to the
|
||||
* timeline, false otherwise.
|
||||
*/
|
||||
async populateFileTimeline(
|
||||
timelineSet,
|
||||
timeline,
|
||||
room,
|
||||
public async populateFileTimeline(
|
||||
timelineSet: EventTimelineSet,
|
||||
timeline: EventTimeline,
|
||||
room: Room,
|
||||
limit = 10,
|
||||
fromEvent = null,
|
||||
direction = EventTimeline.BACKWARDS,
|
||||
fromEvent: string = null,
|
||||
direction: string = EventTimeline.BACKWARDS,
|
||||
) {
|
||||
const matrixEvents = await this.loadFileEvents(room, limit, fromEvent, direction);
|
||||
|
||||
|
@ -837,7 +862,7 @@ export default class EventIndex extends EventEmitter {
|
|||
* @returns {Promise<boolean>} Resolves to a boolean which is true if more
|
||||
* events were successfully retrieved.
|
||||
*/
|
||||
paginateTimelineWindow(room, timelineWindow, direction, limit) {
|
||||
public paginateTimelineWindow(room: Room, timelineWindow: TimelineWindow, direction: string, limit: number) {
|
||||
const tl = timelineWindow.getTimelineIndex(direction);
|
||||
|
||||
if (!tl) return Promise.resolve(false);
|
||||
|
@ -871,7 +896,7 @@ export default class EventIndex extends EventEmitter {
|
|||
* @return {Promise<IndexStats>} A promise that will resolve to the index
|
||||
* statistics.
|
||||
*/
|
||||
async getStats() {
|
||||
public async getStats() {
|
||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||
return indexManager.getStats();
|
||||
}
|
||||
|
@ -885,7 +910,7 @@ export default class EventIndex extends EventEmitter {
|
|||
* @return {Promise<boolean>} Returns true if the index contains events for
|
||||
* the given room, false otherwise.
|
||||
*/
|
||||
async isRoomIndexed(roomId) {
|
||||
public async isRoomIndexed(roomId) {
|
||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||
return indexManager.isRoomIndexed(roomId);
|
||||
}
|
||||
|
@ -896,21 +921,21 @@ export default class EventIndex extends EventEmitter {
|
|||
* @returns {Room} A MatrixRoom that is being currently crawled, null
|
||||
* if no room is currently being crawled.
|
||||
*/
|
||||
currentRoom() {
|
||||
if (this._currentCheckpoint === null && this.crawlerCheckpoints.length === 0) {
|
||||
public currentRoom() {
|
||||
if (this.currentCheckpoint === null && this.crawlerCheckpoints.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
if (this._currentCheckpoint !== null) {
|
||||
return client.getRoom(this._currentCheckpoint.roomId);
|
||||
if (this.currentCheckpoint !== null) {
|
||||
return client.getRoom(this.currentCheckpoint.roomId);
|
||||
} else {
|
||||
return client.getRoom(this.crawlerCheckpoints[0].roomId);
|
||||
}
|
||||
}
|
||||
|
||||
crawlingRooms() {
|
||||
public crawlingRooms() {
|
||||
const totalRooms = new Set();
|
||||
const crawlingRooms = new Set();
|
||||
|
||||
|
@ -918,14 +943,14 @@ export default class EventIndex extends EventEmitter {
|
|||
crawlingRooms.add(checkpoint.roomId);
|
||||
});
|
||||
|
||||
if (this._currentCheckpoint !== null) {
|
||||
crawlingRooms.add(this._currentCheckpoint.roomId);
|
||||
if (this.currentCheckpoint !== null) {
|
||||
crawlingRooms.add(this.currentCheckpoint.roomId);
|
||||
}
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
const rooms = client.getRooms();
|
||||
|
||||
const isRoomEncrypted = (room) => {
|
||||
const isRoomEncrypted = (room: Room) => {
|
||||
return client.isRoomEncrypted(room.roomId);
|
||||
};
|
||||
|
||||
|
@ -934,6 +959,6 @@ export default class EventIndex extends EventEmitter {
|
|||
totalRooms.add(room.roomId);
|
||||
});
|
||||
|
||||
return {crawlingRooms, totalRooms};
|
||||
return { crawlingRooms, totalRooms };
|
||||
}
|
||||
}
|
|
@ -92,8 +92,8 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) {
|
|||
body.append('cross_signing_key', client.getCrossSigningId());
|
||||
|
||||
// add cross-signing status information
|
||||
const crossSigning = client._crypto._crossSigningInfo;
|
||||
const secretStorage = client._crypto._secretStorage;
|
||||
const crossSigning = client.crypto._crossSigningInfo;
|
||||
const secretStorage = client.crypto._secretStorage;
|
||||
|
||||
body.append("cross_signing_ready", String(await client.isCrossSigningReady()));
|
||||
body.append("cross_signing_supported_by_hs",
|
||||
|
@ -114,7 +114,7 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) {
|
|||
body.append("secret_storage_key_in_account", String(!!(await secretStorage.hasKey())));
|
||||
|
||||
body.append("session_backup_key_in_secret_storage", String(!!(await client.isKeyBackupKeyStored())));
|
||||
const sessionBackupKeyFromCache = await client._crypto.getSessionBackupPrivateKey();
|
||||
const sessionBackupKeyFromCache = await client.crypto.getSessionBackupPrivateKey();
|
||||
body.append("session_backup_key_cached", String(!!sessionBackupKeyFromCache));
|
||||
body.append("session_backup_key_well_formed", String(sessionBackupKeyFromCache instanceof Uint8Array));
|
||||
}
|
||||
|
|
|
@ -601,10 +601,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
displayName: _td('Enable widget screenshots on supported widgets'),
|
||||
default: false,
|
||||
},
|
||||
"PinnedEvents.isOpen": {
|
||||
supportedLevels: [SettingLevel.ROOM_DEVICE],
|
||||
default: false,
|
||||
},
|
||||
"promptBeforeInviteUnknownUsers": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Prompt before sending invites to potentially invalid matrix IDs'),
|
||||
|
|
|
@ -63,8 +63,7 @@ export class WatchManager {
|
|||
|
||||
if (!inRoomId) {
|
||||
// Fire updates to all the individual room watchers too, as they probably care about the change higher up.
|
||||
const callbacks = Array.from(roomWatchers.values()).flat(1);
|
||||
callbacks.push(...callbacks);
|
||||
callbacks.push(...Array.from(roomWatchers.values()).flat(1));
|
||||
} else if (roomWatchers.has(IRRELEVANT_ROOM)) {
|
||||
callbacks.push(...roomWatchers.get(IRRELEVANT_ROOM));
|
||||
}
|
||||
|
|
|
@ -126,7 +126,7 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
|
|||
if (membership === EffectiveMembership.Invite) {
|
||||
try {
|
||||
const path = utils.encodeUri("/rooms/$roomId/group_info", {$roomId: room.roomId});
|
||||
const profile = await this.matrixClient._http.authedRequest(
|
||||
const profile = await this.matrixClient.http.authedRequest(
|
||||
undefined, "GET", path,
|
||||
undefined, undefined,
|
||||
{prefix: "/_matrix/client/unstable/im.vector.custom"});
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2017-2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,11 +13,18 @@ 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 { Store } from 'flux/utils';
|
||||
|
||||
import dis from '../dispatcher/dispatcher';
|
||||
import {Store} from 'flux/utils';
|
||||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
|
||||
interface IState {
|
||||
deferredAction: any;
|
||||
}
|
||||
|
||||
const INITIAL_STATE = {
|
||||
deferred_action: null,
|
||||
deferredAction: null,
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -27,39 +32,38 @@ const INITIAL_STATE = {
|
|||
* store that listens for actions and updates its state accordingly, informing any
|
||||
* listeners (views) of state changes.
|
||||
*/
|
||||
class LifecycleStore extends Store {
|
||||
class LifecycleStore extends Store<ActionPayload> {
|
||||
private state: IState = INITIAL_STATE;
|
||||
|
||||
constructor() {
|
||||
super(dis);
|
||||
|
||||
// Initialise state
|
||||
this._state = INITIAL_STATE;
|
||||
}
|
||||
|
||||
_setState(newState) {
|
||||
this._state = Object.assign(this._state, newState);
|
||||
private setState(newState: Partial<IState>) {
|
||||
this.state = Object.assign(this.state, newState);
|
||||
this.__emitChange();
|
||||
}
|
||||
|
||||
__onDispatch(payload) {
|
||||
protected __onDispatch(payload: ActionPayload) {
|
||||
switch (payload.action) {
|
||||
case 'do_after_sync_prepared':
|
||||
this._setState({
|
||||
deferred_action: payload.deferred_action,
|
||||
this.setState({
|
||||
deferredAction: payload.deferred_action,
|
||||
});
|
||||
break;
|
||||
case 'cancel_after_sync_prepared':
|
||||
this._setState({
|
||||
deferred_action: null,
|
||||
this.setState({
|
||||
deferredAction: null,
|
||||
});
|
||||
break;
|
||||
case 'sync_state': {
|
||||
case 'syncstate': {
|
||||
if (payload.state !== 'PREPARED') {
|
||||
break;
|
||||
}
|
||||
if (!this._state.deferred_action) break;
|
||||
const deferredAction = Object.assign({}, this._state.deferred_action);
|
||||
this._setState({
|
||||
deferred_action: null,
|
||||
if (!this.state.deferredAction) break;
|
||||
const deferredAction = Object.assign({}, this.state.deferredAction);
|
||||
this.setState({
|
||||
deferredAction: null,
|
||||
});
|
||||
dis.dispatch(deferredAction);
|
||||
break;
|
||||
|
@ -71,8 +75,8 @@ class LifecycleStore extends Store {
|
|||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this._state = Object.assign({}, INITIAL_STATE);
|
||||
private reset() {
|
||||
this.state = Object.assign({}, INITIAL_STATE);
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ export enum RightPanelPhases {
|
|||
EncryptionPanel = 'EncryptionPanel',
|
||||
RoomSummary = 'RoomSummary',
|
||||
Widget = 'Widget',
|
||||
PinnedMessages = "PinnedMessages",
|
||||
|
||||
Room3pidMemberInfo = 'Room3pidMemberInfo',
|
||||
// Group stuff
|
||||
|
@ -43,6 +44,7 @@ export enum RightPanelPhases {
|
|||
export const RIGHT_PANEL_PHASES_NO_ARGS = [
|
||||
RightPanelPhases.RoomSummary,
|
||||
RightPanelPhases.NotificationPanel,
|
||||
RightPanelPhases.PinnedMessages,
|
||||
RightPanelPhases.FilePanel,
|
||||
RightPanelPhases.RoomMemberList,
|
||||
RightPanelPhases.GroupMemberList,
|
||||
|
|
|
@ -54,8 +54,6 @@ const INITIAL_STATE = {
|
|||
// Any error that has occurred during loading
|
||||
roomLoadError: null,
|
||||
|
||||
forwardingEvent: null,
|
||||
|
||||
quotingEvent: null,
|
||||
|
||||
replyingToEvent: null,
|
||||
|
@ -150,11 +148,6 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
case 'on_logged_out':
|
||||
this.reset();
|
||||
break;
|
||||
case 'forward_event':
|
||||
this.setState({
|
||||
forwardingEvent: payload.event,
|
||||
});
|
||||
break;
|
||||
case 'reply_to_event':
|
||||
// If currently viewed room does not match the room in which we wish to reply then change rooms
|
||||
// this can happen when performing a search across all rooms
|
||||
|
@ -187,7 +180,6 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
roomAlias: payload.room_alias,
|
||||
initialEventId: payload.event_id,
|
||||
isInitialEventHighlighted: payload.highlighted,
|
||||
forwardingEvent: null,
|
||||
roomLoading: false,
|
||||
roomLoadError: null,
|
||||
// should peek by default
|
||||
|
@ -207,14 +199,6 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
newState.replyingToEvent = payload.replyingToEvent;
|
||||
}
|
||||
|
||||
if (this.state.forwardingEvent) {
|
||||
dis.dispatch({
|
||||
action: 'send_event',
|
||||
room_id: newState.roomId,
|
||||
event: this.state.forwardingEvent,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
|
||||
if (payload.auto_join) {
|
||||
|
@ -428,11 +412,6 @@ class RoomViewStore extends Store<ActionPayload> {
|
|||
return this.state.joinError;
|
||||
}
|
||||
|
||||
// The mxEvent if one is about to be forwarded
|
||||
public getForwardingEvent() {
|
||||
return this.state.forwardingEvent;
|
||||
}
|
||||
|
||||
// The mxEvent if one is currently being replied to/quoted
|
||||
public getQuotingEvent() {
|
||||
return this.state.replyingToEvent;
|
||||
|
|
|
@ -196,7 +196,7 @@ export class SetupEncryptionStore extends EventEmitter {
|
|||
this.phase = PHASE_FINISHED;
|
||||
this.emit("update");
|
||||
// async - ask other clients for keys, if necessary
|
||||
MatrixClientPeg.get()._crypto.cancelAndResendAllOutgoingKeyRequests();
|
||||
MatrixClientPeg.get().crypto.cancelAndResendAllOutgoingKeyRequests();
|
||||
}
|
||||
|
||||
async _setActiveVerificationRequest(request) {
|
||||
|
|
|
@ -332,7 +332,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
|||
}
|
||||
|
||||
public getContainerWidgets(room: Room, container: Container): IApp[] {
|
||||
return this.byRoom[room.roomId]?.[container]?.ordered || [];
|
||||
return this.byRoom[room?.roomId]?.[container]?.ordered || [];
|
||||
}
|
||||
|
||||
public isInContainer(room: Room, widget: IApp, container: Container): boolean {
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Automatically focuses the captured reference when receiving a non-null
|
||||
* object. Useful in scenarios where componentDidMount does not have a
|
||||
* useful reference to an element, but one needs to focus the element on
|
||||
* first render. Example usage: ref={focusCapturedRef}
|
||||
* @param {function} ref The React reference to focus on, if not null
|
||||
*/
|
||||
export function focusCapturedRef(ref) {
|
||||
if (ref) {
|
||||
ref.focus();
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -22,7 +22,7 @@ import url from "url";
|
|||
* @param {string} u The url to be abbreviated
|
||||
* @returns {string} The abbreviated url
|
||||
*/
|
||||
export function abbreviateUrl(u) {
|
||||
export function abbreviateUrl(u: string): string {
|
||||
if (!u) return '';
|
||||
|
||||
const parsedUrl = url.parse(u);
|
||||
|
@ -37,7 +37,7 @@ export function abbreviateUrl(u) {
|
|||
return u;
|
||||
}
|
||||
|
||||
export function unabbreviateUrl(u) {
|
||||
export function unabbreviateUrl(u: string): string {
|
||||
if (!u) return '';
|
||||
|
||||
let longUrl = u;
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {_t} from "../languageHandler";
|
||||
import { _t } from "../languageHandler";
|
||||
|
||||
// These are the constants we use for when to break the text
|
||||
const MILLISECONDS_RECENT = 15000;
|
||||
const MILLISECONDS_1_MIN = 75000;
|
||||
const MINUTES_UNDER_1_HOUR = 45;
|
||||
const MINUTES_1_HOUR = 75;
|
||||
const HOURS_UNDER_1_DAY = 23;
|
||||
const HOURS_1_DAY = 26;
|
||||
|
||||
/**
|
||||
* Converts a timestamp into human-readable, translated, text.
|
||||
* @param {number} timeMillis The time in millis to compare against.
|
||||
* @returns {string} The humanized time.
|
||||
*/
|
||||
export function humanizeTime(timeMillis) {
|
||||
// These are the constants we use for when to break the text
|
||||
const MILLISECONDS_RECENT = 15000;
|
||||
const MILLISECONDS_1_MIN = 75000;
|
||||
const MINUTES_UNDER_1_HOUR = 45;
|
||||
const MINUTES_1_HOUR = 75;
|
||||
const HOURS_UNDER_1_DAY = 23;
|
||||
const HOURS_1_DAY = 26;
|
||||
|
||||
export function humanizeTime(timeMillis: number): string {
|
||||
const now = (new Date()).getTime();
|
||||
let msAgo = now - timeMillis;
|
||||
const minutes = Math.abs(Math.ceil(msAgo / 60000));
|
|
@ -19,11 +19,11 @@ limitations under the License.
|
|||
* TODO: Convert this to a real TypeScript interface
|
||||
*/
|
||||
export default class PermalinkConstructor {
|
||||
forEvent(roomId: string, eventId: string, serverCandidates: string[]): string {
|
||||
forEvent(roomId: string, eventId: string, serverCandidates: string[] = []): string {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
forRoom(roomIdOrAlias: string, serverCandidates: string[]): string {
|
||||
forRoom(roomIdOrAlias: string, serverCandidates: string[] = []): string {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
|
@ -73,12 +73,12 @@ export class PermalinkParts {
|
|||
return new PermalinkParts(null, null, null, groupId, null);
|
||||
}
|
||||
|
||||
static forRoom(roomIdOrAlias: string, viaServers: string[]): PermalinkParts {
|
||||
return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers || []);
|
||||
static forRoom(roomIdOrAlias: string, viaServers: string[] = []): PermalinkParts {
|
||||
return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers);
|
||||
}
|
||||
|
||||
static forEvent(roomId: string, eventId: string, viaServers: string[]): PermalinkParts {
|
||||
return new PermalinkParts(roomId, eventId, null, null, viaServers || []);
|
||||
static forEvent(roomId: string, eventId: string, viaServers: string[] = []): PermalinkParts {
|
||||
return new PermalinkParts(roomId, eventId, null, null, viaServers);
|
||||
}
|
||||
|
||||
get primaryEntityId(): string {
|
||||
|
|
|
@ -149,7 +149,7 @@ export class RoomPermalinkCreator {
|
|||
// Prefer to use canonical alias for permalink if possible
|
||||
const alias = this.room.getCanonicalAlias();
|
||||
if (alias) {
|
||||
return getPermalinkConstructor().forRoom(alias, this._serverCandidates);
|
||||
return getPermalinkConstructor().forRoom(alias);
|
||||
}
|
||||
}
|
||||
return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates);
|
||||
|
@ -302,7 +302,7 @@ export function makeRoomPermalink(roomId: string): string {
|
|||
}
|
||||
const permalinkCreator = new RoomPermalinkCreator(room);
|
||||
permalinkCreator.load();
|
||||
return permalinkCreator.forRoom();
|
||||
return permalinkCreator.forShareableRoom();
|
||||
}
|
||||
|
||||
export function makeGroupPermalink(groupId: string): string {
|
||||
|
|
|
@ -23,8 +23,9 @@ import dis from '../src/dispatcher/dispatcher';
|
|||
import { CallEvent, CallState } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import DMRoomMap from '../src/utils/DMRoomMap';
|
||||
import EventEmitter from 'events';
|
||||
import { Action } from '../src/dispatcher/actions';
|
||||
import SdkConfig from '../src/SdkConfig';
|
||||
import { ActionPayload } from '../src/dispatcher/payloads';
|
||||
import { Action } from '../src/dispatcher/actions';
|
||||
|
||||
const REAL_ROOM_ID = '$room1:example.org';
|
||||
const MAPPED_ROOM_ID = '$room2:example.org';
|
||||
|
@ -75,6 +76,18 @@ class FakeCall extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
function untilDispatch(waitForAction: string): Promise<ActionPayload> {
|
||||
let dispatchHandle;
|
||||
return new Promise<ActionPayload>(resolve => {
|
||||
dispatchHandle = dis.register(payload => {
|
||||
if (payload.action === waitForAction) {
|
||||
dis.unregister(dispatchHandle);
|
||||
resolve(payload);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('CallHandler', () => {
|
||||
let dmRoomMap;
|
||||
let callHandler;
|
||||
|
@ -94,6 +107,21 @@ describe('CallHandler', () => {
|
|||
callHandler = new CallHandler();
|
||||
callHandler.start();
|
||||
|
||||
const realRoom = mkStubDM(REAL_ROOM_ID, '@user1:example.org');
|
||||
const mappedRoom = mkStubDM(MAPPED_ROOM_ID, '@user2:example.org');
|
||||
const mappedRoom2 = mkStubDM(MAPPED_ROOM_ID_2, '@user3:example.org');
|
||||
|
||||
MatrixClientPeg.get().getRoom = roomId => {
|
||||
switch (roomId) {
|
||||
case REAL_ROOM_ID:
|
||||
return realRoom;
|
||||
case MAPPED_ROOM_ID:
|
||||
return mappedRoom;
|
||||
case MAPPED_ROOM_ID_2:
|
||||
return mappedRoom2;
|
||||
}
|
||||
};
|
||||
|
||||
dmRoomMap = {
|
||||
getUserIdForRoomId: roomId => {
|
||||
if (roomId === REAL_ROOM_ID) {
|
||||
|
@ -134,38 +162,34 @@ describe('CallHandler', () => {
|
|||
SdkConfig.unset();
|
||||
});
|
||||
|
||||
it('should look up the correct user and open the room when a phone number is dialled', async () => {
|
||||
MatrixClientPeg.get().getThirdpartyUser = jest.fn().mockResolvedValue([{
|
||||
userid: '@user2:example.org',
|
||||
protocol: "im.vector.protocol.sip_native",
|
||||
fields: {
|
||||
is_native: true,
|
||||
lookup_success: true,
|
||||
},
|
||||
}]);
|
||||
|
||||
dis.dispatch({
|
||||
action: Action.DialNumber,
|
||||
number: '01818118181',
|
||||
}, true);
|
||||
|
||||
const viewRoomPayload = await untilDispatch('view_room');
|
||||
expect(viewRoomPayload.room_id).toEqual(MAPPED_ROOM_ID);
|
||||
});
|
||||
|
||||
it('should move calls between rooms when remote asserted identity changes', async () => {
|
||||
const realRoom = mkStubDM(REAL_ROOM_ID, '@user1:example.org');
|
||||
const mappedRoom = mkStubDM(MAPPED_ROOM_ID, '@user2:example.org');
|
||||
const mappedRoom2 = mkStubDM(MAPPED_ROOM_ID_2, '@user3:example.org');
|
||||
|
||||
MatrixClientPeg.get().getRoom = roomId => {
|
||||
switch (roomId) {
|
||||
case REAL_ROOM_ID:
|
||||
return realRoom;
|
||||
case MAPPED_ROOM_ID:
|
||||
return mappedRoom;
|
||||
case MAPPED_ROOM_ID_2:
|
||||
return mappedRoom2;
|
||||
}
|
||||
};
|
||||
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: PlaceCallType.Voice,
|
||||
room_id: REAL_ROOM_ID,
|
||||
}, true);
|
||||
|
||||
let dispatchHandle;
|
||||
// wait for the call to be set up
|
||||
await new Promise<void>(resolve => {
|
||||
dispatchHandle = dis.register(payload => {
|
||||
if (payload.action === 'call_state') {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
dis.unregister(dispatchHandle);
|
||||
await untilDispatch('call_state');
|
||||
|
||||
// should start off in the actual room ID it's in at the protocol level
|
||||
expect(callHandler.getCallForRoom(REAL_ROOM_ID)).toBe(fakeCall);
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { isKeyComboMatch, KeyCombo } from '../src/KeyBindingsManager';
|
||||
const assert = require('assert');
|
||||
|
||||
function mockKeyEvent(key: string, modifiers?: {
|
||||
ctrlKey?: boolean,
|
||||
|
@ -28,7 +27,7 @@ function mockKeyEvent(key: string, modifiers?: {
|
|||
ctrlKey: modifiers?.ctrlKey ?? false,
|
||||
altKey: modifiers?.altKey ?? false,
|
||||
shiftKey: modifiers?.shiftKey ?? false,
|
||||
metaKey: modifiers?.metaKey ?? false
|
||||
metaKey: modifiers?.metaKey ?? false,
|
||||
} as KeyboardEvent;
|
||||
}
|
||||
|
||||
|
@ -37,9 +36,8 @@ describe('KeyBindingsManager', () => {
|
|||
const combo1: KeyCombo = {
|
||||
key: 'k',
|
||||
};
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo1, false), true);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n'), combo1, false), false);
|
||||
|
||||
expect(isKeyComboMatch(mockKeyEvent('k'), combo1, false)).toBe(true);
|
||||
expect(isKeyComboMatch(mockKeyEvent('n'), combo1, false)).toBe(false);
|
||||
});
|
||||
|
||||
it('should match key + modifier key combo', () => {
|
||||
|
@ -47,38 +45,38 @@ describe('KeyBindingsManager', () => {
|
|||
key: 'k',
|
||||
ctrlKey: true,
|
||||
};
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false), true);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false), false);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo, false), false);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo, false), false);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, metaKey: true }), combo, false), false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false)).toBe(true);
|
||||
expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false)).toBe(false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k'), combo, false)).toBe(false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo, false)).toBe(false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, metaKey: true }), combo, false)).toBe(false);
|
||||
|
||||
const combo2: KeyCombo = {
|
||||
key: 'k',
|
||||
metaKey: true,
|
||||
};
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo2, false), true);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { metaKey: true }), combo2, false), false);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo2, false), false);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { altKey: true, metaKey: true }), combo2, false), false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo2, false)).toBe(true);
|
||||
expect(isKeyComboMatch(mockKeyEvent('n', { metaKey: true }), combo2, false)).toBe(false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k'), combo2, false)).toBe(false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { altKey: true, metaKey: true }), combo2, false)).toBe(false);
|
||||
|
||||
const combo3: KeyCombo = {
|
||||
key: 'k',
|
||||
altKey: true,
|
||||
};
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { altKey: true }), combo3, false), true);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { altKey: true }), combo3, false), false);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo3, false), false);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo3, false), false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { altKey: true }), combo3, false)).toBe(true);
|
||||
expect(isKeyComboMatch(mockKeyEvent('n', { altKey: true }), combo3, false)).toBe(false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k'), combo3, false)).toBe(false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo3, false)).toBe(false);
|
||||
|
||||
const combo4: KeyCombo = {
|
||||
key: 'k',
|
||||
shiftKey: true,
|
||||
};
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo4, false), true);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { shiftKey: true }), combo4, false), false);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo4, false), false);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, ctrlKey: true }), combo4, false), false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo4, false)).toBe(true);
|
||||
expect(isKeyComboMatch(mockKeyEvent('n', { shiftKey: true }), combo4, false)).toBe(false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k'), combo4, false)).toBe(false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, ctrlKey: true }), combo4, false)).toBe(false);
|
||||
});
|
||||
|
||||
it('should match key + multiple modifiers key combo', () => {
|
||||
|
@ -87,11 +85,11 @@ describe('KeyBindingsManager', () => {
|
|||
ctrlKey: true,
|
||||
altKey: true,
|
||||
};
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false), true);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, altKey: true }), combo, false), false);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo, false), false);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true, shiftKey: true }), combo,
|
||||
false), false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false)).toBe(true);
|
||||
expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, altKey: true }), combo, false)).toBe(false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo, false)).toBe(false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true, shiftKey: true }), combo,
|
||||
false)).toBe(false);
|
||||
|
||||
const combo2: KeyCombo = {
|
||||
key: 'k',
|
||||
|
@ -99,13 +97,13 @@ describe('KeyBindingsManager', () => {
|
|||
shiftKey: true,
|
||||
altKey: true,
|
||||
};
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, shiftKey: true, altKey: true }), combo2,
|
||||
false), true);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, shiftKey: true, altKey: true }), combo2,
|
||||
false), false);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo2, false), false);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k',
|
||||
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo2, false), false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, shiftKey: true, altKey: true }), combo2,
|
||||
false)).toBe(true);
|
||||
expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, shiftKey: true, altKey: true }), combo2,
|
||||
false)).toBe(false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo2, false)).toBe(false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k',
|
||||
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo2, false)).toBe(false);
|
||||
|
||||
const combo3: KeyCombo = {
|
||||
key: 'k',
|
||||
|
@ -114,12 +112,12 @@ describe('KeyBindingsManager', () => {
|
|||
altKey: true,
|
||||
metaKey: true,
|
||||
};
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k',
|
||||
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false), true);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n',
|
||||
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false), false);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k',
|
||||
{ ctrlKey: true, shiftKey: true, altKey: true }), combo3, false), false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k',
|
||||
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false)).toBe(true);
|
||||
expect(isKeyComboMatch(mockKeyEvent('n',
|
||||
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false)).toBe(false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k',
|
||||
{ ctrlKey: true, shiftKey: true, altKey: true }), combo3, false)).toBe(false);
|
||||
});
|
||||
|
||||
it('should match ctrlOrMeta key combo', () => {
|
||||
|
@ -128,13 +126,13 @@ describe('KeyBindingsManager', () => {
|
|||
ctrlOrCmd: true,
|
||||
};
|
||||
// PC:
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false), true);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, false), false);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false), false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false)).toBe(true);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, false)).toBe(false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false)).toBe(false);
|
||||
// MAC:
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, true), true);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, true), false);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, true), false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, true)).toBe(true);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, true)).toBe(false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, true)).toBe(false);
|
||||
});
|
||||
|
||||
it('should match advanced ctrlOrMeta key combo', () => {
|
||||
|
@ -144,10 +142,10 @@ describe('KeyBindingsManager', () => {
|
|||
altKey: true,
|
||||
};
|
||||
// PC:
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false), true);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, false), false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false)).toBe(true);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, false)).toBe(false);
|
||||
// MAC:
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, true), true);
|
||||
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, true), false);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, true)).toBe(true);
|
||||
expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, true)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
163
test/components/views/dialogs/ForwardDialog-test.js
Normal file
163
test/components/views/dialogs/ForwardDialog-test.js
Normal file
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
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 "../../../skinned-sdk";
|
||||
|
||||
import React from "react";
|
||||
import {configure, mount} from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import {act} from "react-dom/test-utils";
|
||||
|
||||
import * as TestUtils from "../../../test-utils";
|
||||
import {MatrixClientPeg} from "../../../../src/MatrixClientPeg";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import {RoomPermalinkCreator} from "../../../../src/utils/permalinks/Permalinks";
|
||||
import ForwardDialog from "../../../../src/components/views/dialogs/ForwardDialog";
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
describe("ForwardDialog", () => {
|
||||
const sourceRoom = "!111111111111111111:example.org";
|
||||
const defaultMessage = TestUtils.mkMessage({
|
||||
room: sourceRoom,
|
||||
user: "@alice:example.org",
|
||||
msg: "Hello world!",
|
||||
event: true,
|
||||
});
|
||||
const defaultRooms = ["a", "A", "b"].map(name => TestUtils.mkStubRoom(name, name));
|
||||
|
||||
const mountForwardDialog = async (message = defaultMessage, rooms = defaultRooms) => {
|
||||
const client = MatrixClientPeg.get();
|
||||
client.getVisibleRooms = jest.fn().mockReturnValue(rooms);
|
||||
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mount(
|
||||
<ForwardDialog
|
||||
matrixClient={client}
|
||||
event={message}
|
||||
permalinkCreator={new RoomPermalinkCreator(undefined, sourceRoom)}
|
||||
onFinished={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
// Wait one tick for our profile data to load so the state update happens within act
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
});
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestUtils.stubClient();
|
||||
DMRoomMap.makeShared();
|
||||
MatrixClientPeg.get().getUserId = jest.fn().mockReturnValue("@bob:example.org");
|
||||
});
|
||||
|
||||
it("shows a preview with us as the sender", async () => {
|
||||
const wrapper = await mountForwardDialog();
|
||||
|
||||
const previewBody = wrapper.find(".mx_EventTile_body");
|
||||
expect(previewBody.text()).toBe("Hello world!");
|
||||
|
||||
// We would just test SenderProfile for the user ID, but it's stubbed
|
||||
const previewAvatar = wrapper.find(".mx_EventTile_avatar .mx_BaseAvatar_image");
|
||||
expect(previewAvatar.prop("title")).toBe("@bob:example.org");
|
||||
});
|
||||
|
||||
it("filters the rooms", async () => {
|
||||
const wrapper = await mountForwardDialog();
|
||||
|
||||
expect(wrapper.find("Entry")).toHaveLength(3);
|
||||
|
||||
const searchInput = wrapper.find("SearchBox input");
|
||||
searchInput.instance().value = "a";
|
||||
searchInput.simulate("change");
|
||||
|
||||
expect(wrapper.find("Entry")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("tracks message sending progress across multiple rooms", async () => {
|
||||
const wrapper = await mountForwardDialog();
|
||||
|
||||
// Make sendEvent require manual resolution so we can see the sending state
|
||||
let finishSend;
|
||||
let cancelSend;
|
||||
MatrixClientPeg.get().sendEvent = jest.fn(() => new Promise((resolve, reject) => {
|
||||
finishSend = resolve;
|
||||
cancelSend = reject;
|
||||
}));
|
||||
|
||||
const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first();
|
||||
expect(firstButton.render().is(".mx_ForwardList_canSend")).toBe(true);
|
||||
|
||||
act(() => { firstButton.simulate("click"); });
|
||||
expect(firstButton.render().is(".mx_ForwardList_sending")).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
cancelSend();
|
||||
// Wait one tick for the button to realize the send failed
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
});
|
||||
expect(firstButton.render().is(".mx_ForwardList_sendFailed")).toBe(true);
|
||||
|
||||
const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").at(1);
|
||||
expect(secondButton.render().is(".mx_ForwardList_canSend")).toBe(true);
|
||||
|
||||
act(() => { secondButton.simulate("click"); });
|
||||
expect(secondButton.render().is(".mx_ForwardList_sending")).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
finishSend();
|
||||
// Wait one tick for the button to realize the send succeeded
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
});
|
||||
expect(secondButton.render().is(".mx_ForwardList_sent")).toBe(true);
|
||||
});
|
||||
|
||||
it("can render replies", async () => {
|
||||
const replyMessage = TestUtils.mkEvent({
|
||||
type: "m.room.message",
|
||||
room: "!111111111111111111:example.org",
|
||||
user: "@alice:example.org",
|
||||
content: {
|
||||
"msgtype": "m.text",
|
||||
"body": "> <@bob:example.org> Hi Alice!\n\nHi Bob!",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$2222222222222222222222222222222222222222222",
|
||||
},
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
|
||||
const wrapper = await mountForwardDialog(replyMessage);
|
||||
expect(wrapper.find("ReplyThread")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("disables buttons for rooms without send permissions", async () => {
|
||||
const readOnlyRoom = TestUtils.mkStubRoom("a", "a");
|
||||
readOnlyRoom.maySendMessage = jest.fn().mockReturnValue(false);
|
||||
const rooms = [readOnlyRoom, TestUtils.mkStubRoom("b", "b")];
|
||||
|
||||
const wrapper = await mountForwardDialog(undefined, rooms);
|
||||
|
||||
const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first();
|
||||
expect(firstButton.prop("disabled")).toBe(true);
|
||||
const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").last();
|
||||
expect(secondButton.prop("disabled")).toBe(false);
|
||||
});
|
||||
});
|
|
@ -21,7 +21,7 @@ import "../skinned-sdk"; // Must be first for skinning to work
|
|||
import SpaceStore, {
|
||||
UPDATE_INVITED_SPACES,
|
||||
UPDATE_SELECTED_SPACE,
|
||||
UPDATE_TOP_LEVEL_SPACES
|
||||
UPDATE_TOP_LEVEL_SPACES,
|
||||
} from "../../src/stores/SpaceStore";
|
||||
import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../utils/test-utils";
|
||||
import { mkEvent, mkStubRoom, stubClient } from "../test-utils";
|
||||
|
|
|
@ -90,7 +90,7 @@ export function createTestClient() {
|
|||
}),
|
||||
|
||||
// Used by various internal bits we aren't concerned with (yet)
|
||||
_sessionStore: {
|
||||
sessionStore: {
|
||||
store: {
|
||||
getItem: jest.fn(),
|
||||
},
|
||||
|
@ -219,7 +219,7 @@ export function mkMessage(opts) {
|
|||
return mkEvent(opts);
|
||||
}
|
||||
|
||||
export function mkStubRoom(roomId = null) {
|
||||
export function mkStubRoom(roomId = null, name) {
|
||||
const stubTimeline = { getEvents: () => [] };
|
||||
return {
|
||||
roomId,
|
||||
|
@ -238,6 +238,7 @@ export function mkStubRoom(roomId = null) {
|
|||
getPendingEvents: () => [],
|
||||
getLiveTimeline: () => stubTimeline,
|
||||
getUnfilteredTimelineSet: () => null,
|
||||
findEventById: () => null,
|
||||
getAccountData: () => null,
|
||||
hasMembershipState: () => null,
|
||||
getVersion: () => '1',
|
||||
|
@ -255,13 +256,17 @@ export function mkStubRoom(roomId = null) {
|
|||
tags: {},
|
||||
setBlacklistUnverifiedDevices: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
getDMInviter: jest.fn(),
|
||||
name,
|
||||
getAvatarUrl: () => 'mxc://avatar.url/room.png',
|
||||
getMxcAvatarUrl: () => 'mxc://avatar.url/room.png',
|
||||
isSpaceRoom: jest.fn(() => false),
|
||||
getUnreadNotificationCount: jest.fn(() => 0),
|
||||
getEventReadUpTo: jest.fn(() => null),
|
||||
getCanonicalAlias: jest.fn(),
|
||||
getAltAliases: jest.fn().mockReturnValue([]),
|
||||
timeline: [],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ function mockRoom(roomId, members, serverACL) {
|
|||
|
||||
return {
|
||||
roomId,
|
||||
getCanonicalAlias: () => roomId,
|
||||
getCanonicalAlias: () => null,
|
||||
getJoinedMembers: () => members,
|
||||
getMember: (userId) => members.find(m => m.userId === userId),
|
||||
currentState: {
|
||||
|
|
|
@ -5674,8 +5674,8 @@ mathml-tag-names@^2.1.3:
|
|||
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
||||
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
||||
version "11.1.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/acb9bc8cc5234326a7583514a8e120a4ac42eedc"
|
||||
version "11.2.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/35ecbed29d16982deff27a8c37b05167738225a2"
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
another-json "^0.2.0"
|
||||
|
|
Loading…
Reference in a new issue