Merge pull request #6096 from matrix-org/t3chguy/fix/6606

This commit is contained in:
Michael Telatynski 2021-06-04 00:00:31 +01:00 committed by GitHub
commit f22fd7aa3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 668 additions and 637 deletions

View file

@ -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";

View file

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

View file

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

View file

@ -16,62 +16,91 @@ limitations under the License.
.mx_PinnedEventTile { .mx_PinnedEventTile {
min-height: 40px; min-height: 40px;
margin-bottom: 5px;
width: 100%; width: 100%;
border-radius: 5px; // for the hover padding: 0 4px 12px;
}
.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;
}
}
} }

View file

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

View file

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

Before

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

After

Width:  |  Height:  |  Size: 1 KiB

View file

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

View file

@ -1,6 +1,6 @@
/* /*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2015 - 2020 The Matrix.org Foundation C.I.C. Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,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;

View file

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

View file

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

View file

@ -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']}
/>, />
]; </>;
} }
} }

View file

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

View file

@ -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">

View 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("Youre 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;

View file

@ -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']}
/>, />
]; </>;
} }
} }

View file

@ -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,

View file

@ -1,111 +0,0 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import PropTypes from 'prop-types';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
import AccessibleButton from "../elements/AccessibleButton";
import MessageEvent from "../messages/MessageEvent";
import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from '../../../languageHandler';
import {formatFullDate} from '../../../DateUtils';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.rooms.PinnedEventTile")
export default class PinnedEventTile extends React.Component {
static propTypes = {
mxRoom: PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
onUnpinned: PropTypes.func,
};
onTileClicked = () => {
dis.dispatch({
action: 'view_room',
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
});
};
onUnpinClicked = () => {
const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
// Nothing to do: already unpinned
if (this.props.onUnpinned) this.props.onUnpinned();
} else {
const pinned = pinnedEvents.getContent().pinned;
const index = pinned.indexOf(this.props.mxEvent.getId());
if (index !== -1) {
pinned.splice(index, 1);
MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '')
.then(() => {
if (this.props.onUnpinned) this.props.onUnpinned();
});
} else if (this.props.onUnpinned) this.props.onUnpinned();
}
};
_canUnpin() {
return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get());
}
render() {
const sender = this.props.mxEvent.getSender();
// Get the latest sender profile rather than historical
const senderProfile = this.props.mxRoom.getMember(sender);
const avatarSize = 40;
let unpinButton = null;
if (this._canUnpin()) {
unpinButton = (
<AccessibleButton onClick={this.onUnpinClicked} className="mx_PinnedEventTile_unpinButton">
<img src={require("../../../../res/img/cancel-red.svg")} width="8" height="8" alt={_t('Unpin Message')} title={_t('Unpin Message')} />
</AccessibleButton>
);
}
return (
<div className="mx_PinnedEventTile">
<div className="mx_PinnedEventTile_actions">
<AccessibleButton className="mx_PinnedEventTile_gotoButton mx_textButton" onClick={this.onTileClicked}>
{ _t("Jump to message") }
</AccessibleButton>
{ unpinButton }
</div>
<span className="mx_PinnedEventTile_senderAvatar">
<MemberAvatar member={senderProfile} width={avatarSize} height={avatarSize} fallbackUserId={sender} />
</span>
<span className="mx_PinnedEventTile_sender">
{ senderProfile ? senderProfile.name : sender }
</span>
<span className="mx_PinnedEventTile_timestamp">
{ formatFullDate(new Date(this.props.mxEvent.getTs())) }
</span>
<div className="mx_PinnedEventTile_message">
<MessageEvent
mxEvent={this.props.mxEvent}
className="mx_PinnedEventTile_body"
maxImageHeight={150}
onHeightChanged={() => {}} // we need to give this, apparently
/>
</div>
</div>
);
}
}

View file

@ -0,0 +1,104 @@
/*
Copyright 2017 Travis Ralston
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import dis from "../../../dispatcher/dispatcher";
import AccessibleButton from "../elements/AccessibleButton";
import MessageEvent from "../messages/MessageEvent";
import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from '../../../languageHandler';
import { formatDate } from '../../../DateUtils';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
interface IProps {
room: Room;
event: MatrixEvent;
onUnpinClicked?(): void;
}
const AVATAR_SIZE = 24;
@replaceableComponent("views.rooms.PinnedEventTile")
export default class PinnedEventTile extends React.Component<IProps> {
public static contextType = MatrixClientContext;
private onTileClicked = () => {
dis.dispatch({
action: 'view_room',
event_id: this.props.event.getId(),
highlighted: true,
room_id: this.props.event.getRoomId(),
});
};
render() {
const sender = this.props.event.getSender();
const senderProfile = this.props.room.getMember(sender);
let unpinButton = null;
if (this.props.onUnpinClicked) {
unpinButton = (
<AccessibleTooltipButton
onClick={this.props.onUnpinClicked}
className="mx_PinnedEventTile_unpinButton"
title={_t("Unpin")}
/>
);
}
return <div className="mx_PinnedEventTile">
<MemberAvatar
className="mx_PinnedEventTile_senderAvatar"
member={senderProfile}
width={AVATAR_SIZE}
height={AVATAR_SIZE}
fallbackUserId={sender}
/>
<span className={"mx_PinnedEventTile_sender " + getUserNameColorClass(sender)}>
{ senderProfile?.name || sender }
</span>
{ unpinButton }
<div className="mx_PinnedEventTile_message">
<MessageEvent
mxEvent={this.props.event}
className="mx_PinnedEventTile_body"
maxImageHeight={150}
onHeightChanged={() => {}} // we need to give this, apparently
/>
</div>
<div className="mx_PinnedEventTile_footer">
<span className="mx_PinnedEventTile_timestamp">
{ formatDate(new Date(this.props.event.getTs())) }
</span>
<AccessibleButton onClick={this.onTileClicked} kind="link">
{ _t("View message") }
</AccessibleButton>
</div>
</div>;
}
}

View file

@ -1,145 +0,0 @@
/*
Copyright 2017 Travis Ralston
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import PropTypes from 'prop-types';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import AccessibleButton from "../elements/AccessibleButton";
import PinnedEventTile from "./PinnedEventTile";
import { _t } from '../../../languageHandler';
import PinningUtils from "../../../utils/PinningUtils";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.rooms.PinnedEventsPanel")
export default class PinnedEventsPanel extends React.Component {
static propTypes = {
// The Room from the js-sdk we're going to show pinned events for
room: PropTypes.object.isRequired,
onCancelClick: PropTypes.func,
};
state = {
loading: true,
};
componentDidMount() {
this._updatePinnedMessages();
MatrixClientPeg.get().on("RoomState.events", this._onStateEvent);
}
componentWillUnmount() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent);
}
}
_onStateEvent = ev => {
if (ev.getRoomId() === this.props.room.roomId && ev.getType() === "m.room.pinned_events") {
this._updatePinnedMessages();
}
};
_updatePinnedMessages = () => {
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
this.setState({ loading: false, pinned: [] });
} else {
const promises = [];
const cli = MatrixClientPeg.get();
pinnedEvents.getContent().pinned.map((eventId) => {
promises.push(cli.getEventTimeline(this.props.room.getUnfilteredTimelineSet(), eventId, 0).then(
(timeline) => {
const event = timeline.getEvents().find((e) => e.getId() === eventId);
return {eventId, timeline, event};
}).catch((err) => {
console.error("Error looking up pinned event " + eventId + " in room " + this.props.room.roomId);
console.error(err);
return null; // return lack of context to avoid unhandled errors
}));
});
Promise.all(promises).then((contexts) => {
// Filter out the messages before we try to render them
const pinned = contexts.filter((context) => PinningUtils.isPinnable(context.event));
this.setState({ loading: false, pinned });
});
}
this._updateReadState();
};
_updateReadState() {
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents) return; // nothing to read
let readStateEvents = [];
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
if (readPinsEvent && readPinsEvent.getContent()) {
readStateEvents = readPinsEvent.getContent().event_ids || [];
}
if (!readStateEvents.includes(pinnedEvents.getId())) {
readStateEvents.push(pinnedEvents.getId());
// Only keep the last 10 event IDs to avoid infinite growth
readStateEvents = readStateEvents.reverse().splice(0, 10).reverse();
MatrixClientPeg.get().setRoomAccountData(this.props.room.roomId, "im.vector.room.read_pins", {
event_ids: readStateEvents,
});
}
}
_getPinnedTiles() {
if (this.state.pinned.length === 0) {
return (<div>{ _t("No pinned messages.") }</div>);
}
return this.state.pinned.map((context) => {
return (
<PinnedEventTile
key={context.event.getId()}
mxRoom={this.props.room}
mxEvent={context.event}
onUnpinned={this._updatePinnedMessages}
/>
);
});
}
render() {
let tiles = <div>{ _t("Loading...") }</div>;
if (this.state && !this.state.loading) {
tiles = this._getPinnedTiles();
}
return (
<div className="mx_PinnedEventsPanel">
<div className="mx_PinnedEventsPanel_body">
<AccessibleButton className="mx_PinnedEventsPanel_cancel" onClick={this.props.onCancelClick}>
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18" />
</AccessibleButton>
<h3 className="mx_PinnedEventsPanel_header">{ _t("Pinned Messages") }</h3>
{ tiles }
</div>
</div>
);
}
}

View file

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

View file

@ -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,

View file

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

View file

@ -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 youre verifying is connected to": "The homeserver the user youre verifying is connected to", "The homeserver the user youre verifying is connected to": "The homeserver the user youre verifying is connected to",
"Yours, or the other users internet connection": "Yours, or the other users internet connection", "Yours, or the other users internet connection": "Yours, or the other users internet connection",
"Yours, or the other users session": "Yours, or the other users session", "Yours, or the other users session": "Yours, or the other users session",
"Youre all caught up": "Youre 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",
"Youre all caught up": "Youre 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.",

View file

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

View file

@ -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,