Merge branch 'develop' into improved-forwarding-ui
This commit is contained in:
commit
a06306d560
91 changed files with 1240 additions and 1002 deletions
|
@ -291,6 +291,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
||||||
|
|
||||||
.mx_Dialog_staticWrapper .mx_Dialog {
|
.mx_Dialog_staticWrapper .mx_Dialog {
|
||||||
z-index: 4010;
|
z-index: 4010;
|
||||||
|
contain: content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Dialog_background {
|
.mx_Dialog_background {
|
||||||
|
|
|
@ -180,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";
|
||||||
|
@ -204,7 +205,6 @@
|
||||||
@import "./views/rooms/_NewRoomIntro.scss";
|
@import "./views/rooms/_NewRoomIntro.scss";
|
||||||
@import "./views/rooms/_NotificationBadge.scss";
|
@import "./views/rooms/_NotificationBadge.scss";
|
||||||
@import "./views/rooms/_PinnedEventTile.scss";
|
@import "./views/rooms/_PinnedEventTile.scss";
|
||||||
@import "./views/rooms/_PinnedEventsPanel.scss";
|
|
||||||
@import "./views/rooms/_PresenceLabel.scss";
|
@import "./views/rooms/_PresenceLabel.scss";
|
||||||
@import "./views/rooms/_ReplyPreview.scss";
|
@import "./views/rooms/_ReplyPreview.scss";
|
||||||
@import "./views/rooms/_RoomBreadcrumbs.scss";
|
@import "./views/rooms/_RoomBreadcrumbs.scss";
|
||||||
|
|
|
@ -38,6 +38,7 @@ limitations under the License.
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
z-index: 5001;
|
z-index: 5001;
|
||||||
|
contain: content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ContextualMenu_right {
|
.mx_ContextualMenu_right {
|
||||||
|
|
|
@ -25,6 +25,7 @@ $roomListCollapsedWidth: 68px;
|
||||||
|
|
||||||
// Create a row-based flexbox for the GroupFilterPanel and the room list
|
// Create a row-based flexbox for the GroupFilterPanel and the room list
|
||||||
display: flex;
|
display: flex;
|
||||||
|
contain: content;
|
||||||
|
|
||||||
.mx_LeftPanel_GroupFilterPanelContainer {
|
.mx_LeftPanel_GroupFilterPanelContainer {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
@ -70,6 +71,7 @@ $roomListCollapsedWidth: 68px;
|
||||||
// aligned correctly. This is also a row-based flexbox.
|
// aligned correctly. This is also a row-based flexbox.
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
contain: content;
|
||||||
|
|
||||||
&.mx_IndicatorScrollbar_leftOverflow {
|
&.mx_IndicatorScrollbar_leftOverflow {
|
||||||
mask-image: linear-gradient(90deg, transparent, black 5%);
|
mask-image: linear-gradient(90deg, transparent, black 5%);
|
||||||
|
|
|
@ -25,6 +25,7 @@ limitations under the License.
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
contain: strict;
|
||||||
|
|
||||||
.mx_RoomView_MessageList {
|
.mx_RoomView_MessageList {
|
||||||
padding: 14px 18px; // top and bottom is 4px smaller to balance with the padding set above
|
padding: 14px 18px; // top and bottom is 4px smaller to balance with the padding set above
|
||||||
|
@ -98,6 +99,48 @@ limitations under the License.
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$dot-size: 8px;
|
||||||
|
$pulse-color: $pinned-unread-color;
|
||||||
|
|
||||||
|
.mx_RightPanel_pinnedMessagesButton {
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/pin.svg');
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RightPanel_pinnedMessagesButton_unreadIndicator {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
margin: 4px;
|
||||||
|
width: $dot-size;
|
||||||
|
height: $dot-size;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: scale(1);
|
||||||
|
background: rgba($pulse-color, 1);
|
||||||
|
box-shadow: 0 0 0 0 rgba($pulse-color, 1);
|
||||||
|
animation: mx_RightPanel_indicator_pulse 2s infinite;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mx_RightPanel_indicator_pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
box-shadow: 0 0 0 0 rgba($pulse-color, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 10px rgba($pulse-color, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
box-shadow: 0 0 0 0 rgba($pulse-color, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RightPanel_headerButton_highlight {
|
.mx_RightPanel_headerButton_highlight {
|
||||||
&::before {
|
&::before {
|
||||||
background-color: $accent-color !important;
|
background-color: $accent-color !important;
|
||||||
|
|
|
@ -152,6 +152,7 @@ limitations under the License.
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
contain: content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomView_statusArea {
|
.mx_RoomView_statusArea {
|
||||||
|
@ -237,6 +238,7 @@ hr.mx_RoomView_myReadMarker {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
will-change: width;
|
||||||
transition: width 400ms easeinsine 1s, opacity 400ms easeinsine 1s;
|
transition: width 400ms easeinsine 1s, opacity 400ms easeinsine 1s;
|
||||||
width: 99%;
|
width: 99%;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
|
@ -21,5 +21,8 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: 50px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_DecoratedRoomAvatar, .mx_ExtraTile {
|
.mx_DecoratedRoomAvatar, .mx_ExtraTile {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
contain: content;
|
||||||
|
|
||||||
&.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar {
|
&.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar {
|
||||||
mask-image: url('$(res)/img/element-icons/roomlist/decorated-avatar-mask.svg');
|
mask-image: url('$(res)/img/element-icons/roomlist/decorated-avatar-mask.svg');
|
||||||
|
|
35
res/css/views/right_panel/_PinnedMessagesCard.scss
Normal file
35
res/css/views/right_panel/_PinnedMessagesCard.scss
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -104,7 +104,7 @@ $left-gutter: 64px;
|
||||||
.mx_EventTile_line, .mx_EventTile_reply {
|
.mx_EventTile_line, .mx_EventTile_reply {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: $left-gutter;
|
padding-left: $left-gutter;
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomView_timeline_rr_enabled,
|
.mx_RoomView_timeline_rr_enabled,
|
||||||
|
@ -280,6 +280,7 @@ $left-gutter: 64px;
|
||||||
height: $font-14px;
|
height: $font-14px;
|
||||||
width: $font-14px;
|
width: $font-14px;
|
||||||
|
|
||||||
|
will-change: left, top;
|
||||||
transition:
|
transition:
|
||||||
left var(--transition-short) ease-out,
|
left var(--transition-short) ease-out,
|
||||||
top var(--transition-standard) ease-out;
|
top var(--transition-standard) ease-out;
|
||||||
|
|
|
@ -115,8 +115,7 @@ $irc-line-height: $font-18px;
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line {
|
||||||
.mx_EventTile_e2eIcon,
|
.mx_EventTile_e2eIcon,
|
||||||
.mx_TextualEvent,
|
.mx_TextualEvent,
|
||||||
.mx_MTextBody,
|
.mx_MTextBody {
|
||||||
.mx_ReplyThread_wrapper_empty {
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,8 +176,6 @@ $irc-line-height: $font-18px;
|
||||||
.mx_SenderProfile_hover {
|
.mx_SenderProfile_hover {
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
> span {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
> .mx_SenderProfile_name {
|
> .mx_SenderProfile_name {
|
||||||
|
@ -188,7 +185,6 @@ $irc-line-height: $font-18px;
|
||||||
text-align: end;
|
text-align: end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SenderProfile:hover {
|
.mx_SenderProfile:hover {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
|
@ -52,6 +52,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_JumpToBottomButton_scrollDown {
|
.mx_JumpToBottomButton_scrollDown {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: block;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
border-radius: 19px;
|
border-radius: 19px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
|
@ -16,62 +16,91 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_PinnedEventTile {
|
.mx_PinnedEventTile {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
margin-bottom: 5px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 5px; // for the hover
|
padding: 0 4px 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventTile:hover {
|
display: grid;
|
||||||
background-color: $event-selected-color;
|
grid-template-areas:
|
||||||
}
|
"avatar name remove"
|
||||||
|
"content content content"
|
||||||
|
"footer footer footer";
|
||||||
|
grid-template-rows: max-content auto max-content;
|
||||||
|
grid-template-columns: 24px auto 24px;
|
||||||
|
grid-row-gap: 12px;
|
||||||
|
grid-column-gap: 8px;
|
||||||
|
|
||||||
.mx_PinnedEventTile .mx_PinnedEventTile_sender,
|
& + .mx_PinnedEventTile {
|
||||||
.mx_PinnedEventTile .mx_PinnedEventTile_timestamp {
|
padding: 12px 4px;
|
||||||
color: #868686;
|
border-top: 1px solid $menu-border-color;
|
||||||
font-size: 0.8em;
|
}
|
||||||
vertical-align: top;
|
|
||||||
display: inline-block;
|
|
||||||
padding-bottom: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventTile .mx_PinnedEventTile_timestamp {
|
.mx_PinnedEventTile_senderAvatar {
|
||||||
padding-left: 15px;
|
grid-area: avatar;
|
||||||
display: none;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventTile .mx_PinnedEventTile_senderAvatar .mx_BaseAvatar {
|
.mx_PinnedEventTile_sender {
|
||||||
float: left;
|
grid-area: name;
|
||||||
margin-right: 10px;
|
font-weight: $font-semi-bold;
|
||||||
}
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_PinnedEventTile_actions {
|
.mx_PinnedEventTile_unpinButton {
|
||||||
float: right;
|
visibility: hidden;
|
||||||
margin-right: 10px;
|
grid-area: remove;
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventTile:hover .mx_PinnedEventTile_timestamp {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventTile:hover .mx_PinnedEventTile_actions {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventTile_unpinButton {
|
|
||||||
display: inline-block;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventTile_gotoButton {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 0.7em; // Smaller text to avoid conflicting with the layout
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventTile_message {
|
|
||||||
margin-left: 50px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0;
|
width: 24px;
|
||||||
left: 0;
|
height: 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $roomheader-addroom-bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
//top: 0;
|
||||||
|
//left: 0;
|
||||||
|
height: inherit;
|
||||||
|
width: inherit;
|
||||||
|
background: $secondary-fg-color;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: 8px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-image: url('$(res)/img/image-view/close.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_PinnedEventTile_message {
|
||||||
|
grid-area: content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,14 +32,14 @@ limitations under the License.
|
||||||
// first triggering the enter state with the newest breadcrumb off screen (-40px) then
|
// first triggering the enter state with the newest breadcrumb off screen (-40px) then
|
||||||
// sliding it into view.
|
// sliding it into view.
|
||||||
&.mx_RoomBreadcrumbs-enter {
|
&.mx_RoomBreadcrumbs-enter {
|
||||||
margin-left: -40px; // 32px for the avatar, 8px for the margin
|
transform: translateX(-40px); // 32px for the avatar, 8px for the margin
|
||||||
}
|
}
|
||||||
&.mx_RoomBreadcrumbs-enter-active {
|
&.mx_RoomBreadcrumbs-enter-active {
|
||||||
margin-left: 0;
|
transform: translateX(0);
|
||||||
|
|
||||||
// Timing function is as-requested by design.
|
// Timing function is as-requested by design.
|
||||||
// NOTE: The transition time MUST match the value passed to CSSTransition!
|
// NOTE: The transition time MUST match the value passed to CSSTransition!
|
||||||
transition: margin-left 640ms cubic-bezier(0.66, 0.02, 0.36, 1);
|
transition: transform 640ms cubic-bezier(0.66, 0.02, 0.36, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomBreadcrumbs_placeholder {
|
.mx_RoomBreadcrumbs_placeholder {
|
||||||
|
|
|
@ -277,24 +277,6 @@ limitations under the License.
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomHeader_pinnedButton::before {
|
|
||||||
mask-image: url('$(res)/img/element-icons/room/pin.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_pinsIndicator {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 4px;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: $pinned-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_pinsIndicatorUnread {
|
|
||||||
background-color: $pinned-unread-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 480px) {
|
@media only screen and (max-width: 480px) {
|
||||||
.mx_RoomHeader_wrapper {
|
.mx_RoomHeader_wrapper {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -198,6 +198,7 @@ limitations under the License.
|
||||||
// as the box model should be top aligned. Happens in both FF and Chromium
|
// as the box model should be top aligned. Happens in both FF and Chromium
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-self: stretch;
|
||||||
|
|
||||||
mask-image: linear-gradient(0deg, transparent, black 4px);
|
mask-image: linear-gradient(0deg, transparent, black 4px);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,10 @@ limitations under the License.
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
|
||||||
|
contain: content; // Not strict as it will break when resizing a sublist vertically
|
||||||
|
height: 40px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
// The tile is also a flexbox row itself
|
// The tile is also a flexbox row itself
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M18.5151 20.0831L15.6941 17.2621L17.2621 15.6941L20.0831 18.5151C21.5741 20.0061 22.1529 21.7793 21.9661 21.9661C21.7793 22.1529 20.0061 21.5741 18.5151 20.0831Z" fill="black"/>
|
<path d="M18.5151 20.0831L15.6941 17.2621L17.2621 15.6941L20.0831 18.5151C21.5741 20.0061 22.1529 21.7793 21.9661 21.9661C21.7793 22.1529 20.0061 21.5741 18.5151 20.0831Z" fill="#737D8C"/>
|
||||||
<path d="M7.46196 11.3821C7.07677 11.5059 5.49073 12.0989 3.63366 12.0744C1.77658 12.0499 1.67795 10.8941 2.46811 10.1039L6.28598 6.28602L9.42196 9.42203L7.46196 11.3821Z" fill="black"/>
|
<path d="M7.46196 11.3821C7.07677 11.5059 5.49073 12.0989 3.63366 12.0744C1.77658 12.0499 1.67795 10.8941 2.46811 10.1039L6.28598 6.28602L9.42196 9.42203L7.46196 11.3821Z" fill="#737D8C"/>
|
||||||
<path d="M11.3821 7.46202C11.5059 7.07682 12.0989 5.49077 12.0744 3.63368C12.0499 1.77658 10.8941 1.67795 10.1039 2.46812L6.28598 6.28602L9.42196 9.42203L11.3821 7.46202Z" fill="black"/>
|
<path d="M11.3821 7.46202C11.5059 7.07682 12.0989 5.49077 12.0744 3.63368C12.0499 1.77658 10.8941 1.67795 10.1039 2.46812L6.28598 6.28602L9.42196 9.42203L11.3821 7.46202Z" fill="#737D8C"/>
|
||||||
<path d="M7.40596 11.438L11.4379 7.40602L14.9099 10.206L10.2059 14.9101L7.40596 11.438Z" fill="black"/>
|
<path d="M7.40596 11.438L11.4379 7.40602L14.9099 10.206L10.2059 14.9101L7.40596 11.438Z" fill="#737D8C"/>
|
||||||
<path d="M11.774 11.774C9.31114 14.2369 8.61779 17.7115 9.83827 20.3213C10.3104 21.3308 11.6288 21.3273 12.4169 20.5392L20.5391 12.4169C21.3271 11.6289 21.3307 10.3104 20.3212 9.83829C17.7114 8.61779 14.2369 9.31115 11.774 11.774Z" fill="black"/>
|
<path d="M11.774 11.774C9.31114 14.2369 8.61779 17.7115 9.83827 20.3213C10.3104 21.3308 11.6288 21.3273 12.4169 20.5392L20.5391 12.4169C21.3271 11.6289 21.3307 10.3104 20.3212 9.83829C17.7114 8.61779 14.2369 9.31115 11.774 11.774Z" fill="#737D8C"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 1,015 B After Width: | Height: | Size: 1 KiB |
|
@ -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}`);
|
||||||
|
|
||||||
|
@ -802,7 +804,10 @@ export default class CallHandler extends EventEmitter {
|
||||||
|
|
||||||
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
|
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
|
||||||
if (this.getCallForRoom(mappedRoomId)) {
|
if (this.getCallForRoom(mappedRoomId)) {
|
||||||
// ignore multiple incoming calls to the same room
|
console.log(
|
||||||
|
"Got incoming call for room " + mappedRoomId +
|
||||||
|
" but there's already a call for this room: ignoring",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -859,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");
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -816,7 +816,9 @@ export default class CountlyAnalytics {
|
||||||
window.addEventListener("mousemove", this.onUserActivity);
|
window.addEventListener("mousemove", this.onUserActivity);
|
||||||
window.addEventListener("click", this.onUserActivity);
|
window.addEventListener("click", this.onUserActivity);
|
||||||
window.addEventListener("keydown", this.onUserActivity);
|
window.addEventListener("keydown", this.onUserActivity);
|
||||||
window.addEventListener("scroll", this.onUserActivity);
|
// Using the passive option to not block the main thread
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||||
|
window.addEventListener("scroll", this.onUserActivity, { passive: true });
|
||||||
|
|
||||||
this.activityIntervalId = setInterval(() => {
|
this.activityIntervalId = setInterval(() => {
|
||||||
this.inactivityCounter++;
|
this.inactivityCounter++;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -66,7 +66,7 @@ async function serverSideSearchProcess(term, roomId = undefined) {
|
||||||
highlights: [],
|
highlights: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
return client._processRoomEventsSearch(searchResult, result.response);
|
return client.processRoomEventsSearch(searchResult, result.response);
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareEvents(a, b) {
|
function compareEvents(a, b) {
|
||||||
|
@ -131,7 +131,7 @@ async function combinedSearch(searchTerm) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = client._processRoomEventsSearch(emptyResult, response);
|
const result = client.processRoomEventsSearch(emptyResult, response);
|
||||||
|
|
||||||
// Restore our encryption info so we can properly re-verify the events.
|
// Restore our encryption info so we can properly re-verify the events.
|
||||||
restoreEncryptionInfo(result.results);
|
restoreEncryptionInfo(result.results);
|
||||||
|
@ -185,7 +185,7 @@ async function localSearchProcess(searchTerm, roomId = undefined) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const processedResult = MatrixClientPeg.get()._processRoomEventsSearch(emptyResult, response);
|
const processedResult = MatrixClientPeg.get().processRoomEventsSearch(emptyResult, response);
|
||||||
// Restore our encryption info so we can properly re-verify the events.
|
// Restore our encryption info so we can properly re-verify the events.
|
||||||
restoreEncryptionInfo(processedResult.results);
|
restoreEncryptionInfo(processedResult.results);
|
||||||
|
|
||||||
|
@ -210,7 +210,7 @@ async function localPagination(searchResult) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = MatrixClientPeg.get()._processRoomEventsSearch(searchResult, response);
|
const result = MatrixClientPeg.get().processRoomEventsSearch(searchResult, response);
|
||||||
|
|
||||||
// Restore our encryption info so we can properly re-verify the events.
|
// Restore our encryption info so we can properly re-verify the events.
|
||||||
const newSlice = result.results.slice(Math.max(result.results.length - newResultCount, 0));
|
const newSlice = result.results.slice(Math.max(result.results.length - newResultCount, 0));
|
||||||
|
@ -520,7 +520,7 @@ async function combinedPagination(searchResult) {
|
||||||
const oldResultCount = searchResult.results ? searchResult.results.length : 0;
|
const oldResultCount = searchResult.results ? searchResult.results.length : 0;
|
||||||
|
|
||||||
// Let the client process the combined result.
|
// Let the client process the combined result.
|
||||||
const result = client._processRoomEventsSearch(searchResult, response);
|
const result = client.processRoomEventsSearch(searchResult, response);
|
||||||
|
|
||||||
// Restore our encryption info so we can properly re-verify the events.
|
// Restore our encryption info so we can properly re-verify the events.
|
||||||
const newResultCount = result.results.length - oldResultCount;
|
const newResultCount = result.results.length - oldResultCount;
|
||||||
|
|
|
@ -271,7 +271,7 @@ async function onSecretRequested(
|
||||||
}
|
}
|
||||||
return key && encodeBase64(key);
|
return key && encodeBase64(key);
|
||||||
} else if (name === "m.megolm_backup.v1") {
|
} else if (name === "m.megolm_backup.v1") {
|
||||||
const key = await client._crypto.getSessionBackupPrivateKey();
|
const key = await client.crypto.getSessionBackupPrivateKey();
|
||||||
if (!key) {
|
if (!key) {
|
||||||
console.log(
|
console.log(
|
||||||
`session backup key requested by ${deviceId}, but not found in cache`,
|
`session backup key requested by ${deviceId}, but not found in cache`,
|
||||||
|
|
|
@ -103,7 +103,7 @@ export async function startTermsFlow(
|
||||||
|
|
||||||
// fetch the set of agreed policy URLs from account data
|
// fetch the set of agreed policy URLs from account data
|
||||||
const currentAcceptedTerms = await MatrixClientPeg.get().getAccountData('m.accepted_terms');
|
const currentAcceptedTerms = await MatrixClientPeg.get().getAccountData('m.accepted_terms');
|
||||||
let agreedUrlSet;
|
let agreedUrlSet: Set<string>;
|
||||||
if (!currentAcceptedTerms || !currentAcceptedTerms.getContent() || !currentAcceptedTerms.getContent().accepted) {
|
if (!currentAcceptedTerms || !currentAcceptedTerms.getContent() || !currentAcceptedTerms.getContent().accepted) {
|
||||||
agreedUrlSet = new Set();
|
agreedUrlSet = new Set();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -33,7 +33,7 @@ export default class VoipUserMapper {
|
||||||
|
|
||||||
private async userToVirtualUser(userId: string): Promise<string> {
|
private async userToVirtualUser(userId: string): Promise<string> {
|
||||||
const results = await CallHandler.sharedInstance().sipVirtualLookup(userId);
|
const results = await CallHandler.sharedInstance().sipVirtualLookup(userId);
|
||||||
if (results.length === 0) return null;
|
if (results.length === 0 || !results[0].fields.lookup_success) return null;
|
||||||
return results[0].userid;
|
return results[0].userid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,14 +82,14 @@ export default class VoipUserMapper {
|
||||||
return Boolean(claimedNativeRoomId);
|
return Boolean(claimedNativeRoomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onNewInvitedRoom(invitedRoom: Room) {
|
public async onNewInvitedRoom(invitedRoom: Room): Promise<void> {
|
||||||
if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return;
|
if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return;
|
||||||
|
|
||||||
const inviterId = invitedRoom.getDMInviter();
|
const inviterId = invitedRoom.getDMInviter();
|
||||||
console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
|
console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
|
||||||
const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);
|
const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);
|
||||||
if (result.length === 0) {
|
if (result.length === 0) {
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result[0].fields.is_virtual) {
|
if (result[0].fields.is_virtual) {
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export default class AutoHideScrollbar extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this._collectContainerRef = this._collectContainerRef.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
_collectContainerRef(ref) {
|
|
||||||
if (ref && !this.containerRef) {
|
|
||||||
this.containerRef = ref;
|
|
||||||
}
|
|
||||||
if (this.props.wrappedRef) {
|
|
||||||
this.props.wrappedRef(ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getScrollTop() {
|
|
||||||
return this.containerRef.scrollTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (<div
|
|
||||||
ref={this._collectContainerRef}
|
|
||||||
style={this.props.style}
|
|
||||||
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
|
|
||||||
onScroll={this.props.onScroll}
|
|
||||||
onWheel={this.props.onWheel}
|
|
||||||
tabIndex={this.props.tabIndex}
|
|
||||||
>
|
|
||||||
{ this.props.children }
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
}
|
|
65
src/components/structures/AutoHideScrollbar.tsx
Normal file
65
src/components/structures/AutoHideScrollbar.tsx
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className?: string;
|
||||||
|
onScroll?: () => void;
|
||||||
|
onWheel?: () => void;
|
||||||
|
style?: React.CSSProperties
|
||||||
|
tabIndex?: number,
|
||||||
|
wrappedRef?: (ref: HTMLDivElement) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AutoHideScrollbar extends React.Component<IProps> {
|
||||||
|
private containerRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
if (this.containerRef.current && this.props.onScroll) {
|
||||||
|
// Using the passive option to not block the main thread
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||||
|
this.containerRef.current.addEventListener("scroll", this.props.onScroll, { passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.wrappedRef) {
|
||||||
|
this.props.wrappedRef(this.containerRef.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
if (this.containerRef.current && this.props.onScroll) {
|
||||||
|
this.containerRef.current.removeEventListener("scroll", this.props.onScroll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getScrollTop(): number {
|
||||||
|
return this.containerRef.current.scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (<div
|
||||||
|
ref={this.containerRef}
|
||||||
|
style={this.props.style}
|
||||||
|
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
|
||||||
|
onWheel={this.props.onWheel}
|
||||||
|
tabIndex={this.props.tabIndex}
|
||||||
|
>
|
||||||
|
{ this.props.children }
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
}
|
|
@ -59,7 +59,9 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
_collectScroller(scroller) {
|
_collectScroller(scroller) {
|
||||||
if (scroller && !this._scrollElement) {
|
if (scroller && !this._scrollElement) {
|
||||||
this._scrollElement = scroller;
|
this._scrollElement = scroller;
|
||||||
this._scrollElement.addEventListener("scroll", this.checkOverflow);
|
// Using the passive option to not block the main thread
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||||
|
this._scrollElement.addEventListener("scroll", this.checkOverflow, { passive: true });
|
||||||
this.checkOverflow();
|
this.checkOverflow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,6 +97,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
|
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
|
||||||
UIStore.instance.on("ListContainer", this.refreshStickyHeaders);
|
UIStore.instance.on("ListContainer", this.refreshStickyHeaders);
|
||||||
|
// Using the passive option to not block the main thread
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||||
|
this.listContainerRef.current?.addEventListener("scroll", this.onScroll, { passive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
@ -108,6 +111,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
||||||
UIStore.instance.stopTrackingElementDimensions("ListContainer");
|
UIStore.instance.stopTrackingElementDimensions("ListContainer");
|
||||||
UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders);
|
UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders);
|
||||||
|
this.listContainerRef.current?.removeEventListener("scroll", this.onScroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||||
|
@ -295,7 +299,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onScroll = (ev: React.MouseEvent<HTMLDivElement>) => {
|
private onScroll = (ev: Event) => {
|
||||||
const list = ev.target as HTMLDivElement;
|
const list = ev.target as HTMLDivElement;
|
||||||
this.handleStickyHeaders(list);
|
this.handleStickyHeaders(list);
|
||||||
};
|
};
|
||||||
|
@ -459,7 +463,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
<div className="mx_LeftPanel_roomListWrapper">
|
<div className="mx_LeftPanel_roomListWrapper">
|
||||||
<div
|
<div
|
||||||
className={roomListClasses}
|
className={roomListClasses}
|
||||||
onScroll={this.onScroll}
|
|
||||||
ref={this.listContainerRef}
|
ref={this.listContainerRef}
|
||||||
// Firefox sometimes makes this element focusable due to
|
// Firefox sometimes makes this element focusable due to
|
||||||
// overflow:scroll;, so force it out of tab order.
|
// overflow:scroll;, so force it out of tab order.
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -121,6 +121,9 @@ export default class MessagePanel extends React.Component {
|
||||||
// callback which is called when the panel is scrolled.
|
// callback which is called when the panel is scrolled.
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
|
|
||||||
|
// callback which is called when the user interacts with the room timeline
|
||||||
|
onUserScroll: PropTypes.func,
|
||||||
|
|
||||||
// callback which is called when more content is needed.
|
// callback which is called when more content is needed.
|
||||||
onFillRequest: PropTypes.func,
|
onFillRequest: PropTypes.func,
|
||||||
|
|
||||||
|
@ -645,13 +648,12 @@ export default class MessagePanel extends React.Component {
|
||||||
|
|
||||||
// use txnId as key if available so that we don't remount during sending
|
// use txnId as key if available so that we don't remount during sending
|
||||||
ret.push(
|
ret.push(
|
||||||
<li
|
<TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
|
||||||
key={mxEv.getTxnId() || eventId}
|
|
||||||
ref={this._collectEventNode.bind(this, eventId)}
|
|
||||||
data-scroll-tokens={scrollToken}
|
|
||||||
>
|
|
||||||
<TileErrorBoundary mxEvent={mxEv}>
|
|
||||||
<EventTile
|
<EventTile
|
||||||
|
as="li"
|
||||||
|
data-scroll-tokens={scrollToken}
|
||||||
|
ref={this._collectEventNode.bind(this, eventId)}
|
||||||
|
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
|
||||||
mxEvent={mxEv}
|
mxEvent={mxEv}
|
||||||
continuation={continuation}
|
continuation={continuation}
|
||||||
isRedacted={mxEv.isRedacted()}
|
isRedacted={mxEv.isRedacted()}
|
||||||
|
@ -676,8 +678,7 @@ export default class MessagePanel extends React.Component {
|
||||||
enableFlair={this.props.enableFlair}
|
enableFlair={this.props.enableFlair}
|
||||||
showReadReceipts={this.props.showReadReceipts}
|
showReadReceipts={this.props.showReadReceipts}
|
||||||
/>
|
/>
|
||||||
</TileErrorBoundary>
|
</TileErrorBoundary>,
|
||||||
</li>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
@ -779,7 +780,7 @@ export default class MessagePanel extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_collectEventNode = (eventId, node) => {
|
_collectEventNode = (eventId, node) => {
|
||||||
this.eventNodes[eventId] = node;
|
this.eventNodes[eventId] = node?.ref?.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
// once dynamic content in the events load, make the scrollPanel check the
|
// once dynamic content in the events load, make the scrollPanel check the
|
||||||
|
@ -885,6 +886,7 @@ export default class MessagePanel extends React.Component {
|
||||||
ref={this._scrollPanel}
|
ref={this._scrollPanel}
|
||||||
className={className}
|
className={className}
|
||||||
onScroll={this.props.onScroll}
|
onScroll={this.props.onScroll}
|
||||||
|
onUserScroll={this.props.onUserScroll}
|
||||||
onResize={this.onResize}
|
onResize={this.onResize}
|
||||||
onFillRequest={this.props.onFillRequest}
|
onFillRequest={this.props.onFillRequest}
|
||||||
onUnfillRequest={this.props.onUnfillRequest}
|
onUnfillRequest={this.props.onUnfillRequest}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016, 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 New Vector Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,29 +14,25 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
|
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import * as sdk from "../../index";
|
|
||||||
import BaseCard from "../views/right_panel/BaseCard";
|
import BaseCard from "../views/right_panel/BaseCard";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import TimelinePanel from "./TimelinePanel";
|
||||||
|
import Spinner from "../views/elements/Spinner";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Component which shows the global notification list using a TimelinePanel
|
* Component which shows the global notification list using a TimelinePanel
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("structures.NotificationPanel")
|
@replaceableComponent("structures.NotificationPanel")
|
||||||
class NotificationPanel extends React.Component {
|
export default class NotificationPanel extends React.PureComponent<IProps> {
|
||||||
static propTypes = {
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
|
||||||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
|
||||||
|
|
||||||
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
||||||
<h2>{_t('You’re all caught up')}</h2>
|
<h2>{_t('You’re all caught up')}</h2>
|
||||||
<p>{_t('You have no visible notifications.')}</p>
|
<p>{_t('You have no visible notifications.')}</p>
|
||||||
|
@ -47,6 +41,7 @@ class NotificationPanel extends React.Component {
|
||||||
let content;
|
let content;
|
||||||
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
||||||
if (timelineSet) {
|
if (timelineSet) {
|
||||||
|
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||||
content = (
|
content = (
|
||||||
<TimelinePanel
|
<TimelinePanel
|
||||||
manageReadReceipts={false}
|
manageReadReceipts={false}
|
||||||
|
@ -59,7 +54,7 @@ class NotificationPanel extends React.Component {
|
||||||
);
|
);
|
||||||
} 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 +62,3 @@ class NotificationPanel extends React.Component {
|
||||||
</BaseCard>;
|
</BaseCard>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NotificationPanel;
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
Copyright 2015 - 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,70 +16,92 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
|
|
||||||
import * as sdk from '../../index';
|
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import RateLimitedFunc from '../../ratelimitedfunc';
|
import RateLimitedFunc from '../../ratelimitedfunc';
|
||||||
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
|
||||||
import GroupStore from '../../stores/GroupStore';
|
import GroupStore from '../../stores/GroupStore';
|
||||||
import {
|
import {
|
||||||
RightPanelPhases,
|
|
||||||
RIGHT_PANEL_PHASES_NO_ARGS,
|
RIGHT_PANEL_PHASES_NO_ARGS,
|
||||||
RIGHT_PANEL_SPACE_PHASES,
|
RIGHT_PANEL_SPACE_PHASES,
|
||||||
|
RightPanelPhases,
|
||||||
} from "../../stores/RightPanelStorePhases";
|
} from "../../stores/RightPanelStorePhases";
|
||||||
import RightPanelStore from "../../stores/RightPanelStore";
|
import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import {Action} from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
||||||
import WidgetCard from "../views/right_panel/WidgetCard";
|
import WidgetCard from "../views/right_panel/WidgetCard";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
|
import MemberList from "../views/rooms/MemberList";
|
||||||
|
import GroupMemberList from "../views/groups/GroupMemberList";
|
||||||
|
import GroupRoomList from "../views/groups/GroupRoomList";
|
||||||
|
import GroupRoomInfo from "../views/groups/GroupRoomInfo";
|
||||||
|
import UserInfo from "../views/right_panel/UserInfo";
|
||||||
|
import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo";
|
||||||
|
import FilePanel from "./FilePanel";
|
||||||
|
import NotificationPanel from "./NotificationPanel";
|
||||||
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
|
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
room?: Room; // if showing panels for a given room, this is set
|
||||||
|
groupId?: string; // if showing panels for a given group, this is set
|
||||||
|
user?: User; // used if we know the user ahead of opening the panel
|
||||||
|
resizeNotifier: ResizeNotifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
phase: RightPanelPhases;
|
||||||
|
isUserPrivilegedInGroup?: boolean;
|
||||||
|
member?: RoomMember;
|
||||||
|
verificationRequest?: VerificationRequest;
|
||||||
|
verificationRequestPromise?: Promise<VerificationRequest>;
|
||||||
|
space?: Room;
|
||||||
|
widgetId?: string;
|
||||||
|
groupRoomId?: string;
|
||||||
|
groupId?: string;
|
||||||
|
event: MatrixEvent;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.RightPanel")
|
@replaceableComponent("structures.RightPanel")
|
||||||
export default class RightPanel extends React.Component {
|
export default class RightPanel extends React.Component<IProps, IState> {
|
||||||
static get propTypes() {
|
|
||||||
return {
|
|
||||||
room: PropTypes.instanceOf(Room), // if showing panels for a given room, this is set
|
|
||||||
groupId: PropTypes.string, // if showing panels for a given group, this is set
|
|
||||||
user: PropTypes.object, // used if we know the user ahead of opening the panel
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
|
private readonly delayedUpdate: RateLimitedFunc;
|
||||||
|
private dispatcherRef: string;
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.state = {
|
this.state = {
|
||||||
...RightPanelStore.getSharedInstance().roomPanelPhaseParams,
|
...RightPanelStore.getSharedInstance().roomPanelPhaseParams,
|
||||||
phase: this._getPhaseFromProps(),
|
phase: this.getPhaseFromProps(),
|
||||||
isUserPrivilegedInGroup: null,
|
isUserPrivilegedInGroup: null,
|
||||||
member: this._getUserForPanel(),
|
member: this.getUserForPanel(),
|
||||||
};
|
};
|
||||||
this.onAction = this.onAction.bind(this);
|
|
||||||
this.onRoomStateMember = this.onRoomStateMember.bind(this);
|
|
||||||
this.onGroupStoreUpdated = this.onGroupStoreUpdated.bind(this);
|
|
||||||
this.onInviteToGroupButtonClick = this.onInviteToGroupButtonClick.bind(this);
|
|
||||||
this.onAddRoomToGroupButtonClick = this.onAddRoomToGroupButtonClick.bind(this);
|
|
||||||
|
|
||||||
this._delayedUpdate = new RateLimitedFunc(() => {
|
this.delayedUpdate = new RateLimitedFunc(() => {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to split out the logic for _getPhaseFromProps() and the constructor
|
// Helper function to split out the logic for getPhaseFromProps() and the constructor
|
||||||
// as both are called at the same time in the constructor.
|
// as both are called at the same time in the constructor.
|
||||||
_getUserForPanel() {
|
private getUserForPanel() {
|
||||||
if (this.state && this.state.member) return this.state.member;
|
if (this.state && this.state.member) return this.state.member;
|
||||||
const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams;
|
const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams;
|
||||||
return this.props.user || lastParams['member'];
|
return this.props.user || lastParams['member'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// gets the current phase from the props and also maybe the store
|
// gets the current phase from the props and also maybe the store
|
||||||
_getPhaseFromProps() {
|
private getPhaseFromProps() {
|
||||||
const rps = RightPanelStore.getSharedInstance();
|
const rps = RightPanelStore.getSharedInstance();
|
||||||
const userForPanel = this._getUserForPanel();
|
const userForPanel = this.getUserForPanel();
|
||||||
if (this.props.groupId) {
|
if (this.props.groupId) {
|
||||||
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
|
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
|
||||||
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList});
|
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList});
|
||||||
|
@ -118,7 +140,7 @@ export default class RightPanel extends React.Component {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
const cli = this.context;
|
const cli = this.context;
|
||||||
cli.on("RoomState.members", this.onRoomStateMember);
|
cli.on("RoomState.members", this.onRoomStateMember);
|
||||||
this._initGroupStore(this.props.groupId);
|
this.initGroupStore(this.props.groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -126,61 +148,47 @@ export default class RightPanel extends React.Component {
|
||||||
if (this.context) {
|
if (this.context) {
|
||||||
this.context.removeListener("RoomState.members", this.onRoomStateMember);
|
this.context.removeListener("RoomState.members", this.onRoomStateMember);
|
||||||
}
|
}
|
||||||
this._unregisterGroupStore(this.props.groupId);
|
this.unregisterGroupStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
||||||
if (newProps.groupId !== this.props.groupId) {
|
if (newProps.groupId !== this.props.groupId) {
|
||||||
this._unregisterGroupStore(this.props.groupId);
|
this.unregisterGroupStore();
|
||||||
this._initGroupStore(newProps.groupId);
|
this.initGroupStore(newProps.groupId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_initGroupStore(groupId) {
|
private initGroupStore(groupId: string) {
|
||||||
if (!groupId) return;
|
if (!groupId) return;
|
||||||
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
|
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
_unregisterGroupStore() {
|
private unregisterGroupStore() {
|
||||||
GroupStore.unregisterListener(this.onGroupStoreUpdated);
|
GroupStore.unregisterListener(this.onGroupStoreUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupStoreUpdated() {
|
private onGroupStoreUpdated = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
|
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onInviteToGroupButtonClick() {
|
private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => {
|
||||||
showGroupInviteDialog(this.props.groupId).then(() => {
|
|
||||||
this.setState({
|
|
||||||
phase: RightPanelPhases.GroupMemberList,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onAddRoomToGroupButtonClick() {
|
|
||||||
showGroupAddRoomDialog(this.props.groupId).then(() => {
|
|
||||||
this.forceUpdate();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onRoomStateMember(ev, state, member) {
|
|
||||||
if (!this.props.room || member.roomId !== this.props.room.roomId) {
|
if (!this.props.room || member.roomId !== this.props.room.roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// redraw the badge on the membership list
|
// redraw the badge on the membership list
|
||||||
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) {
|
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) {
|
||||||
this._delayedUpdate();
|
this.delayedUpdate();
|
||||||
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId &&
|
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId &&
|
||||||
member.userId === this.state.member.userId) {
|
member.userId === this.state.member.userId) {
|
||||||
// refresh the member info (e.g. new power level)
|
// refresh the member info (e.g. new power level)
|
||||||
this._delayedUpdate();
|
this.delayedUpdate();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onAction(payload) {
|
private onAction = (payload: ActionPayload) => {
|
||||||
if (payload.action === Action.AfterRightPanelPhaseChange) {
|
if (payload.action === Action.AfterRightPanelPhaseChange) {
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: payload.phase,
|
phase: payload.phase,
|
||||||
|
@ -194,9 +202,9 @@ export default class RightPanel extends React.Component {
|
||||||
space: payload.space,
|
space: payload.space,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
onClose = () => {
|
private onClose = () => {
|
||||||
// XXX: There are three different ways of 'closing' this panel depending on what state
|
// XXX: There are three different ways of 'closing' this panel depending on what state
|
||||||
// things are in... this knows far more than it should do about the state of the rest
|
// things are in... this knows far more than it should do about the state of the rest
|
||||||
// of the app and is generally a bit silly.
|
// of the app and is generally a bit silly.
|
||||||
|
@ -224,16 +232,6 @@ export default class RightPanel extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const MemberList = sdk.getComponent('rooms.MemberList');
|
|
||||||
const UserInfo = sdk.getComponent('right_panel.UserInfo');
|
|
||||||
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
|
|
||||||
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
|
||||||
const FilePanel = sdk.getComponent('structures.FilePanel');
|
|
||||||
|
|
||||||
const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
|
|
||||||
const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
|
|
||||||
const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
|
|
||||||
|
|
||||||
let panel = <div />;
|
let panel = <div />;
|
||||||
const roomId = this.props.room ? this.props.room.roomId : undefined;
|
const roomId = this.props.room ? this.props.room.roomId : undefined;
|
||||||
|
|
||||||
|
@ -285,6 +283,7 @@ export default class RightPanel extends React.Component {
|
||||||
user={this.state.member}
|
user={this.state.member}
|
||||||
groupId={this.props.groupId}
|
groupId={this.props.groupId}
|
||||||
key={this.state.member.userId}
|
key={this.state.member.userId}
|
||||||
|
phase={this.state.phase}
|
||||||
onClose={this.onClose} />;
|
onClose={this.onClose} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -299,6 +298,12 @@ export default class RightPanel extends React.Component {
|
||||||
panel = <NotificationPanel onClose={this.onClose} />;
|
panel = <NotificationPanel onClose={this.onClose} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case RightPanelPhases.PinnedMessages:
|
||||||
|
if (SettingsStore.getValue("feature_pinning")) {
|
||||||
|
panel = <PinnedMessagesCard room={this.props.room} onClose={this.onClose} />;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case RightPanelPhases.FilePanel:
|
case RightPanelPhases.FilePanel:
|
||||||
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
|
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
|
||||||
break;
|
break;
|
|
@ -46,7 +46,7 @@ import RoomViewStore from '../../stores/RoomViewStore';
|
||||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import {Layout} from "../../settings/Layout";
|
import { Layout } from "../../settings/Layout";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
import RightPanelStore from "../../stores/RightPanelStore";
|
import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||||
|
@ -54,7 +54,6 @@ 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";
|
||||||
|
@ -62,7 +61,6 @@ import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||||
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
||||||
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";
|
||||||
|
@ -81,7 +79,8 @@ import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager';
|
||||||
import { objectHasDiff } from "../../utils/objects";
|
import { objectHasDiff } from "../../utils/objects";
|
||||||
import SpaceRoomView from "./SpaceRoomView";
|
import SpaceRoomView from "./SpaceRoomView";
|
||||||
import { IOpts } from "../../createRoom";
|
import { IOpts } from "../../createRoom";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import { omit } from 'lodash';
|
||||||
import UIStore from "../../stores/UIStore";
|
import UIStore from "../../stores/UIStore";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
|
@ -154,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
|
||||||
|
@ -174,6 +172,7 @@ export interface IState {
|
||||||
statusBarVisible: boolean;
|
statusBarVisible: boolean;
|
||||||
// We load this later by asking the js-sdk to suggest a version for us.
|
// We load this later by asking the js-sdk to suggest a version for us.
|
||||||
// This object is the result of Room#getRecommendedVersion()
|
// This object is the result of Room#getRecommendedVersion()
|
||||||
|
|
||||||
upgradeRecommendation?: {
|
upgradeRecommendation?: {
|
||||||
version: string;
|
version: string;
|
||||||
needsUpgrade: boolean;
|
needsUpgrade: boolean;
|
||||||
|
@ -231,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,
|
||||||
|
@ -325,7 +323,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
replyToEvent: RoomViewStore.getQuotingEvent(),
|
replyToEvent: RoomViewStore.getQuotingEvent(),
|
||||||
// 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(),
|
||||||
};
|
};
|
||||||
|
@ -526,7 +523,20 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
return (objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, nextState));
|
const hasPropsDiff = objectHasDiff(this.props, nextProps);
|
||||||
|
|
||||||
|
// React only shallow comparison and we only want to trigger
|
||||||
|
// a component re-render if a room requires an upgrade
|
||||||
|
const newUpgradeRecommendation = nextState.upgradeRecommendation || {}
|
||||||
|
|
||||||
|
const state = omit(this.state, ['upgradeRecommendation']);
|
||||||
|
const newState = omit(nextState, ['upgradeRecommendation'])
|
||||||
|
|
||||||
|
const hasStateDiff =
|
||||||
|
objectHasDiff(state, newState) ||
|
||||||
|
(newUpgradeRecommendation.needsUpgrade === true)
|
||||||
|
|
||||||
|
return hasPropsDiff || hasStateDiff;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
|
@ -639,6 +649,17 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
SettingsStore.unwatchSetting(this.layoutWatcherRef);
|
SettingsStore.unwatchSetting(this.layoutWatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onUserScroll = () => {
|
||||||
|
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: this.state.room.roomId,
|
||||||
|
event_id: this.state.initialEventId,
|
||||||
|
highlighted: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private onLayoutChange = () => {
|
private onLayoutChange = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
layout: SettingsStore.getValue("layout"),
|
layout: SettingsStore.getValue("layout"),
|
||||||
|
@ -809,7 +830,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onEvent = (ev) => {
|
private onEvent = (ev) => {
|
||||||
if (ev.isBeingDecrypted() || ev.isDecryptionFailure() || ev.shouldAttemptDecryption()) return;
|
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
|
||||||
this.handleEffects(ev);
|
this.handleEffects(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1374,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',
|
||||||
|
@ -1485,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,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1498,8 +1511,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// jump down to the bottom of this room, where new events are arriving
|
// jump down to the bottom of this room, where new events are arriving
|
||||||
private jumpToLiveTimeline = () => {
|
private jumpToLiveTimeline = () => {
|
||||||
this.messagePanel.jumpToLiveTimeline();
|
dis.dispatch({
|
||||||
dis.fire(Action.FocusComposer);
|
action: 'view_room',
|
||||||
|
room_id: this.state.room.roomId,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// jump up to wherever our read marker is
|
// jump up to wherever our read marker is
|
||||||
|
@ -1807,8 +1822,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} />;
|
||||||
} else if (this.state.showingPinned) {
|
|
||||||
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.
|
||||||
|
@ -1960,6 +1973,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
eventId={this.state.initialEventId}
|
eventId={this.state.initialEventId}
|
||||||
eventPixelOffset={this.state.initialEventPixelOffset}
|
eventPixelOffset={this.state.initialEventPixelOffset}
|
||||||
onScroll={this.onMessageListScroll}
|
onScroll={this.onMessageListScroll}
|
||||||
|
onUserScroll={this.onUserScroll}
|
||||||
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
|
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
|
||||||
showUrlPreview = {this.state.showUrlPreview}
|
showUrlPreview = {this.state.showUrlPreview}
|
||||||
className={messagePanelClassNames}
|
className={messagePanelClassNames}
|
||||||
|
@ -1986,6 +2000,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
highlight={this.state.room.getUnreadNotificationCount('highlight') > 0}
|
highlight={this.state.room.getUnreadNotificationCount('highlight') > 0}
|
||||||
numUnreadMessages={this.state.numUnreadMessages}
|
numUnreadMessages={this.state.numUnreadMessages}
|
||||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||||
|
roomId={this.state.roomId}
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2022,7 +2037,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}
|
|
||||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||||
e2eStatus={this.state.e2eStatus}
|
e2eStatus={this.state.e2eStatus}
|
||||||
|
|
|
@ -133,6 +133,10 @@ export default class ScrollPanel extends React.Component {
|
||||||
*/
|
*/
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
|
|
||||||
|
/* onUserScroll: callback which is called when the user interacts with the room timeline
|
||||||
|
*/
|
||||||
|
onUserScroll: PropTypes.func,
|
||||||
|
|
||||||
/* className: classnames to add to the top-level div
|
/* className: classnames to add to the top-level div
|
||||||
*/
|
*/
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
@ -535,21 +539,29 @@ export default class ScrollPanel extends React.Component {
|
||||||
* @param {object} ev the keyboard event
|
* @param {object} ev the keyboard event
|
||||||
*/
|
*/
|
||||||
handleScrollKey = ev => {
|
handleScrollKey = ev => {
|
||||||
|
let isScrolling = false;
|
||||||
const roomAction = getKeyBindingsManager().getRoomAction(ev);
|
const roomAction = getKeyBindingsManager().getRoomAction(ev);
|
||||||
switch (roomAction) {
|
switch (roomAction) {
|
||||||
case RoomAction.ScrollUp:
|
case RoomAction.ScrollUp:
|
||||||
this.scrollRelative(-1);
|
this.scrollRelative(-1);
|
||||||
|
isScrolling = true;
|
||||||
break;
|
break;
|
||||||
case RoomAction.RoomScrollDown:
|
case RoomAction.RoomScrollDown:
|
||||||
this.scrollRelative(1);
|
this.scrollRelative(1);
|
||||||
|
isScrolling = true;
|
||||||
break;
|
break;
|
||||||
case RoomAction.JumpToFirstMessage:
|
case RoomAction.JumpToFirstMessage:
|
||||||
this.scrollToTop();
|
this.scrollToTop();
|
||||||
|
isScrolling = true;
|
||||||
break;
|
break;
|
||||||
case RoomAction.JumpToLatestMessage:
|
case RoomAction.JumpToLatestMessage:
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
|
isScrolling = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (isScrolling && this.props.onUserScroll) {
|
||||||
|
this.props.onUserScroll(ev);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Scroll the panel to bring the DOM node with the scroll token
|
/* Scroll the panel to bring the DOM node with the scroll token
|
||||||
|
@ -888,9 +900,8 @@ export default class ScrollPanel extends React.Component {
|
||||||
<AutoHideScrollbar
|
<AutoHideScrollbar
|
||||||
wrappedRef={this._collectScroll}
|
wrappedRef={this._collectScroll}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
className={`mx_ScrollPanel ${this.props.className}`}
|
onWheel={this.props.onUserScroll}
|
||||||
style={this.props.style}
|
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
|
||||||
>
|
|
||||||
{ this.props.fixedChildren }
|
{ this.props.fixedChildren }
|
||||||
<div className="mx_RoomView_messageListWrapper">
|
<div className="mx_RoomView_messageListWrapper">
|
||||||
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
|
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
|
||||||
|
|
|
@ -36,7 +36,6 @@ import shouldHideEvent from '../../shouldHideEvent';
|
||||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||||
import {UIFeature} from "../../settings/UIFeature";
|
import {UIFeature} from "../../settings/UIFeature";
|
||||||
import {objectHasDiff} from "../../utils/objects";
|
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
import { arrayFastClone } from "../../utils/arrays";
|
import { arrayFastClone } from "../../utils/arrays";
|
||||||
|
|
||||||
|
@ -94,6 +93,9 @@ class TimelinePanel extends React.Component {
|
||||||
// callback which is called when the panel is scrolled.
|
// callback which is called when the panel is scrolled.
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
|
|
||||||
|
// callback which is called when the user interacts with the room timeline
|
||||||
|
onUserScroll: PropTypes.func,
|
||||||
|
|
||||||
// callback which is called when the read-up-to mark is updated.
|
// callback which is called when the read-up-to mark is updated.
|
||||||
onReadMarkerUpdated: PropTypes.func,
|
onReadMarkerUpdated: PropTypes.func,
|
||||||
|
|
||||||
|
@ -258,37 +260,15 @@ class TimelinePanel extends React.Component {
|
||||||
console.warn("Replacing timelineSet on a TimelinePanel - confusion may ensue");
|
console.warn("Replacing timelineSet on a TimelinePanel - confusion may ensue");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newProps.eventId != this.props.eventId) {
|
const differentEventId = newProps.eventId != this.props.eventId;
|
||||||
|
const differentHighlightedEventId = newProps.highlightedEventId != this.props.highlightedEventId;
|
||||||
|
if (differentEventId || differentHighlightedEventId) {
|
||||||
console.log("TimelinePanel switching to eventId " + newProps.eventId +
|
console.log("TimelinePanel switching to eventId " + newProps.eventId +
|
||||||
" (was " + this.props.eventId + ")");
|
" (was " + this.props.eventId + ")");
|
||||||
return this._initTimeline(newProps);
|
return this._initTimeline(newProps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
if (objectHasDiff(this.props, nextProps)) {
|
|
||||||
if (DEBUG) {
|
|
||||||
console.group("Timeline.shouldComponentUpdate: props change");
|
|
||||||
console.log("props before:", this.props);
|
|
||||||
console.log("props after:", nextProps);
|
|
||||||
console.groupEnd();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (objectHasDiff(this.state, nextState)) {
|
|
||||||
if (DEBUG) {
|
|
||||||
console.group("Timeline.shouldComponentUpdate: state change");
|
|
||||||
console.log("state before:", this.state);
|
|
||||||
console.log("state after:", nextState);
|
|
||||||
console.groupEnd();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
// set a boolean to say we've been unmounted, which any pending
|
// set a boolean to say we've been unmounted, which any pending
|
||||||
// promises can use to throw away their results.
|
// promises can use to throw away their results.
|
||||||
|
@ -1456,6 +1436,7 @@ class TimelinePanel extends React.Component {
|
||||||
ourUserId={MatrixClientPeg.get().credentials.userId}
|
ourUserId={MatrixClientPeg.get().credentials.userId}
|
||||||
stickyBottom={stickyBottom}
|
stickyBottom={stickyBottom}
|
||||||
onScroll={this.onMessageListScroll}
|
onScroll={this.onMessageListScroll}
|
||||||
|
onUserScroll={this.props.onUserScroll}
|
||||||
onFillRequest={this.onMessageListFillRequest}
|
onFillRequest={this.onMessageListFillRequest}
|
||||||
onUnfillRequest={this.onMessageListUnfillRequest}
|
onUnfillRequest={this.onMessageListUnfillRequest}
|
||||||
isTwelveHour={this.state.isTwelveHour}
|
isTwelveHour={this.state.isTwelveHour}
|
||||||
|
|
|
@ -55,6 +55,7 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
||||||
const totalCount = this.state.toasts.length;
|
const totalCount = this.state.toasts.length;
|
||||||
const isStacked = totalCount > 1;
|
const isStacked = totalCount > 1;
|
||||||
let toast;
|
let toast;
|
||||||
|
let containerClasses;
|
||||||
if (totalCount !== 0) {
|
if (totalCount !== 0) {
|
||||||
const topToast = this.state.toasts[0];
|
const topToast = this.state.toasts[0];
|
||||||
const {title, icon, key, component, className, props} = topToast;
|
const {title, icon, key, component, className, props} = topToast;
|
||||||
|
@ -79,16 +80,17 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
|
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
|
||||||
|
|
||||||
const containerClasses = classNames("mx_ToastContainer", {
|
containerClasses = classNames("mx_ToastContainer", {
|
||||||
"mx_ToastContainer_stacked": isStacked,
|
"mx_ToastContainer_stacked": isStacked,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
return (
|
return toast
|
||||||
|
? (
|
||||||
<div className={containerClasses} role="alert">
|
<div className={containerClasses} role="alert">
|
||||||
{toast}
|
{toast}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -17,9 +17,9 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
import { EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||||
|
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -28,9 +28,10 @@ import Resend from '../../../Resend';
|
||||||
import SettingsStore from '../../../settings/SettingsStore';
|
import SettingsStore from '../../../settings/SettingsStore';
|
||||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||||
import { isContentActionable } from '../../../utils/EventUtils';
|
import { isContentActionable } from '../../../utils/EventUtils';
|
||||||
import {MenuItem} from "../../structures/ContextMenu";
|
import { MenuItem } from "../../structures/ContextMenu";
|
||||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
|
||||||
import ForwardDialog from "../dialogs/ForwardDialog";
|
import ForwardDialog from "../dialogs/ForwardDialog";
|
||||||
|
|
||||||
export function canCancel(eventStatus) {
|
export function canCancel(eventStatus) {
|
||||||
|
@ -83,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;
|
||||||
|
@ -93,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());
|
||||||
|
@ -166,25 +167,23 @@ export default class MessageContextMenu extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -212,7 +212,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||||
autoComplete={true}
|
autoComplete={true}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
|
<AutoHideScrollbar className="mx_AddExistingToSpace_content">
|
||||||
{ rooms.length > 0 ? (
|
{ rooms.length > 0 ? (
|
||||||
<div className="mx_AddExistingToSpace_section">
|
<div className="mx_AddExistingToSpace_section">
|
||||||
<h3>{ _t("Rooms") }</h3>
|
<h3>{ _t("Rooms") }</h3>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -49,6 +49,7 @@ import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
import {getAddressType} from "../../../UserAddress";
|
import {getAddressType} from "../../../UserAddress";
|
||||||
import BaseAvatar from '../avatars/BaseAvatar';
|
import BaseAvatar from '../avatars/BaseAvatar';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import { compare } from '../../../utils/strings';
|
||||||
|
|
||||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
@ -578,7 +579,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
members.sort((a, b) => {
|
members.sort((a, b) => {
|
||||||
if (a.score === b.score) {
|
if (a.score === b.score) {
|
||||||
if (a.numRooms === b.numRooms) {
|
if (a.numRooms === b.numRooms) {
|
||||||
return a.member.userId.localeCompare(b.member.userId);
|
return compare(a.member.userId, b.member.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.numRooms - a.numRooms;
|
return b.numRooms - a.numRooms;
|
||||||
|
|
|
@ -39,6 +39,7 @@ import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
import TextInputDialog from "../dialogs/TextInputDialog";
|
import TextInputDialog from "../dialogs/TextInputDialog";
|
||||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
import UIStore from "../../../stores/UIStore";
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
import { compare } from "../../../utils/strings";
|
||||||
|
|
||||||
export const ALL_ROOMS = Symbol("ALL_ROOMS");
|
export const ALL_ROOMS = Symbol("ALL_ROOMS");
|
||||||
|
|
||||||
|
@ -187,7 +188,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
|
||||||
|
|
||||||
protocolsList.forEach(({instances=[]}) => {
|
protocolsList.forEach(({instances=[]}) => {
|
||||||
[...instances].sort((b, a) => {
|
[...instances].sort((b, a) => {
|
||||||
return a.desc.localeCompare(b.desc);
|
return compare(a.desc, b.desc);
|
||||||
}).forEach(({desc, instance_id: instanceId}) => {
|
}).forEach(({desc, instance_id: instanceId}) => {
|
||||||
entries.push(
|
entries.push(
|
||||||
<MenuItemRadio
|
<MenuItemRadio
|
||||||
|
|
|
@ -75,7 +75,7 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
|
||||||
label={tooltip || title}
|
label={tooltip || title}
|
||||||
yOffset={yOffset}
|
yOffset={yOffset}
|
||||||
alignment={alignment}
|
alignment={alignment}
|
||||||
/> : <div />;
|
/> : null;
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -116,7 +116,7 @@ export default class Flair extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.profiles.length === 0) {
|
if (this.state.profiles.length === 0) {
|
||||||
return <span className="mx_Flair" />;
|
return null;
|
||||||
}
|
}
|
||||||
const avatars = this.state.profiles.map((profile, index) => {
|
const avatars = this.state.profiles.map((profile, index) => {
|
||||||
return <FlairAvatar key={index} groupProfile={profile} />;
|
return <FlairAvatar key={index} groupProfile={profile} />;
|
||||||
|
|
|
@ -214,7 +214,7 @@ export default class ReplyThread extends React.Component {
|
||||||
|
|
||||||
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) {
|
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) {
|
||||||
if (!ReplyThread.getParentEventId(parentEv)) {
|
if (!ReplyThread.getParentEventId(parentEv)) {
|
||||||
return <div className="mx_ReplyThread_wrapper_empty" />;
|
return null;
|
||||||
}
|
}
|
||||||
return <ReplyThread
|
return <ReplyThread
|
||||||
parentEv={parentEv}
|
parentEv={parentEv}
|
||||||
|
@ -269,39 +269,30 @@ export default class ReplyThread extends React.Component {
|
||||||
const {parentEv} = this.props;
|
const {parentEv} = this.props;
|
||||||
// at time of making this component we checked that props.parentEv has a parentEventId
|
// at time of making this component we checked that props.parentEv has a parentEventId
|
||||||
const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv));
|
const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv));
|
||||||
|
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
|
|
||||||
if (ev) {
|
if (ev) {
|
||||||
|
const loadedEv = await this.getNextEvent(ev);
|
||||||
this.setState({
|
this.setState({
|
||||||
events: [ev],
|
events: [ev],
|
||||||
}, this.loadNextEvent);
|
loadedEv,
|
||||||
} else {
|
|
||||||
this.setState({err: true});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadNextEvent() {
|
|
||||||
if (this.unmounted) return;
|
|
||||||
const ev = this.state.events[0];
|
|
||||||
const inReplyToEventId = ReplyThread.getParentEventId(ev);
|
|
||||||
|
|
||||||
if (!inReplyToEventId) {
|
|
||||||
this.setState({
|
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadedEv = await this.getEvent(inReplyToEventId);
|
|
||||||
if (this.unmounted) return;
|
|
||||||
|
|
||||||
if (loadedEv) {
|
|
||||||
this.setState({loadedEv});
|
|
||||||
} else {
|
} else {
|
||||||
this.setState({err: true});
|
this.setState({err: true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getNextEvent(ev) {
|
||||||
|
try {
|
||||||
|
const inReplyToEventId = ReplyThread.getParentEventId(ev);
|
||||||
|
return await this.getEvent(inReplyToEventId);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getEvent(eventId) {
|
async getEvent(eventId) {
|
||||||
const event = this.room.findEventById(eventId);
|
const event = this.room.findEventById(eventId);
|
||||||
if (event) return event;
|
if (event) return event;
|
||||||
|
@ -326,13 +317,18 @@ export default class ReplyThread extends React.Component {
|
||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
onQuoteClick() {
|
async onQuoteClick() {
|
||||||
const events = [this.state.loadedEv, ...this.state.events];
|
const events = [this.state.loadedEv, ...this.state.events];
|
||||||
|
|
||||||
|
let loadedEv = null;
|
||||||
|
if (events.length > 0) {
|
||||||
|
loadedEv = await this.getNextEvent(events[0]);
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loadedEv: null,
|
loadedEv,
|
||||||
events,
|
events,
|
||||||
}, this.loadNextEvent);
|
});
|
||||||
|
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusComposer);
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,10 @@ export default class Tooltip extends React.Component<IProps> {
|
||||||
this.tooltipContainer = document.createElement("div");
|
this.tooltipContainer = document.createElement("div");
|
||||||
this.tooltipContainer.className = "mx_Tooltip_wrapper";
|
this.tooltipContainer.className = "mx_Tooltip_wrapper";
|
||||||
document.body.appendChild(this.tooltipContainer);
|
document.body.appendChild(this.tooltipContainer);
|
||||||
window.addEventListener('scroll', this.renderTooltip, true);
|
window.addEventListener('scroll', this.renderTooltip, {
|
||||||
|
passive: true,
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
|
||||||
this.parent = ReactDOM.findDOMNode(this).parentNode as Element;
|
this.parent = ReactDOM.findDOMNode(this).parentNode as Element;
|
||||||
|
|
||||||
|
@ -85,7 +88,9 @@ export default class Tooltip extends React.Component<IProps> {
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
ReactDOM.unmountComponentAtNode(this.tooltipContainer);
|
ReactDOM.unmountComponentAtNode(this.tooltipContainer);
|
||||||
document.body.removeChild(this.tooltipContainer);
|
document.body.removeChild(this.tooltipContainer);
|
||||||
window.removeEventListener('scroll', this.renderTooltip, true);
|
window.removeEventListener('scroll', this.renderTooltip, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private updatePosition(style: CSSProperties) {
|
private updatePosition(style: CSSProperties) {
|
||||||
|
|
|
@ -31,21 +31,23 @@ export default class SenderProfile extends React.Component {
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
state = {
|
constructor(props) {
|
||||||
userGroups: null,
|
super(props);
|
||||||
|
const senderId = this.props.mxEvent.getSender();
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
userGroups: FlairStore.cachedPublicisedGroups(senderId) || [],
|
||||||
relatedGroups: [],
|
relatedGroups: [],
|
||||||
};
|
};
|
||||||
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.unmounted = false;
|
this.unmounted = false;
|
||||||
this._updateRelatedGroups();
|
this._updateRelatedGroups();
|
||||||
|
|
||||||
FlairStore.getPublicisedGroupsCached(
|
if (this.state.userGroups.length === 0) {
|
||||||
this.context, this.props.mxEvent.getSender(),
|
this.getPublicisedGroups();
|
||||||
).then((userGroups) => {
|
}
|
||||||
if (this.unmounted) return;
|
|
||||||
this.setState({userGroups});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.context.on('RoomState.events', this.onRoomStateEvents);
|
this.context.on('RoomState.events', this.onRoomStateEvents);
|
||||||
}
|
}
|
||||||
|
@ -55,6 +57,15 @@ export default class SenderProfile extends React.Component {
|
||||||
this.context.removeListener('RoomState.events', this.onRoomStateEvents);
|
this.context.removeListener('RoomState.events', this.onRoomStateEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPublicisedGroups() {
|
||||||
|
if (!this.unmounted) {
|
||||||
|
const userGroups = await FlairStore.getPublicisedGroupsCached(
|
||||||
|
this.context, this.props.mxEvent.getSender(),
|
||||||
|
);
|
||||||
|
this.setState({userGroups});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onRoomStateEvents = event => {
|
onRoomStateEvents = event => {
|
||||||
if (event.getType() === 'm.room.related_groups' &&
|
if (event.getType() === 'm.room.related_groups' &&
|
||||||
event.getRoomId() === this.props.mxEvent.getRoomId()
|
event.getRoomId() === this.props.mxEvent.getRoomId()
|
||||||
|
@ -93,10 +104,10 @@ export default class SenderProfile extends React.Component {
|
||||||
const {msgtype} = mxEvent.getContent();
|
const {msgtype} = mxEvent.getContent();
|
||||||
|
|
||||||
if (msgtype === 'm.emote') {
|
if (msgtype === 'm.emote') {
|
||||||
return <span />; // emote message must include the name so don't duplicate it
|
return null; // emote message must include the name so don't duplicate it
|
||||||
}
|
}
|
||||||
|
|
||||||
let flair = <div />;
|
let flair = null;
|
||||||
if (this.props.enableFlair) {
|
if (this.props.enableFlair) {
|
||||||
const displayedGroups = this._getDisplayedGroups(
|
const displayedGroups = this._getDisplayedGroups(
|
||||||
this.state.userGroups, this.state.relatedGroups,
|
this.state.userGroups, this.state.relatedGroups,
|
||||||
|
@ -110,19 +121,12 @@ export default class SenderProfile extends React.Component {
|
||||||
|
|
||||||
const nameElem = name || '';
|
const nameElem = name || '';
|
||||||
|
|
||||||
// Name + flair
|
return (
|
||||||
const nameFlair = <span>
|
<div className="mx_SenderProfile mx_SenderProfile_hover" dir="auto" onClick={this.props.onClick}>
|
||||||
<span className={`mx_SenderProfile_name ${colorClass}`}>
|
<span className={`mx_SenderProfile_name ${colorClass}`}>
|
||||||
{ nameElem }
|
{ nameElem }
|
||||||
</span>
|
</span>
|
||||||
{ flair }
|
{ flair }
|
||||||
</span>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
|
|
||||||
<div className="mx_SenderProfile_hover">
|
|
||||||
{ nameFlair }
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -278,15 +278,15 @@ export default class TextualBody extends React.Component {
|
||||||
// pass only the first child which is the event tile otherwise this recurses on edited events
|
// pass only the first child which is the event tile otherwise this recurses on edited events
|
||||||
let links = this.findLinks([this._content.current]);
|
let links = this.findLinks([this._content.current]);
|
||||||
if (links.length) {
|
if (links.length) {
|
||||||
// de-dup the links (but preserve ordering)
|
// de-duplicate the links after stripping hashes as they don't affect the preview
|
||||||
const seen = new Set();
|
// using a set here maintains the order
|
||||||
links = links.filter((link) => {
|
links = Array.from(new Set(links.map(link => {
|
||||||
if (seen.has(link)) return false;
|
const url = new URL(link);
|
||||||
seen.add(link);
|
url.hash = "";
|
||||||
return true;
|
return url.toString();
|
||||||
});
|
})));
|
||||||
|
|
||||||
this.setState({ links: links });
|
this.setState({ links });
|
||||||
|
|
||||||
// lazy-load the hidden state of the preview widget from localstorage
|
// lazy-load the hidden state of the preview widget from localstorage
|
||||||
if (global.localStorage) {
|
if (global.localStorage) {
|
||||||
|
|
|
@ -21,12 +21,12 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import HeaderButton from './HeaderButton';
|
import HeaderButton from './HeaderButton';
|
||||||
import HeaderButtons, {HeaderKind} from './HeaderButtons';
|
import HeaderButtons, { HeaderKind } from './HeaderButtons';
|
||||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import {ActionPayload} from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
|
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
const GROUP_PHASES = [
|
const GROUP_PHASES = [
|
||||||
RightPanelPhases.GroupMemberInfo,
|
RightPanelPhases.GroupMemberInfo,
|
||||||
|
@ -84,19 +84,21 @@ export default class GroupHeaderButtons extends HeaderButtons {
|
||||||
};
|
};
|
||||||
|
|
||||||
renderButtons() {
|
renderButtons() {
|
||||||
return [
|
return <>
|
||||||
<HeaderButton key="groupMembersButton" name="groupMembersButton"
|
<HeaderButton
|
||||||
|
name="groupMembersButton"
|
||||||
title={_t('Members')}
|
title={_t('Members')}
|
||||||
isHighlighted={this.isPhase(GROUP_PHASES)}
|
isHighlighted={this.isPhase(GROUP_PHASES)}
|
||||||
onClick={this.onMembersClicked}
|
onClick={this.onMembersClicked}
|
||||||
analytics={['Right Panel', 'Group Member List Button', 'click']}
|
analytics={['Right Panel', 'Group Member List Button', 'click']}
|
||||||
/>,
|
/>
|
||||||
<HeaderButton key="roomsButton" name="roomsButton"
|
<HeaderButton
|
||||||
|
name="roomsButton"
|
||||||
title={_t('Rooms')}
|
title={_t('Rooms')}
|
||||||
isHighlighted={this.isPhase(ROOM_PHASES)}
|
isHighlighted={this.isPhase(ROOM_PHASES)}
|
||||||
onClick={this.onRoomsClicked}
|
onClick={this.onRoomsClicked}
|
||||||
analytics={['Right Panel', 'Group Room List Button', 'click']}
|
analytics={['Right Panel', 'Group Room List Button', 'click']}
|
||||||
/>,
|
/>
|
||||||
];
|
</>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,15 +22,13 @@ import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Analytics from '../../../Analytics';
|
import Analytics from '../../../Analytics';
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// Whether this button is highlighted
|
// Whether this button is highlighted
|
||||||
isHighlighted: boolean;
|
isHighlighted: boolean;
|
||||||
// click handler
|
// click handler
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
// The badge to display above the icon
|
|
||||||
badge?: React.ReactNode;
|
|
||||||
// The parameters to track the click event
|
// The parameters to track the click event
|
||||||
analytics: Parameters<typeof Analytics.trackEvent>;
|
analytics: Parameters<typeof Analytics.trackEvent>;
|
||||||
|
|
||||||
|
@ -40,31 +38,29 @@ interface IProps {
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: replace this, the composer buttons and the right panel buttons with a unified
|
// TODO: replace this, the composer buttons and the right panel buttons with a unified representation
|
||||||
// representation
|
|
||||||
@replaceableComponent("views.right_panel.HeaderButton")
|
@replaceableComponent("views.right_panel.HeaderButton")
|
||||||
export default class HeaderButton extends React.Component<IProps> {
|
export default class HeaderButton extends React.Component<IProps> {
|
||||||
constructor(props: IProps) {
|
private onClick = () => {
|
||||||
super(props);
|
|
||||||
this.onClick = this.onClick.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onClick() {
|
|
||||||
Analytics.trackEvent(...this.props.analytics);
|
Analytics.trackEvent(...this.props.analytics);
|
||||||
this.props.onClick();
|
this.props.onClick();
|
||||||
}
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const {isHighlighted, onClick, analytics, name, title, ...props} = this.props;
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
mx_RightPanel_headerButton: true,
|
mx_RightPanel_headerButton: true,
|
||||||
mx_RightPanel_headerButton_highlight: this.props.isHighlighted,
|
mx_RightPanel_headerButton_highlight: isHighlighted,
|
||||||
[`mx_RightPanel_${this.props.name}`]: true,
|
[`mx_RightPanel_${name}`]: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <AccessibleTooltipButton
|
return <AccessibleTooltipButton
|
||||||
aria-selected={this.props.isHighlighted}
|
{...props}
|
||||||
|
aria-selected={isHighlighted}
|
||||||
role="tab"
|
role="tab"
|
||||||
title={this.props.title}
|
title={title}
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -21,14 +21,14 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import RightPanelStore from "../../../stores/RightPanelStore";
|
import RightPanelStore from "../../../stores/RightPanelStore";
|
||||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||||
import {Action} from '../../../dispatcher/actions';
|
import { Action } from '../../../dispatcher/actions';
|
||||||
import {
|
import {
|
||||||
SetRightPanelPhasePayload,
|
SetRightPanelPhasePayload,
|
||||||
SetRightPanelPhaseRefireParams,
|
SetRightPanelPhaseRefireParams,
|
||||||
} from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
|
} from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
|
||||||
import {EventSubscription} from "fbemitter";
|
import type { EventSubscription } from "fbemitter";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
export enum HeaderKind {
|
export enum HeaderKind {
|
||||||
Room = "room",
|
Room = "room",
|
||||||
|
@ -43,11 +43,11 @@ interface IState {
|
||||||
interface IProps {}
|
interface IProps {}
|
||||||
|
|
||||||
@replaceableComponent("views.right_panel.HeaderButtons")
|
@replaceableComponent("views.right_panel.HeaderButtons")
|
||||||
export default abstract class HeaderButtons extends React.Component<IProps, IState> {
|
export default abstract class HeaderButtons<P = {}> extends React.Component<IProps & P, IState> {
|
||||||
private storeToken: EventSubscription;
|
private storeToken: EventSubscription;
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
|
|
||||||
constructor(props: IProps, kind: HeaderKind) {
|
constructor(props: IProps & P, kind: HeaderKind) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const rps = RightPanelStore.getSharedInstance();
|
const rps = RightPanelStore.getSharedInstance();
|
||||||
|
@ -95,7 +95,7 @@ export default abstract class HeaderButtons extends React.Component<IProps, ISta
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: Make renderButtons a prop
|
// XXX: Make renderButtons a prop
|
||||||
public abstract renderButtons(): JSX.Element[];
|
public abstract renderButtons(): JSX.Element;
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return <div className="mx_HeaderButtons">
|
return <div className="mx_HeaderButtons">
|
||||||
|
|
176
src/components/views/right_panel/PinnedMessagesCard.tsx
Normal file
176
src/components/views/right_panel/PinnedMessagesCard.tsx
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
/*
|
||||||
|
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} />
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
content = <div className="mx_RightPanel_empty mx_PinnedMessagesCard_empty">
|
||||||
|
<h2>{_t("You’re all caught up")}</h2>
|
||||||
|
<p>{_t("You have no visible notifications.")}</p>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BaseCard
|
||||||
|
header={<h2>{ _t("Pinned messages") }</h2>}
|
||||||
|
className="mx_PinnedMessagesCard"
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
{ content }
|
||||||
|
</BaseCard>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PinnedMessagesCard;
|
|
@ -18,15 +18,19 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import {_t} from '../../../languageHandler';
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
import HeaderButton from './HeaderButton';
|
import HeaderButton from './HeaderButton';
|
||||||
import HeaderButtons, {HeaderKind} from './HeaderButtons';
|
import HeaderButtons, { HeaderKind } from './HeaderButtons';
|
||||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import {ActionPayload} from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import RightPanelStore from "../../../stores/RightPanelStore";
|
import RightPanelStore from "../../../stores/RightPanelStore";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { useSettingValue } from "../../../hooks/useSettings";
|
||||||
|
import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard';
|
||||||
|
|
||||||
const ROOM_INFO_PHASES = [
|
const ROOM_INFO_PHASES = [
|
||||||
RightPanelPhases.RoomSummary,
|
RightPanelPhases.RoomSummary,
|
||||||
|
@ -38,9 +42,35 @@ const ROOM_INFO_PHASES = [
|
||||||
RightPanelPhases.Room3pidMemberInfo,
|
RightPanelPhases.Room3pidMemberInfo,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => {
|
||||||
|
const pinningEnabled = useSettingValue("feature_pinning");
|
||||||
|
const pinnedEvents = usePinnedEvents(pinningEnabled && room);
|
||||||
|
const readPinnedEvents = useReadPinnedEvents(pinningEnabled && room);
|
||||||
|
if (!pinningEnabled) return null;
|
||||||
|
|
||||||
|
let unreadIndicator;
|
||||||
|
if (pinnedEvents.some(id => !readPinnedEvents.has(id))) {
|
||||||
|
unreadIndicator = <div className="mx_RightPanel_pinnedMessagesButton_unreadIndicator" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <HeaderButton
|
||||||
|
name="pinnedMessagesButton"
|
||||||
|
title={_t("Pinned messages")}
|
||||||
|
isHighlighted={isHighlighted}
|
||||||
|
onClick={onClick}
|
||||||
|
analytics={["Right Panel", "Pinned Messages Button", "click"]}
|
||||||
|
>
|
||||||
|
{ unreadIndicator }
|
||||||
|
</HeaderButton>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
room?: Room;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.right_panel.RoomHeaderButtons")
|
@replaceableComponent("views.right_panel.RoomHeaderButtons")
|
||||||
export default class RoomHeaderButtons extends HeaderButtons {
|
export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props, HeaderKind.Room);
|
super(props, HeaderKind.Room);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,24 +110,32 @@ export default class RoomHeaderButtons extends HeaderButtons {
|
||||||
this.setPhase(RightPanelPhases.NotificationPanel);
|
this.setPhase(RightPanelPhases.NotificationPanel);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onPinnedMessagesClicked = () => {
|
||||||
|
// This toggles for us, if needed
|
||||||
|
this.setPhase(RightPanelPhases.PinnedMessages);
|
||||||
|
};
|
||||||
|
|
||||||
public renderButtons() {
|
public renderButtons() {
|
||||||
return [
|
return <>
|
||||||
|
<PinnedMessagesHeaderButton
|
||||||
|
room={this.props.room}
|
||||||
|
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
|
||||||
|
onClick={this.onPinnedMessagesClicked}
|
||||||
|
/>
|
||||||
<HeaderButton
|
<HeaderButton
|
||||||
key="notifsButton"
|
|
||||||
name="notifsButton"
|
name="notifsButton"
|
||||||
title={_t('Notifications')}
|
title={_t('Notifications')}
|
||||||
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
|
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
|
||||||
onClick={this.onNotificationsClicked}
|
onClick={this.onNotificationsClicked}
|
||||||
analytics={['Right Panel', 'Notification List Button', 'click']}
|
analytics={['Right Panel', 'Notification List Button', 'click']}
|
||||||
/>,
|
/>
|
||||||
<HeaderButton
|
<HeaderButton
|
||||||
key="roomSummaryButton"
|
|
||||||
name="roomSummaryButton"
|
name="roomSummaryButton"
|
||||||
title={_t('Room Info')}
|
title={_t('Room Info')}
|
||||||
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
|
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
|
||||||
onClick={this.onRoomSummaryClicked}
|
onClick={this.onRoomSummaryClicked}
|
||||||
analytics={['Right Panel', 'Room Summary Button', 'click']}
|
analytics={['Right Panel', 'Room Summary Button', 'click']}
|
||||||
/>,
|
/>
|
||||||
];
|
</>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,18 +17,19 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {MatrixClient} from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import {RoomMember} from 'matrix-js-sdk/src/models/room-member';
|
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||||
import {User} from 'matrix-js-sdk/src/models/user';
|
import { User } from 'matrix-js-sdk/src/models/user';
|
||||||
import {Room} from 'matrix-js-sdk/src/models/room';
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
|
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||||
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
|
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
|
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
|
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import {_t} from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import createRoom, { findDMForUser, privateShouldBeEncrypted } from '../../../createRoom';
|
import createRoom, { findDMForUser, privateShouldBeEncrypted } from '../../../createRoom';
|
||||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
@ -39,18 +40,18 @@ import MultiInviter from "../../../utils/MultiInviter";
|
||||||
import GroupStore from "../../../stores/GroupStore";
|
import GroupStore from "../../../stores/GroupStore";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
import E2EIcon from "../rooms/E2EIcon";
|
import E2EIcon from "../rooms/E2EIcon";
|
||||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||||
import {textualPowerLevel} from '../../../Roles';
|
import { textualPowerLevel } from '../../../Roles';
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||||
import EncryptionPanel from "./EncryptionPanel";
|
import EncryptionPanel from "./EncryptionPanel";
|
||||||
import {useAsyncMemo} from '../../../hooks/useAsyncMemo';
|
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
|
||||||
import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification';
|
import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification';
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
|
import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
|
||||||
import {useIsEncrypted} from "../../../hooks/useIsEncrypted";
|
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
|
||||||
import BaseCard from "./BaseCard";
|
import BaseCard from "./BaseCard";
|
||||||
import {E2EStatus} from "../../../utils/ShieldUtils";
|
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||||
import ImageView from "../elements/ImageView";
|
import ImageView from "../elements/ImageView";
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import PowerSelector from "../elements/PowerSelector";
|
import PowerSelector from "../elements/PowerSelector";
|
||||||
|
@ -65,7 +66,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
import {mediaFromMxc} from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
import UIStore from "../../../stores/UIStore";
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
|
||||||
export interface IDevice {
|
export interface IDevice {
|
||||||
|
@ -514,9 +515,6 @@ export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
||||||
} else {
|
} else {
|
||||||
setPowerLevels({});
|
setPowerLevels({});
|
||||||
}
|
}
|
||||||
return () => {
|
|
||||||
setPowerLevels({});
|
|
||||||
};
|
|
||||||
}, [room]);
|
}, [room]);
|
||||||
|
|
||||||
useEventEmitter(cli, "RoomState.events", update);
|
useEventEmitter(cli, "RoomState.events", update);
|
||||||
|
@ -1530,21 +1528,16 @@ interface IProps {
|
||||||
user: Member;
|
user: Member;
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
room?: Room;
|
room?: Room;
|
||||||
phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo | RightPanelPhases.SpaceMemberInfo;
|
phase: RightPanelPhases.RoomMemberInfo
|
||||||
|
| RightPanelPhases.GroupMemberInfo
|
||||||
|
| RightPanelPhases.SpaceMemberInfo
|
||||||
|
| RightPanelPhases.EncryptionPanel;
|
||||||
onClose(): void;
|
onClose(): void;
|
||||||
|
verificationRequest?: VerificationRequest;
|
||||||
|
verificationRequestPromise?: Promise<VerificationRequest>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPropsWithEncryptionPanel extends React.ComponentProps<typeof EncryptionPanel> {
|
const UserInfo: React.FC<IProps> = ({
|
||||||
user: Member;
|
|
||||||
groupId: void;
|
|
||||||
room: Room;
|
|
||||||
phase: RightPanelPhases.EncryptionPanel;
|
|
||||||
onClose(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = IProps | IPropsWithEncryptionPanel;
|
|
||||||
|
|
||||||
const UserInfo: React.FC<Props> = ({
|
|
||||||
user,
|
user,
|
||||||
groupId,
|
groupId,
|
||||||
room,
|
room,
|
||||||
|
|
|
@ -277,6 +277,12 @@ interface IProps {
|
||||||
|
|
||||||
// Helper to build permalinks for the room
|
// Helper to build permalinks for the room
|
||||||
permalinkCreator?: RoomPermalinkCreator;
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
|
|
||||||
|
// Symbol of the root node
|
||||||
|
as?: string
|
||||||
|
|
||||||
|
// whether or not to always show timestamps
|
||||||
|
alwaysShowTimestamps?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -291,12 +297,15 @@ interface IState {
|
||||||
previouslyRequestedKeys: boolean;
|
previouslyRequestedKeys: boolean;
|
||||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||||
reactions: Relations;
|
reactions: Relations;
|
||||||
|
|
||||||
|
hover: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.EventTile")
|
@replaceableComponent("views.rooms.EventTile")
|
||||||
export default class EventTile extends React.Component<IProps, IState> {
|
export default class EventTile extends React.Component<IProps, IState> {
|
||||||
private suppressReadReceiptAnimation: boolean;
|
private suppressReadReceiptAnimation: boolean;
|
||||||
private isListeningForReceipts: boolean;
|
private isListeningForReceipts: boolean;
|
||||||
|
private ref: React.RefObject<unknown>;
|
||||||
private tile = React.createRef();
|
private tile = React.createRef();
|
||||||
private replyThread = React.createRef();
|
private replyThread = React.createRef();
|
||||||
|
|
||||||
|
@ -322,6 +331,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
previouslyRequestedKeys: false,
|
previouslyRequestedKeys: false,
|
||||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||||
reactions: this.getReactions(),
|
reactions: this.getReactions(),
|
||||||
|
|
||||||
|
hover: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// don't do RR animations until we are mounted
|
// don't do RR animations until we are mounted
|
||||||
|
@ -333,6 +344,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
// to determine if we've already subscribed and use a combination of other flags to find
|
// to determine if we've already subscribed and use a combination of other flags to find
|
||||||
// out if we should even be subscribed at all.
|
// out if we should even be subscribed at all.
|
||||||
this.isListeningForReceipts = false;
|
this.isListeningForReceipts = false;
|
||||||
|
|
||||||
|
this.ref = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -631,7 +644,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');
|
||||||
|
@ -640,6 +653,11 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
let left = 0;
|
let left = 0;
|
||||||
|
|
||||||
const receipts = this.props.readReceipts || [];
|
const receipts = this.props.readReceipts || [];
|
||||||
|
|
||||||
|
if (receipts.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < receipts.length; ++i) {
|
for (let i = 0; i < receipts.length; ++i) {
|
||||||
const receipt = receipts[i];
|
const receipt = receipts[i];
|
||||||
|
|
||||||
|
@ -690,10 +708,14 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <span className="mx_EventTile_readAvatars">
|
return (
|
||||||
|
<div className="mx_EventTile_msgOption">
|
||||||
|
<span className="mx_EventTile_readAvatars">
|
||||||
{ remText }
|
{ remText }
|
||||||
{ avatars }
|
{ avatars }
|
||||||
</span>;
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
onSenderProfileClick = event => {
|
onSenderProfileClick = event => {
|
||||||
|
@ -953,7 +975,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
onFocusChange={this.onActionBarFocusChange}
|
onFocusChange={this.onActionBarFocusChange}
|
||||||
/> : undefined;
|
/> : undefined;
|
||||||
|
|
||||||
const timestamp = this.props.mxEvent.getTs() ?
|
const showTimestamp = this.props.mxEvent.getTs() && (this.props.alwaysShowTimestamps || this.state.hover);
|
||||||
|
const timestamp = showTimestamp ?
|
||||||
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
||||||
|
|
||||||
const keyRequestHelpText =
|
const keyRequestHelpText =
|
||||||
|
@ -1016,11 +1039,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
let msgOption;
|
let msgOption;
|
||||||
if (this.props.showReadReceipts) {
|
if (this.props.showReadReceipts) {
|
||||||
const readAvatars = this.getReadAvatars();
|
const readAvatars = this.getReadAvatars();
|
||||||
msgOption = (
|
msgOption = readAvatars;
|
||||||
<div className="mx_EventTile_msgOption">
|
|
||||||
{ readAvatars }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this.props.tileShape) {
|
switch (this.props.tileShape) {
|
||||||
|
@ -1124,11 +1143,20 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||||
return (
|
return (
|
||||||
<div className={classes} tabIndex={-1} aria-live={ariaLive} aria-atomic="true">
|
React.createElement(this.props.as || "div", {
|
||||||
{ ircTimestamp }
|
"ref": this.ref,
|
||||||
{ sender }
|
"className": classes,
|
||||||
{ ircPadlock }
|
"tabIndex": -1,
|
||||||
<div className="mx_EventTile_line">
|
"aria-live": ariaLive,
|
||||||
|
"aria-atomic": "true",
|
||||||
|
"data-scroll-tokens": this.props["data-scroll-tokens"],
|
||||||
|
"onMouseEnter": () => this.setState({ hover: true }),
|
||||||
|
"onMouseLeave": () => this.setState({ hover: false }),
|
||||||
|
}, [
|
||||||
|
ircTimestamp,
|
||||||
|
sender,
|
||||||
|
ircPadlock,
|
||||||
|
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||||
{ groupTimestamp }
|
{ groupTimestamp }
|
||||||
{ groupPadlock }
|
{ groupPadlock }
|
||||||
{ thread }
|
{ thread }
|
||||||
|
@ -1145,16 +1173,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
{ keyRequestInfo }
|
{ keyRequestInfo }
|
||||||
{ reactionsRow }
|
{ reactionsRow }
|
||||||
{ actionBar }
|
{ actionBar }
|
||||||
</div>
|
</div>,
|
||||||
{msgOption}
|
msgOption,
|
||||||
{
|
avatar,
|
||||||
// The avatar goes after the event tile as it's absolutely positioned to be over the
|
|
||||||
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
|
])
|
||||||
// the need for further z-indexing chaos)
|
)
|
||||||
}
|
|
||||||
{ avatar }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1316,11 +1340,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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -238,6 +238,8 @@ export default class MemberList extends React.Component {
|
||||||
member.user = cli.getUser(member.userId);
|
member.user = cli.getUser(member.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
member.sortName = (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, "");
|
||||||
|
|
||||||
// XXX: this user may have no lastPresenceTs value!
|
// XXX: this user may have no lastPresenceTs value!
|
||||||
// the right solution here is to fix the race rather than leave it as 0
|
// the right solution here is to fix the race rather than leave it as 0
|
||||||
});
|
});
|
||||||
|
@ -252,6 +254,8 @@ export default class MemberList extends React.Component {
|
||||||
m.membership === 'join' || m.membership === 'invite'
|
m.membership === 'join' || m.membership === 'invite'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
const language = SettingsStore.getValue("language");
|
||||||
|
this.collator = new Intl.Collator(language, { sensitivity: 'base', usePunctuation: true });
|
||||||
filteredAndSortedMembers.sort(this.memberSort);
|
filteredAndSortedMembers.sort(this.memberSort);
|
||||||
return filteredAndSortedMembers;
|
return filteredAndSortedMembers;
|
||||||
}
|
}
|
||||||
|
@ -351,13 +355,7 @@ export default class MemberList extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fourth by name (alphabetical)
|
// Fourth by name (alphabetical)
|
||||||
const nameA = (memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name).replace(SORT_REGEX, "");
|
return this.collator.compare(memberA.sortName, memberB.sortName);
|
||||||
const nameB = (memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name).replace(SORT_REGEX, "");
|
|
||||||
// console.log(`Comparing userA_name=${nameA} against userB_name=${nameB} - returning`);
|
|
||||||
return nameA.localeCompare(nameB, {
|
|
||||||
ignorePunctuation: true,
|
|
||||||
sensitivity: "base",
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearchQueryChanged = searchQuery => {
|
onSearchQueryChanged = searchQuery => {
|
||||||
|
|
|
@ -1,111 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2017 Travis Ralston
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
import MessageEvent from "../messages/MessageEvent";
|
|
||||||
import MemberAvatar from "../avatars/MemberAvatar";
|
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import {formatFullDate} from '../../../DateUtils';
|
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.PinnedEventTile")
|
|
||||||
export default class PinnedEventTile extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
mxRoom: PropTypes.object.isRequired,
|
|
||||||
mxEvent: PropTypes.object.isRequired,
|
|
||||||
onUnpinned: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
onTileClicked = () => {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
event_id: this.props.mxEvent.getId(),
|
|
||||||
highlighted: true,
|
|
||||||
room_id: this.props.mxEvent.getRoomId(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnpinClicked = () => {
|
|
||||||
const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", "");
|
|
||||||
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
|
|
||||||
// Nothing to do: already unpinned
|
|
||||||
if (this.props.onUnpinned) this.props.onUnpinned();
|
|
||||||
} else {
|
|
||||||
const pinned = pinnedEvents.getContent().pinned;
|
|
||||||
const index = pinned.indexOf(this.props.mxEvent.getId());
|
|
||||||
if (index !== -1) {
|
|
||||||
pinned.splice(index, 1);
|
|
||||||
MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '')
|
|
||||||
.then(() => {
|
|
||||||
if (this.props.onUnpinned) this.props.onUnpinned();
|
|
||||||
});
|
|
||||||
} else if (this.props.onUnpinned) this.props.onUnpinned();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_canUnpin() {
|
|
||||||
return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const sender = this.props.mxEvent.getSender();
|
|
||||||
// Get the latest sender profile rather than historical
|
|
||||||
const senderProfile = this.props.mxRoom.getMember(sender);
|
|
||||||
const avatarSize = 40;
|
|
||||||
|
|
||||||
let unpinButton = null;
|
|
||||||
if (this._canUnpin()) {
|
|
||||||
unpinButton = (
|
|
||||||
<AccessibleButton onClick={this.onUnpinClicked} className="mx_PinnedEventTile_unpinButton">
|
|
||||||
<img src={require("../../../../res/img/cancel-red.svg")} width="8" height="8" alt={_t('Unpin Message')} title={_t('Unpin Message')} />
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_PinnedEventTile">
|
|
||||||
<div className="mx_PinnedEventTile_actions">
|
|
||||||
<AccessibleButton className="mx_PinnedEventTile_gotoButton mx_textButton" onClick={this.onTileClicked}>
|
|
||||||
{ _t("Jump to message") }
|
|
||||||
</AccessibleButton>
|
|
||||||
{ unpinButton }
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className="mx_PinnedEventTile_senderAvatar">
|
|
||||||
<MemberAvatar member={senderProfile} width={avatarSize} height={avatarSize} fallbackUserId={sender} />
|
|
||||||
</span>
|
|
||||||
<span className="mx_PinnedEventTile_sender">
|
|
||||||
{ senderProfile ? senderProfile.name : sender }
|
|
||||||
</span>
|
|
||||||
<span className="mx_PinnedEventTile_timestamp">
|
|
||||||
{ formatFullDate(new Date(this.props.mxEvent.getTs())) }
|
|
||||||
</span>
|
|
||||||
<div className="mx_PinnedEventTile_message">
|
|
||||||
<MessageEvent
|
|
||||||
mxEvent={this.props.mxEvent}
|
|
||||||
className="mx_PinnedEventTile_body"
|
|
||||||
maxImageHeight={150}
|
|
||||||
onHeightChanged={() => {}} // we need to give this, apparently
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
104
src/components/views/rooms/PinnedEventTile.tsx
Normal file
104
src/components/views/rooms/PinnedEventTile.tsx
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Travis Ralston
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import MessageEvent from "../messages/MessageEvent";
|
||||||
|
import MemberAvatar from "../avatars/MemberAvatar";
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import { formatDate } from '../../../DateUtils';
|
||||||
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
|
||||||
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
room: Room;
|
||||||
|
event: MatrixEvent;
|
||||||
|
onUnpinClicked?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AVATAR_SIZE = 24;
|
||||||
|
|
||||||
|
@replaceableComponent("views.rooms.PinnedEventTile")
|
||||||
|
export default class PinnedEventTile extends React.Component<IProps> {
|
||||||
|
public static contextType = MatrixClientContext;
|
||||||
|
|
||||||
|
private onTileClicked = () => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
event_id: this.props.event.getId(),
|
||||||
|
highlighted: true,
|
||||||
|
room_id: this.props.event.getRoomId(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const sender = this.props.event.getSender();
|
||||||
|
const senderProfile = this.props.room.getMember(sender);
|
||||||
|
|
||||||
|
let unpinButton = null;
|
||||||
|
if (this.props.onUnpinClicked) {
|
||||||
|
unpinButton = (
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
onClick={this.props.onUnpinClicked}
|
||||||
|
className="mx_PinnedEventTile_unpinButton"
|
||||||
|
title={_t("Unpin")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_PinnedEventTile">
|
||||||
|
<MemberAvatar
|
||||||
|
className="mx_PinnedEventTile_senderAvatar"
|
||||||
|
member={senderProfile}
|
||||||
|
width={AVATAR_SIZE}
|
||||||
|
height={AVATAR_SIZE}
|
||||||
|
fallbackUserId={sender}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className={"mx_PinnedEventTile_sender " + getUserNameColorClass(sender)}>
|
||||||
|
{ senderProfile?.name || sender }
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{ unpinButton }
|
||||||
|
|
||||||
|
<div className="mx_PinnedEventTile_message">
|
||||||
|
<MessageEvent
|
||||||
|
mxEvent={this.props.event}
|
||||||
|
className="mx_PinnedEventTile_body"
|
||||||
|
maxImageHeight={150}
|
||||||
|
onHeightChanged={() => {}} // we need to give this, apparently
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx_PinnedEventTile_footer">
|
||||||
|
<span className="mx_PinnedEventTile_timestamp">
|
||||||
|
{ formatDate(new Date(this.props.event.getTs())) }
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<AccessibleButton onClick={this.onTileClicked} kind="link">
|
||||||
|
{ _t("View message") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,145 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2017 Travis Ralston
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
import PinnedEventTile from "./PinnedEventTile";
|
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import PinningUtils from "../../../utils/PinningUtils";
|
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.PinnedEventsPanel")
|
|
||||||
export default class PinnedEventsPanel extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
// The Room from the js-sdk we're going to show pinned events for
|
|
||||||
room: PropTypes.object.isRequired,
|
|
||||||
|
|
||||||
onCancelClick: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
loading: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this._updatePinnedMessages();
|
|
||||||
MatrixClientPeg.get().on("RoomState.events", this._onStateEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (MatrixClientPeg.get()) {
|
|
||||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onStateEvent = ev => {
|
|
||||||
if (ev.getRoomId() === this.props.room.roomId && ev.getType() === "m.room.pinned_events") {
|
|
||||||
this._updatePinnedMessages();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_updatePinnedMessages = () => {
|
|
||||||
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
|
|
||||||
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
|
|
||||||
this.setState({ loading: false, pinned: [] });
|
|
||||||
} else {
|
|
||||||
const promises = [];
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
pinnedEvents.getContent().pinned.map((eventId) => {
|
|
||||||
promises.push(cli.getEventTimeline(this.props.room.getUnfilteredTimelineSet(), eventId, 0).then(
|
|
||||||
(timeline) => {
|
|
||||||
const event = timeline.getEvents().find((e) => e.getId() === eventId);
|
|
||||||
return {eventId, timeline, event};
|
|
||||||
}).catch((err) => {
|
|
||||||
console.error("Error looking up pinned event " + eventId + " in room " + this.props.room.roomId);
|
|
||||||
console.error(err);
|
|
||||||
return null; // return lack of context to avoid unhandled errors
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all(promises).then((contexts) => {
|
|
||||||
// Filter out the messages before we try to render them
|
|
||||||
const pinned = contexts.filter((context) => PinningUtils.isPinnable(context.event));
|
|
||||||
|
|
||||||
this.setState({ loading: false, pinned });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this._updateReadState();
|
|
||||||
};
|
|
||||||
|
|
||||||
_updateReadState() {
|
|
||||||
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
|
|
||||||
if (!pinnedEvents) return; // nothing to read
|
|
||||||
|
|
||||||
let readStateEvents = [];
|
|
||||||
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
|
|
||||||
if (readPinsEvent && readPinsEvent.getContent()) {
|
|
||||||
readStateEvents = readPinsEvent.getContent().event_ids || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!readStateEvents.includes(pinnedEvents.getId())) {
|
|
||||||
readStateEvents.push(pinnedEvents.getId());
|
|
||||||
|
|
||||||
// Only keep the last 10 event IDs to avoid infinite growth
|
|
||||||
readStateEvents = readStateEvents.reverse().splice(0, 10).reverse();
|
|
||||||
|
|
||||||
MatrixClientPeg.get().setRoomAccountData(this.props.room.roomId, "im.vector.room.read_pins", {
|
|
||||||
event_ids: readStateEvents,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_getPinnedTiles() {
|
|
||||||
if (this.state.pinned.length === 0) {
|
|
||||||
return (<div>{ _t("No pinned messages.") }</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.state.pinned.map((context) => {
|
|
||||||
return (
|
|
||||||
<PinnedEventTile
|
|
||||||
key={context.event.getId()}
|
|
||||||
mxRoom={this.props.room}
|
|
||||||
mxEvent={context.event}
|
|
||||||
onUnpinned={this._updatePinnedMessages}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let tiles = <div>{ _t("Loading...") }</div>;
|
|
||||||
if (this.state && !this.state.loading) {
|
|
||||||
tiles = this._getPinnedTiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_PinnedEventsPanel">
|
|
||||||
<div className="mx_PinnedEventsPanel_body">
|
|
||||||
<AccessibleButton className="mx_PinnedEventsPanel_cancel" onClick={this.props.onCancelClick}>
|
|
||||||
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18" />
|
|
||||||
</AccessibleButton>
|
|
||||||
<h3 className="mx_PinnedEventsPanel_header">{ _t("Pinned Messages") }</h3>
|
|
||||||
{ tiles }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,7 +19,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||||
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
@ -29,8 +29,8 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import RoomTopic from "../elements/RoomTopic";
|
import RoomTopic from "../elements/RoomTopic";
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
import {PlaceCallType} from "../../../CallHandler";
|
import { PlaceCallType } from "../../../CallHandler";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.RoomHeader")
|
@replaceableComponent("views.rooms.RoomHeader")
|
||||||
export default class RoomHeader extends React.Component {
|
export default class RoomHeader extends React.Component {
|
||||||
|
@ -39,7 +39,6 @@ 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,
|
||||||
e2eStatus: PropTypes.string,
|
e2eStatus: PropTypes.string,
|
||||||
|
@ -56,14 +55,12 @@ export default class RoomHeader extends React.Component {
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,47 +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 pinnedEventsButton = null;
|
|
||||||
|
|
||||||
// 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.
|
||||||
|
@ -173,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 =
|
||||||
|
@ -240,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 }
|
||||||
|
@ -256,7 +200,7 @@ export default class RoomHeader extends React.Component {
|
||||||
{ name }
|
{ name }
|
||||||
{ topicElement }
|
{ topicElement }
|
||||||
{ rightRow }
|
{ rightRow }
|
||||||
<RoomHeaderButtons />
|
<RoomHeaderButtons room={this.props.room} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -105,6 +105,7 @@ interface IState {
|
||||||
export default class RoomSublist extends React.Component<IProps, IState> {
|
export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
private headerButton = createRef<HTMLDivElement>();
|
private headerButton = createRef<HTMLDivElement>();
|
||||||
private sublistRef = createRef<HTMLDivElement>();
|
private sublistRef = createRef<HTMLDivElement>();
|
||||||
|
private tilesRef = createRef<HTMLDivElement>();
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private layout: ListLayout;
|
private layout: ListLayout;
|
||||||
private heightAtStart: number;
|
private heightAtStart: number;
|
||||||
|
@ -246,11 +247,15 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||||
|
// Using the passive option to not block the main thread
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||||
|
this.tilesRef.current?.addEventListener("scroll", this.onScrollPrevent, { passive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
defaultDispatcher.unregister(this.dispatcherRef);
|
defaultDispatcher.unregister(this.dispatcherRef);
|
||||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||||
|
this.tilesRef.current?.removeEventListener("scroll", this.onScrollPrevent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onListsUpdated = () => {
|
private onListsUpdated = () => {
|
||||||
|
@ -755,7 +760,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onScrollPrevent(e: React.UIEvent<HTMLDivElement>) {
|
private onScrollPrevent(e: Event) {
|
||||||
// the RoomTile calls scrollIntoView and the browser may scroll a div we do not wish to be scrollable
|
// the RoomTile calls scrollIntoView and the browser may scroll a div we do not wish to be scrollable
|
||||||
// this fixes https://github.com/vector-im/element-web/issues/14413
|
// this fixes https://github.com/vector-im/element-web/issues/14413
|
||||||
(e.target as HTMLDivElement).scrollTop = 0;
|
(e.target as HTMLDivElement).scrollTop = 0;
|
||||||
|
@ -884,7 +889,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
className="mx_RoomSublist_resizeBox"
|
className="mx_RoomSublist_resizeBox"
|
||||||
enable={handles}
|
enable={handles}
|
||||||
>
|
>
|
||||||
<div className="mx_RoomSublist_tiles" onScroll={this.onScrollPrevent}>
|
<div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
|
||||||
{visibleTiles}
|
{visibleTiles}
|
||||||
</div>
|
</div>
|
||||||
{showNButton}
|
{showNButton}
|
||||||
|
|
|
@ -43,14 +43,12 @@ export default class SimpleRoomHeader extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomHeader" >
|
<div className="mx_RoomHeader mx_RoomHeader_wrapper" >
|
||||||
<div className="mx_RoomHeader_wrapper">
|
|
||||||
<div className="mx_RoomHeader_simpleHeader">
|
<div className="mx_RoomHeader_simpleHeader">
|
||||||
{ icon }
|
{ icon }
|
||||||
{ this.props.title }
|
{ this.props.title }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import Timer from '../../../utils/Timer';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import MemberAvatar from '../avatars/MemberAvatar';
|
import MemberAvatar from '../avatars/MemberAvatar';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { compare } from "../../../utils/strings";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// the room this statusbar is representing.
|
// the room this statusbar is representing.
|
||||||
|
@ -207,14 +208,14 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
|
||||||
usersTyping = usersTyping.concat(stoppedUsersOnTimer);
|
usersTyping = usersTyping.concat(stoppedUsersOnTimer);
|
||||||
// sort them so the typing members don't change order when
|
// sort them so the typing members don't change order when
|
||||||
// moved to delayedStopTypingTimers
|
// moved to delayedStopTypingTimers
|
||||||
usersTyping.sort((a, b) => a.name.localeCompare(b.name));
|
usersTyping.sort((a, b) => compare(a.name, b.name));
|
||||||
|
|
||||||
const typingString = WhoIsTyping.whoIsTypingString(
|
const typingString = WhoIsTyping.whoIsTypingString(
|
||||||
usersTyping,
|
usersTyping,
|
||||||
this.props.whoIsTypingLimit,
|
this.props.whoIsTypingLimit,
|
||||||
);
|
);
|
||||||
if (!typingString) {
|
if (!typingString) {
|
||||||
return (<div className="mx_WhoIsTypingTile_empty" />);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -79,8 +79,8 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||||
async _getUpdatedStatus() {
|
async _getUpdatedStatus() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const pkCache = cli.getCrossSigningCacheCallbacks();
|
const pkCache = cli.getCrossSigningCacheCallbacks();
|
||||||
const crossSigning = cli._crypto._crossSigningInfo;
|
const crossSigning = cli.crypto._crossSigningInfo;
|
||||||
const secretStorage = cli._crypto._secretStorage;
|
const secretStorage = cli.crypto._secretStorage;
|
||||||
const crossSigningPublicKeysOnDevice = crossSigning.getId();
|
const crossSigningPublicKeysOnDevice = crossSigning.getId();
|
||||||
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
|
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
|
||||||
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
|
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
|
||||||
|
|
|
@ -131,10 +131,10 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
|
|
||||||
async _getUpdatedDiagnostics() {
|
async _getUpdatedDiagnostics() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const secretStorage = cli._crypto._secretStorage;
|
const secretStorage = cli.crypto._secretStorage;
|
||||||
|
|
||||||
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
|
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
|
||||||
const backupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey();
|
const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey();
|
||||||
const backupKeyCached = !!(backupKeyFromCache);
|
const backupKeyCached = !!(backupKeyFromCache);
|
||||||
const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array;
|
const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array;
|
||||||
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||||
|
import { compare } from "../../../../../utils/strings";
|
||||||
|
|
||||||
const plEventsToLabels = {
|
const plEventsToLabels = {
|
||||||
// These will be translated for us later.
|
// These will be translated for us later.
|
||||||
|
@ -312,7 +313,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
||||||
// comparator for sorting PL users lexicographically on PL descending, MXID ascending. (case-insensitive)
|
// comparator for sorting PL users lexicographically on PL descending, MXID ascending. (case-insensitive)
|
||||||
const comparator = (a, b) => {
|
const comparator = (a, b) => {
|
||||||
const plDiff = userLevels[b.key] - userLevels[a.key];
|
const plDiff = userLevels[b.key] - userLevels[a.key];
|
||||||
return plDiff !== 0 ? plDiff : a.key.toLocaleLowerCase().localeCompare(b.key.toLocaleLowerCase());
|
return plDiff !== 0 ? plDiff : compare(a.key.toLocaleLowerCase(), b.key.toLocaleLowerCase());
|
||||||
};
|
};
|
||||||
|
|
||||||
privilegedUsers.sort(comparator);
|
privilegedUsers.sort(comparator);
|
||||||
|
|
|
@ -35,9 +35,10 @@ import Field from '../../../elements/Field';
|
||||||
import EventTilePreview from '../../../elements/EventTilePreview';
|
import EventTilePreview from '../../../elements/EventTilePreview';
|
||||||
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
||||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
import {UIFeature} from "../../../../../settings/UIFeature";
|
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||||
import {Layout} from "../../../../../settings/Layout";
|
import { Layout } from "../../../../../settings/Layout";
|
||||||
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
|
import { compare } from "../../../../../utils/strings";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
}
|
}
|
||||||
|
@ -295,7 +296,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
.map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability
|
.map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability
|
||||||
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
|
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
|
||||||
const customThemes = themes.filter(p => !builtInThemes.includes(p))
|
const customThemes = themes.filter(p => !builtInThemes.includes(p))
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
.sort((a, b) => compare(a.name, b.name));
|
||||||
const orderedThemes = [...builtInThemes, ...customThemes];
|
const orderedThemes = [...builtInThemes, ...customThemes];
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_themeSection">
|
<div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_themeSection">
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,8 @@ limitations under the License.
|
||||||
|
|
||||||
import { createContext } from "react";
|
import { createContext } from "react";
|
||||||
|
|
||||||
import {IState} from "../components/structures/RoomView";
|
import { IState } from "../components/structures/RoomView";
|
||||||
import {Layout} from "../settings/Layout";
|
import { Layout } from "../settings/Layout";
|
||||||
|
|
||||||
const RoomContext = createContext<IState>({
|
const RoomContext = createContext<IState>({
|
||||||
roomLoading: true,
|
roomLoading: true,
|
||||||
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
|
@ -14,14 +14,22 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {useState, useEffect, DependencyList} from 'react';
|
import { useState, useEffect, DependencyList } from 'react';
|
||||||
|
|
||||||
type Fn<T> = () => Promise<T>;
|
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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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",
|
||||||
|
@ -1509,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",
|
||||||
|
@ -1719,9 +1716,11 @@
|
||||||
"The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to",
|
"The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to",
|
||||||
"Yours, or the other users’ internet connection": "Yours, or the other users’ internet connection",
|
"Yours, or the other users’ 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",
|
||||||
|
"You’re all caught up": "You’re all caught up",
|
||||||
|
"You have no visible notifications.": "You have no visible notifications.",
|
||||||
|
"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",
|
||||||
|
@ -1897,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",
|
||||||
|
@ -1951,7 +1951,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",
|
||||||
|
@ -2472,6 +2471,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",
|
||||||
|
@ -2634,8 +2634,6 @@
|
||||||
"Create a new community": "Create a new community",
|
"Create a new community": "Create a new community",
|
||||||
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
|
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
|
||||||
"Communities are changing to Spaces": "Communities are changing to Spaces",
|
"Communities are changing to Spaces": "Communities are changing to Spaces",
|
||||||
"You’re all caught up": "You’re all caught up",
|
|
||||||
"You have no visible notifications.": "You have no visible notifications.",
|
|
||||||
"%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.",
|
"%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.",
|
||||||
"%(brand)s failed to get the public room list.": "%(brand)s failed to get the public room list.",
|
"%(brand)s failed to get the public room list.": "%(brand)s failed to get the public room list.",
|
||||||
"The homeserver may be unavailable or overloaded.": "The homeserver may be unavailable or overloaded.",
|
"The homeserver may be unavailable or overloaded.": "The homeserver may be unavailable or overloaded.",
|
||||||
|
|
|
@ -453,7 +453,7 @@ 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.token, this._eventsPerCrawl,
|
||||||
checkpoint.direction);
|
checkpoint.direction);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -28,6 +28,7 @@ import WidgetUtils from "../utils/WidgetUtils";
|
||||||
import {MatrixClientPeg} from "../MatrixClientPeg";
|
import {MatrixClientPeg} from "../MatrixClientPeg";
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
|
import { compare } from "../utils/strings";
|
||||||
|
|
||||||
const KIND_PREFERENCE = [
|
const KIND_PREFERENCE = [
|
||||||
// Ordered: first is most preferred, last is least preferred.
|
// Ordered: first is most preferred, last is least preferred.
|
||||||
|
@ -152,7 +153,7 @@ export class IntegrationManagers {
|
||||||
|
|
||||||
if (kind === Kind.Account) {
|
if (kind === Kind.Account) {
|
||||||
// Order by state_keys (IDs)
|
// Order by state_keys (IDs)
|
||||||
managers.sort((a, b) => a.id.localeCompare(b.id));
|
managers.sort((a, b) => compare(a.id, b.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
ordered.push(...managers);
|
ordered.push(...managers);
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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"});
|
||||||
|
|
|
@ -65,6 +65,10 @@ class FlairStore extends EventEmitter {
|
||||||
delete this._userGroups[userId];
|
delete this._userGroups[userId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cachedPublicisedGroups(userId) {
|
||||||
|
return this._userGroups[userId];
|
||||||
|
}
|
||||||
|
|
||||||
getPublicisedGroupsCached(matrixClient, userId) {
|
getPublicisedGroupsCached(matrixClient, userId) {
|
||||||
if (this._userGroups[userId]) {
|
if (this._userGroups[userId]) {
|
||||||
return Promise.resolve(this._userGroups[userId]);
|
return Promise.resolve(this._userGroups[userId]);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { TagID } from "../../models";
|
import { TagID } from "../../models";
|
||||||
import { IAlgorithm } from "./IAlgorithm";
|
import { IAlgorithm } from "./IAlgorithm";
|
||||||
|
import { compare } from "../../../../utils/strings";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sorts rooms according to the browser's determination of alphabetic.
|
* Sorts rooms according to the browser's determination of alphabetic.
|
||||||
|
@ -24,7 +25,7 @@ import { IAlgorithm } from "./IAlgorithm";
|
||||||
export class AlphabeticAlgorithm implements IAlgorithm {
|
export class AlphabeticAlgorithm implements IAlgorithm {
|
||||||
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
|
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
|
||||||
return rooms.sort((a, b) => {
|
return rooms.sort((a, b) => {
|
||||||
return a.name.localeCompare(b.name);
|
return compare(a.name, b.name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { SettingLevel } from "../../settings/SettingLevel";
|
import { SettingLevel } from "../../settings/SettingLevel";
|
||||||
import { arrayFastClone } from "../../utils/arrays";
|
import { arrayFastClone } from "../../utils/arrays";
|
||||||
import { UPDATE_EVENT } from "../AsyncStore";
|
import { UPDATE_EVENT } from "../AsyncStore";
|
||||||
|
import { compare } from "../../utils/strings";
|
||||||
|
|
||||||
export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout";
|
export const WIDGET_LAYOUT_EVENT_TYPE = "io.element.widgets.layout";
|
||||||
|
|
||||||
|
@ -240,7 +241,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
||||||
|
|
||||||
if (orderA === orderB) {
|
if (orderA === orderB) {
|
||||||
// We just need a tiebreak
|
// We just need a tiebreak
|
||||||
return a.id.localeCompare(b.id);
|
return compare(a.id, b.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return orderA - orderB;
|
return orderA - orderB;
|
||||||
|
|
|
@ -73,3 +73,14 @@ export function copyNode(ref: Element): boolean {
|
||||||
selectText(ref);
|
selectText(ref);
|
||||||
return document.execCommand('copy');
|
return document.execCommand('copy');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const collator = new Intl.Collator();
|
||||||
|
/**
|
||||||
|
* Performant language-sensitive string comparison
|
||||||
|
* @param a the first string to compare
|
||||||
|
* @param b the second string to compare
|
||||||
|
*/
|
||||||
|
export function compare(a: string, b: string): number {
|
||||||
|
return collator.compare(a, b);
|
||||||
|
}
|
||||||
|
|
|
@ -23,8 +23,10 @@ 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 { Actions } from '../src/notifications/types';
|
||||||
|
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 +77,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 +108,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 +163,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);
|
||||||
|
|
|
@ -309,7 +309,7 @@ describe('MessagePanel', function() {
|
||||||
const rm = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_RoomView_myReadMarker_container');
|
const rm = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_RoomView_myReadMarker_container');
|
||||||
|
|
||||||
// it should follow the <li> which wraps the event tile for event 4
|
// it should follow the <li> which wraps the event tile for event 4
|
||||||
const eventContainer = ReactDOM.findDOMNode(tiles[4]).parentNode;
|
const eventContainer = ReactDOM.findDOMNode(tiles[4]);
|
||||||
expect(rm.previousSibling).toEqual(eventContainer);
|
expect(rm.previousSibling).toEqual(eventContainer);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -365,7 +365,7 @@ describe('MessagePanel', function() {
|
||||||
const tiles = TestUtils.scryRenderedComponentsWithType(
|
const tiles = TestUtils.scryRenderedComponentsWithType(
|
||||||
mp, sdk.getComponent('rooms.EventTile'));
|
mp, sdk.getComponent('rooms.EventTile'));
|
||||||
const tileContainers = tiles.map(function(t) {
|
const tileContainers = tiles.map(function(t) {
|
||||||
return ReactDOM.findDOMNode(t).parentNode;
|
return ReactDOM.findDOMNode(t);
|
||||||
});
|
});
|
||||||
|
|
||||||
// find the <li> which wraps the read marker
|
// find the <li> which wraps the read marker
|
||||||
|
|
|
@ -9,6 +9,8 @@ import sdk from '../../../skinned-sdk';
|
||||||
|
|
||||||
import {Room, RoomMember, User} from 'matrix-js-sdk';
|
import {Room, RoomMember, User} from 'matrix-js-sdk';
|
||||||
|
|
||||||
|
import { compare } from "../../../../src/utils/strings";
|
||||||
|
|
||||||
function generateRoomId() {
|
function generateRoomId() {
|
||||||
return '!' + Math.random().toString().slice(2, 10) + ':domain';
|
return '!' + Math.random().toString().slice(2, 10) + ':domain';
|
||||||
}
|
}
|
||||||
|
@ -173,7 +175,7 @@ describe('MemberList', () => {
|
||||||
if (!groupChange) {
|
if (!groupChange) {
|
||||||
const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name;
|
const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name;
|
||||||
const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name;
|
const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name;
|
||||||
const nameCompare = nameB.localeCompare(nameA);
|
const nameCompare = compare(nameB, nameA);
|
||||||
console.log("Comparing name");
|
console.log("Comparing name");
|
||||||
expect(nameCompare).toBeGreaterThanOrEqual(0);
|
expect(nameCompare).toBeGreaterThanOrEqual(0);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -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(),
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue