Merge pull request #6096 from matrix-org/t3chguy/fix/6606
This commit is contained in:
commit
f22fd7aa3b
25 changed files with 668 additions and 637 deletions
|
@ -179,6 +179,7 @@
|
||||||
@import "./views/messages/_common_CryptoEvent.scss";
|
@import "./views/messages/_common_CryptoEvent.scss";
|
||||||
@import "./views/right_panel/_BaseCard.scss";
|
@import "./views/right_panel/_BaseCard.scss";
|
||||||
@import "./views/right_panel/_EncryptionInfo.scss";
|
@import "./views/right_panel/_EncryptionInfo.scss";
|
||||||
|
@import "./views/right_panel/_PinnedMessagesCard.scss";
|
||||||
@import "./views/right_panel/_RoomSummaryCard.scss";
|
@import "./views/right_panel/_RoomSummaryCard.scss";
|
||||||
@import "./views/right_panel/_UserInfo.scss";
|
@import "./views/right_panel/_UserInfo.scss";
|
||||||
@import "./views/right_panel/_VerificationPanel.scss";
|
@import "./views/right_panel/_VerificationPanel.scss";
|
||||||
|
@ -203,7 +204,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";
|
||||||
|
|
|
@ -99,6 +99,48 @@ limitations under the License.
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$dot-size: 8px;
|
||||||
|
$pulse-color: $pinned-unread-color;
|
||||||
|
|
||||||
|
.mx_RightPanel_pinnedMessagesButton {
|
||||||
|
&::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/pin.svg');
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RightPanel_pinnedMessagesButton_unreadIndicator {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
margin: 4px;
|
||||||
|
width: $dot-size;
|
||||||
|
height: $dot-size;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: scale(1);
|
||||||
|
background: rgba($pulse-color, 1);
|
||||||
|
box-shadow: 0 0 0 0 rgba($pulse-color, 1);
|
||||||
|
animation: mx_RightPanel_indicator_pulse 2s infinite;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mx_RightPanel_indicator_pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
box-shadow: 0 0 0 0 rgba($pulse-color, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 10px rgba($pulse-color, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
box-shadow: 0 0 0 0 rgba($pulse-color, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RightPanel_headerButton_highlight {
|
.mx_RightPanel_headerButton_highlight {
|
||||||
&::before {
|
&::before {
|
||||||
background-color: $accent-color !important;
|
background-color: $accent-color !important;
|
||||||
|
|
|
@ -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,22 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_PinnedEventsPanel {
|
.mx_PinnedMessagesCard {
|
||||||
border-top: 1px solid $primary-hairline-color;
|
padding-top: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventsPanel_body {
|
.mx_BaseCard_header {
|
||||||
max-height: 300px;
|
text-align: center;
|
||||||
overflow-y: auto;
|
margin-top: 0;
|
||||||
padding-bottom: 15px;
|
border-bottom: 1px solid $menu-border-color;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_PinnedEventsPanel_header {
|
> h2 {
|
||||||
margin: 0;
|
font-weight: $font-semi-bold;
|
||||||
padding-top: 8px;
|
font-size: $font-18px;
|
||||||
padding-bottom: 15px;
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_PinnedEventsPanel_cancel {
|
.mx_BaseCard_close {
|
||||||
margin: 12px;
|
margin-right: 6px;
|
||||||
float: right;
|
}
|
||||||
display: inline-block;
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 |
|
@ -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";
|
||||||
|
@ -63,7 +62,6 @@ import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
||||||
import ForwardMessage from "../views/rooms/ForwardMessage";
|
import ForwardMessage from "../views/rooms/ForwardMessage";
|
||||||
import SearchBar from "../views/rooms/SearchBar";
|
import SearchBar from "../views/rooms/SearchBar";
|
||||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||||
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
|
|
||||||
import AuxPanel from "../views/rooms/AuxPanel";
|
import AuxPanel from "../views/rooms/AuxPanel";
|
||||||
import RoomHeader from "../views/rooms/RoomHeader";
|
import RoomHeader from "../views/rooms/RoomHeader";
|
||||||
import { XOR } from "../../@types/common";
|
import { XOR } from "../../@types/common";
|
||||||
|
@ -82,7 +80,7 @@ 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 { omit } from 'lodash';
|
||||||
import UIStore from "../../stores/UIStore";
|
import UIStore from "../../stores/UIStore";
|
||||||
|
|
||||||
|
@ -157,7 +155,6 @@ export interface IState {
|
||||||
canPeek: boolean;
|
canPeek: boolean;
|
||||||
showApps: boolean;
|
showApps: boolean;
|
||||||
isPeeking: boolean;
|
isPeeking: boolean;
|
||||||
showingPinned: boolean;
|
|
||||||
showReadReceipts: boolean;
|
showReadReceipts: boolean;
|
||||||
showRightPanel: boolean;
|
showRightPanel: boolean;
|
||||||
// error object, as from the matrix client/server API
|
// error object, as from the matrix client/server API
|
||||||
|
@ -235,7 +232,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,
|
||||||
|
@ -330,7 +326,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||||
// we should only peek once we have a ready client
|
// we should only peek once we have a ready client
|
||||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
||||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
|
|
||||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||||
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
|
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
|
||||||
};
|
};
|
||||||
|
@ -1403,13 +1398,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',
|
||||||
|
@ -1526,7 +1514,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,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1855,9 +1842,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
} else if (showRoomUpgradeBar) {
|
} else if (showRoomUpgradeBar) {
|
||||||
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
||||||
hideCancel = true;
|
hideCancel = true;
|
||||||
} else if (this.state.showingPinned) {
|
|
||||||
hideCancel = true; // has own cancel
|
|
||||||
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
|
|
||||||
} else if (myMembership !== "join") {
|
} else if (myMembership !== "join") {
|
||||||
// We do have a room object for this room, but we're not currently in it.
|
// We do have a room object for this room, but we're not currently in it.
|
||||||
// We may have a 3rd party invite to it.
|
// We may have a 3rd party invite to it.
|
||||||
|
@ -2077,7 +2061,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
inRoom={myMembership === 'join'}
|
inRoom={myMembership === 'join'}
|
||||||
onSearchClick={this.onSearchClick}
|
onSearchClick={this.onSearchClick}
|
||||||
onSettingsClick={this.onSettingsClick}
|
onSettingsClick={this.onSettingsClick}
|
||||||
onPinnedClick={this.onPinnedClick}
|
|
||||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
||||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
export function canCancel(eventStatus) {
|
export function canCancel(eventStatus) {
|
||||||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||||
|
@ -82,7 +83,7 @@ export default class MessageContextMenu extends React.Component {
|
||||||
const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId)
|
const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId)
|
||||||
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl
|
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl
|
||||||
&& this.props.mxEvent.getType() !== EventType.RoomEncryption;
|
&& this.props.mxEvent.getType() !== EventType.RoomEncryption;
|
||||||
let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli);
|
let canPin = room.currentState.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli);
|
||||||
|
|
||||||
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
|
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
|
||||||
if (!SettingsStore.getValue("feature_pinning")) canPin = false;
|
if (!SettingsStore.getValue("feature_pinning")) canPin = false;
|
||||||
|
@ -92,7 +93,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());
|
||||||
|
@ -165,25 +166,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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,10 +19,10 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||||
|
|
||||||
import {CancelButton} from './SimpleRoomHeader';
|
import { CancelButton } from './SimpleRoomHeader';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||||
import E2EIcon from './E2EIcon';
|
import E2EIcon from './E2EIcon';
|
||||||
|
@ -30,8 +30,8 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import RoomTopic from "../elements/RoomTopic";
|
import RoomTopic from "../elements/RoomTopic";
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
import {PlaceCallType} from "../../../CallHandler";
|
import { PlaceCallType } from "../../../CallHandler";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.RoomHeader")
|
@replaceableComponent("views.rooms.RoomHeader")
|
||||||
export default class RoomHeader extends React.Component {
|
export default class RoomHeader extends React.Component {
|
||||||
|
@ -40,7 +40,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,
|
||||||
onCancelClick: PropTypes.func,
|
onCancelClick: PropTypes.func,
|
||||||
|
@ -59,14 +58,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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,48 +76,14 @@ export default class RoomHeader extends React.Component {
|
||||||
this._rateLimitedUpdate();
|
this._rateLimitedUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onRoomAccountData = (event, room) => {
|
|
||||||
if (!this.props.room || room.roomId !== this.props.room.roomId) return;
|
|
||||||
if (event.getType() !== "im.vector.room.read_pins") return;
|
|
||||||
|
|
||||||
this._rateLimitedUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
_rateLimitedUpdate = new RateLimitedFunc(function() {
|
_rateLimitedUpdate = new RateLimitedFunc(function() {
|
||||||
/* eslint-disable babel/no-invalid-this */
|
/* eslint-disable babel/no-invalid-this */
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
_hasUnreadPins() {
|
|
||||||
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
|
||||||
if (!currentPinEvent) return false;
|
|
||||||
if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) {
|
|
||||||
return false; // no pins == nothing to read
|
|
||||||
}
|
|
||||||
|
|
||||||
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
|
|
||||||
if (readPinsEvent && readPinsEvent.getContent()) {
|
|
||||||
const readStateEvents = readPinsEvent.getContent().event_ids || [];
|
|
||||||
if (readStateEvents) {
|
|
||||||
return !readStateEvents.includes(currentPinEvent.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// There's pins, and we haven't read any of them
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_hasPins() {
|
|
||||||
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
|
||||||
if (!currentPinEvent) return false;
|
|
||||||
|
|
||||||
return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let searchStatus = null;
|
let searchStatus = null;
|
||||||
let cancelButton = null;
|
let cancelButton = null;
|
||||||
let pinnedEventsButton = null;
|
|
||||||
|
|
||||||
if (this.props.onCancelClick) {
|
if (this.props.onCancelClick) {
|
||||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||||
|
@ -181,24 +144,6 @@ export default class RoomHeader extends React.Component {
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) {
|
|
||||||
let pinsIndicator = null;
|
|
||||||
if (this._hasUnreadPins()) {
|
|
||||||
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);
|
|
||||||
} else if (this._hasPins()) {
|
|
||||||
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator" />);
|
|
||||||
}
|
|
||||||
|
|
||||||
pinnedEventsButton =
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className="mx_RoomHeader_button mx_RoomHeader_pinnedButton"
|
|
||||||
onClick={this.props.onPinnedClick}
|
|
||||||
title={_t("Pinned Messages")}
|
|
||||||
>
|
|
||||||
{ pinsIndicator }
|
|
||||||
</AccessibleTooltipButton>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let forgetButton;
|
let forgetButton;
|
||||||
if (this.props.onForgetClick) {
|
if (this.props.onForgetClick) {
|
||||||
forgetButton =
|
forgetButton =
|
||||||
|
@ -248,7 +193,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 }
|
||||||
|
@ -265,7 +209,7 @@ export default class RoomHeader extends React.Component {
|
||||||
{ topicElement }
|
{ topicElement }
|
||||||
{ cancelButton }
|
{ cancelButton }
|
||||||
{ rightRow }
|
{ rightRow }
|
||||||
<RoomHeaderButtons />
|
<RoomHeaderButtons room={this.props.room} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1510,11 +1510,8 @@
|
||||||
"Invite to just this room": "Invite to just this room",
|
"Invite to just this room": "Invite to just this room",
|
||||||
"Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.",
|
"Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.",
|
||||||
"This is the start of <roomName/>.": "This is the start of <roomName/>.",
|
"This is the start of <roomName/>.": "This is the start of <roomName/>.",
|
||||||
"No pinned messages.": "No pinned messages.",
|
"Unpin": "Unpin",
|
||||||
"Loading...": "Loading...",
|
"View message": "View message",
|
||||||
"Pinned Messages": "Pinned Messages",
|
|
||||||
"Unpin Message": "Unpin Message",
|
|
||||||
"Jump to message": "Jump to message",
|
|
||||||
"%(duration)ss": "%(duration)ss",
|
"%(duration)ss": "%(duration)ss",
|
||||||
"%(duration)sm": "%(duration)sm",
|
"%(duration)sm": "%(duration)sm",
|
||||||
"%(duration)sh": "%(duration)sh",
|
"%(duration)sh": "%(duration)sh",
|
||||||
|
@ -1720,9 +1717,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",
|
||||||
|
@ -1898,6 +1897,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",
|
||||||
|
@ -1952,7 +1952,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",
|
||||||
|
@ -2466,6 +2465,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",
|
||||||
|
@ -2628,8 +2628,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.",
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue