Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/17365

This commit is contained in:
Michael Telatynski 2021-06-08 16:54:49 +01:00
commit f34a2e61bc
95 changed files with 2180 additions and 1311 deletions

View file

@ -18,7 +18,7 @@ module.exports = {
}, },
overrides: [{ overrides: [{
"files": ["src/**/*.{ts,tsx}"], "files": ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"],
"extends": ["matrix-org/ts"], "extends": ["matrix-org/ts"],
"rules": { "rules": {
// We're okay being explicit at the moment // We're okay being explicit at the moment

View file

@ -1,3 +1,109 @@
Changes in [3.23.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0) (2021-06-07)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0-rc.1...v3.23.0)
* Upgrade to JS SDK 11.2.0
* [Release] Fix notif panel timestamp padding
[\#6158](https://github.com/matrix-org/matrix-react-sdk/pull/6158)
Changes in [3.23.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0-rc.1) (2021-06-01)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0...v3.23.0-rc.1)
* Upgrade to JS SDK 11.2.0-rc.1
* Translations update from Weblate
[\#6128](https://github.com/matrix-org/matrix-react-sdk/pull/6128)
* Fix all DMs wrongly appearing in room list when `m.direct` is changed
[\#6122](https://github.com/matrix-org/matrix-react-sdk/pull/6122)
* Update way of checking for registration disabled
[\#6123](https://github.com/matrix-org/matrix-react-sdk/pull/6123)
* Fix the ability to remove avatar from a space via settings
[\#6126](https://github.com/matrix-org/matrix-react-sdk/pull/6126)
* Switch to stable endpoint/fields for MSC2858
[\#6125](https://github.com/matrix-org/matrix-react-sdk/pull/6125)
* Clear stored editor state when canceling editing using a shortcut
[\#6117](https://github.com/matrix-org/matrix-react-sdk/pull/6117)
* Respect newlines in space topics
[\#6124](https://github.com/matrix-org/matrix-react-sdk/pull/6124)
* Add url param `defaultUsername` to prefill the login username field
[\#5674](https://github.com/matrix-org/matrix-react-sdk/pull/5674)
* Bump ws from 7.4.2 to 7.4.6
[\#6115](https://github.com/matrix-org/matrix-react-sdk/pull/6115)
* Sticky headers repositioning without layout trashing
[\#6110](https://github.com/matrix-org/matrix-react-sdk/pull/6110)
* Handle user_busy in voip calls
[\#6112](https://github.com/matrix-org/matrix-react-sdk/pull/6112)
* Avoid showing warning modals from the invite dialog after it unmounts
[\#6105](https://github.com/matrix-org/matrix-react-sdk/pull/6105)
* Fix misleading child counts in spaces
[\#6109](https://github.com/matrix-org/matrix-react-sdk/pull/6109)
* Close creation menu when expanding space panel via expand hierarchy
[\#6090](https://github.com/matrix-org/matrix-react-sdk/pull/6090)
* Prevent having duplicates in pending room state
[\#6108](https://github.com/matrix-org/matrix-react-sdk/pull/6108)
* Update reactions row on event decryption
[\#6106](https://github.com/matrix-org/matrix-react-sdk/pull/6106)
* Destroy playback instance on voice message unmount
[\#6101](https://github.com/matrix-org/matrix-react-sdk/pull/6101)
* Fix message preview not up to date
[\#6102](https://github.com/matrix-org/matrix-react-sdk/pull/6102)
* Convert some Flow typed files to TS (round 2)
[\#6076](https://github.com/matrix-org/matrix-react-sdk/pull/6076)
* Remove unused middlePanelResized event listener
[\#6086](https://github.com/matrix-org/matrix-react-sdk/pull/6086)
* Fix accessing currentState on an invalid joinedRoom
[\#6100](https://github.com/matrix-org/matrix-react-sdk/pull/6100)
* Remove Promise allSettled polyfill as js-sdk uses it directly
[\#6097](https://github.com/matrix-org/matrix-react-sdk/pull/6097)
* Prevent DecoratedRoomAvatar to update its state for the same value
[\#6099](https://github.com/matrix-org/matrix-react-sdk/pull/6099)
* Skip generatePreview if event is not part of the live timeline
[\#6098](https://github.com/matrix-org/matrix-react-sdk/pull/6098)
* fix sticky headers when results num get displayed
[\#6095](https://github.com/matrix-org/matrix-react-sdk/pull/6095)
* Improve addEventsToTimeline performance scoping WhoIsTypingTile::setState
[\#6094](https://github.com/matrix-org/matrix-react-sdk/pull/6094)
* Safeguards to prevent layout trashing for window dimensions
[\#6092](https://github.com/matrix-org/matrix-react-sdk/pull/6092)
* Use local room state to render space hierarchy if the room is known
[\#6089](https://github.com/matrix-org/matrix-react-sdk/pull/6089)
* Add spinner in UserMenu to list pending long running actions
[\#6085](https://github.com/matrix-org/matrix-react-sdk/pull/6085)
* Stop overscroll in Firefox Nightly for macOS
[\#6093](https://github.com/matrix-org/matrix-react-sdk/pull/6093)
* Move SettingsStore watchers/monitors over to ES6 maps for performance
[\#6063](https://github.com/matrix-org/matrix-react-sdk/pull/6063)
* Bump libolm version.
[\#6080](https://github.com/matrix-org/matrix-react-sdk/pull/6080)
* Improve styling of the message action bar
[\#6066](https://github.com/matrix-org/matrix-react-sdk/pull/6066)
* Improve explore rooms when no results are found
[\#6070](https://github.com/matrix-org/matrix-react-sdk/pull/6070)
* Remove logo spinner
[\#6078](https://github.com/matrix-org/matrix-react-sdk/pull/6078)
* Fix add reaction prompt showing even when user is not joined to room
[\#6073](https://github.com/matrix-org/matrix-react-sdk/pull/6073)
* Vectorize spinners
[\#5680](https://github.com/matrix-org/matrix-react-sdk/pull/5680)
* Fix handling of via servers for suggested rooms
[\#6077](https://github.com/matrix-org/matrix-react-sdk/pull/6077)
* Upgrade showChatEffects to room-level setting exposure
[\#6075](https://github.com/matrix-org/matrix-react-sdk/pull/6075)
* Delete RoomView dead code
[\#6071](https://github.com/matrix-org/matrix-react-sdk/pull/6071)
* Reduce noise in tests
[\#6074](https://github.com/matrix-org/matrix-react-sdk/pull/6074)
* Fix room name issues in right panel summary card
[\#6069](https://github.com/matrix-org/matrix-react-sdk/pull/6069)
* Cache normalized room name
[\#6072](https://github.com/matrix-org/matrix-react-sdk/pull/6072)
* Update MemberList to reflect changes for invite permission change
[\#6061](https://github.com/matrix-org/matrix-react-sdk/pull/6061)
* Delete RoomView dead code
[\#6065](https://github.com/matrix-org/matrix-react-sdk/pull/6065)
* Show subspace rooms count even if it is 0 for consistency
[\#6067](https://github.com/matrix-org/matrix-react-sdk/pull/6067)
Changes in [3.22.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0) (2021-05-24) Changes in [3.22.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.22.0) (2021-05-24)
===================================================================================================== =====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0-rc.1...v3.22.0) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.22.0-rc.1...v3.22.0)

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "3.22.0", "version": "3.23.0",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {

View file

@ -76,6 +76,7 @@
@import "./views/dialogs/_DevtoolsDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss";
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss"; @import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
@import "./views/dialogs/_FeedbackDialog.scss"; @import "./views/dialogs/_FeedbackDialog.scss";
@import "./views/dialogs/_ForwardDialog.scss";
@import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_GroupAddressPicker.scss";
@import "./views/dialogs/_HostSignupDialog.scss"; @import "./views/dialogs/_HostSignupDialog.scss";
@import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_IncomingSasDialog.scss";
@ -179,6 +180,7 @@
@import "./views/messages/_common_CryptoEvent.scss"; @import "./views/messages/_common_CryptoEvent.scss";
@import "./views/right_panel/_BaseCard.scss"; @import "./views/right_panel/_BaseCard.scss";
@import "./views/right_panel/_EncryptionInfo.scss"; @import "./views/right_panel/_EncryptionInfo.scss";
@import "./views/right_panel/_PinnedMessagesCard.scss";
@import "./views/right_panel/_RoomSummaryCard.scss"; @import "./views/right_panel/_RoomSummaryCard.scss";
@import "./views/right_panel/_UserInfo.scss"; @import "./views/right_panel/_UserInfo.scss";
@import "./views/right_panel/_VerificationPanel.scss"; @import "./views/right_panel/_VerificationPanel.scss";
@ -203,7 +205,6 @@
@import "./views/rooms/_NewRoomIntro.scss"; @import "./views/rooms/_NewRoomIntro.scss";
@import "./views/rooms/_NotificationBadge.scss"; @import "./views/rooms/_NotificationBadge.scss";
@import "./views/rooms/_PinnedEventTile.scss"; @import "./views/rooms/_PinnedEventTile.scss";
@import "./views/rooms/_PinnedEventsPanel.scss";
@import "./views/rooms/_PresenceLabel.scss"; @import "./views/rooms/_PresenceLabel.scss";
@import "./views/rooms/_ReplyPreview.scss"; @import "./views/rooms/_ReplyPreview.scss";
@import "./views/rooms/_RoomBreadcrumbs.scss"; @import "./views/rooms/_RoomBreadcrumbs.scss";

View file

@ -82,7 +82,6 @@ limitations under the License.
color: $primary-fg-color; color: $primary-fg-color;
font-size: $font-12px; font-size: $font-12px;
display: inline; display: inline;
padding-left: 0px;
} }
.mx_NotificationPanel .mx_EventTile_senderDetails { .mx_NotificationPanel .mx_EventTile_senderDetails {
@ -103,6 +102,7 @@ limitations under the License.
visibility: visible; visibility: visible;
position: initial; position: initial;
display: inline; display: inline;
padding-left: 5px;
} }
.mx_NotificationPanel .mx_EventTile_line { .mx_NotificationPanel .mx_EventTile_line {

View file

@ -99,6 +99,48 @@ limitations under the License.
mask-position: center; mask-position: center;
} }
$dot-size: 8px;
$pulse-color: $pinned-unread-color;
.mx_RightPanel_pinnedMessagesButton {
&::before {
mask-image: url('$(res)/img/element-icons/room/pin.svg');
mask-position: center;
}
.mx_RightPanel_pinnedMessagesButton_unreadIndicator {
position: absolute;
right: 0;
top: 0;
margin: 4px;
width: $dot-size;
height: $dot-size;
border-radius: 50%;
transform: scale(1);
background: rgba($pulse-color, 1);
box-shadow: 0 0 0 0 rgba($pulse-color, 1);
animation: mx_RightPanel_indicator_pulse 2s infinite;
animation-iteration-count: 1;
}
}
@keyframes mx_RightPanel_indicator_pulse {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba($pulse-color, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 10px rgba($pulse-color, 0);
}
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba($pulse-color, 0);
}
}
.mx_RightPanel_headerButton_highlight { .mx_RightPanel_headerButton_highlight {
&::before { &::before {
background-color: $accent-color !important; background-color: $accent-color !important;

View file

@ -222,7 +222,6 @@ limitations under the License.
.mx_RoomView_MessageList li { .mx_RoomView_MessageList li {
clear: both; clear: both;
contain: content;
} }
li.mx_RoomView_myReadMarker_container { li.mx_RoomView_myReadMarker_container {

View file

@ -365,6 +365,45 @@ $SpaceRoomViewInnerWidth: 428px;
} }
} }
.mx_SpaceRoomView_betaWarning {
padding: 12px 12px 12px 54px;
position: relative;
font-size: $font-15px;
line-height: $font-24px;
width: 432px;
border-radius: 8px;
background-color: $info-plinth-bg-color;
color: $secondary-fg-color;
box-sizing: border-box;
> h3 {
font-weight: $font-semi-bold;
font-size: inherit;
line-height: inherit;
margin: 0;
}
> p {
font-size: inherit;
line-height: inherit;
margin: 0;
}
&::before {
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
content: '';
width: 20px;
height: 20px;
position: absolute;
top: 14px;
left: 14px;
background-color: $secondary-fg-color;
}
}
.mx_SpaceRoomView_inviteTeammates { .mx_SpaceRoomView_inviteTeammates {
// XXX remove this when spaces leaves Beta // XXX remove this when spaces leaves Beta
.mx_SpaceRoomView_inviteTeammates_betaDisclaimer { .mx_SpaceRoomView_inviteTeammates_betaDisclaimer {

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

View file

@ -22,6 +22,7 @@ limitations under the License.
} }
.mx_ImageView_image_wrapper { .mx_ImageView_image_wrapper {
pointer-events: initial;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -30,7 +31,6 @@ limitations under the License.
} }
.mx_ImageView_image { .mx_ImageView_image {
pointer-events: all;
flex-shrink: 0; flex-shrink: 0;
} }
@ -43,7 +43,7 @@ limitations under the License.
} }
.mx_ImageView_info_wrapper { .mx_ImageView_info_wrapper {
pointer-events: all; pointer-events: initial;
padding-left: 32px; padding-left: 32px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -63,7 +63,7 @@ limitations under the License.
.mx_ImageView_toolbar { .mx_ImageView_toolbar {
padding-right: 16px; padding-right: 16px;
pointer-events: all; pointer-events: initial;
display: flex; display: flex;
align-items: center; align-items: center;
} }

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

View file

@ -85,12 +85,11 @@ $left-gutter: 64px;
} }
.mx_EventTile_isEditing .mx_MessageTimestamp { .mx_EventTile_isEditing .mx_MessageTimestamp {
visibility: hidden !important; visibility: hidden;
} }
.mx_EventTile .mx_MessageTimestamp { .mx_EventTile .mx_MessageTimestamp {
display: block; display: block;
visibility: hidden;
white-space: nowrap; white-space: nowrap;
left: 0px; left: 0px;
text-align: center; text-align: center;
@ -142,29 +141,11 @@ $left-gutter: 64px;
line-height: 57px !important; line-height: 57px !important;
} }
.mx_MessagePanel_alwaysShowTimestamps .mx_MessageTimestamp {
visibility: visible;
}
.mx_EventTile_selected > div > a > .mx_MessageTimestamp { .mx_EventTile_selected > div > a > .mx_MessageTimestamp {
left: 3px; left: 3px;
width: auto; width: auto;
} }
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
// The first set is to handle the 'group layout' (default) and the second for the IRC layout
.mx_EventTile_last > div > a > .mx_MessageTimestamp,
.mx_EventTile:hover > div > a > .mx_MessageTimestamp,
.mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp,
.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_EventTile_last > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_EventTile:hover > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_ReplyThread .mx_EventTile > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_EventTile.mx_EventTile_actionBarFocused > a > .mx_MessageTimestamp,
.mx_IRCLayout .mx_EventTile.focus-visible:focus-within > a > .mx_MessageTimestamp {
visibility: visible;
}
.mx_EventTile:hover .mx_MessageActionBar, .mx_EventTile:hover .mx_MessageActionBar,
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar, .mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar,
[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar, [data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar,

View file

@ -16,62 +16,91 @@ limitations under the License.
.mx_PinnedEventTile { .mx_PinnedEventTile {
min-height: 40px; min-height: 40px;
margin-bottom: 5px;
width: 100%; width: 100%;
border-radius: 5px; // for the hover padding: 0 4px 12px;
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 {
padding: 12px 4px;
border-top: 1px solid $menu-border-color;
} }
.mx_PinnedEventTile:hover { .mx_PinnedEventTile_senderAvatar {
background-color: $event-selected-color; grid-area: avatar;
} }
.mx_PinnedEventTile .mx_PinnedEventTile_sender, .mx_PinnedEventTile_sender {
.mx_PinnedEventTile .mx_PinnedEventTile_timestamp { grid-area: name;
color: #868686; font-weight: $font-semi-bold;
font-size: 0.8em; font-size: $font-15px;
vertical-align: top; line-height: $font-24px;
display: inline-block; text-overflow: ellipsis;
padding-bottom: 3px; overflow: hidden;
} white-space: nowrap;
.mx_PinnedEventTile .mx_PinnedEventTile_timestamp {
padding-left: 15px;
display: none;
}
.mx_PinnedEventTile .mx_PinnedEventTile_senderAvatar .mx_BaseAvatar {
float: left;
margin-right: 10px;
}
.mx_PinnedEventTile_actions {
float: right;
margin-right: 10px;
display: none;
}
.mx_PinnedEventTile:hover .mx_PinnedEventTile_timestamp {
display: inline-block;
}
.mx_PinnedEventTile:hover .mx_PinnedEventTile_actions {
display: block;
} }
.mx_PinnedEventTile_unpinButton { .mx_PinnedEventTile_unpinButton {
display: inline-block; visibility: hidden;
cursor: pointer; grid-area: remove;
margin-left: 10px; position: relative;
width: 24px;
height: 24px;
border-radius: 8px;
&:hover {
background-color: $roomheader-addroom-bg-color;
} }
.mx_PinnedEventTile_gotoButton { &::before {
display: inline-block; content: "";
font-size: 0.7em; // Smaller text to avoid conflicting with the layout 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_message { .mx_PinnedEventTile_message {
margin-left: 50px; grid-area: content;
position: relative; }
top: 0;
left: 0; .mx_PinnedEventTile_footer {
grid-area: footer;
font-size: 10px;
line-height: 12px;
.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;
}
}
} }

View file

@ -277,24 +277,6 @@ limitations under the License.
margin-top: 18px; margin-top: 18px;
} }
.mx_RoomHeader_pinnedButton::before {
mask-image: url('$(res)/img/element-icons/room/pin.svg');
}
.mx_RoomHeader_pinsIndicator {
position: absolute;
right: 0;
bottom: 4px;
width: 8px;
height: 8px;
border-radius: 8px;
background-color: $pinned-color;
}
.mx_RoomHeader_pinsIndicatorUnread {
background-color: $pinned-unread-color;
}
@media only screen and (max-width: 480px) { @media only screen and (max-width: 480px) {
.mx_RoomHeader_wrapper { .mx_RoomHeader_wrapper {
padding: 0; padding: 0;

View file

@ -19,7 +19,7 @@ limitations under the License.
margin-bottom: 4px; margin-bottom: 4px;
padding: 4px; padding: 4px;
contain: strict; contain: content; // Not strict as it will break when resizing a sublist vertically
height: 40px; height: 40px;
box-sizing: border-box; box-sizing: border-box;

View file

@ -1,7 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.5151 20.0831L15.6941 17.2621L17.2621 15.6941L20.0831 18.5151C21.5741 20.0061 22.1529 21.7793 21.9661 21.9661C21.7793 22.1529 20.0061 21.5741 18.5151 20.0831Z" fill="black"/> <path d="M18.5151 20.0831L15.6941 17.2621L17.2621 15.6941L20.0831 18.5151C21.5741 20.0061 22.1529 21.7793 21.9661 21.9661C21.7793 22.1529 20.0061 21.5741 18.5151 20.0831Z" fill="#737D8C"/>
<path d="M7.46196 11.3821C7.07677 11.5059 5.49073 12.0989 3.63366 12.0744C1.77658 12.0499 1.67795 10.8941 2.46811 10.1039L6.28598 6.28602L9.42196 9.42203L7.46196 11.3821Z" fill="black"/> <path d="M7.46196 11.3821C7.07677 11.5059 5.49073 12.0989 3.63366 12.0744C1.77658 12.0499 1.67795 10.8941 2.46811 10.1039L6.28598 6.28602L9.42196 9.42203L7.46196 11.3821Z" fill="#737D8C"/>
<path d="M11.3821 7.46202C11.5059 7.07682 12.0989 5.49077 12.0744 3.63368C12.0499 1.77658 10.8941 1.67795 10.1039 2.46812L6.28598 6.28602L9.42196 9.42203L11.3821 7.46202Z" fill="black"/> <path d="M11.3821 7.46202C11.5059 7.07682 12.0989 5.49077 12.0744 3.63368C12.0499 1.77658 10.8941 1.67795 10.1039 2.46812L6.28598 6.28602L9.42196 9.42203L11.3821 7.46202Z" fill="#737D8C"/>
<path d="M7.40596 11.438L11.4379 7.40602L14.9099 10.206L10.2059 14.9101L7.40596 11.438Z" fill="black"/> <path d="M7.40596 11.438L11.4379 7.40602L14.9099 10.206L10.2059 14.9101L7.40596 11.438Z" fill="#737D8C"/>
<path d="M11.774 11.774C9.31114 14.2369 8.61779 17.7115 9.83827 20.3213C10.3104 21.3308 11.6288 21.3273 12.4169 20.5392L20.5391 12.4169C21.3271 11.6289 21.3307 10.3104 20.3212 9.83829C17.7114 8.61779 14.2369 9.31115 11.774 11.774Z" fill="black"/> <path d="M11.774 11.774C9.31114 14.2369 8.61779 17.7115 9.83827 20.3213C10.3104 21.3308 11.6288 21.3273 12.4169 20.5392L20.5391 12.4169C21.3271 11.6289 21.3307 10.3104 20.3212 9.83829C17.7114 8.61779 14.2369 9.31115 11.774 11.774Z" fill="#737D8C"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1,015 B

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015-2021 The Matrix.org Foundation C.I.C.
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,40 +14,49 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react"; import React, { ComponentType } from "react";
import * as sdk from './index'; import * as sdk from './index';
import PropTypes from 'prop-types';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import { IDialogProps } from "./components/views/dialogs/IDialogProps";
type AsyncImport<T> = { default: T };
interface IProps extends IDialogProps {
// A promise which resolves with the real component
prom: Promise<ComponentType | AsyncImport<ComponentType>>;
}
interface IState {
component?: ComponentType;
error?: Error;
}
/** /**
* Wrap an asynchronous loader function with a react component which shows a * Wrap an asynchronous loader function with a react component which shows a
* spinner until the real component loads. * spinner until the real component loads.
*/ */
export default class AsyncWrapper extends React.Component { export default class AsyncWrapper extends React.Component<IProps, IState> {
static propTypes = { private unmounted = false;
/** A promise which resolves with the real component
*/
prom: PropTypes.object.isRequired,
};
state = { public state = {
component: null, component: null,
error: null, error: null,
}; };
componentDidMount() { componentDidMount() {
this._unmounted = false;
// XXX: temporary logging to try to diagnose // XXX: temporary logging to try to diagnose
// https://github.com/vector-im/element-web/issues/3148 // https://github.com/vector-im/element-web/issues/3148
console.log('Starting load of AsyncWrapper for modal'); console.log('Starting load of AsyncWrapper for modal');
this.props.prom.then((result) => { this.props.prom.then((result) => {
if (this._unmounted) { if (this.unmounted) return;
return;
}
// Take the 'default' member if it's there, then we support // Take the 'default' member if it's there, then we support
// passing in just an import()ed module, since ES6 async import // passing in just an import()ed module, since ES6 async import
// always returns a module *namespace*. // always returns a module *namespace*.
const component = result.default ? result.default : result; const component = (result as AsyncImport<ComponentType>).default
? (result as AsyncImport<ComponentType>).default
: result as ComponentType;
this.setState({ component }); this.setState({ component });
}).catch((e) => { }).catch((e) => {
console.warn('AsyncWrapper promise failed', e); console.warn('AsyncWrapper promise failed', e);
@ -57,10 +65,10 @@ export default class AsyncWrapper extends React.Component {
} }
componentWillUnmount() { componentWillUnmount() {
this._unmounted = true; this.unmounted = true;
} }
_onWrapperCancelClick = () => { private onWrapperCancelClick = () => {
this.props.onFinished(false); this.props.onFinished(false);
}; };
@ -71,12 +79,10 @@ export default class AsyncWrapper extends React.Component {
} else if (this.state.error) { } else if (this.state.error) {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <BaseDialog onFinished={this.props.onFinished} return <BaseDialog onFinished={this.props.onFinished} title={_t("Error")}>
title={_t("Error")}
>
{ _t("Unable to load! Check your network connectivity and try again.") } { _t("Unable to load! Check your network connectivity and try again.") }
<DialogButtons primaryButton={_t("Dismiss")} <DialogButtons primaryButton={_t("Dismiss")}
onPrimaryButtonClick={this._onWrapperCancelClick} onPrimaryButtonClick={this.onWrapperCancelClick}
hasCancel={false} hasCancel={false}
/> />
</BaseDialog>; </BaseDialog>;

View file

@ -264,7 +264,7 @@ export default class CallHandler extends EventEmitter {
} }
public getSupportsVirtualRooms() { public getSupportsVirtualRooms() {
return this.supportsPstnProtocol; return this.supportsSipNativeVirtual;
} }
public pstnLookup(phoneNumber: string): Promise<ThirdpartyLookupResponse[]> { public pstnLookup(phoneNumber: string): Promise<ThirdpartyLookupResponse[]> {
@ -521,7 +521,9 @@ export default class CallHandler extends EventEmitter {
let newNativeAssertedIdentity = newAssertedIdentity; let newNativeAssertedIdentity = newAssertedIdentity;
if (newAssertedIdentity) { if (newAssertedIdentity) {
const response = await this.sipNativeLookup(newAssertedIdentity); const response = await this.sipNativeLookup(newAssertedIdentity);
if (response.length) newNativeAssertedIdentity = response[0].userid; if (response.length && response[0].fields.lookup_success) {
newNativeAssertedIdentity = response[0].userid;
}
} }
console.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`); console.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`);
@ -862,9 +864,43 @@ export default class CallHandler extends EventEmitter {
}); });
break; break;
} }
case Action.DialNumber:
this.dialNumber(payload.number);
break;
} }
} }
private async dialNumber(number: string) {
const results = await this.pstnLookup(number);
if (!results || results.length === 0 || !results[0].userid) {
Modal.createTrackedDialog('', '', ErrorDialog, {
title: _t("Unable to look up phone number"),
description: _t("There was an error looking up the phone number"),
});
return;
}
const userId = results[0].userid;
// Now check to see if this is a virtual user, in which case we should find the
// native user
let nativeUserId;
if (this.getSupportsVirtualRooms()) {
const nativeLookupResults = await this.sipNativeLookup(userId);
const lookupSuccess = nativeLookupResults.length > 0 && nativeLookupResults[0].fields.lookup_success;
nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId;
console.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId);
} else {
nativeUserId = userId;
}
const roomId = await ensureDMExists(MatrixClientPeg.get(), nativeUserId);
dis.dispatch({
action: 'view_room',
room_id: roomId,
});
}
setActiveCallRoomId(activeCallRoomId: string) { setActiveCallRoomId(activeCallRoomId: string) {
logger.info("Setting call in room " + activeCallRoomId + " active"); logger.info("Setting call in room " + activeCallRoomId + " active");

View file

@ -40,6 +40,7 @@ import {
UploadStartedPayload, UploadStartedPayload,
} from "./dispatcher/payloads/UploadPayload"; } from "./dispatcher/payloads/UploadPayload";
import {IUpload} from "./models/IUpload"; import {IUpload} from "./models/IUpload";
import { IImageInfo } from "matrix-js-sdk/src/@types/partials";
const MAX_WIDTH = 800; const MAX_WIDTH = 800;
const MAX_HEIGHT = 600; const MAX_HEIGHT = 600;
@ -208,12 +209,12 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
} }
let imageInfo; let imageInfo;
return loadImageElement(imageFile).then(function(r) { return loadImageElement(imageFile).then((r) => {
return createThumbnail(r.img, r.width, r.height, thumbnailType); return createThumbnail(r.img, r.width, r.height, thumbnailType);
}).then(function(result) { }).then((result) => {
imageInfo = result.info; imageInfo = result.info;
return uploadFile(matrixClient, roomId, result.thumbnail); return uploadFile(matrixClient, roomId, result.thumbnail);
}).then(function(result) { }).then((result) => {
imageInfo.thumbnail_url = result.url; imageInfo.thumbnail_url = result.url;
imageInfo.thumbnail_file = result.file; imageInfo.thumbnail_file = result.file;
return imageInfo; return imageInfo;
@ -264,12 +265,12 @@ function infoForVideoFile(matrixClient, roomId, videoFile) {
const thumbnailType = "image/jpeg"; const thumbnailType = "image/jpeg";
let videoInfo; let videoInfo;
return loadVideoElement(videoFile).then(function(video) { return loadVideoElement(videoFile).then((video) => {
return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType); return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
}).then(function(result) { }).then((result) => {
videoInfo = result.info; videoInfo = result.info;
return uploadFile(matrixClient, roomId, result.thumbnail); return uploadFile(matrixClient, roomId, result.thumbnail);
}).then(function(result) { }).then((result) => {
videoInfo.thumbnail_url = result.url; videoInfo.thumbnail_url = result.url;
videoInfo.thumbnail_file = result.file; videoInfo.thumbnail_file = result.file;
return videoInfo; return videoInfo;
@ -308,7 +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 unencrypted then the object will have a "url" key.
* If the file is encrypted then the object will have a "file" key. * If the file is encrypted then the object will have a "file" key.
*/ */
function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blob, progressHandler?: any) { function uploadFile(
matrixClient: MatrixClient,
roomId: string,
file: File | Blob,
progressHandler?: any, // TODO: Types
): Promise<{url?: string, file?: any}> { // TODO: Types
let canceled = false; let canceled = false;
if (matrixClient.isRoomEncrypted(roomId)) { if (matrixClient.isRoomEncrypted(roomId)) {
// If the room is encrypted then encrypt the file before uploading it. // If the room is encrypted then encrypt the file before uploading it.
@ -355,7 +361,7 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo
// If the attachment isn't encrypted then include the URL directly. // If the attachment isn't encrypted then include the URL directly.
return {"url": url}; return {"url": url};
}); });
promise1.abort = () => { (promise1 as any).abort = () => {
canceled = true; canceled = true;
MatrixClientPeg.get().cancelUpload(basePromise); MatrixClientPeg.get().cancelUpload(basePromise);
}; };
@ -367,7 +373,7 @@ export default class ContentMessages {
private inprogress: IUpload[] = []; private inprogress: IUpload[] = [];
private mediaConfig: IMediaConfig = null; private mediaConfig: IMediaConfig = null;
sendStickerContentToRoom(url: string, roomId: string, info: string, text: string, matrixClient: MatrixClient) { sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) {
const startTime = CountlyAnalytics.getTimestamp(); const startTime = CountlyAnalytics.getTimestamp();
const prom = MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => { const prom = MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => {
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e); console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
@ -441,7 +447,7 @@ export default class ContentMessages {
let uploadAll = false; let uploadAll = false;
// Promise to complete before sending next file into room, used for synchronisation of file-sending // Promise to complete before sending next file into room, used for synchronisation of file-sending
// to match the order the files were specified in // to match the order the files were specified in
let promBefore = Promise.resolve(); let promBefore: Promise<any> = Promise.resolve();
for (let i = 0; i < okFiles.length; ++i) { for (let i = 0; i < okFiles.length; ++i) {
const file = okFiles[i]; const file = okFiles[i];
if (!uploadAll) { if (!uploadAll) {

View file

@ -98,7 +98,7 @@ class Presence {
} }
try { try {
await MatrixClientPeg.get().setPresence(this.state); await MatrixClientPeg.get().setPresence({presence: this.state});
console.info("Presence:", newState); console.info("Presence:", newState);
} catch (err) { } catch (err) {
console.error("Failed to set presence:", err); console.error("Failed to set presence:", err);

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015-2021 The Matrix.org Foundation C.I.C.
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,35 +14,37 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event';
import { Room } from 'matrix-js-sdk/src/models/room';
import { MatrixClientPeg } from './MatrixClientPeg'; import { MatrixClientPeg } from './MatrixClientPeg';
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import { EventStatus } from 'matrix-js-sdk/src/models/event';
export default class Resend { export default class Resend {
static resendUnsentEvents(room) { static resendUnsentEvents(room: Room): Promise<void[]> {
return Promise.all(room.getPendingEvents().filter(function(ev) { return Promise.all(room.getPendingEvents().filter(function(ev: MatrixEvent) {
return ev.status === EventStatus.NOT_SENT; return ev.status === EventStatus.NOT_SENT;
}).map(function(event) { }).map(function(event: MatrixEvent) {
return Resend.resend(event); return Resend.resend(event);
})); }));
} }
static cancelUnsentEvents(room) { static cancelUnsentEvents(room: Room): void {
room.getPendingEvents().filter(function(ev) { room.getPendingEvents().filter(function(ev: MatrixEvent) {
return ev.status === EventStatus.NOT_SENT; return ev.status === EventStatus.NOT_SENT;
}).forEach(function(event) { }).forEach(function(event: MatrixEvent) {
Resend.removeFromQueue(event); Resend.removeFromQueue(event);
}); });
} }
static resend(event) { static resend(event: MatrixEvent): Promise<void> {
const room = MatrixClientPeg.get().getRoom(event.getRoomId()); const room = MatrixClientPeg.get().getRoom(event.getRoomId());
return MatrixClientPeg.get().resendEvent(event, room).then(function(res) { return MatrixClientPeg.get().resendEvent(event, room).then(function(res) {
dis.dispatch({ dis.dispatch({
action: 'message_sent', action: 'message_sent',
event: event, event: event,
}); });
}, function(err) { }, function(err: Error) {
// XXX: temporary logging to try to diagnose // XXX: temporary logging to try to diagnose
// https://github.com/vector-im/element-web/issues/3148 // https://github.com/vector-im/element-web/issues/3148
console.log('Resend got send failure: ' + err.name + '(' + err + ')'); console.log('Resend got send failure: ' + err.name + '(' + err + ')');
@ -55,7 +56,7 @@ export default class Resend {
}); });
} }
static removeFromQueue(event) { static removeFromQueue(event: MatrixEvent): void {
MatrixClientPeg.get().cancelPendingEvent(event); MatrixClientPeg.get().cancelPendingEvent(event);
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -24,12 +24,12 @@ limitations under the License.
* A similar thing could also be achieved via `pushState` with a state object, * A similar thing could also be achieved via `pushState` with a state object,
* but keeping it separate like this seems easier in case we do want to extend. * but keeping it separate like this seems easier in case we do want to extend.
*/ */
const aliasToIDMap = new Map(); const aliasToIDMap = new Map<string, string>();
export function storeRoomAliasInCache(alias, id) { export function storeRoomAliasInCache(alias: string, id: string): void {
aliasToIDMap.set(alias, id); aliasToIDMap.set(alias, id);
} }
export function getCachedRoomIDForAlias(alias) { export function getCachedRoomIDForAlias(alias: string): string {
return aliasToIDMap.get(alias); return aliasToIDMap.get(alias);
} }

View file

@ -66,7 +66,7 @@ async function serverSideSearchProcess(term, roomId = undefined) {
highlights: [], highlights: [],
}; };
return client._processRoomEventsSearch(searchResult, result.response); return client.processRoomEventsSearch(searchResult, result.response);
} }
function compareEvents(a, b) { function compareEvents(a, b) {
@ -131,7 +131,7 @@ async function combinedSearch(searchTerm) {
}, },
}; };
const result = client._processRoomEventsSearch(emptyResult, response); const result = client.processRoomEventsSearch(emptyResult, response);
// Restore our encryption info so we can properly re-verify the events. // Restore our encryption info so we can properly re-verify the events.
restoreEncryptionInfo(result.results); restoreEncryptionInfo(result.results);
@ -185,7 +185,7 @@ async function localSearchProcess(searchTerm, roomId = undefined) {
}, },
}; };
const processedResult = MatrixClientPeg.get()._processRoomEventsSearch(emptyResult, response); const processedResult = MatrixClientPeg.get().processRoomEventsSearch(emptyResult, response);
// Restore our encryption info so we can properly re-verify the events. // Restore our encryption info so we can properly re-verify the events.
restoreEncryptionInfo(processedResult.results); restoreEncryptionInfo(processedResult.results);
@ -210,7 +210,7 @@ async function localPagination(searchResult) {
}, },
}; };
const result = MatrixClientPeg.get()._processRoomEventsSearch(searchResult, response); const result = MatrixClientPeg.get().processRoomEventsSearch(searchResult, response);
// Restore our encryption info so we can properly re-verify the events. // Restore our encryption info so we can properly re-verify the events.
const newSlice = result.results.slice(Math.max(result.results.length - newResultCount, 0)); const newSlice = result.results.slice(Math.max(result.results.length - newResultCount, 0));
@ -520,7 +520,7 @@ async function combinedPagination(searchResult) {
const oldResultCount = searchResult.results ? searchResult.results.length : 0; const oldResultCount = searchResult.results ? searchResult.results.length : 0;
// Let the client process the combined result. // Let the client process the combined result.
const result = client._processRoomEventsSearch(searchResult, response); const result = client.processRoomEventsSearch(searchResult, response);
// Restore our encryption info so we can properly re-verify the events. // Restore our encryption info so we can properly re-verify the events.
const newResultCount = result.results.length - oldResultCount; const newResultCount = result.results.length - oldResultCount;

View file

@ -271,7 +271,7 @@ async function onSecretRequested(
} }
return key && encodeBase64(key); return key && encodeBase64(key);
} else if (name === "m.megolm_backup.v1") { } else if (name === "m.megolm_backup.v1") {
const key = await client._crypto.getSessionBackupPrivateKey(); const key = await client.crypto.getSessionBackupPrivateKey();
if (!key) { if (!key) {
console.log( console.log(
`session backup key requested by ${deviceId}, but not found in cache`, `session backup key requested by ${deviceId}, but not found in cache`,

View file

@ -103,7 +103,7 @@ export async function startTermsFlow(
// fetch the set of agreed policy URLs from account data // fetch the set of agreed policy URLs from account data
const currentAcceptedTerms = await MatrixClientPeg.get().getAccountData('m.accepted_terms'); const currentAcceptedTerms = await MatrixClientPeg.get().getAccountData('m.accepted_terms');
let agreedUrlSet; let agreedUrlSet: Set<string>;
if (!currentAcceptedTerms || !currentAcceptedTerms.getContent() || !currentAcceptedTerms.getContent().accepted) { if (!currentAcceptedTerms || !currentAcceptedTerms.getContent() || !currentAcceptedTerms.getContent().accepted) {
agreedUrlSet = new Set(); agreedUrlSet = new Set();
} else { } else {

View file

@ -33,7 +33,7 @@ export default class VoipUserMapper {
private async userToVirtualUser(userId: string): Promise<string> { private async userToVirtualUser(userId: string): Promise<string> {
const results = await CallHandler.sharedInstance().sipVirtualLookup(userId); const results = await CallHandler.sharedInstance().sipVirtualLookup(userId);
if (results.length === 0) return null; if (results.length === 0 || !results[0].fields.lookup_success) return null;
return results[0].userid; return results[0].userid;
} }
@ -82,14 +82,14 @@ export default class VoipUserMapper {
return Boolean(claimedNativeRoomId); return Boolean(claimedNativeRoomId);
} }
public async onNewInvitedRoom(invitedRoom: Room) { public async onNewInvitedRoom(invitedRoom: Room): Promise<void> {
if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return; if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return;
const inviterId = invitedRoom.getDMInviter(); const inviterId = invitedRoom.getDMInviter();
console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId); const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);
if (result.length === 0) { if (result.length === 0) {
return true; return;
} }
if (result[0].fields.is_virtual) { if (result[0].fields.is_virtual) {

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2017 New Vector Ltd Copyright 2017, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import dis from '../dispatcher/dispatcher'; import { MatrixClient } from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
import dis from "../dispatcher/dispatcher";
import {ActionPayload} from "../dispatcher/payloads";
// TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events // TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events
// become dispatches in the same place. // become dispatches in the same place.
@ -27,7 +33,7 @@ import dis from '../dispatcher/dispatcher';
* @param {string} prevState the previous sync state. * @param {string} prevState the previous sync state.
* @returns {Object} an action of type MatrixActions.sync. * @returns {Object} an action of type MatrixActions.sync.
*/ */
function createSyncAction(matrixClient, state, prevState) { function createSyncAction(matrixClient: MatrixClient, state: string, prevState: string): ActionPayload {
return { return {
action: 'MatrixActions.sync', action: 'MatrixActions.sync',
state, state,
@ -53,7 +59,7 @@ function createSyncAction(matrixClient, state, prevState) {
* @param {MatrixEvent} accountDataEvent the account data event. * @param {MatrixEvent} accountDataEvent the account data event.
* @returns {AccountDataAction} an action of type MatrixActions.accountData. * @returns {AccountDataAction} an action of type MatrixActions.accountData.
*/ */
function createAccountDataAction(matrixClient, accountDataEvent) { function createAccountDataAction(matrixClient: MatrixClient, accountDataEvent: MatrixEvent): ActionPayload {
return { return {
action: 'MatrixActions.accountData', action: 'MatrixActions.accountData',
event: accountDataEvent, event: accountDataEvent,
@ -81,7 +87,11 @@ function createAccountDataAction(matrixClient, accountDataEvent) {
* @param {Room} room the room where account data was changed * @param {Room} room the room where account data was changed
* @returns {RoomAccountDataAction} an action of type MatrixActions.Room.accountData. * @returns {RoomAccountDataAction} an action of type MatrixActions.Room.accountData.
*/ */
function createRoomAccountDataAction(matrixClient, accountDataEvent, room) { function createRoomAccountDataAction(
matrixClient: MatrixClient,
accountDataEvent: MatrixEvent,
room: Room,
): ActionPayload {
return { return {
action: 'MatrixActions.Room.accountData', action: 'MatrixActions.Room.accountData',
event: accountDataEvent, event: accountDataEvent,
@ -106,7 +116,7 @@ function createRoomAccountDataAction(matrixClient, accountDataEvent, room) {
* @param {Room} room the Room that was stored. * @param {Room} room the Room that was stored.
* @returns {RoomAction} an action of type `MatrixActions.Room`. * @returns {RoomAction} an action of type `MatrixActions.Room`.
*/ */
function createRoomAction(matrixClient, room) { function createRoomAction(matrixClient: MatrixClient, room: Room): ActionPayload {
return { action: 'MatrixActions.Room', room }; return { action: 'MatrixActions.Room', room };
} }
@ -127,7 +137,7 @@ function createRoomAction(matrixClient, room) {
* @param {Room} room the Room whose tags were changed. * @param {Room} room the Room whose tags were changed.
* @returns {RoomTagsAction} an action of type `MatrixActions.Room.tags`. * @returns {RoomTagsAction} an action of type `MatrixActions.Room.tags`.
*/ */
function createRoomTagsAction(matrixClient, roomTagsEvent, room) { function createRoomTagsAction(matrixClient: MatrixClient, roomTagsEvent: MatrixEvent, room: Room): ActionPayload {
return { action: 'MatrixActions.Room.tags', room }; return { action: 'MatrixActions.Room.tags', room };
} }
@ -140,7 +150,7 @@ function createRoomTagsAction(matrixClient, roomTagsEvent, room) {
* @param {Room} room the room the receipt happened in. * @param {Room} room the room the receipt happened in.
* @returns {Object} an action of type MatrixActions.Room.receipt. * @returns {Object} an action of type MatrixActions.Room.receipt.
*/ */
function createRoomReceiptAction(matrixClient, event, room) { function createRoomReceiptAction(matrixClient: MatrixClient, event: MatrixEvent, room: Room): ActionPayload {
return { return {
action: 'MatrixActions.Room.receipt', action: 'MatrixActions.Room.receipt',
event, event,
@ -178,7 +188,17 @@ function createRoomReceiptAction(matrixClient, event, room) {
* @param {EventTimeline} data.timeline the timeline being altered. * @param {EventTimeline} data.timeline the timeline being altered.
* @returns {RoomTimelineAction} an action of type `MatrixActions.Room.timeline`. * @returns {RoomTimelineAction} an action of type `MatrixActions.Room.timeline`.
*/ */
function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTimeline, removed, data) { function createRoomTimelineAction(
matrixClient: MatrixClient,
timelineEvent: MatrixEvent,
room: Room,
toStartOfTimeline: boolean,
removed: boolean,
data: {
liveEvent: boolean;
timeline: EventTimeline;
},
): ActionPayload {
return { return {
action: 'MatrixActions.Room.timeline', action: 'MatrixActions.Room.timeline',
event: timelineEvent, event: timelineEvent,
@ -208,7 +228,12 @@ function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTi
* @param {string} oldMembership the previous membership, can be null. * @param {string} oldMembership the previous membership, can be null.
* @returns {RoomMembershipAction} an action of type `MatrixActions.Room.myMembership`. * @returns {RoomMembershipAction} an action of type `MatrixActions.Room.myMembership`.
*/ */
function createSelfMembershipAction(matrixClient, room, membership, oldMembership) { function createSelfMembershipAction(
matrixClient: MatrixClient,
room: Room,
membership: string,
oldMembership: string,
): ActionPayload {
return { action: 'MatrixActions.Room.myMembership', room, membership, oldMembership }; return { action: 'MatrixActions.Room.myMembership', room, membership, oldMembership };
} }
@ -228,34 +253,15 @@ function createSelfMembershipAction(matrixClient, room, membership, oldMembershi
* @param {MatrixEvent} event the matrix event that was decrypted. * @param {MatrixEvent} event the matrix event that was decrypted.
* @returns {EventDecryptedAction} an action of type `MatrixActions.Event.decrypted`. * @returns {EventDecryptedAction} an action of type `MatrixActions.Event.decrypted`.
*/ */
function createEventDecryptedAction(matrixClient, event) { function createEventDecryptedAction(matrixClient: MatrixClient, event: MatrixEvent): ActionPayload {
return { action: 'MatrixActions.Event.decrypted', event }; return { action: 'MatrixActions.Event.decrypted', event };
} }
/** type Listener = () => void;
* This object is responsible for dispatching actions when certain events are emitted by type ActionCreator = (matrixClient: MatrixClient, ...args: any) => ActionPayload;
* the given MatrixClient.
*/
export default {
// A list of callbacks to call to unregister all listeners added
_matrixClientListenersStop: [],
/** // A list of callbacks to call to unregister all listeners added
* Start listening to certain events from the MatrixClient and dispatch actions when let matrixClientListenersStop: Listener[] = [];
* 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, * Start listening to events of type eventName on matrixClient and when they are emitted,
@ -266,23 +272,46 @@ export default {
* when given the MatrixClient as an argument as well as * when given the MatrixClient as an argument as well as
* arguments emitted in the MatrixClient event. * arguments emitted in the MatrixClient event.
*/ */
_addMatrixClientListener(matrixClient, eventName, actionCreator) { function addMatrixClientListener(matrixClient: MatrixClient, eventName: string, actionCreator: ActionCreator): void {
const listener = (...args) => { const listener: Listener = (...args) => {
const payload = actionCreator(matrixClient, ...args); const payload = actionCreator(matrixClient, ...args);
if (payload) { if (payload) {
dis.dispatch(payload, true); dis.dispatch(payload, true);
} }
}; };
matrixClient.on(eventName, listener); matrixClient.on(eventName, listener);
this._matrixClientListenersStop.push(() => { matrixClientListenersStop.push(() => {
matrixClient.removeListener(eventName, listener); matrixClient.removeListener(eventName, listener);
}); });
}
/**
* This object is responsible for dispatching actions when certain events are emitted by
* the given MatrixClient.
*/
export default {
/**
* 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: 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 listening to events.
*/ */
stop() { stop() {
this._matrixClientListenersStop.forEach((stopListener) => stopListener()); matrixClientListenersStop.forEach((stopListener) => stopListener());
matrixClientListenersStop = [];
}, },
}; };

View file

@ -358,7 +358,7 @@ class LoggedInView extends React.Component<IProps, IState> {
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM); const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
for (const eventId of pinnedEventIds) { for (const eventId of pinnedEventIds) {
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0); const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
const event = timeline.getEvents().find(ev => ev.getId() === eventId); const event = timeline.getEvents().find(ev => ev.getId() === eventId);
if (event) events.push(event); if (event) events.push(event);
} }

View file

@ -378,7 +378,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.onLoggedIn(); this.onLoggedIn();
} }
const promisesList = [this.firstSyncPromise.promise]; const promisesList: Promise<any>[] = [this.firstSyncPromise.promise];
if (cryptoEnabled) { if (cryptoEnabled) {
// wait for the client to finish downloading cross-signing keys for us so we // wait for the client to finish downloading cross-signing keys for us so we
// know whether or not we have keys set up on this account // know whether or not we have keys set up on this account

View file

@ -19,7 +19,6 @@ limitations under the License.
import React, {createRef} from 'react'; import React, {createRef} from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames';
import shouldHideEvent from '../../shouldHideEvent'; import shouldHideEvent from '../../shouldHideEvent';
import {wantsDateSeparator} from '../../DateUtils'; import {wantsDateSeparator} from '../../DateUtils';
import * as sdk from '../../index'; import * as sdk from '../../index';
@ -616,10 +615,6 @@ export default class MessagePanel extends React.Component {
const eventId = mxEv.getId(); const eventId = mxEv.getId();
const highlight = (eventId === this.props.highlightedEventId); const highlight = (eventId === this.props.highlightedEventId);
// we can't use local echoes as scroll tokens, because their event IDs change.
// Local echos have a send "status".
const scrollToken = mxEv.status ? undefined : eventId;
const readReceipts = this._readReceiptsByEvent[eventId]; const readReceipts = this._readReceiptsByEvent[eventId];
let isLastSuccessful = false; let isLastSuccessful = false;
@ -651,7 +646,6 @@ export default class MessagePanel extends React.Component {
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}> <TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
<EventTile <EventTile
as="li" as="li"
data-scroll-tokens={scrollToken}
ref={this._collectEventNode.bind(this, eventId)} ref={this._collectEventNode.bind(this, eventId)}
alwaysShowTimestamps={this.props.alwaysShowTimestamps} alwaysShowTimestamps={this.props.alwaysShowTimestamps}
mxEvent={mxEv} mxEvent={mxEv}
@ -854,13 +848,6 @@ export default class MessagePanel extends React.Component {
const style = this.props.hidden ? { display: 'none' } : {}; const style = this.props.hidden ? { display: 'none' } : {};
const className = classNames(
this.props.className,
{
"mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
},
);
let whoIsTyping; let whoIsTyping;
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) { if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
whoIsTyping = (<WhoIsTypingTile whoIsTyping = (<WhoIsTypingTile
@ -884,7 +871,7 @@ export default class MessagePanel extends React.Component {
<ErrorBoundary> <ErrorBoundary>
<ScrollPanel <ScrollPanel
ref={this._scrollPanel} ref={this._scrollPanel}
className={className} className={this.props.className}
onScroll={this.props.onScroll} onScroll={this.props.onScroll}
onUserScroll={this.props.onUserScroll} onUserScroll={this.props.onUserScroll}
onResize={this.onResize} onResize={this.onResize}

View file

@ -1,7 +1,5 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016, 2019, 2021 The Matrix.org Foundation C.I.C.
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,29 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from "react";
import PropTypes from "prop-types";
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import { MatrixClientPeg } from "../../MatrixClientPeg"; import { MatrixClientPeg } from "../../MatrixClientPeg";
import * as sdk from "../../index";
import BaseCard from "../views/right_panel/BaseCard"; import BaseCard from "../views/right_panel/BaseCard";
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
interface IProps {
onClose(): void;
}
/* /*
* Component which shows the global notification list using a TimelinePanel * Component which shows the global notification list using a TimelinePanel
*/ */
@replaceableComponent("structures.NotificationPanel") @replaceableComponent("structures.NotificationPanel")
class NotificationPanel extends React.Component { export default class NotificationPanel extends React.PureComponent<IProps> {
static propTypes = {
onClose: PropTypes.func.isRequired,
};
render() { render() {
// wrap a TimelinePanel with the jump-to-event bits turned off.
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
const Loader = sdk.getComponent("elements.Spinner");
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty"> const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
<h2>{_t('Youre all caught up')}</h2> <h2>{_t('Youre all caught up')}</h2>
<p>{_t('You have no visible notifications.')}</p> <p>{_t('You have no visible notifications.')}</p>
@ -47,6 +41,7 @@ class NotificationPanel extends React.Component {
let content; let content;
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet(); const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
if (timelineSet) { if (timelineSet) {
// wrap a TimelinePanel with the jump-to-event bits turned off.
content = ( content = (
<TimelinePanel <TimelinePanel
manageReadReceipts={false} manageReadReceipts={false}
@ -55,11 +50,12 @@ class NotificationPanel extends React.Component {
showUrlPreview={false} showUrlPreview={false}
tileShape="notif" tileShape="notif"
empty={emptyState} empty={emptyState}
alwaysShowTimestamps={true}
/> />
); );
} else { } else {
console.error("No notifTimelineSet available!"); console.error("No notifTimelineSet available!");
content = <Loader />; content = <Spinner />;
} }
return <BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer> return <BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
@ -67,5 +63,3 @@ class NotificationPanel extends React.Component {
</BaseCard>; </BaseCard>;
} }
} }
export default NotificationPanel;

View file

@ -1,6 +1,6 @@
/* /*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2015 - 2020 The Matrix.org Foundation C.I.C. Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,18 +16,19 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { User } from "matrix-js-sdk/src/models/user";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import RateLimitedFunc from '../../ratelimitedfunc'; import RateLimitedFunc from '../../ratelimitedfunc';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStore from '../../stores/GroupStore'; import GroupStore from '../../stores/GroupStore';
import { import {
RightPanelPhases,
RIGHT_PANEL_PHASES_NO_ARGS, RIGHT_PANEL_PHASES_NO_ARGS,
RIGHT_PANEL_SPACE_PHASES, RIGHT_PANEL_SPACE_PHASES,
RightPanelPhases,
} from "../../stores/RightPanelStorePhases"; } from "../../stores/RightPanelStorePhases";
import RightPanelStore from "../../stores/RightPanelStore"; import RightPanelStore from "../../stores/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
@ -36,50 +37,71 @@ import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
import WidgetCard from "../views/right_panel/WidgetCard"; import WidgetCard from "../views/right_panel/WidgetCard";
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import { ActionPayload } from "../../dispatcher/payloads";
import MemberList from "../views/rooms/MemberList";
import GroupMemberList from "../views/groups/GroupMemberList";
import GroupRoomList from "../views/groups/GroupRoomList";
import GroupRoomInfo from "../views/groups/GroupRoomInfo";
import UserInfo from "../views/right_panel/UserInfo";
import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo";
import FilePanel from "./FilePanel";
import NotificationPanel from "./NotificationPanel";
import ResizeNotifier from "../../utils/ResizeNotifier";
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
@replaceableComponent("structures.RightPanel") interface IProps {
export default class RightPanel extends React.Component { room?: Room; // if showing panels for a given room, this is set
static get propTypes() { groupId?: string; // if showing panels for a given group, this is set
return { user?: User; // used if we know the user ahead of opening the panel
room: PropTypes.instanceOf(Room), // if showing panels for a given room, this is set resizeNotifier: ResizeNotifier;
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
};
} }
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<IProps, IState> {
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
private readonly delayedUpdate: RateLimitedFunc;
private dispatcherRef: string;
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = { this.state = {
...RightPanelStore.getSharedInstance().roomPanelPhaseParams, ...RightPanelStore.getSharedInstance().roomPanelPhaseParams,
phase: this._getPhaseFromProps(), phase: this.getPhaseFromProps(),
isUserPrivilegedInGroup: null, isUserPrivilegedInGroup: null,
member: this._getUserForPanel(), member: this.getUserForPanel(),
}; };
this.onAction = this.onAction.bind(this);
this.onRoomStateMember = this.onRoomStateMember.bind(this);
this.onGroupStoreUpdated = this.onGroupStoreUpdated.bind(this);
this.onInviteToGroupButtonClick = this.onInviteToGroupButtonClick.bind(this);
this.onAddRoomToGroupButtonClick = this.onAddRoomToGroupButtonClick.bind(this);
this._delayedUpdate = new RateLimitedFunc(() => { this.delayedUpdate = new RateLimitedFunc(() => {
this.forceUpdate(); this.forceUpdate();
}, 500); }, 500);
} }
// Helper function to split out the logic for _getPhaseFromProps() and the constructor // Helper function to split out the logic for getPhaseFromProps() and the constructor
// as both are called at the same time in the constructor. // as both are called at the same time in the constructor.
_getUserForPanel() { private getUserForPanel() {
if (this.state && this.state.member) return this.state.member; if (this.state && this.state.member) return this.state.member;
const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams; const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams;
return this.props.user || lastParams['member']; return this.props.user || lastParams['member'];
} }
// gets the current phase from the props and also maybe the store // gets the current phase from the props and also maybe the store
_getPhaseFromProps() { private getPhaseFromProps() {
const rps = RightPanelStore.getSharedInstance(); const rps = RightPanelStore.getSharedInstance();
const userForPanel = this._getUserForPanel(); const userForPanel = this.getUserForPanel();
if (this.props.groupId) { if (this.props.groupId) {
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) { if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList}); dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList});
@ -118,7 +140,7 @@ export default class RightPanel extends React.Component {
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
const cli = this.context; const cli = this.context;
cli.on("RoomState.members", this.onRoomStateMember); cli.on("RoomState.members", this.onRoomStateMember);
this._initGroupStore(this.props.groupId); this.initGroupStore(this.props.groupId);
} }
componentWillUnmount() { componentWillUnmount() {
@ -126,61 +148,47 @@ export default class RightPanel extends React.Component {
if (this.context) { if (this.context) {
this.context.removeListener("RoomState.members", this.onRoomStateMember); this.context.removeListener("RoomState.members", this.onRoomStateMember);
} }
this._unregisterGroupStore(this.props.groupId); this.unregisterGroupStore();
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
if (newProps.groupId !== this.props.groupId) { if (newProps.groupId !== this.props.groupId) {
this._unregisterGroupStore(this.props.groupId); this.unregisterGroupStore();
this._initGroupStore(newProps.groupId); this.initGroupStore(newProps.groupId);
} }
} }
_initGroupStore(groupId) { private initGroupStore(groupId: string) {
if (!groupId) return; if (!groupId) return;
GroupStore.registerListener(groupId, this.onGroupStoreUpdated); GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
} }
_unregisterGroupStore() { private unregisterGroupStore() {
GroupStore.unregisterListener(this.onGroupStoreUpdated); GroupStore.unregisterListener(this.onGroupStoreUpdated);
} }
onGroupStoreUpdated() { private onGroupStoreUpdated = () => {
this.setState({ this.setState({
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId), isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
}); });
} };
onInviteToGroupButtonClick() { private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => {
showGroupInviteDialog(this.props.groupId).then(() => {
this.setState({
phase: RightPanelPhases.GroupMemberList,
});
});
}
onAddRoomToGroupButtonClick() {
showGroupAddRoomDialog(this.props.groupId).then(() => {
this.forceUpdate();
});
}
onRoomStateMember(ev, state, member) {
if (!this.props.room || member.roomId !== this.props.room.roomId) { if (!this.props.room || member.roomId !== this.props.room.roomId) {
return; return;
} }
// redraw the badge on the membership list // redraw the badge on the membership list
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) { if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) {
this._delayedUpdate(); this.delayedUpdate();
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId && } else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId &&
member.userId === this.state.member.userId) { member.userId === this.state.member.userId) {
// refresh the member info (e.g. new power level) // refresh the member info (e.g. new power level)
this._delayedUpdate(); this.delayedUpdate();
}
} }
};
onAction(payload) { private onAction = (payload: ActionPayload) => {
if (payload.action === Action.AfterRightPanelPhaseChange) { if (payload.action === Action.AfterRightPanelPhaseChange) {
this.setState({ this.setState({
phase: payload.phase, phase: payload.phase,
@ -194,9 +202,9 @@ export default class RightPanel extends React.Component {
space: payload.space, space: payload.space,
}); });
} }
} };
onClose = () => { private onClose = () => {
// XXX: There are three different ways of 'closing' this panel depending on what state // XXX: There are three different ways of 'closing' this panel depending on what state
// things are in... this knows far more than it should do about the state of the rest // things are in... this knows far more than it should do about the state of the rest
// of the app and is generally a bit silly. // of the app and is generally a bit silly.
@ -224,16 +232,6 @@ export default class RightPanel extends React.Component {
}; };
render() { render() {
const MemberList = sdk.getComponent('rooms.MemberList');
const UserInfo = sdk.getComponent('right_panel.UserInfo');
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
const FilePanel = sdk.getComponent('structures.FilePanel');
const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
let panel = <div />; let panel = <div />;
const roomId = this.props.room ? this.props.room.roomId : undefined; const roomId = this.props.room ? this.props.room.roomId : undefined;
@ -285,6 +283,7 @@ export default class RightPanel extends React.Component {
user={this.state.member} user={this.state.member}
groupId={this.props.groupId} groupId={this.props.groupId}
key={this.state.member.userId} key={this.state.member.userId}
phase={this.state.phase}
onClose={this.onClose} />; onClose={this.onClose} />;
break; break;
@ -299,6 +298,12 @@ export default class RightPanel extends React.Component {
panel = <NotificationPanel onClose={this.onClose} />; panel = <NotificationPanel onClose={this.onClose} />;
break; break;
case RightPanelPhases.PinnedMessages:
if (SettingsStore.getValue("feature_pinning")) {
panel = <PinnedMessagesCard room={this.props.room} onClose={this.onClose} />;
}
break;
case RightPanelPhases.FilePanel: case RightPanelPhases.FilePanel:
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />; panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
break; break;

View file

@ -54,16 +54,13 @@ import RoomContext from "../../contexts/RoomContext";
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils'; import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import { SettingLevel } from "../../settings/SettingLevel";
import { IMatrixClientCreds } from "../../MatrixClientPeg"; import { IMatrixClientCreds } from "../../MatrixClientPeg";
import ScrollPanel from "./ScrollPanel"; import ScrollPanel from "./ScrollPanel";
import TimelinePanel from "./TimelinePanel"; import TimelinePanel from "./TimelinePanel";
import ErrorBoundary from "../views/elements/ErrorBoundary"; import ErrorBoundary from "../views/elements/ErrorBoundary";
import RoomPreviewBar from "../views/rooms/RoomPreviewBar"; import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
import ForwardMessage from "../views/rooms/ForwardMessage";
import SearchBar from "../views/rooms/SearchBar"; import SearchBar from "../views/rooms/SearchBar";
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar"; import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
import AuxPanel from "../views/rooms/AuxPanel"; import AuxPanel from "../views/rooms/AuxPanel";
import RoomHeader from "../views/rooms/RoomHeader"; import RoomHeader from "../views/rooms/RoomHeader";
import { XOR } from "../../@types/common"; import { XOR } from "../../@types/common";
@ -138,7 +135,6 @@ export interface IState {
// Whether to highlight the event scrolled to // Whether to highlight the event scrolled to
isInitialEventHighlighted?: boolean; isInitialEventHighlighted?: boolean;
replyToEvent?: MatrixEvent; replyToEvent?: MatrixEvent;
forwardingEvent?: MatrixEvent;
numUnreadMessages: number; numUnreadMessages: number;
draggingFile: boolean; draggingFile: boolean;
searching: boolean; searching: boolean;
@ -157,7 +153,6 @@ export interface IState {
canPeek: boolean; canPeek: boolean;
showApps: boolean; showApps: boolean;
isPeeking: boolean; isPeeking: boolean;
showingPinned: boolean;
showReadReceipts: boolean; showReadReceipts: boolean;
showRightPanel: boolean; showRightPanel: boolean;
// error object, as from the matrix client/server API // error object, as from the matrix client/server API
@ -235,7 +230,6 @@ export default class RoomView extends React.Component<IProps, IState> {
canPeek: false, canPeek: false,
showApps: false, showApps: false,
isPeeking: false, isPeeking: false,
showingPinned: false,
showReadReceipts: true, showReadReceipts: true,
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
joining: false, joining: false,
@ -327,10 +321,8 @@ export default class RoomView extends React.Component<IProps, IState> {
initialEventId: RoomViewStore.getInitialEventId(), initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
replyToEvent: RoomViewStore.getQuotingEvent(), replyToEvent: RoomViewStore.getQuotingEvent(),
forwardingEvent: RoomViewStore.getForwardingEvent(),
// we should only peek once we have a ready client // we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(), shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
wasContextSwitch: RoomViewStore.getWasContextSwitch(), wasContextSwitch: RoomViewStore.getWasContextSwitch(),
}; };
@ -1403,13 +1395,6 @@ export default class RoomView extends React.Component<IProps, IState> {
return ret; return ret;
} }
private onPinnedClick = () => {
const nowShowingPinned = !this.state.showingPinned;
const roomId = this.state.room.roomId;
this.setState({showingPinned: nowShowingPinned, searching: false});
SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned);
};
private onCallPlaced = (type: PlaceCallType) => { private onCallPlaced = (type: PlaceCallType) => {
dis.dispatch({ dis.dispatch({
action: 'place_call', action: 'place_call',
@ -1422,18 +1407,6 @@ export default class RoomView extends React.Component<IProps, IState> {
dis.dispatch({ action: "open_room_settings" }); dis.dispatch({ action: "open_room_settings" });
}; };
private onCancelClick = () => {
console.log("updateTint from onCancelClick");
this.updateTint();
if (this.state.forwardingEvent) {
dis.dispatch({
action: 'forward_event',
event: null,
});
}
dis.fire(Action.FocusComposer);
};
private onAppsClick = () => { private onAppsClick = () => {
dis.dispatch({ dis.dispatch({
action: "appsDrawer", action: "appsDrawer",
@ -1526,7 +1499,6 @@ export default class RoomView extends React.Component<IProps, IState> {
private onSearchClick = () => { private onSearchClick = () => {
this.setState({ this.setState({
searching: !this.state.searching, searching: !this.state.searching,
showingPinned: false,
}); });
}; };
@ -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 // jump down to the bottom of this room, where new events are arriving
private jumpToLiveTimeline = () => { private jumpToLiveTimeline = () => {
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({ dis.dispatch({
action: 'view_room', action: 'view_room',
room_id: this.state.room.roomId, room_id: this.state.room.roomId,
}); });
} else {
// Otherwise we have to jump manually
this.messagePanel.jumpToLiveTimeline();
dis.fire(Action.FocusComposer);
}
}; };
// jump up to wherever our read marker is // jump up to wherever our read marker is
@ -1841,11 +1822,7 @@ export default class RoomView extends React.Component<IProps, IState> {
let aux = null; let aux = null;
let previewBar; let previewBar;
let hideCancel = false; if (this.state.searching) {
if (this.state.forwardingEvent) {
aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
} else if (this.state.searching) {
hideCancel = true; // has own cancel
aux = <SearchBar aux = <SearchBar
searchInProgress={this.state.searchInProgress} searchInProgress={this.state.searchInProgress}
onCancelClick={this.onCancelSearchClick} onCancelClick={this.onCancelSearchClick}
@ -1854,10 +1831,6 @@ export default class RoomView extends React.Component<IProps, IState> {
/>; />;
} else if (showRoomUpgradeBar) { } else if (showRoomUpgradeBar) {
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />; aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
hideCancel = true;
} else if (this.state.showingPinned) {
hideCancel = true; // has own cancel
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
} else if (myMembership !== "join") { } else if (myMembership !== "join") {
// We do have a room object for this room, but we're not currently in it. // We do have a room object for this room, but we're not currently in it.
// We may have a 3rd party invite to it. // We may have a 3rd party invite to it.
@ -1866,7 +1839,6 @@ export default class RoomView extends React.Component<IProps, IState> {
inviterName = this.props.oobData.inviterName; inviterName = this.props.oobData.inviterName;
} }
const invitedEmail = this.props.threepidInvite?.toEmail; const invitedEmail = this.props.threepidInvite?.toEmail;
hideCancel = true;
previewBar = ( previewBar = (
<RoomPreviewBar <RoomPreviewBar
onJoinClick={this.onJoinButtonClicked} onJoinClick={this.onJoinButtonClicked}
@ -1984,11 +1956,8 @@ export default class RoomView extends React.Component<IProps, IState> {
hideMessagePanel = true; hideMessagePanel = true;
} }
const shouldHighlight = this.state.isInitialEventHighlighted;
let highlightedEventId = null; let highlightedEventId = null;
if (this.state.forwardingEvent) { if (this.state.isInitialEventHighlighted) {
highlightedEventId = this.state.forwardingEvent.getId();
} else if (shouldHighlight) {
highlightedEventId = this.state.initialEventId; highlightedEventId = this.state.initialEventId;
} }
@ -2077,8 +2046,6 @@ export default class RoomView extends React.Component<IProps, IState> {
inRoom={myMembership === 'join'} inRoom={myMembership === 'join'}
onSearchClick={this.onSearchClick} onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick} onSettingsClick={this.onSettingsClick}
onPinnedClick={this.onPinnedClick}
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null} onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null} onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
e2eStatus={this.state.e2eStatus} e2eStatus={this.state.e2eStatus}

View file

@ -587,6 +587,10 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
<h3>{ _t("Me and my teammates") }</h3> <h3>{ _t("Me and my teammates") }</h3>
<div>{ _t("A private space for you and your teammates") }</div> <div>{ _t("A private space for you and your teammates") }</div>
</AccessibleButton> </AccessibleButton>
<div className="mx_SpaceRoomView_betaWarning">
<h3>{ _t("Teammates might not be able to view or join any private rooms you make.") }</h3>
<p>{ _t("We're working on this as part of the beta, but just want to let you know.") }</p>
</div>
<SpaceFeedbackPrompt /> <SpaceFeedbackPrompt />
</div>; </div>;
}; };

View file

@ -120,6 +120,9 @@ class TimelinePanel extends React.Component {
// which layout to use // which layout to use
layout: LayoutPropType, layout: LayoutPropType,
// whether to always show timestamps for an event
alwaysShowTimestamps: PropTypes.bool,
} }
// a map from room id to read marker event timestamp // a map from room id to read marker event timestamp
@ -1440,7 +1443,7 @@ class TimelinePanel extends React.Component {
onFillRequest={this.onMessageListFillRequest} onFillRequest={this.onMessageListFillRequest}
onUnfillRequest={this.onMessageListUnfillRequest} onUnfillRequest={this.onMessageListUnfillRequest}
isTwelveHour={this.state.isTwelveHour} isTwelveHour={this.state.isTwelveHour}
alwaysShowTimestamps={this.state.alwaysShowTimestamps} alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps}
className={this.props.className} className={this.props.className}
tileShape={this.props.tileShape} tileShape={this.props.tileShape}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}

View file

@ -61,7 +61,7 @@ interface IProps {
is_url?: string; is_url?: string;
session_id: string; session_id: string;
/* eslint-enable camelcase */ /* eslint-enable camelcase */
}): void; }): string;
// registration shouldn't know or care how login is done. // registration shouldn't know or care how login is done.
onLoginClick(): void; onLoginClick(): void;
onServerConfigChange(config: ValidatedServerConfig): void; onServerConfigChange(config: ValidatedServerConfig): void;

View file

@ -31,6 +31,8 @@ import { isContentActionable } from '../../../utils/EventUtils';
import { MenuItem } from "../../structures/ContextMenu"; import { MenuItem } from "../../structures/ContextMenu";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
import ForwardDialog from "../dialogs/ForwardDialog";
export function canCancel(eventStatus) { export function canCancel(eventStatus) {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
@ -82,7 +84,7 @@ export default class MessageContextMenu extends React.Component {
const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId) const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId)
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl && this.props.mxEvent.getType() !== EventType.RoomServerAcl
&& this.props.mxEvent.getType() !== EventType.RoomEncryption; && this.props.mxEvent.getType() !== EventType.RoomEncryption;
let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli); let canPin = room.currentState.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli);
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality // HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
if (!SettingsStore.getValue("feature_pinning")) canPin = false; if (!SettingsStore.getValue("feature_pinning")) canPin = false;
@ -92,7 +94,7 @@ export default class MessageContextMenu extends React.Component {
_isPinned() { _isPinned() {
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', ''); const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, '');
if (!pinnedEvent) return false; if (!pinnedEvent) return false;
const content = pinnedEvent.getContent(); const content = pinnedEvent.getContent();
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId()); return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
@ -156,34 +158,32 @@ export default class MessageContextMenu extends React.Component {
}; };
onForwardClick = () => { onForwardClick = () => {
if (this.props.onCloseDialog) this.props.onCloseDialog(); Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
dis.dispatch({ matrixClient: MatrixClientPeg.get(),
action: 'forward_event',
event: this.props.mxEvent, event: this.props.mxEvent,
permalinkCreator: this.props.permalinkCreator,
}); });
this.closeMenu(); this.closeMenu();
}; };
onPinClick = () => { onPinClick = () => {
MatrixClientPeg.get().getStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', '')
.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 cli = MatrixClientPeg.get();
cli.sendStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', {pinned: eventIds}, ''); const room = cli.getRoom(this.props.mxEvent.getRoomId());
const eventId = this.props.mxEvent.getId();
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(); this.closeMenu();
}; };

View file

@ -40,6 +40,8 @@ interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
showUnpin?: boolean; showUnpin?: boolean;
// override delete handler // override delete handler
onDeleteClick?(): void; onDeleteClick?(): void;
// override edit handler
onEditClick?(): void;
} }
const WidgetContextMenu: React.FC<IProps> = ({ const WidgetContextMenu: React.FC<IProps> = ({
@ -47,6 +49,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
app, app,
userWidget, userWidget,
onDeleteClick, onDeleteClick,
onEditClick,
showUnpin, showUnpin,
...props ...props
}) => { }) => {
@ -89,12 +92,16 @@ const WidgetContextMenu: React.FC<IProps> = ({
let editButton; let editButton;
if (canModify && WidgetUtils.isManagedByManager(app)) { if (canModify && WidgetUtils.isManagedByManager(app)) {
const onEditClick = () => { const _onEditClick = () => {
if (onEditClick) {
onEditClick();
} else {
WidgetUtils.editWidget(room, app); WidgetUtils.editWidget(room, app);
}
onFinished(); onFinished();
}; };
editButton = <IconizedContextMenuOption onClick={onEditClick} label={_t("Edit")} />; editButton = <IconizedContextMenuOption onClick={_onEditClick} label={_t("Edit")} />;
} }
let snapshotButton; let snapshotButton;
@ -116,7 +123,10 @@ const WidgetContextMenu: React.FC<IProps> = ({
let deleteButton; let deleteButton;
if (onDeleteClick || canModify) { if (onDeleteClick || canModify) {
const onDeleteClickDefault = () => { const _onDeleteClick = () => {
if (onDeleteClick) {
onDeleteClick();
} else {
// Show delete confirmation dialog // Show delete confirmation dialog
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, { Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
title: _t("Delete Widget"), title: _t("Delete Widget"),
@ -129,11 +139,13 @@ const WidgetContextMenu: React.FC<IProps> = ({
WidgetUtils.setRoomWidget(roomId, app.id); WidgetUtils.setRoomWidget(roomId, app.id);
}, },
}); });
}
onFinished(); onFinished();
}; };
deleteButton = <IconizedContextMenuOption deleteButton = <IconizedContextMenuOption
onClick={onDeleteClick || onDeleteClickDefault} onClick={_onDeleteClick}
label={userWidget ? _t("Remove") : _t("Remove for everyone")} label={userWidget ? _t("Remove") : _t("Remove for everyone")}
/>; />;
} }

View file

@ -766,7 +766,7 @@ class VerificationExplorer extends React.PureComponent<IExplorerProps> {
render() { render() {
const cli = this.context; const cli = this.context;
const room = this.props.room; const room = this.props.room;
const inRoomChannel = cli._crypto._inRoomVerificationRequests; const inRoomChannel = cli.crypto._inRoomVerificationRequests;
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map(); const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
return (<div> return (<div>

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

View file

@ -15,5 +15,5 @@ limitations under the License.
*/ */
export interface IDialogProps { export interface IDialogProps {
onFinished: (bool) => void; onFinished(...args: any): void;
} }

View file

@ -159,7 +159,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
stickyBottom={false} stickyBottom={false}
startAtBottom={false} startAtBottom={false}
> >
<ul className="mx_MessageEditHistoryDialog_edits mx_MessagePanel_alwaysShowTimestamps">{this._renderEdits()}</ul> <ul className="mx_MessageEditHistoryDialog_edits">{this._renderEdits()}</ul>
</ScrollPanel>); </ScrollPanel>);
} }
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');

View file

@ -19,7 +19,7 @@ import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import AccessibleButton from "./AccessibleButton"; import AccessibleButton from "./AccessibleButton";
import Tooltip from './Tooltip'; import Tooltip, {Alignment} from './Tooltip';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> { interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
@ -28,6 +28,7 @@ interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
tooltipClassName?: string; tooltipClassName?: string;
forceHide?: boolean; forceHide?: boolean;
yOffset?: number; yOffset?: number;
alignment?: Alignment;
} }
interface IState { interface IState {
@ -66,13 +67,14 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
render() { render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const {title, tooltip, children, tooltipClassName, forceHide, yOffset, ...props} = this.props; const {title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, ...props} = this.props;
const tip = this.state.hover ? <Tooltip const tip = this.state.hover ? <Tooltip
className="mx_AccessibleTooltipButton_container" className="mx_AccessibleTooltipButton_container"
tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)} tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
label={tooltip || title} label={tooltip || title}
yOffset={yOffset} yOffset={yOffset}
alignment={alignment}
/> : null; /> : null;
return ( return (
<AccessibleButton <AccessibleButton

View file

@ -47,9 +47,14 @@ export default class AppTile extends React.Component {
// The key used for PersistedElement // The key used for PersistedElement
this._persistKey = getPersistKey(this.props.app.id); this._persistKey = getPersistKey(this.props.app.id);
try {
this._sgWidget = new StopGapWidget(this.props); this._sgWidget = new StopGapWidget(this.props);
this._sgWidget.on("preparing", this._onWidgetPrepared); this._sgWidget.on("preparing", this._onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady); this._sgWidget.on("ready", this._onWidgetReady);
} catch (e) {
console.log("Failed to construct widget", e);
this._sgWidget = null;
}
this.iframe = null; // ref to the iframe (callback style) this.iframe = null; // ref to the iframe (callback style)
this.state = this._getNewState(props); this.state = this._getNewState(props);
@ -97,7 +102,7 @@ export default class AppTile extends React.Component {
// Force the widget to be non-persistent (able to be deleted/forgotten) // Force the widget to be non-persistent (able to be deleted/forgotten)
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
PersistedElement.destroyElement(this._persistKey); PersistedElement.destroyElement(this._persistKey);
this._sgWidget.stop(); if (this._sgWidget) this._sgWidget.stop();
} }
this.setState({ hasPermissionToLoad }); this.setState({ hasPermissionToLoad });
@ -117,7 +122,7 @@ export default class AppTile extends React.Component {
componentDidMount() { componentDidMount() {
// Only fetch IM token on mount if we're showing and have permission to load // Only fetch IM token on mount if we're showing and have permission to load
if (this.state.hasPermissionToLoad) { if (this._sgWidget && this.state.hasPermissionToLoad) {
this._startWidget(); this._startWidget();
} }
@ -146,10 +151,15 @@ export default class AppTile extends React.Component {
if (this._sgWidget) { if (this._sgWidget) {
this._sgWidget.stop(); this._sgWidget.stop();
} }
try {
this._sgWidget = new StopGapWidget(newProps); this._sgWidget = new StopGapWidget(newProps);
this._sgWidget.on("preparing", this._onWidgetPrepared); this._sgWidget.on("preparing", this._onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady); this._sgWidget.on("ready", this._onWidgetReady);
this._startWidget(); this._startWidget();
} catch (e) {
console.log("Failed to construct widget", e);
this._sgWidget = null;
}
} }
_startWidget() { _startWidget() {
@ -161,7 +171,7 @@ export default class AppTile extends React.Component {
_iframeRefChange = (ref) => { _iframeRefChange = (ref) => {
this.iframe = ref; this.iframe = ref;
if (ref) { if (ref) {
this._sgWidget.start(ref); if (this._sgWidget) this._sgWidget.start(ref);
} else { } else {
this._resetWidget(this.props); this._resetWidget(this.props);
} }
@ -209,7 +219,7 @@ export default class AppTile extends React.Component {
// Delete the widget from the persisted store for good measure. // Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this._persistKey); PersistedElement.destroyElement(this._persistKey);
this._sgWidget.stop({forceDestroy: true}); if (this._sgWidget) this._sgWidget.stop({forceDestroy: true});
} }
_onWidgetPrepared = () => { _onWidgetPrepared = () => {
@ -340,7 +350,13 @@ export default class AppTile extends React.Component {
<Spinner message={_t("Loading...")} /> <Spinner message={_t("Loading...")} />
</div> </div>
); );
if (!this.state.hasPermissionToLoad) { if (this._sgWidget === null) {
appTileBody = (
<div className={appTileBodyClass} style={appTileBodyStyles}>
<AppWarning errorMsg={_t("Error loading Widget")} />
</div>
);
} else if (!this.state.hasPermissionToLoad) {
// only possible for room widgets, can assert this.props.room here // only possible for room widgets, can assert this.props.room here
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
appTileBody = ( appTileBody = (
@ -364,7 +380,7 @@ export default class AppTile extends React.Component {
if (this.isMixedContent()) { if (this.isMixedContent()) {
appTileBody = ( appTileBody = (
<div className={appTileBodyClass} style={appTileBodyStyles}> <div className={appTileBodyClass} style={appTileBodyStyles}>
<AppWarning errorMsg="Error - Mixed content" /> <AppWarning errorMsg={_t("Error - Mixed content")} />
</div> </div>
); );
} else { } else {
@ -417,6 +433,8 @@ export default class AppTile extends React.Component {
onFinished={this._closeContextMenu} onFinished={this._closeContextMenu}
showUnpin={!this.props.userWidget} showUnpin={!this.props.userWidget}
userWidget={this.props.userWidget} userWidget={this.props.userWidget}
onEditClick={this.props.onEditClick}
onDeleteClick={this.props.onDeleteClick}
/> />
); );
} }

View file

@ -63,9 +63,9 @@ const EventListSummary: React.FC<IProps> = ({
// If we are only given few events then just pass them through // If we are only given few events then just pass them through
if (events.length < threshold) { if (events.length < threshold) {
return ( return (
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}> <li className="mx_EventListSummary" data-scroll-tokens={eventIds}>
{ children } { children }
</div> </li>
); );
} }

View file

@ -95,8 +95,6 @@ export default class ImageView extends React.Component<IProps, IState> {
private initX = 0; private initX = 0;
private initY = 0; private initY = 0;
private lastX = 0;
private lastY = 0;
private previousX = 0; private previousX = 0;
private previousY = 0; private previousY = 0;
@ -105,23 +103,35 @@ export default class ImageView extends React.Component<IProps, IState> {
// needs to be passive in order to work with Chromium // needs to be passive in order to work with Chromium
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
// We want to recalculate zoom whenever the window's size changes // We want to recalculate zoom whenever the window's size changes
window.addEventListener("resize", this.calculateZoom); window.addEventListener("resize", this.recalculateZoom);
// After the image loads for the first time we want to calculate the zoom // After the image loads for the first time we want to calculate the zoom
this.image.current.addEventListener("load", this.calculateZoom); this.image.current.addEventListener("load", this.recalculateZoom);
} }
componentWillUnmount() { componentWillUnmount() {
this.focusLock.current.removeEventListener('wheel', this.onWheel); this.focusLock.current.removeEventListener('wheel', this.onWheel);
window.removeEventListener("resize", this.calculateZoom); window.removeEventListener("resize", this.recalculateZoom);
this.image.current.removeEventListener("load", this.calculateZoom); this.image.current.removeEventListener("load", this.recalculateZoom);
} }
private calculateZoom = () => { private recalculateZoom = () => {
this.setZoomAndRotation();
}
private setZoomAndRotation = (inputRotation?: number) => {
const image = this.image.current; const image = this.image.current;
const imageWrapper = this.imageWrapper.current; const imageWrapper = this.imageWrapper.current;
const zoomX = imageWrapper.clientWidth / image.naturalWidth; const rotation = inputRotation || this.state.rotation;
const zoomY = imageWrapper.clientHeight / image.naturalHeight;
const imageIsNotFlipped = rotation % 180 === 0;
// If the image is rotated take it into account
const width = imageIsNotFlipped ? image.naturalWidth : image.naturalHeight;
const height = imageIsNotFlipped ? image.naturalHeight : image.naturalWidth;
const zoomX = imageWrapper.clientWidth / width;
const zoomY = imageWrapper.clientHeight / height;
// If the image is smaller in both dimensions set its the zoom to 1 to // If the image is smaller in both dimensions set its the zoom to 1 to
// display it in its original size // display it in its original size
@ -130,6 +140,7 @@ export default class ImageView extends React.Component<IProps, IState> {
zoom: 1, zoom: 1,
minZoom: 1, minZoom: 1,
maxZoom: 1, maxZoom: 1,
rotation: rotation,
}); });
return; return;
} }
@ -138,10 +149,14 @@ export default class ImageView extends React.Component<IProps, IState> {
// image by default // image by default
const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE; const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE;
if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom}); // If zoom is smaller than minZoom don't go below that value
const zoom = (this.state.zoom <= this.state.minZoom) ? minZoom : this.state.zoom;
this.setState({ this.setState({
minZoom: minZoom, minZoom: minZoom,
maxZoom: 1, maxZoom: 1,
rotation: rotation,
zoom: zoom,
}); });
} }
@ -192,14 +207,12 @@ export default class ImageView extends React.Component<IProps, IState> {
private onRotateCounterClockwiseClick = () => { private onRotateCounterClockwiseClick = () => {
const cur = this.state.rotation; const cur = this.state.rotation;
const rotationDegrees = cur - 90; this.setZoomAndRotation(cur - 90);
this.setState({ rotation: rotationDegrees });
}; };
private onRotateClockwiseClick = () => { private onRotateClockwiseClick = () => {
const cur = this.state.rotation; const cur = this.state.rotation;
const rotationDegrees = cur + 90; this.setZoomAndRotation(cur + 90);
this.setState({ rotation: rotationDegrees });
}; };
private onDownloadClick = () => { private onDownloadClick = () => {
@ -253,8 +266,8 @@ export default class ImageView extends React.Component<IProps, IState> {
this.setState({ moving: true }); this.setState({ moving: true });
this.previousX = this.state.translationX; this.previousX = this.state.translationX;
this.previousY = this.state.translationY; this.previousY = this.state.translationY;
this.initX = ev.pageX - this.lastX; this.initX = ev.pageX - this.state.translationX;
this.initY = ev.pageY - this.lastY; this.initY = ev.pageY - this.state.translationY;
}; };
private onMoving = (ev: React.MouseEvent) => { private onMoving = (ev: React.MouseEvent) => {
@ -263,11 +276,9 @@ export default class ImageView extends React.Component<IProps, IState> {
if (!this.state.moving) return; if (!this.state.moving) return;
this.lastX = ev.pageX - this.initX;
this.lastY = ev.pageY - this.initY;
this.setState({ this.setState({
translationX: this.lastX, translationX: ev.pageX - this.initX,
translationY: this.lastY, translationY: ev.pageY - this.initY,
}); });
}; };
@ -283,6 +294,8 @@ export default class ImageView extends React.Component<IProps, IState> {
translationX: 0, translationX: 0,
translationY: 0, translationY: 0,
}); });
this.initX = 0;
this.initY = 0;
} }
this.setState({ moving: false }); this.setState({ moving: false });
}; };
@ -471,7 +484,12 @@ export default class ImageView extends React.Component<IProps, IState> {
</div> </div>
<div <div
className="mx_ImageView_image_wrapper" className="mx_ImageView_image_wrapper"
ref={this.imageWrapper}> ref={this.imageWrapper}
onMouseDown={this.props.onFinished}
onMouseMove={this.onMoving}
onMouseUp={this.onEndMoving}
onMouseLeave={this.onEndMoving}
>
<img <img
src={this.props.src} src={this.props.src}
title={this.props.name} title={this.props.name}
@ -480,9 +498,6 @@ export default class ImageView extends React.Component<IProps, IState> {
className="mx_ImageView_image" className="mx_ImageView_image"
draggable={true} draggable={true}
onMouseDown={this.onStartMoving} onMouseDown={this.onStartMoving}
onMouseMove={this.onMoving}
onMouseUp={this.onEndMoving}
onMouseLeave={this.onEndMoving}
/> />
</div> </div>
</FocusLock> </FocusLock>

View file

@ -46,6 +46,8 @@ export default class ReplyThread extends React.Component {
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired, permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
// Specifies which layout to use. // Specifies which layout to use.
layout: LayoutPropType, layout: LayoutPropType,
// Whether to always show a timestamp
alwaysShowTimestamps: PropTypes.bool,
}; };
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
@ -212,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)) { if (!ReplyThread.getParentEventId(parentEv)) {
return null; return null;
} }
@ -222,6 +224,7 @@ export default class ReplyThread extends React.Component {
ref={ref} ref={ref}
permalinkCreator={permalinkCreator} permalinkCreator={permalinkCreator}
layout={layout} layout={layout}
alwaysShowTimestamps={alwaysShowTimestamps}
/>; />;
} }
@ -386,6 +389,7 @@ export default class ReplyThread extends React.Component {
isRedacted={ev.isRedacted()} isRedacted={ev.isRedacted()}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
layout={this.props.layout} layout={this.props.layout}
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
enableFlair={SettingsStore.getValue(UIFeature.Flair)} enableFlair={SettingsStore.getValue(UIFeature.Flair)}
replacingEventId={ev.replacingEventId()} replacingEventId={ev.replacingEventId()}
/> />

View file

@ -84,19 +84,21 @@ export default class GroupHeaderButtons extends HeaderButtons {
}; };
renderButtons() { renderButtons() {
return [ return <>
<HeaderButton key="groupMembersButton" name="groupMembersButton" <HeaderButton
name="groupMembersButton"
title={_t('Members')} title={_t('Members')}
isHighlighted={this.isPhase(GROUP_PHASES)} isHighlighted={this.isPhase(GROUP_PHASES)}
onClick={this.onMembersClicked} onClick={this.onMembersClicked}
analytics={['Right Panel', 'Group Member List Button', 'click']} analytics={['Right Panel', 'Group Member List Button', 'click']}
/>, />
<HeaderButton key="roomsButton" name="roomsButton" <HeaderButton
name="roomsButton"
title={_t('Rooms')} title={_t('Rooms')}
isHighlighted={this.isPhase(ROOM_PHASES)} isHighlighted={this.isPhase(ROOM_PHASES)}
onClick={this.onRoomsClicked} onClick={this.onRoomsClicked}
analytics={['Right Panel', 'Group Room List Button', 'click']} analytics={['Right Panel', 'Group Room List Button', 'click']}
/>, />
]; </>;
} }
} }

View file

@ -29,8 +29,6 @@ interface IProps {
isHighlighted: boolean; isHighlighted: boolean;
// click handler // click handler
onClick: () => void; onClick: () => void;
// The badge to display above the icon
badge?: React.ReactNode;
// The parameters to track the click event // The parameters to track the click event
analytics: Parameters<typeof Analytics.trackEvent>; analytics: Parameters<typeof Analytics.trackEvent>;
@ -40,31 +38,29 @@ interface IProps {
title: string; title: string;
} }
// TODO: replace this, the composer buttons and the right panel buttons with a unified // TODO: replace this, the composer buttons and the right panel buttons with a unified representation
// representation
@replaceableComponent("views.right_panel.HeaderButton") @replaceableComponent("views.right_panel.HeaderButton")
export default class HeaderButton extends React.Component<IProps> { export default class HeaderButton extends React.Component<IProps> {
constructor(props: IProps) { private onClick = () => {
super(props);
this.onClick = this.onClick.bind(this);
}
private onClick() {
Analytics.trackEvent(...this.props.analytics); Analytics.trackEvent(...this.props.analytics);
this.props.onClick(); this.props.onClick();
} };
public render() { public render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {isHighlighted, onClick, analytics, name, title, ...props} = this.props;
const classes = classNames({ const classes = classNames({
mx_RightPanel_headerButton: true, mx_RightPanel_headerButton: true,
mx_RightPanel_headerButton_highlight: this.props.isHighlighted, mx_RightPanel_headerButton_highlight: isHighlighted,
[`mx_RightPanel_${this.props.name}`]: true, [`mx_RightPanel_${name}`]: true,
}); });
return <AccessibleTooltipButton return <AccessibleTooltipButton
aria-selected={this.props.isHighlighted} {...props}
aria-selected={isHighlighted}
role="tab" role="tab"
title={this.props.title} title={title}
className={classes} className={classes}
onClick={this.onClick} onClick={this.onClick}
/>; />;

View file

@ -27,7 +27,7 @@ import {
SetRightPanelPhasePayload, SetRightPanelPhasePayload,
SetRightPanelPhaseRefireParams, SetRightPanelPhaseRefireParams,
} from '../../../dispatcher/payloads/SetRightPanelPhasePayload'; } from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
import {EventSubscription} from "fbemitter"; import type { EventSubscription } from "fbemitter";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
export enum HeaderKind { export enum HeaderKind {
@ -43,11 +43,11 @@ interface IState {
interface IProps {} interface IProps {}
@replaceableComponent("views.right_panel.HeaderButtons") @replaceableComponent("views.right_panel.HeaderButtons")
export default abstract class HeaderButtons extends React.Component<IProps, IState> { export default abstract class HeaderButtons<P = {}> extends React.Component<IProps & P, IState> {
private storeToken: EventSubscription; private storeToken: EventSubscription;
private dispatcherRef: string; private dispatcherRef: string;
constructor(props: IProps, kind: HeaderKind) { constructor(props: IProps & P, kind: HeaderKind) {
super(props); super(props);
const rps = RightPanelStore.getSharedInstance(); const rps = RightPanelStore.getSharedInstance();
@ -95,7 +95,7 @@ export default abstract class HeaderButtons extends React.Component<IProps, ISta
} }
// XXX: Make renderButtons a prop // XXX: Make renderButtons a prop
public abstract renderButtons(): JSX.Element[]; public abstract renderButtons(): JSX.Element;
public render() { public render() {
return <div className="mx_HeaderButtons"> return <div className="mx_HeaderButtons">

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

View file

@ -18,7 +18,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import HeaderButton from './HeaderButton'; import HeaderButton from './HeaderButton';
import HeaderButtons, { HeaderKind } from './HeaderButtons'; import HeaderButtons, { HeaderKind } from './HeaderButtons';
@ -27,6 +29,8 @@ import {Action} from "../../../dispatcher/actions";
import { ActionPayload } from "../../../dispatcher/payloads"; import { ActionPayload } from "../../../dispatcher/payloads";
import RightPanelStore from "../../../stores/RightPanelStore"; import RightPanelStore from "../../../stores/RightPanelStore";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { useSettingValue } from "../../../hooks/useSettings";
import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard';
const ROOM_INFO_PHASES = [ const ROOM_INFO_PHASES = [
RightPanelPhases.RoomSummary, RightPanelPhases.RoomSummary,
@ -38,9 +42,35 @@ const ROOM_INFO_PHASES = [
RightPanelPhases.Room3pidMemberInfo, RightPanelPhases.Room3pidMemberInfo,
]; ];
const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => {
const pinningEnabled = useSettingValue("feature_pinning");
const pinnedEvents = usePinnedEvents(pinningEnabled && room);
const readPinnedEvents = useReadPinnedEvents(pinningEnabled && room);
if (!pinningEnabled) return null;
let unreadIndicator;
if (pinnedEvents.some(id => !readPinnedEvents.has(id))) {
unreadIndicator = <div className="mx_RightPanel_pinnedMessagesButton_unreadIndicator" />;
}
return <HeaderButton
name="pinnedMessagesButton"
title={_t("Pinned messages")}
isHighlighted={isHighlighted}
onClick={onClick}
analytics={["Right Panel", "Pinned Messages Button", "click"]}
>
{ unreadIndicator }
</HeaderButton>;
};
interface IProps {
room?: Room;
}
@replaceableComponent("views.right_panel.RoomHeaderButtons") @replaceableComponent("views.right_panel.RoomHeaderButtons")
export default class RoomHeaderButtons extends HeaderButtons { export default class RoomHeaderButtons extends HeaderButtons<IProps> {
constructor(props) { constructor(props: IProps) {
super(props, HeaderKind.Room); super(props, HeaderKind.Room);
} }
@ -80,24 +110,32 @@ export default class RoomHeaderButtons extends HeaderButtons {
this.setPhase(RightPanelPhases.NotificationPanel); this.setPhase(RightPanelPhases.NotificationPanel);
}; };
private onPinnedMessagesClicked = () => {
// This toggles for us, if needed
this.setPhase(RightPanelPhases.PinnedMessages);
};
public renderButtons() { public renderButtons() {
return [ return <>
<PinnedMessagesHeaderButton
room={this.props.room}
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
onClick={this.onPinnedMessagesClicked}
/>
<HeaderButton <HeaderButton
key="notifsButton"
name="notifsButton" name="notifsButton"
title={_t('Notifications')} title={_t('Notifications')}
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)} isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
onClick={this.onNotificationsClicked} onClick={this.onNotificationsClicked}
analytics={['Right Panel', 'Notification List Button', 'click']} analytics={['Right Panel', 'Notification List Button', 'click']}
/>, />
<HeaderButton <HeaderButton
key="roomSummaryButton"
name="roomSummaryButton" name="roomSummaryButton"
title={_t('Room Info')} title={_t('Room Info')}
isHighlighted={this.isPhase(ROOM_INFO_PHASES)} isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
onClick={this.onRoomSummaryClicked} onClick={this.onRoomSummaryClicked}
analytics={['Right Panel', 'Room Summary Button', 'click']} analytics={['Right Panel', 'Room Summary Button', 'click']}
/>, />
]; </>;
} }
} }

View file

@ -25,6 +25,7 @@ import {User} from 'matrix-js-sdk/src/models/user';
import { Room } from 'matrix-js-sdk/src/models/room'; import { Room } from 'matrix-js-sdk/src/models/room';
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
@ -514,9 +515,6 @@ export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
} else { } else {
setPowerLevels({}); setPowerLevels({});
} }
return () => {
setPowerLevels({});
};
}, [room]); }, [room]);
useEventEmitter(cli, "RoomState.events", update); useEventEmitter(cli, "RoomState.events", update);
@ -1530,21 +1528,16 @@ interface IProps {
user: Member; user: Member;
groupId?: string; groupId?: string;
room?: Room; room?: Room;
phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo | RightPanelPhases.SpaceMemberInfo; phase: RightPanelPhases.RoomMemberInfo
| RightPanelPhases.GroupMemberInfo
| RightPanelPhases.SpaceMemberInfo
| RightPanelPhases.EncryptionPanel;
onClose(): void; onClose(): void;
verificationRequest?: VerificationRequest;
verificationRequestPromise?: Promise<VerificationRequest>;
} }
interface IPropsWithEncryptionPanel extends React.ComponentProps<typeof EncryptionPanel> { const UserInfo: React.FC<IProps> = ({
user: Member;
groupId: void;
room: Room;
phase: RightPanelPhases.EncryptionPanel;
onClose(): void;
}
type Props = IProps | IPropsWithEncryptionPanel;
const UserInfo: React.FC<Props> = ({
user, user,
groupId, groupId,
room, room,

View file

@ -644,7 +644,7 @@ export default class EventTile extends React.Component<IProps, IState> {
// return early if there are no read receipts // return early if there are no read receipts
if (!this.props.readReceipts || this.props.readReceipts.length === 0) { if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
return (<span className="mx_EventTile_readAvatars" />); return null;
} }
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker'); 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()); permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
} }
// we can't use local echoes as scroll tokens, because their event IDs change.
// Local echos have a send "status".
const scrollToken = this.props.mxEvent.status
? undefined
: this.props.mxEvent.getId();
let avatar; let avatar;
let sender; let sender;
let avatarSize; let avatarSize;
@ -975,7 +981,8 @@ export default class EventTile extends React.Component<IProps, IState> {
onFocusChange={this.onActionBarFocusChange} onFocusChange={this.onActionBarFocusChange}
/> : undefined; /> : 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 ? const timestamp = showTimestamp ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null; <MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
@ -1046,7 +1053,7 @@ export default class EventTile extends React.Component<IProps, IState> {
case 'notif': { case 'notif': {
const room = this.context.getRoom(this.props.mxEvent.getRoomId()); const room = this.context.getRoom(this.props.mxEvent.getRoomId());
return ( 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"> <div className="mx_EventTile_roomName">
<RoomAvatar room={room} width={28} height={28} /> <RoomAvatar room={room} width={28} height={28} />
<a href={permalink} onClick={this.onPermalinkClicked}> <a href={permalink} onClick={this.onPermalinkClicked}>
@ -1069,12 +1076,12 @@ export default class EventTile extends React.Component<IProps, IState> {
onHeightChanged={this.props.onHeightChanged} onHeightChanged={this.props.onHeightChanged}
/> />
</div> </div>
</div> </li>
); );
} }
case 'file_grid': { case 'file_grid': {
return ( 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"> <div className="mx_EventTile_line">
<EventTileType ref={this.tile} <EventTileType ref={this.tile}
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
@ -1095,7 +1102,7 @@ export default class EventTile extends React.Component<IProps, IState> {
{ timestamp } { timestamp }
</div> </div>
</a> </a>
</div> </li>
); );
} }
@ -1108,10 +1115,12 @@ export default class EventTile extends React.Component<IProps, IState> {
this.props.onHeightChanged, this.props.onHeightChanged,
this.props.permalinkCreator, this.props.permalinkCreator,
this.replyThread, this.replyThread,
null,
this.props.alwaysShowTimestamps || this.state.hover,
); );
} }
return ( return (
<div className={classes} aria-live={ariaLive} aria-atomic="true"> <li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
{ ircTimestamp } { ircTimestamp }
{ avatar } { avatar }
{ sender } { sender }
@ -1129,7 +1138,7 @@ export default class EventTile extends React.Component<IProps, IState> {
showUrlPreview={false} showUrlPreview={false}
/> />
</div> </div>
</div> </li>
); );
} }
default: { default: {
@ -1139,17 +1148,18 @@ export default class EventTile extends React.Component<IProps, IState> {
this.props.permalinkCreator, this.props.permalinkCreator,
this.replyThread, this.replyThread,
this.props.layout, this.props.layout,
this.props.alwaysShowTimestamps || this.state.hover,
); );
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return ( return (
React.createElement(this.props.as || "div", { React.createElement(this.props.as || "li", {
"ref": this.ref, "ref": this.ref,
"className": classes, "className": classes,
"tabIndex": -1, "tabIndex": -1,
"aria-live": ariaLive, "aria-live": ariaLive,
"aria-atomic": "true", "aria-atomic": "true",
"data-scroll-tokens": this.props["data-scroll-tokens"], "data-scroll-tokens": scrollToken,
"onMouseEnter": () => this.setState({ hover: true }), "onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }), "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} />; tooltip = <Tooltip className="mx_EventTile_readAvatars_receiptTooltip" label={label} yOffset={20} />;
} }
return <span className="mx_EventTile_readAvatars"> return (
<div className="mx_EventTile_msgOption">
<span className="mx_EventTile_readAvatars">
<span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}> <span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
{nonCssBadge} {nonCssBadge}
{tooltip} {tooltip}
</span> </span>
</span>; </span>
</div>
);
} }
} }

View file

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

View file

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

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

View file

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

View file

@ -89,7 +89,7 @@ export default class ReplyPreview extends React.Component {
</div> </div>
<div className="mx_ReplyPreview_clear" /> <div className="mx_ReplyPreview_clear" />
<EventTile <EventTile
last={true} alwaysShowTimestamps={true}
tileShape="reply_preview" tileShape="reply_preview"
mxEvent={this.state.event} mxEvent={this.state.event}
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}

View file

@ -22,7 +22,6 @@ import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import RateLimitedFunc from '../../../ratelimitedfunc'; import RateLimitedFunc from '../../../ratelimitedfunc';
import {CancelButton} from './SimpleRoomHeader';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
import E2EIcon from './E2EIcon'; import E2EIcon from './E2EIcon';
@ -40,10 +39,8 @@ export default class RoomHeader extends React.Component {
oobData: PropTypes.object, oobData: PropTypes.object,
inRoom: PropTypes.bool, inRoom: PropTypes.bool,
onSettingsClick: PropTypes.func, onSettingsClick: PropTypes.func,
onPinnedClick: PropTypes.func,
onSearchClick: PropTypes.func, onSearchClick: PropTypes.func,
onLeaveClick: PropTypes.func, onLeaveClick: PropTypes.func,
onCancelClick: PropTypes.func,
e2eStatus: PropTypes.string, e2eStatus: PropTypes.string,
onAppsClick: PropTypes.func, onAppsClick: PropTypes.func,
appsShown: PropTypes.bool, appsShown: PropTypes.bool,
@ -53,20 +50,17 @@ export default class RoomHeader extends React.Component {
static defaultProps = { static defaultProps = {
editing: false, editing: false,
inRoom: false, inRoom: false,
onCancelClick: null,
}; };
componentDidMount() { componentDidMount() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this._onRoomStateEvents); cli.on("RoomState.events", this._onRoomStateEvents);
cli.on("Room.accountData", this._onRoomAccountData);
} }
componentWillUnmount() { componentWillUnmount() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (cli) { if (cli) {
cli.removeListener("RoomState.events", this._onRoomStateEvents); cli.removeListener("RoomState.events", this._onRoomStateEvents);
cli.removeListener("Room.accountData", this._onRoomAccountData);
} }
} }
@ -79,52 +73,13 @@ export default class RoomHeader extends React.Component {
this._rateLimitedUpdate(); this._rateLimitedUpdate();
}; };
_onRoomAccountData = (event, room) => {
if (!this.props.room || room.roomId !== this.props.room.roomId) return;
if (event.getType() !== "im.vector.room.read_pins") return;
this._rateLimitedUpdate();
};
_rateLimitedUpdate = new RateLimitedFunc(function() { _rateLimitedUpdate = new RateLimitedFunc(function() {
/* eslint-disable babel/no-invalid-this */ /* eslint-disable babel/no-invalid-this */
this.forceUpdate(); this.forceUpdate();
}, 500); }, 500);
_hasUnreadPins() {
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
if (!currentPinEvent) return false;
if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) {
return false; // no pins == nothing to read
}
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
if (readPinsEvent && readPinsEvent.getContent()) {
const readStateEvents = readPinsEvent.getContent().event_ids || [];
if (readStateEvents) {
return !readStateEvents.includes(currentPinEvent.getId());
}
}
// There's pins, and we haven't read any of them
return true;
}
_hasPins() {
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
if (!currentPinEvent) return false;
return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0);
}
render() { render() {
let searchStatus = null; let searchStatus = null;
let cancelButton = null;
let pinnedEventsButton = null;
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
}
// don't display the search count until the search completes and // don't display the search count until the search completes and
// gives us a valid (possibly zero) searchCount. // gives us a valid (possibly zero) searchCount.
@ -181,24 +136,6 @@ export default class RoomHeader extends React.Component {
/>; />;
} }
if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) {
let pinsIndicator = null;
if (this._hasUnreadPins()) {
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);
} else if (this._hasPins()) {
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator" />);
}
pinnedEventsButton =
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_pinnedButton"
onClick={this.props.onPinnedClick}
title={_t("Pinned Messages")}
>
{ pinsIndicator }
</AccessibleTooltipButton>;
}
let forgetButton; let forgetButton;
if (this.props.onForgetClick) { if (this.props.onForgetClick) {
forgetButton = forgetButton =
@ -248,7 +185,6 @@ export default class RoomHeader extends React.Component {
<div className="mx_RoomHeader_buttons"> <div className="mx_RoomHeader_buttons">
{ videoCallButton } { videoCallButton }
{ voiceCallButton } { voiceCallButton }
{ pinnedEventsButton }
{ forgetButton } { forgetButton }
{ appsButton } { appsButton }
{ searchButton } { searchButton }
@ -263,9 +199,8 @@ export default class RoomHeader extends React.Component {
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div> <div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
{ name } { name }
{ topicElement } { topicElement }
{ cancelButton }
{ rightRow } { rightRow }
<RoomHeaderButtons /> <RoomHeaderButtons room={this.props.room} />
</div> </div>
</div> </div>
); );

View file

@ -47,6 +47,7 @@ export default class SearchResultTile extends React.Component {
const ts1 = mxEv.getTs(); const ts1 = mxEv.getTs();
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />]; const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
const timeline = result.context.getTimeline(); const timeline = result.context.getTimeline();
for (let j = 0; j < timeline.length; j++) { for (let j = 0; j < timeline.length; j++) {
@ -67,6 +68,7 @@ export default class SearchResultTile extends React.Component {
highlightLink={this.props.resultLink} highlightLink={this.props.resultLink}
onHeightChanged={this.props.onHeightChanged} onHeightChanged={this.props.onHeightChanged}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
alwaysShowTimestamps={alwaysShowTimestamps}
enableFlair={SettingsStore.getValue(UIFeature.Flair)} enableFlair={SettingsStore.getValue(UIFeature.Flair)}
/> />
)); ));

View file

@ -16,23 +16,9 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import AccessibleButton from '../elements/AccessibleButton';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
// cancel button which is shared between room header and simple room header
export function CancelButton(props) {
const {onClick} = props;
return (
<AccessibleButton className='mx_RoomHeader_cancelButton' onClick={onClick}>
<img src={require("../../../../res/img/cancel.svg")} className='mx_filterFlipColor'
width="18" height="18" alt={_t("Cancel")} />
</AccessibleButton>
);
}
/* /*
* A stripped-down room header used for things like the user settings * A stripped-down room header used for things like the user settings
* and room directory. * and room directory.
@ -41,18 +27,13 @@ export function CancelButton(props) {
export default class SimpleRoomHeader extends React.Component { export default class SimpleRoomHeader extends React.Component {
static propTypes = { static propTypes = {
title: PropTypes.string, title: PropTypes.string,
onCancelClick: PropTypes.func,
// `src` to a TintableSvg. Optional. // `src` to a TintableSvg. Optional.
icon: PropTypes.string, icon: PropTypes.string,
}; };
render() { render() {
let cancelButton;
let icon; let icon;
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
}
if (this.props.icon) { if (this.props.icon) {
const TintableSvg = sdk.getComponent('elements.TintableSvg'); const TintableSvg = sdk.getComponent('elements.TintableSvg');
icon = <TintableSvg icon = <TintableSvg
@ -66,7 +47,6 @@ export default class SimpleRoomHeader extends React.Component {
<div className="mx_RoomHeader_simpleHeader"> <div className="mx_RoomHeader_simpleHeader">
{ icon } { icon }
{ this.props.title } { this.props.title }
{ cancelButton }
</div> </div>
</div> </div>
); );

View file

@ -367,7 +367,7 @@ export default class Stickerpicker extends React.PureComponent {
/** /**
* Launch the integration manager on the stickers integration page * Launch the integration manager on the stickers integration page
*/ */
_launchManageIntegrations() { _launchManageIntegrations = () => {
// TODO: Open the right integration manager for the widget // TODO: Open the right integration manager for the widget
if (SettingsStore.getValue("feature_many_integration_managers")) { if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll( IntegrationManagers.sharedInstance().openAll(
@ -382,7 +382,7 @@ export default class Stickerpicker extends React.PureComponent {
this.state.widgetId, this.state.widgetId,
); );
} }
} };
render() { render() {
let stickerPicker; let stickerPicker;
@ -401,7 +401,7 @@ export default class Stickerpicker extends React.PureComponent {
key="controls_hide_stickers" key="controls_hide_stickers"
className={className} className={className}
onClick={this._onHideStickersClick} onClick={this._onHideStickersClick}
active={this.state.showStickers} active={this.state.showStickers.toString()}
title={_t("Hide Stickers")} title={_t("Hide Stickers")}
> >
</AccessibleButton>; </AccessibleButton>;

View file

@ -79,8 +79,8 @@ export default class CrossSigningPanel extends React.PureComponent {
async _getUpdatedStatus() { async _getUpdatedStatus() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const pkCache = cli.getCrossSigningCacheCallbacks(); const pkCache = cli.getCrossSigningCacheCallbacks();
const crossSigning = cli._crypto._crossSigningInfo; const crossSigning = cli.crypto._crossSigningInfo;
const secretStorage = cli._crypto._secretStorage; const secretStorage = cli.crypto._secretStorage;
const crossSigningPublicKeysOnDevice = crossSigning.getId(); const crossSigningPublicKeysOnDevice = crossSigning.getId();
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage); const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master")); const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));

View file

@ -131,10 +131,10 @@ export default class SecureBackupPanel extends React.PureComponent {
async _getUpdatedDiagnostics() { async _getUpdatedDiagnostics() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const secretStorage = cli._crypto._secretStorage; const secretStorage = cli.crypto._secretStorage;
const backupKeyStored = !!(await cli.isKeyBackupKeyStored()); const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
const backupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey(); const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey();
const backupKeyCached = !!(backupKeyFromCache); const backupKeyCached = !!(backupKeyFromCache);
const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array; const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array;
const secretStorageKeyInAccount = await secretStorage.hasKey(); const secretStorageKeyInAccount = await secretStorage.hasKey();

View file

@ -15,17 +15,14 @@ limitations under the License.
*/ */
import * as React from "react"; import * as React from "react";
import { ensureDMExists } from "../../../createRoom";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import Field from "../elements/Field"; import Field from "../elements/Field";
import DialPad from './DialPad'; import DialPad from './DialPad';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import Modal from "../../../Modal";
import ErrorDialog from "../../views/dialogs/ErrorDialog";
import CallHandler from "../../../CallHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import { DialNumberPayload } from "../../../dispatcher/payloads/DialNumberPayload";
import { Action } from "../../../dispatcher/actions";
interface IProps { interface IProps {
onFinished: (boolean) => void; onFinished: (boolean) => void;
@ -67,21 +64,11 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
} }
onDialPress = async () => { onDialPress = async () => {
const results = await CallHandler.sharedInstance().pstnLookup(this.state.value); const payload: DialNumberPayload = {
if (!results || results.length === 0 || !results[0].userid) { action: Action.DialNumber,
Modal.createTrackedDialog('', '', ErrorDialog, { number: this.state.value,
title: _t("Unable to look up phone number"), };
description: _t("There was an error looking up the phone number"), dis.dispatch(payload);
});
}
const userId = results[0].userid;
const roomId = await ensureDMExists(MatrixClientPeg.get(), userId);
dis.dispatch({
action: 'view_room',
room_id: roomId,
});
this.props.onFinished(true); this.props.onFinished(true);
} }

View file

@ -31,7 +31,6 @@ const RoomContext = createContext<IState>({
canPeek: false, canPeek: false,
showApps: false, showApps: false,
isPeeking: false, isPeeking: false,
showingPinned: false,
showReadReceipts: true, showReadReceipts: true,
showRightPanel: true, showRightPanel: true,
joining: false, joining: false,

View file

@ -100,6 +100,12 @@ export enum Action {
*/ */
OpenDialPad = "open_dial_pad", 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 * Fired when CallHandler has checked for PSTN protocol support
* payload: none * payload: none

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,24 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_PinnedEventsPanel { import { ActionPayload } from "../payloads";
border-top: 1px solid $primary-hairline-color; import { Action } from "../actions";
}
.mx_PinnedEventsPanel_body { export interface DialNumberPayload extends ActionPayload {
max-height: 300px; action: Action.DialNumber;
overflow-y: auto; number: string;
padding-bottom: 15px;
}
.mx_PinnedEventsPanel_header {
margin: 0;
padding-top: 8px;
padding-bottom: 15px;
}
.mx_PinnedEventsPanel_cancel {
margin: 12px;
float: right;
display: inline-block;
} }

View file

@ -21,7 +21,15 @@ type Fn<T> = () => Promise<T>;
export const useAsyncMemo = <T>(fn: Fn<T>, deps: DependencyList, initialValue?: T): T => { export const useAsyncMemo = <T>(fn: Fn<T>, deps: DependencyList, initialValue?: T): T => {
const [value, setValue] = useState<T>(initialValue); const [value, setValue] = useState<T>(initialValue);
useEffect(() => { 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 }, deps); // eslint-disable-line react-hooks/exhaustive-deps
return value; return value;
}; };

View file

@ -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 // Hook to fetch whether a feature is enabled and dynamically update when that changes
export const useFeatureEnabled = (featureName: string, roomId: string = null) => { export const useFeatureEnabled = (featureName: string, roomId: string = null): boolean => {
const [enabled, setEnabled] = useState(SettingsStore.getValue(featureName, roomId)); const [enabled, setEnabled] = useState(SettingsStore.getValue<boolean>(featureName, roomId));
useEffect(() => { useEffect(() => {
const ref = SettingsStore.watchSetting(featureName, roomId, () => { const ref = SettingsStore.watchSetting(featureName, roomId, () => {

View file

@ -63,6 +63,8 @@
"Already in call": "Already in call", "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'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.", "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", "Call in Progress": "Call in Progress",
"A call is currently being placed!": "A call is currently being placed!", "A call is currently being placed!": "A call is currently being placed!",
"Permission Required": "Permission Required", "Permission Required": "Permission Required",
@ -898,8 +900,6 @@
"Fill Screen": "Fill Screen", "Fill Screen": "Fill Screen",
"Return to call": "Return to call", "Return to call": "Return to call",
"%(name)s on hold": "%(name)s on hold", "%(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", "Dial pad": "Dial pad",
"Unknown caller": "Unknown caller", "Unknown caller": "Unknown caller",
"Incoming voice call": "Incoming voice call", "Incoming voice call": "Incoming voice call",
@ -1473,7 +1473,6 @@
"Encrypting your message...": "Encrypting your message...", "Encrypting your message...": "Encrypting your message...",
"Your message was sent": "Your message was sent", "Your message was sent": "Your message was sent",
"Failed to send": "Failed to send", "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", "Scroll to most recent messages": "Scroll to most recent messages",
"Close preview": "Close preview", "Close preview": "Close preview",
"and %(count)s others...|other": "and %(count)s others...", "and %(count)s others...|other": "and %(count)s others...",
@ -1510,11 +1509,8 @@
"Invite to just this room": "Invite to just this room", "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.", "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/>.", "This is the start of <roomName/>.": "This is the start of <roomName/>.",
"No pinned messages.": "No pinned messages.", "Unpin": "Unpin",
"Loading...": "Loading...", "View message": "View message",
"Pinned Messages": "Pinned Messages",
"Unpin Message": "Unpin Message",
"Jump to message": "Jump to message",
"%(duration)ss": "%(duration)ss", "%(duration)ss": "%(duration)ss",
"%(duration)sm": "%(duration)sm", "%(duration)sm": "%(duration)sm",
"%(duration)sh": "%(duration)sh", "%(duration)sh": "%(duration)sh",
@ -1720,9 +1716,11 @@
"The homeserver the user youre verifying is connected to": "The homeserver the user youre verifying is connected to", "The homeserver the user youre verifying is connected to": "The homeserver the user youre verifying is connected to",
"Yours, or the other users internet connection": "Yours, or the other users internet connection", "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", "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", "Room Info": "Room Info",
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", "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", "Unpin a widget to view it in this panel": "Unpin a widget to view it in this panel",
"Options": "Options", "Options": "Options",
"Set my room layout for everyone": "Set my room layout for everyone", "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", "Add rooms to this community": "Add rooms to this community",
"Filter community rooms": "Filter community rooms", "Filter community rooms": "Filter community rooms",
"Something went wrong when trying to get your communities.": "Something went wrong when trying to get your communities.", "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.", "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.", "You're not currently a member of any communities.": "You're not currently a member of any communities.",
"Frequently Used": "Frequently Used", "Frequently Used": "Frequently Used",
@ -1926,6 +1925,8 @@
"Widgets do not use message encryption.": "Widgets do not use message encryption.", "Widgets do not use message encryption.": "Widgets do not use message encryption.",
"Widget added by": "Widget added by", "Widget added by": "Widget added by",
"This widget may use cookies.": "This widget may use cookies.", "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", "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 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", "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", "Rotate Right": "Rotate Right",
"Download": "Download", "Download": "Download",
"Information": "Information", "Information": "Information",
"View message": "View message",
"Language Dropdown": "Language Dropdown", "Language Dropdown": "Language Dropdown",
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
"%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times", "%(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.", "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", "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>.", "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", "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.", "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", "Abort": "Abort",
@ -2469,6 +2476,7 @@
"Unable to reject invite": "Unable to reject invite", "Unable to reject invite": "Unable to reject invite",
"Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)", "Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)",
"Forward Message": "Forward Message", "Forward Message": "Forward Message",
"Unpin Message": "Unpin Message",
"Pin Message": "Pin Message", "Pin Message": "Pin Message",
"Unhide Preview": "Unhide Preview", "Unhide Preview": "Unhide Preview",
"Share Permalink": "Share Permalink", "Share Permalink": "Share Permalink",
@ -2667,7 +2675,6 @@
"Some of your messages have not been sent": "Some of your messages have not been sent", "Some of your messages have not been sent": "Some of your messages have not been sent",
"Delete all": "Delete all", "Delete all": "Delete all",
"Retry all": "Retry 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", "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.", "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.", "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", "A private space to organise your rooms": "A private space to organise your rooms",
"Me and my teammates": "Me and my teammates", "Me and my teammates": "Me and my teammates",
"A private space for you and your teammates": "A private space for you and your 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", "Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s",
"Inviting...": "Inviting...", "Inviting...": "Inviting...",
"Invite your teammates": "Invite your teammates", "Invite your teammates": "Invite your teammates",

View file

@ -35,7 +35,7 @@ export interface MatrixProfile {
export interface CrawlerCheckpoint { export interface CrawlerCheckpoint {
roomId: string; roomId: string;
token: string; token: string;
fullCrawl: boolean; fullCrawl?: boolean;
direction: string; direction: string;
} }
@ -73,14 +73,14 @@ export interface EventAndProfile {
export interface LoadArgs { export interface LoadArgs {
roomId: string; roomId: string;
limit: number; limit: number;
fromEvent: string; fromEvent?: string;
direction: string; direction?: string;
} }
export interface IndexStats { export interface IndexStats {
size: number; size: number;
event_count: number; eventCount: number;
room_count: number; roomCount: number;
} }
/** /**

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,33 +14,42 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import PlatformPeg from "../PlatformPeg"; import { EventEmitter } from "events";
import {MatrixClientPeg} from "../MatrixClientPeg";
import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; 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 { sleep } from "../utils/promise"; import { sleep } from "../utils/promise";
import SettingsStore from "../settings/SettingsStore"; 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. * Event indexing class that wraps the platform specific event indexing.
*/ */
export default class EventIndex extends EventEmitter { export default class EventIndex extends EventEmitter {
constructor() { private crawlerCheckpoints: CrawlerCheckpoint[] = [];
super(); private crawler: ICrawler = null;
this.crawlerCheckpoints = []; private currentCheckpoint: CrawlerCheckpoint = null;
// 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;
}
async init() { public async init() {
const indexManager = PlatformPeg.get().getEventIndexingManager(); const indexManager = PlatformPeg.get().getEventIndexingManager();
this.crawlerCheckpoints = await indexManager.loadCheckpoints(); 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. * Register event listeners that are necessary for the event index to work.
*/ */
registerListeners() { public registerListeners() {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
client.on('sync', this.onSync); client.on('sync', this.onSync);
@ -66,7 +75,7 @@ export default class EventIndex extends EventEmitter {
/** /**
* Remove the event index specific event listeners. * Remove the event index specific event listeners.
*/ */
removeListeners() { public removeListeners() {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (client === null) return; 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. * Get crawler checkpoints for the encrypted rooms and store them in the index.
*/ */
async addInitialCheckpoints() { public async addInitialCheckpoints() {
const indexManager = PlatformPeg.get().getEventIndexingManager(); const indexManager = PlatformPeg.get().getEventIndexingManager();
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const rooms = client.getRooms(); const rooms = client.getRooms();
@ -102,14 +111,14 @@ export default class EventIndex extends EventEmitter {
const timeline = room.getLiveTimeline(); const timeline = room.getLiveTimeline();
const token = timeline.getPaginationToken("b"); const token = timeline.getPaginationToken("b");
const backCheckpoint = { const backCheckpoint: CrawlerCheckpoint = {
roomId: room.roomId, roomId: room.roomId,
token: token, token: token,
direction: "b", direction: "b",
fullCrawl: true, fullCrawl: true,
}; };
const forwardCheckpoint = { const forwardCheckpoint: CrawlerCheckpoint = {
roomId: room.roomId, roomId: room.roomId,
token: token, token: token,
direction: "f", 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 * - Every other sync, tell the event index to commit all the queued up
* live events * live events
*/ */
onSync = async (state, prevState, data) => { private onSync = async (state: string, prevState: string, data: object) => {
const indexManager = PlatformPeg.get().getEventIndexingManager(); const indexManager = PlatformPeg.get().getEventIndexingManager();
if (prevState === "PREPARED" && state === "SYNCING") { 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 * otherwise we save their event id and wait for them in the Event.decrypted
* listener. * 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(); const client = MatrixClientPeg.get();
// We only index encrypted rooms locally. // We only index encrypted rooms locally.
@ -194,7 +211,7 @@ export default class EventIndex extends EventEmitter {
await this.addLiveEventToIndex(ev); await this.addLiveEventToIndex(ev);
} }
onRoomStateEvent = async (ev, state) => { private onRoomStateEvent = async (ev: MatrixEvent, state: RoomState) => {
if (!MatrixClientPeg.get().isRoomEncrypted(state.roomId)) return; if (!MatrixClientPeg.get().isRoomEncrypted(state.roomId)) return;
if (ev.getType() === "m.room.encryption" && !await this.isRoomIndexed(state.roomId)) { 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 * Checks if the event was marked for addition in the Room.timeline
* listener, if so queues it up to be added to the index. * 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 the event isn't in our live event set, ignore it.
if (err) return; if (err) return;
await this.addLiveEventToIndex(ev); await this.addLiveEventToIndex(ev);
@ -220,7 +237,7 @@ export default class EventIndex extends EventEmitter {
* *
* Removes a redacted event from our event index. * 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. // We only index encrypted rooms locally.
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return; if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) return;
const indexManager = PlatformPeg.get().getEventIndexingManager(); 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 * Listens for timeline resets that are caused by a limited timeline to
* re-add checkpoints for rooms that need to be crawled again. * 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 (room === null) return;
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) 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 * @returns {bool} Returns true if the event can be indexed, false
* otherwise. * otherwise.
*/ */
isValidEvent(ev) { private isValidEvent(ev: MatrixEvent) {
const isUsefulType = ["m.room.message", "m.room.name", "m.room.topic"].includes(ev.getType()); const isUsefulType = ["m.room.message", "m.room.name", "m.room.topic"].includes(ev.getType());
const validEventType = isUsefulType && !ev.isRedacted() && !ev.isDecryptionFailure(); const validEventType = isUsefulType && !ev.isRedacted() && !ev.isDecryptionFailure();
@ -282,7 +299,7 @@ export default class EventIndex extends EventEmitter {
return validEventType && validMsgType && hasContentValue; return validEventType && validMsgType && hasContentValue;
} }
eventToJson(ev) { private eventToJson(ev: MatrixEvent) {
const jsonEvent = ev.toJSON(); const jsonEvent = ev.toJSON();
const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent; 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. * @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(); const indexManager = PlatformPeg.get().getEventIndexingManager();
if (!this.isValidEvent(ev)) return; 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 * Emmit that the crawler has changed the checkpoint that it's currently
* handling. * handling.
*/ */
emitNewCheckpoint() { private emitNewCheckpoint() {
this.emit("changedCheckpoint", this.currentRoom()); this.emit("changedCheckpoint", this.currentRoom());
} }
async addEventsFromLiveTimeline(timeline) { private async addEventsFromLiveTimeline(timeline: EventTimeline) {
const events = timeline.getEvents(); const events = timeline.getEvents();
for (let i = 0; i < events.length; i++) { 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 indexManager = PlatformPeg.get().getEventIndexingManager();
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(roomId); 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 * crawl, otherwise create a new checkpoint and push it to the
* crawlerCheckpoints queue so we go through them in a round-robin way. * crawlerCheckpoints queue so we go through them in a round-robin way.
*/ */
async crawlerFunc() { private async crawlerFunc() {
let cancelled = false; let cancelled = false;
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const indexManager = PlatformPeg.get().getEventIndexingManager(); const indexManager = PlatformPeg.get().getEventIndexingManager();
this._crawler = {}; this.crawler = {
cancel: () => {
this._crawler.cancel = () => {
cancelled = true; cancelled = true;
},
}; };
let idle = false; let idle = false;
@ -417,11 +434,11 @@ export default class EventIndex extends EventEmitter {
sleepTime = Math.max(sleepTime, 100); sleepTime = Math.max(sleepTime, 100);
if (idle) { if (idle) {
sleepTime = this._crawlerIdleTime; sleepTime = CRAWLER_IDLE_TIME;
} }
if (this._currentCheckpoint !== null) { if (this.currentCheckpoint !== null) {
this._currentCheckpoint = null; this.currentCheckpoint = null;
this.emitNewCheckpoint(); this.emitNewCheckpoint();
} }
@ -440,7 +457,7 @@ export default class EventIndex extends EventEmitter {
continue; continue;
} }
this._currentCheckpoint = checkpoint; this.currentCheckpoint = checkpoint;
this.emitNewCheckpoint(); this.emitNewCheckpoint();
idle = false; idle = false;
@ -453,9 +470,12 @@ export default class EventIndex extends EventEmitter {
let res; let res;
try { try {
res = await client._createMessagesRequest( res = await client.createMessagesRequest(
checkpoint.roomId, checkpoint.token, this._eventsPerCrawl, checkpoint.roomId,
checkpoint.direction); checkpoint.token,
EVENTS_PER_CRAWL,
checkpoint.direction,
);
} catch (e) { } catch (e) {
if (e.httpStatus === 403) { if (e.httpStatus === 403) {
console.log("EventIndex: Removing checkpoint as we don't have ", 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. * Start the crawler background task.
*/ */
startCrawler() { public startCrawler() {
if (this._crawler !== null) return; if (this.crawler !== null) return;
this.crawlerFunc(); this.crawlerFunc();
} }
/** /**
* Stop the crawler background task. * Stop the crawler background task.
*/ */
stopCrawler() { public stopCrawler() {
if (this._crawler === null) return; if (this.crawler === null) return;
this._crawler.cancel(); this.crawler.cancel();
} }
/** /**
@ -637,7 +657,7 @@ export default class EventIndex extends EventEmitter {
* This removes all the MatrixClient event listeners, stops the crawler * This removes all the MatrixClient event listeners, stops the crawler
* task, and closes the index. * task, and closes the index.
*/ */
async close() { public async close() {
const indexManager = PlatformPeg.get().getEventIndexingManager(); const indexManager = PlatformPeg.get().getEventIndexingManager();
this.removeListeners(); this.removeListeners();
this.stopCrawler(); this.stopCrawler();
@ -654,7 +674,7 @@ export default class EventIndex extends EventEmitter {
* @return {Promise<[SearchResult]>} A promise that will resolve to an array * @return {Promise<[SearchResult]>} A promise that will resolve to an array
* of search results once the search is done. * of search results once the search is done.
*/ */
async search(searchArgs) { public async search(searchArgs: SearchArgs) {
const indexManager = PlatformPeg.get().getEventIndexingManager(); const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.searchEventIndex(searchArgs); return indexManager.searchEventIndex(searchArgs);
} }
@ -680,11 +700,16 @@ export default class EventIndex extends EventEmitter {
* @returns {Promise<MatrixEvent[]>} Resolves to an array of events that * @returns {Promise<MatrixEvent[]>} Resolves to an array of events that
* contain URLs. * 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 client = MatrixClientPeg.get();
const indexManager = PlatformPeg.get().getEventIndexingManager(); const indexManager = PlatformPeg.get().getEventIndexingManager();
const loadArgs = { const loadArgs: LoadArgs = {
roomId: room.roomId, roomId: room.roomId,
limit: limit, limit: limit,
}; };
@ -772,13 +797,13 @@ export default class EventIndex extends EventEmitter {
* @returns {Promise<boolean>} Resolves to true if events were added to the * @returns {Promise<boolean>} Resolves to true if events were added to the
* timeline, false otherwise. * timeline, false otherwise.
*/ */
async populateFileTimeline( public async populateFileTimeline(
timelineSet, timelineSet: EventTimelineSet,
timeline, timeline: EventTimeline,
room, room: Room,
limit = 10, limit = 10,
fromEvent = null, fromEvent: string = null,
direction = EventTimeline.BACKWARDS, direction: string = EventTimeline.BACKWARDS,
) { ) {
const matrixEvents = await this.loadFileEvents(room, limit, fromEvent, direction); 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 * @returns {Promise<boolean>} Resolves to a boolean which is true if more
* events were successfully retrieved. * events were successfully retrieved.
*/ */
paginateTimelineWindow(room, timelineWindow, direction, limit) { public paginateTimelineWindow(room: Room, timelineWindow: TimelineWindow, direction: string, limit: number) {
const tl = timelineWindow.getTimelineIndex(direction); const tl = timelineWindow.getTimelineIndex(direction);
if (!tl) return Promise.resolve(false); 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 * @return {Promise<IndexStats>} A promise that will resolve to the index
* statistics. * statistics.
*/ */
async getStats() { public async getStats() {
const indexManager = PlatformPeg.get().getEventIndexingManager(); const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.getStats(); return indexManager.getStats();
} }
@ -885,7 +910,7 @@ export default class EventIndex extends EventEmitter {
* @return {Promise<boolean>} Returns true if the index contains events for * @return {Promise<boolean>} Returns true if the index contains events for
* the given room, false otherwise. * the given room, false otherwise.
*/ */
async isRoomIndexed(roomId) { public async isRoomIndexed(roomId) {
const indexManager = PlatformPeg.get().getEventIndexingManager(); const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.isRoomIndexed(roomId); return indexManager.isRoomIndexed(roomId);
} }
@ -896,21 +921,21 @@ export default class EventIndex extends EventEmitter {
* @returns {Room} A MatrixRoom that is being currently crawled, null * @returns {Room} A MatrixRoom that is being currently crawled, null
* if no room is currently being crawled. * if no room is currently being crawled.
*/ */
currentRoom() { public currentRoom() {
if (this._currentCheckpoint === null && this.crawlerCheckpoints.length === 0) { if (this.currentCheckpoint === null && this.crawlerCheckpoints.length === 0) {
return null; return null;
} }
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (this._currentCheckpoint !== null) { if (this.currentCheckpoint !== null) {
return client.getRoom(this._currentCheckpoint.roomId); return client.getRoom(this.currentCheckpoint.roomId);
} else { } else {
return client.getRoom(this.crawlerCheckpoints[0].roomId); return client.getRoom(this.crawlerCheckpoints[0].roomId);
} }
} }
crawlingRooms() { public crawlingRooms() {
const totalRooms = new Set(); const totalRooms = new Set();
const crawlingRooms = new Set(); const crawlingRooms = new Set();
@ -918,14 +943,14 @@ export default class EventIndex extends EventEmitter {
crawlingRooms.add(checkpoint.roomId); crawlingRooms.add(checkpoint.roomId);
}); });
if (this._currentCheckpoint !== null) { if (this.currentCheckpoint !== null) {
crawlingRooms.add(this._currentCheckpoint.roomId); crawlingRooms.add(this.currentCheckpoint.roomId);
} }
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const rooms = client.getRooms(); const rooms = client.getRooms();
const isRoomEncrypted = (room) => { const isRoomEncrypted = (room: Room) => {
return client.isRoomEncrypted(room.roomId); return client.isRoomEncrypted(room.roomId);
}; };

View file

@ -92,8 +92,8 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) {
body.append('cross_signing_key', client.getCrossSigningId()); body.append('cross_signing_key', client.getCrossSigningId());
// add cross-signing status information // add cross-signing status information
const crossSigning = client._crypto._crossSigningInfo; const crossSigning = client.crypto._crossSigningInfo;
const secretStorage = client._crypto._secretStorage; const secretStorage = client.crypto._secretStorage;
body.append("cross_signing_ready", String(await client.isCrossSigningReady())); body.append("cross_signing_ready", String(await client.isCrossSigningReady()));
body.append("cross_signing_supported_by_hs", 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("secret_storage_key_in_account", String(!!(await secretStorage.hasKey())));
body.append("session_backup_key_in_secret_storage", String(!!(await client.isKeyBackupKeyStored()))); 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_cached", String(!!sessionBackupKeyFromCache));
body.append("session_backup_key_well_formed", String(sessionBackupKeyFromCache instanceof Uint8Array)); body.append("session_backup_key_well_formed", String(sessionBackupKeyFromCache instanceof Uint8Array));
} }

View file

@ -601,10 +601,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
displayName: _td('Enable widget screenshots on supported widgets'), displayName: _td('Enable widget screenshots on supported widgets'),
default: false, default: false,
}, },
"PinnedEvents.isOpen": {
supportedLevels: [SettingLevel.ROOM_DEVICE],
default: false,
},
"promptBeforeInviteUnknownUsers": { "promptBeforeInviteUnknownUsers": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS, supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Prompt before sending invites to potentially invalid matrix IDs'), displayName: _td('Prompt before sending invites to potentially invalid matrix IDs'),

View file

@ -63,8 +63,7 @@ export class WatchManager {
if (!inRoomId) { if (!inRoomId) {
// Fire updates to all the individual room watchers too, as they probably care about the change higher up. // 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(...Array.from(roomWatchers.values()).flat(1));
callbacks.push(...callbacks);
} else if (roomWatchers.has(IRRELEVANT_ROOM)) { } else if (roomWatchers.has(IRRELEVANT_ROOM)) {
callbacks.push(...roomWatchers.get(IRRELEVANT_ROOM)); callbacks.push(...roomWatchers.get(IRRELEVANT_ROOM));
} }

View file

@ -126,7 +126,7 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
if (membership === EffectiveMembership.Invite) { if (membership === EffectiveMembership.Invite) {
try { try {
const path = utils.encodeUri("/rooms/$roomId/group_info", {$roomId: room.roomId}); 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, "GET", path,
undefined, undefined, undefined, undefined,
{prefix: "/_matrix/client/unstable/im.vector.custom"}); {prefix: "/_matrix/client/unstable/im.vector.custom"});

View file

@ -1,7 +1,5 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017-2021 The Matrix.org Foundation C.I.C.
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -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 See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import dis from '../dispatcher/dispatcher';
import { Store } from 'flux/utils'; import { Store } from 'flux/utils';
import dis from '../dispatcher/dispatcher';
import { ActionPayload } from "../dispatcher/payloads";
interface IState {
deferredAction: any;
}
const INITIAL_STATE = { 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 * store that listens for actions and updates its state accordingly, informing any
* listeners (views) of state changes. * listeners (views) of state changes.
*/ */
class LifecycleStore extends Store { class LifecycleStore extends Store<ActionPayload> {
private state: IState = INITIAL_STATE;
constructor() { constructor() {
super(dis); super(dis);
// Initialise state
this._state = INITIAL_STATE;
} }
_setState(newState) { private setState(newState: Partial<IState>) {
this._state = Object.assign(this._state, newState); this.state = Object.assign(this.state, newState);
this.__emitChange(); this.__emitChange();
} }
__onDispatch(payload) { protected __onDispatch(payload: ActionPayload) {
switch (payload.action) { switch (payload.action) {
case 'do_after_sync_prepared': case 'do_after_sync_prepared':
this._setState({ this.setState({
deferred_action: payload.deferred_action, deferredAction: payload.deferred_action,
}); });
break; break;
case 'cancel_after_sync_prepared': case 'cancel_after_sync_prepared':
this._setState({ this.setState({
deferred_action: null, deferredAction: null,
}); });
break; break;
case 'sync_state': { case 'syncstate': {
if (payload.state !== 'PREPARED') { if (payload.state !== 'PREPARED') {
break; break;
} }
if (!this._state.deferred_action) break; if (!this.state.deferredAction) break;
const deferredAction = Object.assign({}, this._state.deferred_action); const deferredAction = Object.assign({}, this.state.deferredAction);
this._setState({ this.setState({
deferred_action: null, deferredAction: null,
}); });
dis.dispatch(deferredAction); dis.dispatch(deferredAction);
break; break;
@ -71,8 +75,8 @@ class LifecycleStore extends Store {
} }
} }
reset() { private reset() {
this._state = Object.assign({}, INITIAL_STATE); this.state = Object.assign({}, INITIAL_STATE);
} }
} }

View file

@ -24,6 +24,7 @@ export enum RightPanelPhases {
EncryptionPanel = 'EncryptionPanel', EncryptionPanel = 'EncryptionPanel',
RoomSummary = 'RoomSummary', RoomSummary = 'RoomSummary',
Widget = 'Widget', Widget = 'Widget',
PinnedMessages = "PinnedMessages",
Room3pidMemberInfo = 'Room3pidMemberInfo', Room3pidMemberInfo = 'Room3pidMemberInfo',
// Group stuff // Group stuff
@ -43,6 +44,7 @@ export enum RightPanelPhases {
export const RIGHT_PANEL_PHASES_NO_ARGS = [ export const RIGHT_PANEL_PHASES_NO_ARGS = [
RightPanelPhases.RoomSummary, RightPanelPhases.RoomSummary,
RightPanelPhases.NotificationPanel, RightPanelPhases.NotificationPanel,
RightPanelPhases.PinnedMessages,
RightPanelPhases.FilePanel, RightPanelPhases.FilePanel,
RightPanelPhases.RoomMemberList, RightPanelPhases.RoomMemberList,
RightPanelPhases.GroupMemberList, RightPanelPhases.GroupMemberList,

View file

@ -54,8 +54,6 @@ const INITIAL_STATE = {
// Any error that has occurred during loading // Any error that has occurred during loading
roomLoadError: null, roomLoadError: null,
forwardingEvent: null,
quotingEvent: null, quotingEvent: null,
replyingToEvent: null, replyingToEvent: null,
@ -150,11 +148,6 @@ class RoomViewStore extends Store<ActionPayload> {
case 'on_logged_out': case 'on_logged_out':
this.reset(); this.reset();
break; break;
case 'forward_event':
this.setState({
forwardingEvent: payload.event,
});
break;
case 'reply_to_event': case 'reply_to_event':
// If currently viewed room does not match the room in which we wish to reply then change rooms // 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 // this can happen when performing a search across all rooms
@ -187,7 +180,6 @@ class RoomViewStore extends Store<ActionPayload> {
roomAlias: payload.room_alias, roomAlias: payload.room_alias,
initialEventId: payload.event_id, initialEventId: payload.event_id,
isInitialEventHighlighted: payload.highlighted, isInitialEventHighlighted: payload.highlighted,
forwardingEvent: null,
roomLoading: false, roomLoading: false,
roomLoadError: null, roomLoadError: null,
// should peek by default // should peek by default
@ -207,14 +199,6 @@ class RoomViewStore extends Store<ActionPayload> {
newState.replyingToEvent = payload.replyingToEvent; newState.replyingToEvent = payload.replyingToEvent;
} }
if (this.state.forwardingEvent) {
dis.dispatch({
action: 'send_event',
room_id: newState.roomId,
event: this.state.forwardingEvent,
});
}
this.setState(newState); this.setState(newState);
if (payload.auto_join) { if (payload.auto_join) {
@ -428,11 +412,6 @@ class RoomViewStore extends Store<ActionPayload> {
return this.state.joinError; 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 // The mxEvent if one is currently being replied to/quoted
public getQuotingEvent() { public getQuotingEvent() {
return this.state.replyingToEvent; return this.state.replyingToEvent;

View file

@ -196,7 +196,7 @@ export class SetupEncryptionStore extends EventEmitter {
this.phase = PHASE_FINISHED; this.phase = PHASE_FINISHED;
this.emit("update"); this.emit("update");
// async - ask other clients for keys, if necessary // async - ask other clients for keys, if necessary
MatrixClientPeg.get()._crypto.cancelAndResendAllOutgoingKeyRequests(); MatrixClientPeg.get().crypto.cancelAndResendAllOutgoingKeyRequests();
} }
async _setActiveVerificationRequest(request) { async _setActiveVerificationRequest(request) {

View file

@ -332,7 +332,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
} }
public getContainerWidgets(room: Room, container: Container): IApp[] { 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 { public isInContainer(room: Room, widget: IApp, container: Container): boolean {

View file

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

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -22,7 +22,7 @@ import url from "url";
* @param {string} u The url to be abbreviated * @param {string} u The url to be abbreviated
* @returns {string} The abbreviated url * @returns {string} The abbreviated url
*/ */
export function abbreviateUrl(u) { export function abbreviateUrl(u: string): string {
if (!u) return ''; if (!u) return '';
const parsedUrl = url.parse(u); const parsedUrl = url.parse(u);
@ -37,7 +37,7 @@ export function abbreviateUrl(u) {
return u; return u;
} }
export function unabbreviateUrl(u) { export function unabbreviateUrl(u: string): string {
if (!u) return ''; if (!u) return '';
let longUrl = u; let longUrl = u;

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,12 +16,6 @@ limitations under the License.
import { _t } from "../languageHandler"; import { _t } from "../languageHandler";
/**
* 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 // These are the constants we use for when to break the text
const MILLISECONDS_RECENT = 15000; const MILLISECONDS_RECENT = 15000;
const MILLISECONDS_1_MIN = 75000; const MILLISECONDS_1_MIN = 75000;
@ -30,6 +24,12 @@ export function humanizeTime(timeMillis) {
const HOURS_UNDER_1_DAY = 23; const HOURS_UNDER_1_DAY = 23;
const HOURS_1_DAY = 26; 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: number): string {
const now = (new Date()).getTime(); const now = (new Date()).getTime();
let msAgo = now - timeMillis; let msAgo = now - timeMillis;
const minutes = Math.abs(Math.ceil(msAgo / 60000)); const minutes = Math.abs(Math.ceil(msAgo / 60000));

View file

@ -19,11 +19,11 @@ limitations under the License.
* TODO: Convert this to a real TypeScript interface * TODO: Convert this to a real TypeScript interface
*/ */
export default class PermalinkConstructor { 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"); throw new Error("Not implemented");
} }
forRoom(roomIdOrAlias: string, serverCandidates: string[]): string { forRoom(roomIdOrAlias: string, serverCandidates: string[] = []): string {
throw new Error("Not implemented"); throw new Error("Not implemented");
} }
@ -73,12 +73,12 @@ export class PermalinkParts {
return new PermalinkParts(null, null, null, groupId, null); return new PermalinkParts(null, null, null, groupId, null);
} }
static forRoom(roomIdOrAlias: string, viaServers: string[]): PermalinkParts { static forRoom(roomIdOrAlias: string, viaServers: string[] = []): PermalinkParts {
return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers || []); return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers);
} }
static forEvent(roomId: string, eventId: string, viaServers: string[]): PermalinkParts { static forEvent(roomId: string, eventId: string, viaServers: string[] = []): PermalinkParts {
return new PermalinkParts(roomId, eventId, null, null, viaServers || []); return new PermalinkParts(roomId, eventId, null, null, viaServers);
} }
get primaryEntityId(): string { get primaryEntityId(): string {

View file

@ -149,7 +149,7 @@ export class RoomPermalinkCreator {
// Prefer to use canonical alias for permalink if possible // Prefer to use canonical alias for permalink if possible
const alias = this.room.getCanonicalAlias(); const alias = this.room.getCanonicalAlias();
if (alias) { if (alias) {
return getPermalinkConstructor().forRoom(alias, this._serverCandidates); return getPermalinkConstructor().forRoom(alias);
} }
} }
return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates); return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates);
@ -302,7 +302,7 @@ export function makeRoomPermalink(roomId: string): string {
} }
const permalinkCreator = new RoomPermalinkCreator(room); const permalinkCreator = new RoomPermalinkCreator(room);
permalinkCreator.load(); permalinkCreator.load();
return permalinkCreator.forRoom(); return permalinkCreator.forShareableRoom();
} }
export function makeGroupPermalink(groupId: string): string { export function makeGroupPermalink(groupId: string): string {

View file

@ -23,8 +23,9 @@ import dis from '../src/dispatcher/dispatcher';
import { CallEvent, CallState } from 'matrix-js-sdk/src/webrtc/call'; import { CallEvent, CallState } from 'matrix-js-sdk/src/webrtc/call';
import DMRoomMap from '../src/utils/DMRoomMap'; import DMRoomMap from '../src/utils/DMRoomMap';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { Action } from '../src/dispatcher/actions';
import SdkConfig from '../src/SdkConfig'; 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 REAL_ROOM_ID = '$room1:example.org';
const MAPPED_ROOM_ID = '$room2: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', () => { describe('CallHandler', () => {
let dmRoomMap; let dmRoomMap;
let callHandler; let callHandler;
@ -94,6 +107,21 @@ describe('CallHandler', () => {
callHandler = new CallHandler(); callHandler = new CallHandler();
callHandler.start(); 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 = { dmRoomMap = {
getUserIdForRoomId: roomId => { getUserIdForRoomId: roomId => {
if (roomId === REAL_ROOM_ID) { if (roomId === REAL_ROOM_ID) {
@ -134,38 +162,34 @@ describe('CallHandler', () => {
SdkConfig.unset(); 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 () => { 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({ dis.dispatch({
action: 'place_call', action: 'place_call',
type: PlaceCallType.Voice, type: PlaceCallType.Voice,
room_id: REAL_ROOM_ID, room_id: REAL_ROOM_ID,
}, true); }, true);
let dispatchHandle;
// wait for the call to be set up // wait for the call to be set up
await new Promise<void>(resolve => { await untilDispatch('call_state');
dispatchHandle = dis.register(payload => {
if (payload.action === 'call_state') {
resolve();
}
});
});
dis.unregister(dispatchHandle);
// should start off in the actual room ID it's in at the protocol level // should start off in the actual room ID it's in at the protocol level
expect(callHandler.getCallForRoom(REAL_ROOM_ID)).toBe(fakeCall); expect(callHandler.getCallForRoom(REAL_ROOM_ID)).toBe(fakeCall);

View file

@ -15,7 +15,6 @@ limitations under the License.
*/ */
import { isKeyComboMatch, KeyCombo } from '../src/KeyBindingsManager'; import { isKeyComboMatch, KeyCombo } from '../src/KeyBindingsManager';
const assert = require('assert');
function mockKeyEvent(key: string, modifiers?: { function mockKeyEvent(key: string, modifiers?: {
ctrlKey?: boolean, ctrlKey?: boolean,
@ -28,7 +27,7 @@ function mockKeyEvent(key: string, modifiers?: {
ctrlKey: modifiers?.ctrlKey ?? false, ctrlKey: modifiers?.ctrlKey ?? false,
altKey: modifiers?.altKey ?? false, altKey: modifiers?.altKey ?? false,
shiftKey: modifiers?.shiftKey ?? false, shiftKey: modifiers?.shiftKey ?? false,
metaKey: modifiers?.metaKey ?? false metaKey: modifiers?.metaKey ?? false,
} as KeyboardEvent; } as KeyboardEvent;
} }
@ -37,9 +36,8 @@ describe('KeyBindingsManager', () => {
const combo1: KeyCombo = { const combo1: KeyCombo = {
key: 'k', key: 'k',
}; };
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo1, false), true); expect(isKeyComboMatch(mockKeyEvent('k'), combo1, false)).toBe(true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n'), combo1, false), false); expect(isKeyComboMatch(mockKeyEvent('n'), combo1, false)).toBe(false);
}); });
it('should match key + modifier key combo', () => { it('should match key + modifier key combo', () => {
@ -47,38 +45,38 @@ describe('KeyBindingsManager', () => {
key: 'k', key: 'k',
ctrlKey: true, ctrlKey: true,
}; };
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false), true); expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false)).toBe(true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false), false); expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false)).toBe(false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo, false), false); expect(isKeyComboMatch(mockKeyEvent('k'), combo, false)).toBe(false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo, false), false); expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo, false)).toBe(false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, metaKey: true }), combo, false), false); expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, metaKey: true }), combo, false)).toBe(false);
const combo2: KeyCombo = { const combo2: KeyCombo = {
key: 'k', key: 'k',
metaKey: true, metaKey: true,
}; };
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo2, false), true); expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo2, false)).toBe(true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { metaKey: true }), combo2, false), false); expect(isKeyComboMatch(mockKeyEvent('n', { metaKey: true }), combo2, false)).toBe(false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo2, false), false); expect(isKeyComboMatch(mockKeyEvent('k'), combo2, false)).toBe(false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { altKey: true, metaKey: true }), combo2, false), false); expect(isKeyComboMatch(mockKeyEvent('k', { altKey: true, metaKey: true }), combo2, false)).toBe(false);
const combo3: KeyCombo = { const combo3: KeyCombo = {
key: 'k', key: 'k',
altKey: true, altKey: true,
}; };
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { altKey: true }), combo3, false), true); expect(isKeyComboMatch(mockKeyEvent('k', { altKey: true }), combo3, false)).toBe(true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { altKey: true }), combo3, false), false); expect(isKeyComboMatch(mockKeyEvent('n', { altKey: true }), combo3, false)).toBe(false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo3, false), false); expect(isKeyComboMatch(mockKeyEvent('k'), combo3, false)).toBe(false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo3, false), false); expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo3, false)).toBe(false);
const combo4: KeyCombo = { const combo4: KeyCombo = {
key: 'k', key: 'k',
shiftKey: true, shiftKey: true,
}; };
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo4, false), true); expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo4, false)).toBe(true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { shiftKey: true }), combo4, false), false); expect(isKeyComboMatch(mockKeyEvent('n', { shiftKey: true }), combo4, false)).toBe(false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo4, false), false); expect(isKeyComboMatch(mockKeyEvent('k'), combo4, false)).toBe(false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, ctrlKey: true }), combo4, false), false); expect(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, ctrlKey: true }), combo4, false)).toBe(false);
}); });
it('should match key + multiple modifiers key combo', () => { it('should match key + multiple modifiers key combo', () => {
@ -87,11 +85,11 @@ describe('KeyBindingsManager', () => {
ctrlKey: true, ctrlKey: true,
altKey: true, altKey: true,
}; };
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false), true); expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false)).toBe(true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, altKey: true }), combo, false), false); expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, altKey: true }), combo, false)).toBe(false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo, false), false); expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo, false)).toBe(false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true, shiftKey: true }), combo, expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true, shiftKey: true }), combo,
false), false); false)).toBe(false);
const combo2: KeyCombo = { const combo2: KeyCombo = {
key: 'k', key: 'k',
@ -99,13 +97,13 @@ describe('KeyBindingsManager', () => {
shiftKey: true, shiftKey: true,
altKey: true, altKey: true,
}; };
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, shiftKey: true, altKey: true }), combo2, expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, shiftKey: true, altKey: true }), combo2,
false), true); false)).toBe(true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, shiftKey: true, altKey: true }), combo2, expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, shiftKey: true, altKey: true }), combo2,
false), false); false)).toBe(false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo2, false), false); expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo2, false)).toBe(false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', expect(isKeyComboMatch(mockKeyEvent('k',
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo2, false), false); { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo2, false)).toBe(false);
const combo3: KeyCombo = { const combo3: KeyCombo = {
key: 'k', key: 'k',
@ -114,12 +112,12 @@ describe('KeyBindingsManager', () => {
altKey: true, altKey: true,
metaKey: true, metaKey: true,
}; };
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', expect(isKeyComboMatch(mockKeyEvent('k',
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false), true); { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false)).toBe(true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', expect(isKeyComboMatch(mockKeyEvent('n',
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false), false); { ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false)).toBe(false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', expect(isKeyComboMatch(mockKeyEvent('k',
{ ctrlKey: true, shiftKey: true, altKey: true }), combo3, false), false); { ctrlKey: true, shiftKey: true, altKey: true }), combo3, false)).toBe(false);
}); });
it('should match ctrlOrMeta key combo', () => { it('should match ctrlOrMeta key combo', () => {
@ -128,13 +126,13 @@ describe('KeyBindingsManager', () => {
ctrlOrCmd: true, ctrlOrCmd: true,
}; };
// PC: // PC:
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false), true); expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false)).toBe(true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, false), false); expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, false)).toBe(false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false), false); expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false)).toBe(false);
// MAC: // MAC:
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, true), true); expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, true)).toBe(true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, true), false); expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, true)).toBe(false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, true), false); expect(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, true)).toBe(false);
}); });
it('should match advanced ctrlOrMeta key combo', () => { it('should match advanced ctrlOrMeta key combo', () => {
@ -144,10 +142,10 @@ describe('KeyBindingsManager', () => {
altKey: true, altKey: true,
}; };
// PC: // PC:
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false), true); expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false)).toBe(true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, false), false); expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, false)).toBe(false);
// MAC: // MAC:
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, true), true); expect(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, true)).toBe(true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, true), false); expect(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, true)).toBe(false);
}); });
}); });

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

View file

@ -21,7 +21,7 @@ import "../skinned-sdk"; // Must be first for skinning to work
import SpaceStore, { import SpaceStore, {
UPDATE_INVITED_SPACES, UPDATE_INVITED_SPACES,
UPDATE_SELECTED_SPACE, UPDATE_SELECTED_SPACE,
UPDATE_TOP_LEVEL_SPACES UPDATE_TOP_LEVEL_SPACES,
} from "../../src/stores/SpaceStore"; } from "../../src/stores/SpaceStore";
import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../utils/test-utils"; import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../utils/test-utils";
import { mkEvent, mkStubRoom, stubClient } from "../test-utils"; import { mkEvent, mkStubRoom, stubClient } from "../test-utils";

View file

@ -90,7 +90,7 @@ export function createTestClient() {
}), }),
// Used by various internal bits we aren't concerned with (yet) // Used by various internal bits we aren't concerned with (yet)
_sessionStore: { sessionStore: {
store: { store: {
getItem: jest.fn(), getItem: jest.fn(),
}, },
@ -219,7 +219,7 @@ export function mkMessage(opts) {
return mkEvent(opts); return mkEvent(opts);
} }
export function mkStubRoom(roomId = null) { export function mkStubRoom(roomId = null, name) {
const stubTimeline = { getEvents: () => [] }; const stubTimeline = { getEvents: () => [] };
return { return {
roomId, roomId,
@ -238,6 +238,7 @@ export function mkStubRoom(roomId = null) {
getPendingEvents: () => [], getPendingEvents: () => [],
getLiveTimeline: () => stubTimeline, getLiveTimeline: () => stubTimeline,
getUnfilteredTimelineSet: () => null, getUnfilteredTimelineSet: () => null,
findEventById: () => null,
getAccountData: () => null, getAccountData: () => null,
hasMembershipState: () => null, hasMembershipState: () => null,
getVersion: () => '1', getVersion: () => '1',
@ -255,13 +256,17 @@ export function mkStubRoom(roomId = null) {
tags: {}, tags: {},
setBlacklistUnverifiedDevices: jest.fn(), setBlacklistUnverifiedDevices: jest.fn(),
on: jest.fn(), on: jest.fn(),
off: jest.fn(),
removeListener: jest.fn(), removeListener: jest.fn(),
getDMInviter: jest.fn(), getDMInviter: jest.fn(),
name,
getAvatarUrl: () => 'mxc://avatar.url/room.png', getAvatarUrl: () => 'mxc://avatar.url/room.png',
getMxcAvatarUrl: () => 'mxc://avatar.url/room.png', getMxcAvatarUrl: () => 'mxc://avatar.url/room.png',
isSpaceRoom: jest.fn(() => false), isSpaceRoom: jest.fn(() => false),
getUnreadNotificationCount: jest.fn(() => 0), getUnreadNotificationCount: jest.fn(() => 0),
getEventReadUpTo: jest.fn(() => null), getEventReadUpTo: jest.fn(() => null),
getCanonicalAlias: jest.fn(),
getAltAliases: jest.fn().mockReturnValue([]),
timeline: [], timeline: [],
}; };
} }

View file

@ -34,7 +34,7 @@ function mockRoom(roomId, members, serverACL) {
return { return {
roomId, roomId,
getCanonicalAlias: () => roomId, getCanonicalAlias: () => null,
getJoinedMembers: () => members, getJoinedMembers: () => members,
getMember: (userId) => members.find(m => m.userId === userId), getMember: (userId) => members.find(m => m.userId === userId),
currentState: { currentState: {

View file

@ -5674,8 +5674,8 @@ mathml-tag-names@^2.1.3:
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
version "11.1.0" version "11.2.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/acb9bc8cc5234326a7583514a8e120a4ac42eedc" resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/35ecbed29d16982deff27a8c37b05167738225a2"
dependencies: dependencies:
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
another-json "^0.2.0" another-json "^0.2.0"