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/right_panel/_BaseCard.scss";
|
||||
@import "./views/right_panel/_EncryptionInfo.scss";
|
||||
@import "./views/right_panel/_PinnedMessagesCard.scss";
|
||||
@import "./views/right_panel/_RoomSummaryCard.scss";
|
||||
@import "./views/right_panel/_UserInfo.scss";
|
||||
@import "./views/right_panel/_VerificationPanel.scss";
|
||||
|
@ -203,7 +204,6 @@
|
|||
@import "./views/rooms/_NewRoomIntro.scss";
|
||||
@import "./views/rooms/_NotificationBadge.scss";
|
||||
@import "./views/rooms/_PinnedEventTile.scss";
|
||||
@import "./views/rooms/_PinnedEventsPanel.scss";
|
||||
@import "./views/rooms/_PresenceLabel.scss";
|
||||
@import "./views/rooms/_ReplyPreview.scss";
|
||||
@import "./views/rooms/_RoomBreadcrumbs.scss";
|
||||
|
|
|
@ -99,6 +99,48 @@ limitations under the License.
|
|||
mask-position: center;
|
||||
}
|
||||
|
||||
$dot-size: 8px;
|
||||
$pulse-color: $pinned-unread-color;
|
||||
|
||||
.mx_RightPanel_pinnedMessagesButton {
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/pin.svg');
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
.mx_RightPanel_pinnedMessagesButton_unreadIndicator {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
margin: 4px;
|
||||
width: $dot-size;
|
||||
height: $dot-size;
|
||||
border-radius: 50%;
|
||||
transform: scale(1);
|
||||
background: rgba($pulse-color, 1);
|
||||
box-shadow: 0 0 0 0 rgba($pulse-color, 1);
|
||||
animation: mx_RightPanel_indicator_pulse 2s infinite;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mx_RightPanel_indicator_pulse {
|
||||
0% {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 0 0 0 rgba($pulse-color, 0.7);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 10px rgba($pulse-color, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 0 0 0 rgba($pulse-color, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RightPanel_headerButton_highlight {
|
||||
&::before {
|
||||
background-color: $accent-color !important;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,24 +14,22 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_PinnedEventsPanel {
|
||||
border-top: 1px solid $primary-hairline-color;
|
||||
}
|
||||
.mx_PinnedMessagesCard {
|
||||
padding-top: 0;
|
||||
|
||||
.mx_PinnedEventsPanel_body {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
.mx_BaseCard_header {
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
border-bottom: 1px solid $menu-border-color;
|
||||
|
||||
.mx_PinnedEventsPanel_header {
|
||||
margin: 0;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
> h2 {
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-18px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.mx_PinnedEventsPanel_cancel {
|
||||
margin: 12px;
|
||||
float: right;
|
||||
display: inline-block;
|
||||
.mx_BaseCard_close {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,62 +16,91 @@ limitations under the License.
|
|||
|
||||
.mx_PinnedEventTile {
|
||||
min-height: 40px;
|
||||
margin-bottom: 5px;
|
||||
width: 100%;
|
||||
border-radius: 5px; // for the hover
|
||||
}
|
||||
padding: 0 4px 12px;
|
||||
|
||||
.mx_PinnedEventTile:hover {
|
||||
background-color: $event-selected-color;
|
||||
}
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"avatar name remove"
|
||||
"content content content"
|
||||
"footer footer footer";
|
||||
grid-template-rows: max-content auto max-content;
|
||||
grid-template-columns: 24px auto 24px;
|
||||
grid-row-gap: 12px;
|
||||
grid-column-gap: 8px;
|
||||
|
||||
.mx_PinnedEventTile .mx_PinnedEventTile_sender,
|
||||
.mx_PinnedEventTile .mx_PinnedEventTile_timestamp {
|
||||
color: #868686;
|
||||
font-size: 0.8em;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
& + .mx_PinnedEventTile {
|
||||
padding: 12px 4px;
|
||||
border-top: 1px solid $menu-border-color;
|
||||
}
|
||||
|
||||
.mx_PinnedEventTile .mx_PinnedEventTile_timestamp {
|
||||
padding-left: 15px;
|
||||
display: none;
|
||||
}
|
||||
.mx_PinnedEventTile_senderAvatar {
|
||||
grid-area: avatar;
|
||||
}
|
||||
|
||||
.mx_PinnedEventTile .mx_PinnedEventTile_senderAvatar .mx_BaseAvatar {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.mx_PinnedEventTile_sender {
|
||||
grid-area: name;
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mx_PinnedEventTile_actions {
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
display: none;
|
||||
}
|
||||
.mx_PinnedEventTile_unpinButton {
|
||||
visibility: hidden;
|
||||
grid-area: remove;
|
||||
position: relative;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 8px;
|
||||
|
||||
.mx_PinnedEventTile:hover .mx_PinnedEventTile_timestamp {
|
||||
display: inline-block;
|
||||
}
|
||||
&:hover {
|
||||
background-color: $roomheader-addroom-bg-color;
|
||||
}
|
||||
|
||||
.mx_PinnedEventTile:hover .mx_PinnedEventTile_actions {
|
||||
display: block;
|
||||
}
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
//top: 0;
|
||||
//left: 0;
|
||||
height: inherit;
|
||||
width: inherit;
|
||||
background: $secondary-fg-color;
|
||||
mask-position: center;
|
||||
mask-size: 8px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-image: url('$(res)/img/image-view/close.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_PinnedEventTile_unpinButton {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.mx_PinnedEventTile_message {
|
||||
grid-area: content;
|
||||
}
|
||||
|
||||
.mx_PinnedEventTile_gotoButton {
|
||||
display: inline-block;
|
||||
font-size: 0.7em; // Smaller text to avoid conflicting with the layout
|
||||
}
|
||||
.mx_PinnedEventTile_footer {
|
||||
grid-area: footer;
|
||||
font-size: 10px;
|
||||
line-height: 12px;
|
||||
|
||||
.mx_PinnedEventTile_message {
|
||||
margin-left: 50px;
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
.mx_PinnedEventTile_timestamp {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
color: $secondary-fg-color;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_link {
|
||||
padding: 0;
|
||||
margin-left: 12px;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.mx_PinnedEventTile_unpinButton {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -277,24 +277,6 @@ limitations under the License.
|
|||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.mx_RoomHeader_pinnedButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/pin.svg');
|
||||
}
|
||||
|
||||
.mx_RoomHeader_pinsIndicator {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 8px;
|
||||
background-color: $pinned-color;
|
||||
}
|
||||
|
||||
.mx_RoomHeader_pinsIndicatorUnread {
|
||||
background-color: $pinned-unread-color;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 480px) {
|
||||
.mx_RoomHeader_wrapper {
|
||||
padding: 0;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.5151 20.0831L15.6941 17.2621L17.2621 15.6941L20.0831 18.5151C21.5741 20.0061 22.1529 21.7793 21.9661 21.9661C21.7793 22.1529 20.0061 21.5741 18.5151 20.0831Z" fill="black"/>
|
||||
<path d="M7.46196 11.3821C7.07677 11.5059 5.49073 12.0989 3.63366 12.0744C1.77658 12.0499 1.67795 10.8941 2.46811 10.1039L6.28598 6.28602L9.42196 9.42203L7.46196 11.3821Z" fill="black"/>
|
||||
<path d="M11.3821 7.46202C11.5059 7.07682 12.0989 5.49077 12.0744 3.63368C12.0499 1.77658 10.8941 1.67795 10.1039 2.46812L6.28598 6.28602L9.42196 9.42203L11.3821 7.46202Z" fill="black"/>
|
||||
<path d="M7.40596 11.438L11.4379 7.40602L14.9099 10.206L10.2059 14.9101L7.40596 11.438Z" fill="black"/>
|
||||
<path d="M11.774 11.774C9.31114 14.2369 8.61779 17.7115 9.83827 20.3213C10.3104 21.3308 11.6288 21.3273 12.4169 20.5392L20.5391 12.4169C21.3271 11.6289 21.3307 10.3104 20.3212 9.83829C17.7114 8.61779 14.2369 9.31115 11.774 11.774Z" fill="black"/>
|
||||
<path d="M18.5151 20.0831L15.6941 17.2621L17.2621 15.6941L20.0831 18.5151C21.5741 20.0061 22.1529 21.7793 21.9661 21.9661C21.7793 22.1529 20.0061 21.5741 18.5151 20.0831Z" fill="#737D8C"/>
|
||||
<path d="M7.46196 11.3821C7.07677 11.5059 5.49073 12.0989 3.63366 12.0744C1.77658 12.0499 1.67795 10.8941 2.46811 10.1039L6.28598 6.28602L9.42196 9.42203L7.46196 11.3821Z" fill="#737D8C"/>
|
||||
<path d="M11.3821 7.46202C11.5059 7.07682 12.0989 5.49077 12.0744 3.63368C12.0499 1.77658 10.8941 1.67795 10.1039 2.46812L6.28598 6.28602L9.42196 9.42203L11.3821 7.46202Z" fill="#737D8C"/>
|
||||
<path d="M7.40596 11.438L11.4379 7.40602L14.9099 10.206L10.2059 14.9101L7.40596 11.438Z" fill="#737D8C"/>
|
||||
<path d="M11.774 11.774C9.31114 14.2369 8.61779 17.7115 9.83827 20.3213C10.3104 21.3308 11.6288 21.3273 12.4169 20.5392L20.5391 12.4169C21.3271 11.6289 21.3307 10.3104 20.3212 9.83829C17.7114 8.61779 14.2369 9.31115 11.774 11.774Z" fill="#737D8C"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1,015 B After Width: | Height: | Size: 1 KiB |
|
@ -1,7 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2016, 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,29 +14,25 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import React from "react";
|
||||
|
||||
import { _t } from '../../languageHandler';
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import * as sdk from "../../index";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import BaseCard from "../views/right_panel/BaseCard";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import TimelinePanel from "./TimelinePanel";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
|
||||
interface IProps {
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
/*
|
||||
* Component which shows the global notification list using a TimelinePanel
|
||||
*/
|
||||
@replaceableComponent("structures.NotificationPanel")
|
||||
class NotificationPanel extends React.Component {
|
||||
static propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default class NotificationPanel extends React.PureComponent<IProps> {
|
||||
render() {
|
||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
||||
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
||||
<h2>{_t('You’re all caught up')}</h2>
|
||||
<p>{_t('You have no visible notifications.')}</p>
|
||||
|
@ -47,6 +41,7 @@ class NotificationPanel extends React.Component {
|
|||
let content;
|
||||
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
||||
if (timelineSet) {
|
||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||
content = (
|
||||
<TimelinePanel
|
||||
manageReadReceipts={false}
|
||||
|
@ -59,7 +54,7 @@ class NotificationPanel extends React.Component {
|
|||
);
|
||||
} else {
|
||||
console.error("No notifTimelineSet available!");
|
||||
content = <Loader />;
|
||||
content = <Spinner />;
|
||||
}
|
||||
|
||||
return <BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
|
||||
|
@ -67,5 +62,3 @@ class NotificationPanel extends React.Component {
|
|||
</BaseCard>;
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationPanel;
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2015 - 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,70 +16,92 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import RateLimitedFunc from '../../ratelimitedfunc';
|
||||
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||
import GroupStore from '../../stores/GroupStore';
|
||||
import {
|
||||
RightPanelPhases,
|
||||
RIGHT_PANEL_PHASES_NO_ARGS,
|
||||
RIGHT_PANEL_SPACE_PHASES,
|
||||
RightPanelPhases,
|
||||
} from "../../stores/RightPanelStorePhases";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
||||
import WidgetCard from "../views/right_panel/WidgetCard";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import MemberList from "../views/rooms/MemberList";
|
||||
import GroupMemberList from "../views/groups/GroupMemberList";
|
||||
import GroupRoomList from "../views/groups/GroupRoomList";
|
||||
import GroupRoomInfo from "../views/groups/GroupRoomInfo";
|
||||
import UserInfo from "../views/right_panel/UserInfo";
|
||||
import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo";
|
||||
import FilePanel from "./FilePanel";
|
||||
import NotificationPanel from "./NotificationPanel";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
|
||||
|
||||
interface IProps {
|
||||
room?: Room; // if showing panels for a given room, this is set
|
||||
groupId?: string; // if showing panels for a given group, this is set
|
||||
user?: User; // used if we know the user ahead of opening the panel
|
||||
resizeNotifier: ResizeNotifier;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
phase: RightPanelPhases;
|
||||
isUserPrivilegedInGroup?: boolean;
|
||||
member?: RoomMember;
|
||||
verificationRequest?: VerificationRequest;
|
||||
verificationRequestPromise?: Promise<VerificationRequest>;
|
||||
space?: Room;
|
||||
widgetId?: string;
|
||||
groupRoomId?: string;
|
||||
groupId?: string;
|
||||
event: MatrixEvent;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RightPanel")
|
||||
export default class RightPanel extends React.Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
room: PropTypes.instanceOf(Room), // if showing panels for a given room, this is set
|
||||
groupId: PropTypes.string, // if showing panels for a given group, this is set
|
||||
user: PropTypes.object, // used if we know the user ahead of opening the panel
|
||||
};
|
||||
}
|
||||
|
||||
export default class RightPanel extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
private readonly delayedUpdate: RateLimitedFunc;
|
||||
private dispatcherRef: string;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
...RightPanelStore.getSharedInstance().roomPanelPhaseParams,
|
||||
phase: this._getPhaseFromProps(),
|
||||
phase: this.getPhaseFromProps(),
|
||||
isUserPrivilegedInGroup: null,
|
||||
member: this._getUserForPanel(),
|
||||
member: this.getUserForPanel(),
|
||||
};
|
||||
this.onAction = this.onAction.bind(this);
|
||||
this.onRoomStateMember = this.onRoomStateMember.bind(this);
|
||||
this.onGroupStoreUpdated = this.onGroupStoreUpdated.bind(this);
|
||||
this.onInviteToGroupButtonClick = this.onInviteToGroupButtonClick.bind(this);
|
||||
this.onAddRoomToGroupButtonClick = this.onAddRoomToGroupButtonClick.bind(this);
|
||||
|
||||
this._delayedUpdate = new RateLimitedFunc(() => {
|
||||
this.delayedUpdate = new RateLimitedFunc(() => {
|
||||
this.forceUpdate();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Helper function to split out the logic for _getPhaseFromProps() and the constructor
|
||||
// Helper function to split out the logic for getPhaseFromProps() and the constructor
|
||||
// as both are called at the same time in the constructor.
|
||||
_getUserForPanel() {
|
||||
private getUserForPanel() {
|
||||
if (this.state && this.state.member) return this.state.member;
|
||||
const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams;
|
||||
return this.props.user || lastParams['member'];
|
||||
}
|
||||
|
||||
// gets the current phase from the props and also maybe the store
|
||||
_getPhaseFromProps() {
|
||||
private getPhaseFromProps() {
|
||||
const rps = RightPanelStore.getSharedInstance();
|
||||
const userForPanel = this._getUserForPanel();
|
||||
const userForPanel = this.getUserForPanel();
|
||||
if (this.props.groupId) {
|
||||
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
|
||||
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList});
|
||||
|
@ -118,7 +140,7 @@ export default class RightPanel extends React.Component {
|
|||
this.dispatcherRef = dis.register(this.onAction);
|
||||
const cli = this.context;
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
this._initGroupStore(this.props.groupId);
|
||||
this.initGroupStore(this.props.groupId);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -126,61 +148,47 @@ export default class RightPanel extends React.Component {
|
|||
if (this.context) {
|
||||
this.context.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
}
|
||||
this._unregisterGroupStore(this.props.groupId);
|
||||
this.unregisterGroupStore();
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
||||
if (newProps.groupId !== this.props.groupId) {
|
||||
this._unregisterGroupStore(this.props.groupId);
|
||||
this._initGroupStore(newProps.groupId);
|
||||
this.unregisterGroupStore();
|
||||
this.initGroupStore(newProps.groupId);
|
||||
}
|
||||
}
|
||||
|
||||
_initGroupStore(groupId) {
|
||||
private initGroupStore(groupId: string) {
|
||||
if (!groupId) return;
|
||||
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
|
||||
}
|
||||
|
||||
_unregisterGroupStore() {
|
||||
private unregisterGroupStore() {
|
||||
GroupStore.unregisterListener(this.onGroupStoreUpdated);
|
||||
}
|
||||
|
||||
onGroupStoreUpdated() {
|
||||
private onGroupStoreUpdated = () => {
|
||||
this.setState({
|
||||
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onInviteToGroupButtonClick() {
|
||||
showGroupInviteDialog(this.props.groupId).then(() => {
|
||||
this.setState({
|
||||
phase: RightPanelPhases.GroupMemberList,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onAddRoomToGroupButtonClick() {
|
||||
showGroupAddRoomDialog(this.props.groupId).then(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
onRoomStateMember(ev, state, member) {
|
||||
private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => {
|
||||
if (!this.props.room || member.roomId !== this.props.room.roomId) {
|
||||
return;
|
||||
}
|
||||
// redraw the badge on the membership list
|
||||
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) {
|
||||
this._delayedUpdate();
|
||||
this.delayedUpdate();
|
||||
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId &&
|
||||
member.userId === this.state.member.userId) {
|
||||
// refresh the member info (e.g. new power level)
|
||||
this._delayedUpdate();
|
||||
this.delayedUpdate();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onAction(payload) {
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === Action.AfterRightPanelPhaseChange) {
|
||||
this.setState({
|
||||
phase: payload.phase,
|
||||
|
@ -194,9 +202,9 @@ export default class RightPanel extends React.Component {
|
|||
space: payload.space,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
private onClose = () => {
|
||||
// XXX: There are three different ways of 'closing' this panel depending on what state
|
||||
// things are in... this knows far more than it should do about the state of the rest
|
||||
// of the app and is generally a bit silly.
|
||||
|
@ -224,16 +232,6 @@ export default class RightPanel extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const MemberList = sdk.getComponent('rooms.MemberList');
|
||||
const UserInfo = sdk.getComponent('right_panel.UserInfo');
|
||||
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
|
||||
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
||||
const FilePanel = sdk.getComponent('structures.FilePanel');
|
||||
|
||||
const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
|
||||
const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
|
||||
const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
|
||||
|
||||
let panel = <div />;
|
||||
const roomId = this.props.room ? this.props.room.roomId : undefined;
|
||||
|
||||
|
@ -285,6 +283,7 @@ export default class RightPanel extends React.Component {
|
|||
user={this.state.member}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.userId}
|
||||
phase={this.state.phase}
|
||||
onClose={this.onClose} />;
|
||||
break;
|
||||
|
||||
|
@ -299,6 +298,12 @@ export default class RightPanel extends React.Component {
|
|||
panel = <NotificationPanel onClose={this.onClose} />;
|
||||
break;
|
||||
|
||||
case RightPanelPhases.PinnedMessages:
|
||||
if (SettingsStore.getValue("feature_pinning")) {
|
||||
panel = <PinnedMessagesCard room={this.props.room} onClose={this.onClose} />;
|
||||
}
|
||||
break;
|
||||
|
||||
case RightPanelPhases.FilePanel:
|
||||
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
|
||||
break;
|
|
@ -46,7 +46,7 @@ import RoomViewStore from '../../stores/RoomViewStore';
|
|||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import {Layout} from "../../settings/Layout";
|
||||
import { Layout } from "../../settings/Layout";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||
|
@ -54,7 +54,6 @@ import RoomContext from "../../contexts/RoomContext";
|
|||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
import ScrollPanel from "./ScrollPanel";
|
||||
import TimelinePanel from "./TimelinePanel";
|
||||
|
@ -63,7 +62,6 @@ import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
|||
import ForwardMessage from "../views/rooms/ForwardMessage";
|
||||
import SearchBar from "../views/rooms/SearchBar";
|
||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
|
||||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import { XOR } from "../../@types/common";
|
||||
|
@ -82,7 +80,7 @@ import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager';
|
|||
import { objectHasDiff } from "../../utils/objects";
|
||||
import SpaceRoomView from "./SpaceRoomView";
|
||||
import { IOpts } from "../../createRoom";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { omit } from 'lodash';
|
||||
import UIStore from "../../stores/UIStore";
|
||||
|
||||
|
@ -157,7 +155,6 @@ export interface IState {
|
|||
canPeek: boolean;
|
||||
showApps: boolean;
|
||||
isPeeking: boolean;
|
||||
showingPinned: boolean;
|
||||
showReadReceipts: boolean;
|
||||
showRightPanel: boolean;
|
||||
// error object, as from the matrix client/server API
|
||||
|
@ -235,7 +232,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
canPeek: false,
|
||||
showApps: false,
|
||||
isPeeking: false,
|
||||
showingPinned: false,
|
||||
showReadReceipts: true,
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||
joining: false,
|
||||
|
@ -330,7 +326,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||
// we should only peek once we have a ready client
|
||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
|
||||
};
|
||||
|
@ -1403,13 +1398,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
return ret;
|
||||
}
|
||||
|
||||
private onPinnedClick = () => {
|
||||
const nowShowingPinned = !this.state.showingPinned;
|
||||
const roomId = this.state.room.roomId;
|
||||
this.setState({showingPinned: nowShowingPinned, searching: false});
|
||||
SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned);
|
||||
};
|
||||
|
||||
private onCallPlaced = (type: PlaceCallType) => {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
|
@ -1526,7 +1514,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
private onSearchClick = () => {
|
||||
this.setState({
|
||||
searching: !this.state.searching,
|
||||
showingPinned: false,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1855,9 +1842,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
} else if (showRoomUpgradeBar) {
|
||||
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
|
||||
hideCancel = true;
|
||||
} else if (this.state.showingPinned) {
|
||||
hideCancel = true; // has own cancel
|
||||
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
|
||||
} else if (myMembership !== "join") {
|
||||
// We do have a room object for this room, but we're not currently in it.
|
||||
// We may have a 3rd party invite to it.
|
||||
|
@ -2077,7 +2061,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
inRoom={myMembership === 'join'}
|
||||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onPinnedClick={this.onPinnedClick}
|
||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||
|
|
|
@ -17,9 +17,9 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
||||
import { EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -28,9 +28,10 @@ import Resend from '../../../Resend';
|
|||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { MenuItem } from "../../structures/ContextMenu";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
|
||||
|
||||
export function canCancel(eventStatus) {
|
||||
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)
|
||||
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl
|
||||
&& this.props.mxEvent.getType() !== EventType.RoomEncryption;
|
||||
let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli);
|
||||
let canPin = room.currentState.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli);
|
||||
|
||||
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
|
||||
if (!SettingsStore.getValue("feature_pinning")) canPin = false;
|
||||
|
@ -92,7 +93,7 @@ export default class MessageContextMenu extends React.Component {
|
|||
|
||||
_isPinned() {
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', '');
|
||||
const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, '');
|
||||
if (!pinnedEvent) return false;
|
||||
const content = pinnedEvent.getContent();
|
||||
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
|
||||
|
@ -165,25 +166,23 @@ export default class MessageContextMenu extends React.Component {
|
|||
};
|
||||
|
||||
onPinClick = () => {
|
||||
MatrixClientPeg.get().getStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', '')
|
||||
.catch((e) => {
|
||||
// Intercept the Event Not Found error and fall through the promise chain with no event.
|
||||
if (e.errcode === "M_NOT_FOUND") return null;
|
||||
throw e;
|
||||
})
|
||||
.then((event) => {
|
||||
const eventIds = (event ? event.pinned : []) || [];
|
||||
if (!eventIds.includes(this.props.mxEvent.getId())) {
|
||||
// Not pinned - add
|
||||
eventIds.push(this.props.mxEvent.getId());
|
||||
} else {
|
||||
// Pinned - remove
|
||||
eventIds.splice(eventIds.indexOf(this.props.mxEvent.getId()), 1);
|
||||
}
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||
const eventId = this.props.mxEvent.getId();
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.sendStateEvent(this.props.mxEvent.getRoomId(), 'm.room.pinned_events', {pinned: eventIds}, '');
|
||||
const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.pinned || [];
|
||||
if (pinnedIds.includes(eventId)) {
|
||||
pinnedIds.splice(pinnedIds.indexOf(eventId), 1);
|
||||
} else {
|
||||
pinnedIds.push(eventId);
|
||||
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
||||
event_ids: [
|
||||
...room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids,
|
||||
eventId,
|
||||
],
|
||||
});
|
||||
}
|
||||
cli.sendStateEvent(this.props.mxEvent.getRoomId(), EventType.RoomPinnedEvents, { pinned: pinnedIds }, "");
|
||||
this.closeMenu();
|
||||
};
|
||||
|
||||
|
|
|
@ -21,12 +21,12 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import HeaderButton from './HeaderButton';
|
||||
import HeaderButtons, {HeaderKind} from './HeaderButtons';
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {ActionPayload} from "../../../dispatcher/payloads";
|
||||
import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import HeaderButtons, { HeaderKind } from './HeaderButtons';
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
const GROUP_PHASES = [
|
||||
RightPanelPhases.GroupMemberInfo,
|
||||
|
@ -84,19 +84,21 @@ export default class GroupHeaderButtons extends HeaderButtons {
|
|||
};
|
||||
|
||||
renderButtons() {
|
||||
return [
|
||||
<HeaderButton key="groupMembersButton" name="groupMembersButton"
|
||||
return <>
|
||||
<HeaderButton
|
||||
name="groupMembersButton"
|
||||
title={_t('Members')}
|
||||
isHighlighted={this.isPhase(GROUP_PHASES)}
|
||||
onClick={this.onMembersClicked}
|
||||
analytics={['Right Panel', 'Group Member List Button', 'click']}
|
||||
/>,
|
||||
<HeaderButton key="roomsButton" name="roomsButton"
|
||||
/>
|
||||
<HeaderButton
|
||||
name="roomsButton"
|
||||
title={_t('Rooms')}
|
||||
isHighlighted={this.isPhase(ROOM_PHASES)}
|
||||
onClick={this.onRoomsClicked}
|
||||
analytics={['Right Panel', 'Group Room List Button', 'click']}
|
||||
/>,
|
||||
];
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,15 +22,13 @@ import React from 'react';
|
|||
import classNames from 'classnames';
|
||||
import Analytics from '../../../Analytics';
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
// Whether this button is highlighted
|
||||
isHighlighted: boolean;
|
||||
// click handler
|
||||
onClick: () => void;
|
||||
// The badge to display above the icon
|
||||
badge?: React.ReactNode;
|
||||
// The parameters to track the click event
|
||||
analytics: Parameters<typeof Analytics.trackEvent>;
|
||||
|
||||
|
@ -40,31 +38,29 @@ interface IProps {
|
|||
title: string;
|
||||
}
|
||||
|
||||
// TODO: replace this, the composer buttons and the right panel buttons with a unified
|
||||
// representation
|
||||
// TODO: replace this, the composer buttons and the right panel buttons with a unified representation
|
||||
@replaceableComponent("views.right_panel.HeaderButton")
|
||||
export default class HeaderButton extends React.Component<IProps> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
private onClick() {
|
||||
private onClick = () => {
|
||||
Analytics.trackEvent(...this.props.analytics);
|
||||
this.props.onClick();
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const {isHighlighted, onClick, analytics, name, title, ...props} = this.props;
|
||||
|
||||
const classes = classNames({
|
||||
mx_RightPanel_headerButton: true,
|
||||
mx_RightPanel_headerButton_highlight: this.props.isHighlighted,
|
||||
[`mx_RightPanel_${this.props.name}`]: true,
|
||||
mx_RightPanel_headerButton_highlight: isHighlighted,
|
||||
[`mx_RightPanel_${name}`]: true,
|
||||
});
|
||||
|
||||
return <AccessibleTooltipButton
|
||||
aria-selected={this.props.isHighlighted}
|
||||
{...props}
|
||||
aria-selected={isHighlighted}
|
||||
role="tab"
|
||||
title={this.props.title}
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
/>;
|
||||
|
|
|
@ -21,14 +21,14 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import RightPanelStore from "../../../stores/RightPanelStore";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {Action} from '../../../dispatcher/actions';
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import {
|
||||
SetRightPanelPhasePayload,
|
||||
SetRightPanelPhaseRefireParams,
|
||||
} from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
|
||||
import {EventSubscription} from "fbemitter";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import type { EventSubscription } from "fbemitter";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
export enum HeaderKind {
|
||||
Room = "room",
|
||||
|
@ -43,11 +43,11 @@ interface IState {
|
|||
interface IProps {}
|
||||
|
||||
@replaceableComponent("views.right_panel.HeaderButtons")
|
||||
export default abstract class HeaderButtons extends React.Component<IProps, IState> {
|
||||
export default abstract class HeaderButtons<P = {}> extends React.Component<IProps & P, IState> {
|
||||
private storeToken: EventSubscription;
|
||||
private dispatcherRef: string;
|
||||
|
||||
constructor(props: IProps, kind: HeaderKind) {
|
||||
constructor(props: IProps & P, kind: HeaderKind) {
|
||||
super(props);
|
||||
|
||||
const rps = RightPanelStore.getSharedInstance();
|
||||
|
@ -95,7 +95,7 @@ export default abstract class HeaderButtons extends React.Component<IProps, ISta
|
|||
}
|
||||
|
||||
// XXX: Make renderButtons a prop
|
||||
public abstract renderButtons(): JSX.Element[];
|
||||
public abstract renderButtons(): JSX.Element;
|
||||
|
||||
public render() {
|
||||
return <div className="mx_HeaderButtons">
|
||||
|
|
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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import React from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import HeaderButton from './HeaderButton';
|
||||
import HeaderButtons, {HeaderKind} from './HeaderButtons';
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {ActionPayload} from "../../../dispatcher/payloads";
|
||||
import HeaderButtons, { HeaderKind } from './HeaderButtons';
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import RightPanelStore from "../../../stores/RightPanelStore";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard';
|
||||
|
||||
const ROOM_INFO_PHASES = [
|
||||
RightPanelPhases.RoomSummary,
|
||||
|
@ -38,9 +42,35 @@ const ROOM_INFO_PHASES = [
|
|||
RightPanelPhases.Room3pidMemberInfo,
|
||||
];
|
||||
|
||||
const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => {
|
||||
const pinningEnabled = useSettingValue("feature_pinning");
|
||||
const pinnedEvents = usePinnedEvents(pinningEnabled && room);
|
||||
const readPinnedEvents = useReadPinnedEvents(pinningEnabled && room);
|
||||
if (!pinningEnabled) return null;
|
||||
|
||||
let unreadIndicator;
|
||||
if (pinnedEvents.some(id => !readPinnedEvents.has(id))) {
|
||||
unreadIndicator = <div className="mx_RightPanel_pinnedMessagesButton_unreadIndicator" />;
|
||||
}
|
||||
|
||||
return <HeaderButton
|
||||
name="pinnedMessagesButton"
|
||||
title={_t("Pinned messages")}
|
||||
isHighlighted={isHighlighted}
|
||||
onClick={onClick}
|
||||
analytics={["Right Panel", "Pinned Messages Button", "click"]}
|
||||
>
|
||||
{ unreadIndicator }
|
||||
</HeaderButton>;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
room?: Room;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.right_panel.RoomHeaderButtons")
|
||||
export default class RoomHeaderButtons extends HeaderButtons {
|
||||
constructor(props) {
|
||||
export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||
constructor(props: IProps) {
|
||||
super(props, HeaderKind.Room);
|
||||
}
|
||||
|
||||
|
@ -80,24 +110,32 @@ export default class RoomHeaderButtons extends HeaderButtons {
|
|||
this.setPhase(RightPanelPhases.NotificationPanel);
|
||||
};
|
||||
|
||||
private onPinnedMessagesClicked = () => {
|
||||
// This toggles for us, if needed
|
||||
this.setPhase(RightPanelPhases.PinnedMessages);
|
||||
};
|
||||
|
||||
public renderButtons() {
|
||||
return [
|
||||
return <>
|
||||
<PinnedMessagesHeaderButton
|
||||
room={this.props.room}
|
||||
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
|
||||
onClick={this.onPinnedMessagesClicked}
|
||||
/>
|
||||
<HeaderButton
|
||||
key="notifsButton"
|
||||
name="notifsButton"
|
||||
title={_t('Notifications')}
|
||||
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
|
||||
onClick={this.onNotificationsClicked}
|
||||
analytics={['Right Panel', 'Notification List Button', 'click']}
|
||||
/>,
|
||||
/>
|
||||
<HeaderButton
|
||||
key="roomSummaryButton"
|
||||
name="roomSummaryButton"
|
||||
title={_t('Room Info')}
|
||||
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
|
||||
onClick={this.onRoomSummaryClicked}
|
||||
analytics={['Right Panel', 'Room Summary Button', 'click']}
|
||||
/>,
|
||||
];
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,18 +17,19 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {MatrixClient} from 'matrix-js-sdk/src/client';
|
||||
import {RoomMember} from 'matrix-js-sdk/src/models/room-member';
|
||||
import {User} from 'matrix-js-sdk/src/models/user';
|
||||
import {Room} from 'matrix-js-sdk/src/models/room';
|
||||
import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
|
||||
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||
import { User } from 'matrix-js-sdk/src/models/user';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import createRoom, { findDMForUser, privateShouldBeEncrypted } from '../../../createRoom';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
@ -39,18 +40,18 @@ import MultiInviter from "../../../utils/MultiInviter";
|
|||
import GroupStore from "../../../stores/GroupStore";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import E2EIcon from "../rooms/E2EIcon";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {textualPowerLevel} from '../../../Roles';
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { textualPowerLevel } from '../../../Roles';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import EncryptionPanel from "./EncryptionPanel";
|
||||
import {useAsyncMemo} from '../../../hooks/useAsyncMemo';
|
||||
import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification';
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
|
||||
import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
|
||||
import {useIsEncrypted} from "../../../hooks/useIsEncrypted";
|
||||
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
|
||||
import BaseCard from "./BaseCard";
|
||||
import {E2EStatus} from "../../../utils/ShieldUtils";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
import ImageView from "../elements/ImageView";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import PowerSelector from "../elements/PowerSelector";
|
||||
|
@ -65,7 +66,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
|||
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
|
||||
export interface IDevice {
|
||||
|
@ -514,9 +515,6 @@ export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
|||
} else {
|
||||
setPowerLevels({});
|
||||
}
|
||||
return () => {
|
||||
setPowerLevels({});
|
||||
};
|
||||
}, [room]);
|
||||
|
||||
useEventEmitter(cli, "RoomState.events", update);
|
||||
|
@ -1530,21 +1528,16 @@ interface IProps {
|
|||
user: Member;
|
||||
groupId?: string;
|
||||
room?: Room;
|
||||
phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo | RightPanelPhases.SpaceMemberInfo;
|
||||
phase: RightPanelPhases.RoomMemberInfo
|
||||
| RightPanelPhases.GroupMemberInfo
|
||||
| RightPanelPhases.SpaceMemberInfo
|
||||
| RightPanelPhases.EncryptionPanel;
|
||||
onClose(): void;
|
||||
verificationRequest?: VerificationRequest;
|
||||
verificationRequestPromise?: Promise<VerificationRequest>;
|
||||
}
|
||||
|
||||
interface IPropsWithEncryptionPanel extends React.ComponentProps<typeof EncryptionPanel> {
|
||||
user: Member;
|
||||
groupId: void;
|
||||
room: Room;
|
||||
phase: RightPanelPhases.EncryptionPanel;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
type Props = IProps | IPropsWithEncryptionPanel;
|
||||
|
||||
const UserInfo: React.FC<Props> = ({
|
||||
const UserInfo: React.FC<IProps> = ({
|
||||
user,
|
||||
groupId,
|
||||
room,
|
||||
|
|
|
@ -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 classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
|
||||
import {CancelButton} from './SimpleRoomHeader';
|
||||
import { CancelButton } from './SimpleRoomHeader';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||
import E2EIcon from './E2EIcon';
|
||||
|
@ -30,8 +30,8 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
|||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import RoomTopic from "../elements/RoomTopic";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import {PlaceCallType} from "../../../CallHandler";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { PlaceCallType } from "../../../CallHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.rooms.RoomHeader")
|
||||
export default class RoomHeader extends React.Component {
|
||||
|
@ -40,7 +40,6 @@ export default class RoomHeader extends React.Component {
|
|||
oobData: PropTypes.object,
|
||||
inRoom: PropTypes.bool,
|
||||
onSettingsClick: PropTypes.func,
|
||||
onPinnedClick: PropTypes.func,
|
||||
onSearchClick: PropTypes.func,
|
||||
onLeaveClick: PropTypes.func,
|
||||
onCancelClick: PropTypes.func,
|
||||
|
@ -59,14 +58,12 @@ export default class RoomHeader extends React.Component {
|
|||
componentDidMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.events", this._onRoomStateEvents);
|
||||
cli.on("Room.accountData", this._onRoomAccountData);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.events", this._onRoomStateEvents);
|
||||
cli.removeListener("Room.accountData", this._onRoomAccountData);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,48 +76,14 @@ export default class RoomHeader extends React.Component {
|
|||
this._rateLimitedUpdate();
|
||||
};
|
||||
|
||||
_onRoomAccountData = (event, room) => {
|
||||
if (!this.props.room || room.roomId !== this.props.room.roomId) return;
|
||||
if (event.getType() !== "im.vector.room.read_pins") return;
|
||||
|
||||
this._rateLimitedUpdate();
|
||||
};
|
||||
|
||||
_rateLimitedUpdate = new RateLimitedFunc(function() {
|
||||
/* eslint-disable babel/no-invalid-this */
|
||||
this.forceUpdate();
|
||||
}, 500);
|
||||
|
||||
_hasUnreadPins() {
|
||||
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
||||
if (!currentPinEvent) return false;
|
||||
if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) {
|
||||
return false; // no pins == nothing to read
|
||||
}
|
||||
|
||||
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
|
||||
if (readPinsEvent && readPinsEvent.getContent()) {
|
||||
const readStateEvents = readPinsEvent.getContent().event_ids || [];
|
||||
if (readStateEvents) {
|
||||
return !readStateEvents.includes(currentPinEvent.getId());
|
||||
}
|
||||
}
|
||||
|
||||
// There's pins, and we haven't read any of them
|
||||
return true;
|
||||
}
|
||||
|
||||
_hasPins() {
|
||||
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
||||
if (!currentPinEvent) return false;
|
||||
|
||||
return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0);
|
||||
}
|
||||
|
||||
render() {
|
||||
let searchStatus = null;
|
||||
let cancelButton = null;
|
||||
let pinnedEventsButton = null;
|
||||
|
||||
if (this.props.onCancelClick) {
|
||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||
|
@ -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;
|
||||
if (this.props.onForgetClick) {
|
||||
forgetButton =
|
||||
|
@ -248,7 +193,6 @@ export default class RoomHeader extends React.Component {
|
|||
<div className="mx_RoomHeader_buttons">
|
||||
{ videoCallButton }
|
||||
{ voiceCallButton }
|
||||
{ pinnedEventsButton }
|
||||
{ forgetButton }
|
||||
{ appsButton }
|
||||
{ searchButton }
|
||||
|
@ -265,7 +209,7 @@ export default class RoomHeader extends React.Component {
|
|||
{ topicElement }
|
||||
{ cancelButton }
|
||||
{ rightRow }
|
||||
<RoomHeaderButtons />
|
||||
<RoomHeaderButtons room={this.props.room} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -16,8 +16,8 @@ limitations under the License.
|
|||
|
||||
import { createContext } from "react";
|
||||
|
||||
import {IState} from "../components/structures/RoomView";
|
||||
import {Layout} from "../settings/Layout";
|
||||
import { IState } from "../components/structures/RoomView";
|
||||
import { Layout } from "../settings/Layout";
|
||||
|
||||
const RoomContext = createContext<IState>({
|
||||
roomLoading: true,
|
||||
|
@ -31,7 +31,6 @@ const RoomContext = createContext<IState>({
|
|||
canPeek: false,
|
||||
showApps: false,
|
||||
isPeeking: false,
|
||||
showingPinned: false,
|
||||
showReadReceipts: true,
|
||||
showRightPanel: true,
|
||||
joining: false,
|
||||
|
|
|
@ -14,14 +14,22 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {useState, useEffect, DependencyList} from 'react';
|
||||
import { useState, useEffect, DependencyList } from 'react';
|
||||
|
||||
type Fn<T> = () => Promise<T>;
|
||||
|
||||
export const useAsyncMemo = <T>(fn: Fn<T>, deps: DependencyList, initialValue?: T): T => {
|
||||
const [value, setValue] = useState<T>(initialValue);
|
||||
useEffect(() => {
|
||||
fn().then(setValue);
|
||||
let discard = false;
|
||||
fn().then(v => {
|
||||
if (!discard) {
|
||||
setValue(v);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
discard = true;
|
||||
};
|
||||
}, deps); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
return value;
|
||||
};
|
||||
|
|
|
@ -1510,11 +1510,8 @@
|
|||
"Invite to just this room": "Invite to just this room",
|
||||
"Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.",
|
||||
"This is the start of <roomName/>.": "This is the start of <roomName/>.",
|
||||
"No pinned messages.": "No pinned messages.",
|
||||
"Loading...": "Loading...",
|
||||
"Pinned Messages": "Pinned Messages",
|
||||
"Unpin Message": "Unpin Message",
|
||||
"Jump to message": "Jump to message",
|
||||
"Unpin": "Unpin",
|
||||
"View message": "View message",
|
||||
"%(duration)ss": "%(duration)ss",
|
||||
"%(duration)sm": "%(duration)sm",
|
||||
"%(duration)sh": "%(duration)sh",
|
||||
|
@ -1720,9 +1717,11 @@
|
|||
"The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to",
|
||||
"Yours, or the other users’ internet connection": "Yours, or the other users’ internet connection",
|
||||
"Yours, or the other users’ session": "Yours, or the other users’ session",
|
||||
"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",
|
||||
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
|
||||
"Unpin": "Unpin",
|
||||
"Unpin a widget to view it in this panel": "Unpin a widget to view it in this panel",
|
||||
"Options": "Options",
|
||||
"Set my room layout for everyone": "Set my room layout for everyone",
|
||||
|
@ -1898,6 +1897,7 @@
|
|||
"Add rooms to this community": "Add rooms to this community",
|
||||
"Filter community rooms": "Filter community rooms",
|
||||
"Something went wrong when trying to get your communities.": "Something went wrong when trying to get your communities.",
|
||||
"Loading...": "Loading...",
|
||||
"Display your community flair in rooms configured to show it.": "Display your community flair in rooms configured to show it.",
|
||||
"You're not currently a member of any communities.": "You're not currently a member of any communities.",
|
||||
"Frequently Used": "Frequently Used",
|
||||
|
@ -1952,7 +1952,6 @@
|
|||
"Rotate Right": "Rotate Right",
|
||||
"Download": "Download",
|
||||
"Information": "Information",
|
||||
"View message": "View message",
|
||||
"Language Dropdown": "Language Dropdown",
|
||||
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
|
||||
"%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times",
|
||||
|
@ -2466,6 +2465,7 @@
|
|||
"Unable to reject invite": "Unable to reject invite",
|
||||
"Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)",
|
||||
"Forward Message": "Forward Message",
|
||||
"Unpin Message": "Unpin Message",
|
||||
"Pin Message": "Pin Message",
|
||||
"Unhide Preview": "Unhide Preview",
|
||||
"Share Permalink": "Share Permalink",
|
||||
|
@ -2628,8 +2628,6 @@
|
|||
"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.",
|
||||
"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 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.",
|
||||
|
|
|
@ -601,10 +601,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
displayName: _td('Enable widget screenshots on supported widgets'),
|
||||
default: false,
|
||||
},
|
||||
"PinnedEvents.isOpen": {
|
||||
supportedLevels: [SettingLevel.ROOM_DEVICE],
|
||||
default: false,
|
||||
},
|
||||
"promptBeforeInviteUnknownUsers": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td('Prompt before sending invites to potentially invalid matrix IDs'),
|
||||
|
|
|
@ -24,6 +24,7 @@ export enum RightPanelPhases {
|
|||
EncryptionPanel = 'EncryptionPanel',
|
||||
RoomSummary = 'RoomSummary',
|
||||
Widget = 'Widget',
|
||||
PinnedMessages = "PinnedMessages",
|
||||
|
||||
Room3pidMemberInfo = 'Room3pidMemberInfo',
|
||||
// Group stuff
|
||||
|
@ -43,6 +44,7 @@ export enum RightPanelPhases {
|
|||
export const RIGHT_PANEL_PHASES_NO_ARGS = [
|
||||
RightPanelPhases.RoomSummary,
|
||||
RightPanelPhases.NotificationPanel,
|
||||
RightPanelPhases.PinnedMessages,
|
||||
RightPanelPhases.FilePanel,
|
||||
RightPanelPhases.RoomMemberList,
|
||||
RightPanelPhases.GroupMemberList,
|
||||
|
|
Loading…
Reference in a new issue