From 170b11d130e28b75285baa0a807b41f833435831 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 12:13:16 +0100 Subject: [PATCH 01/18] Convert RightPanel to Typescript --- .../{RightPanel.js => RightPanel.tsx} | 126 +++++++++--------- src/components/views/right_panel/UserInfo.tsx | 20 ++- 2 files changed, 70 insertions(+), 76 deletions(-) rename src/components/structures/{RightPanel.js => RightPanel.tsx} (79%) diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.tsx similarity index 79% rename from src/components/structures/RightPanel.js rename to src/components/structures/RightPanel.tsx index d8c763eabd6..15fa94e035e 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.tsx @@ -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,13 +16,14 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; 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, @@ -36,50 +37,70 @@ import RoomSummaryCard from "../views/right_panel/RoomSummaryCard"; import WidgetCard from "../views/right_panel/WidgetCard"; 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"; + +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; +} -@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 - }; - } +interface IState { + phase: RightPanelPhases; + isUserPrivilegedInGroup?: boolean; + member?: RoomMember; + verificationRequest?: VerificationRequest; + verificationRequestPromise?: Promise; + space?: Room; + widgetId?: string; + groupRoomId?: string; + groupId?: string; + event: MatrixEvent; +} +@replaceableComponent("structures.RightPanel") +export default class RightPanel extends React.Component { 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 +139,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 +147,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 +201,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 +231,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 =
; const roomId = this.props.room ? this.props.room.roomId : undefined; @@ -285,6 +282,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; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 9798b282f6a..9ed2943bbb3 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -25,6 +25,7 @@ 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'; @@ -1529,21 +1530,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; } -interface IPropsWithEncryptionPanel extends React.ComponentProps { - user: Member; - groupId: void; - room: Room; - phase: RightPanelPhases.EncryptionPanel; - onClose(): void; -} - -type Props = IProps | IPropsWithEncryptionPanel; - -const UserInfo: React.FC = ({ +const UserInfo: React.FC = ({ user, groupId, room, From 152c178ea9786fa071df680598600c4ba34fa18d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 12:15:37 +0100 Subject: [PATCH 02/18] Convert NotificationPanel to Typescript --- ...ficationPanel.js => NotificationPanel.tsx} | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) rename src/components/structures/{NotificationPanel.js => NotificationPanel.tsx} (76%) diff --git a/src/components/structures/NotificationPanel.js b/src/components/structures/NotificationPanel.tsx similarity index 76% rename from src/components/structures/NotificationPanel.js rename to src/components/structures/NotificationPanel.tsx index 41aafc8b138..7e1566521d2 100644 --- a/src/components/structures/NotificationPanel.js +++ b/src/components/structures/NotificationPanel.tsx @@ -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 BaseCard from "../views/right_panel/BaseCard"; 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, - }; - +class NotificationPanel extends React.Component { 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 = (

{_t('You’re all caught up')}

{_t('You have no visible notifications.')}

@@ -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 = ( ; + content = ; } return From 13427aaf0778a8a09d574d99ea33388586ee9a79 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 13:02:09 +0100 Subject: [PATCH 03/18] Add a pulse animation to the pinned messages indicator and move it --- res/css/views/rooms/_RoomHeader.scss | 39 ++++++++++++++++++------ res/img/element-icons/room/pin.svg | 10 +++--- src/components/views/rooms/RoomHeader.js | 11 +------ 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 387d1588a38..a102dfcdd74 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -281,18 +281,39 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/pin.svg'); } -.mx_RoomHeader_pinsIndicator { +$dot-size: 8px; +$pulse-color: $pinned-unread-color; + +.mx_RoomHeader_pinsIndicatorUnread { position: absolute; right: 0; - bottom: 4px; - width: 8px; - height: 8px; - border-radius: 8px; - background-color: $pinned-color; -} + 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_RoomHeader_indicator_pulse 2s infinite; + animation-iteration-count: 1; +} + +@keyframes mx_RoomHeader_indicator_pulse { + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba($pulse-color, 0.7); + } -.mx_RoomHeader_pinsIndicatorUnread { - background-color: $pinned-unread-color; + 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); + } } @media only screen and (max-width: 480px) { diff --git a/res/img/element-icons/room/pin.svg b/res/img/element-icons/room/pin.svg index 16941b329ba..2448fc61c5d 100644 --- a/res/img/element-icons/room/pin.svg +++ b/res/img/element-icons/room/pin.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 6d3b50c10d9..7eeb5875ef1 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -110,13 +110,6 @@ export default class RoomHeader extends React.Component { 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; @@ -184,9 +177,7 @@ export default class RoomHeader extends React.Component { if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) { let pinsIndicator = null; if (this._hasUnreadPins()) { - pinsIndicator = (
); - } else if (this._hasPins()) { - pinsIndicator = (
); + pinsIndicator = (
); } pinnedEventsButton = From 96928e5d319653352ad8a5ae56840cfb5dae1e02 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 13:17:14 +0100 Subject: [PATCH 04/18] Header Buttons switch to a fragment from an array of nodes --- .../views/right_panel/GroupHeaderButtons.tsx | 14 ++++++++------ src/components/views/right_panel/HeaderButtons.tsx | 2 +- .../views/right_panel/RoomHeaderButtons.tsx | 10 ++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/views/right_panel/GroupHeaderButtons.tsx b/src/components/views/right_panel/GroupHeaderButtons.tsx index f006975b080..5ae5709d44e 100644 --- a/src/components/views/right_panel/GroupHeaderButtons.tsx +++ b/src/components/views/right_panel/GroupHeaderButtons.tsx @@ -84,19 +84,21 @@ export default class GroupHeaderButtons extends HeaderButtons { }; renderButtons() { - return [ - + , - + , - ]; + /> + ; } } diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx index 21442926793..6b8445e9b9f 100644 --- a/src/components/views/right_panel/HeaderButtons.tsx +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -95,7 +95,7 @@ export default abstract class HeaderButtons extends React.Component diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 0571622e64f..2a7a9e191b8 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -81,23 +81,21 @@ export default class RoomHeaderButtons extends HeaderButtons { }; public renderButtons() { - return [ + return <> , + /> , - ]; + /> + ; } } From 02d11b8926494b16f897e49f97076af8461c1278 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 13:53:46 +0100 Subject: [PATCH 05/18] Extend HeaderButton and HeaderButtons to be more generic --- .../views/right_panel/HeaderButton.tsx | 26 ++++++++----------- .../views/right_panel/HeaderButtons.tsx | 4 +-- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/components/views/right_panel/HeaderButton.tsx b/src/components/views/right_panel/HeaderButton.tsx index 2bc360e380a..8451f69fd37 100644 --- a/src/components/views/right_panel/HeaderButton.tsx +++ b/src/components/views/right_panel/HeaderButton.tsx @@ -29,8 +29,6 @@ interface IProps { 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; @@ -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 { - 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 ; diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx index 6b8445e9b9f..281d7edd8b0 100644 --- a/src/components/views/right_panel/HeaderButtons.tsx +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -43,11 +43,11 @@ interface IState { interface IProps {} @replaceableComponent("views.right_panel.HeaderButtons") -export default abstract class HeaderButtons extends React.Component { +export default abstract class HeaderButtons

extends React.Component { private storeToken: EventSubscription; private dispatcherRef: string; - constructor(props: IProps, kind: HeaderKind) { + constructor(props: IProps & P, kind: HeaderKind) { super(props); const rps = RightPanelStore.getSharedInstance(); From a6ca8f797dd851552608b5ea0c8355d34bb9d25c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 15:34:44 +0100 Subject: [PATCH 06/18] Fix useAsyncMemo out-of-order resolutions --- src/hooks/useAsyncMemo.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/hooks/useAsyncMemo.ts b/src/hooks/useAsyncMemo.ts index 38c70de2592..eda44b1ae46 100644 --- a/src/hooks/useAsyncMemo.ts +++ b/src/hooks/useAsyncMemo.ts @@ -21,7 +21,15 @@ type Fn = () => Promise; export const useAsyncMemo = (fn: Fn, deps: DependencyList, initialValue?: T): T => { const [value, setValue] = useState(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; }; From 4fa6d3599b2b1902f5faa78cf9a5f130397de1ae Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 15:44:39 +0100 Subject: [PATCH 07/18] Convert PinnedEventTile to Typescript --- ...PinnedEventTile.js => PinnedEventTile.tsx} | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) rename src/components/views/rooms/{PinnedEventTile.js => PinnedEventTile.tsx} (76%) diff --git a/src/components/views/rooms/PinnedEventTile.js b/src/components/views/rooms/PinnedEventTile.tsx similarity index 76% rename from src/components/views/rooms/PinnedEventTile.js rename to src/components/views/rooms/PinnedEventTile.tsx index 78cf422cc6e..482fd6ab174 100644 --- a/src/components/views/rooms/PinnedEventTile.js +++ b/src/components/views/rooms/PinnedEventTile.tsx @@ -1,5 +1,6 @@ /* 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. @@ -15,7 +16,9 @@ limitations under the License. */ import React from "react"; -import PropTypes from 'prop-types'; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + import {MatrixClientPeg} from "../../../MatrixClientPeg"; import dis from "../../../dispatcher/dispatcher"; import AccessibleButton from "../elements/AccessibleButton"; @@ -25,15 +28,15 @@ 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, - }; +interface IProps { + mxRoom: Room; + mxEvent: MatrixEvent; + onUnpinned?(): void; +} - onTileClicked = () => { +@replaceableComponent("views.rooms.PinnedEventTile") +export default class PinnedEventTile extends React.Component { + private onTileClicked = () => { dis.dispatch({ action: 'view_room', event_id: this.props.mxEvent.getId(), @@ -42,7 +45,7 @@ export default class PinnedEventTile extends React.Component { }); }; - onUnpinClicked = () => { + private onUnpinClicked = () => { const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", ""); if (!pinnedEvents || !pinnedEvents.getContent().pinned) { // Nothing to do: already unpinned @@ -60,7 +63,7 @@ export default class PinnedEventTile extends React.Component { } }; - _canUnpin() { + private canUnpin() { return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get()); } @@ -71,10 +74,16 @@ export default class PinnedEventTile extends React.Component { const avatarSize = 40; let unpinButton = null; - if (this._canUnpin()) { + if (this.canUnpin()) { unpinButton = ( - {_t('Unpin + {_t('Unpin ); } @@ -82,14 +91,22 @@ export default class PinnedEventTile extends React.Component { return (

- + { _t("Jump to message") } { unpinButton }
- + { senderProfile ? senderProfile.name : sender } From 59f4c728c9a9b2b6072ae26fb1e5f0953fe17fd0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 16:10:44 +0100 Subject: [PATCH 08/18] Initial cut of Pinned event card in the right panel --- res/css/_components.scss | 2 +- res/css/structures/_RightPanel.scss | 42 +++ .../right_panel/_PinnedMessagesCard.scss | 241 ++++++++++++++++++ res/css/views/rooms/_PinnedEventsPanel.scss | 37 --- res/css/views/rooms/_RoomHeader.scss | 39 --- src/components/structures/RightPanel.tsx | 11 +- src/components/structures/RoomView.tsx | 15 -- .../views/right_panel/PinnedMessagesCard.tsx | 145 +++++++++++ .../views/right_panel/RoomHeaderButtons.tsx | 46 +++- src/components/views/right_panel/UserInfo.tsx | 3 - src/components/views/rooms/RoomHeader.js | 49 +--- src/settings/Settings.tsx | 4 - src/stores/RightPanelStorePhases.ts | 1 + 13 files changed, 483 insertions(+), 152 deletions(-) create mode 100644 res/css/views/right_panel/_PinnedMessagesCard.scss delete mode 100644 res/css/views/rooms/_PinnedEventsPanel.scss create mode 100644 src/components/views/right_panel/PinnedMessagesCard.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index c8985cbb51a..418b8f51c92 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -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"; diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 5515fe40600..e24100a9bb4 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -98,6 +98,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; diff --git a/res/css/views/right_panel/_PinnedMessagesCard.scss b/res/css/views/right_panel/_PinnedMessagesCard.scss new file mode 100644 index 00000000000..4cbc8b9abc4 --- /dev/null +++ b/res/css/views/right_panel/_PinnedMessagesCard.scss @@ -0,0 +1,241 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_PinnedMessagesCard { + .mx_BaseCard_header { + text-align: center; + margin-top: 20px; + + h2 { + font-weight: $font-semi-bold; + font-size: $font-18px; + margin: 12px 0 4px; + } + + .mx_RoomSummaryCard_alias { + font-size: $font-13px; + color: $secondary-fg-color; + } + + h2, .mx_RoomSummaryCard_alias { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + white-space: pre-wrap; + } + + .mx_RoomSummaryCard_avatar { + display: inline-flex; + + .mx_RoomSummaryCard_e2ee { + display: inline-block; + position: relative; + width: 54px; + height: 54px; + border-radius: 50%; + background-color: #737d8c; + margin-top: -3px; // alignment + margin-left: -10px; // overlap + border: 3px solid $dark-panel-bg-color; + + &::before { + content: ''; + position: absolute; + top: 13px; + left: 13px; + height: 28px; + width: 28px; + mask-size: cover; + mask-repeat: no-repeat; + mask-position: center; + mask-image: url('$(res)/img/e2e/disabled.svg'); + background-color: #ffffff; + } + } + + .mx_RoomSummaryCard_e2ee_normal { + background-color: #424446; + &::before { + mask-image: url('$(res)/img/e2e/normal.svg'); + } + } + + .mx_RoomSummaryCard_e2ee_verified { + background-color: #0dbd8b; + &::before { + mask-image: url('$(res)/img/e2e/verified.svg'); + } + } + + .mx_RoomSummaryCard_e2ee_warning { + background-color: #ff4b55; + &::before { + mask-image: url('$(res)/img/e2e/warning.svg'); + } + } + } + } + + .mx_RoomSummaryCard_aboutGroup { + .mx_RoomSummaryCard_Button { + padding-left: 44px; + + &::before { + content: ''; + position: absolute; + top: 8px; + left: 10px; + height: 24px; + width: 24px; + mask-repeat: no-repeat; + mask-position: center; + background-color: $icon-button-color; + } + } + } + + .mx_RoomSummaryCard_appsGroup { + .mx_RoomSummaryCard_Button { + // this button is special so we have to override some of the original styling + // as we will be applying it in its children + padding: 0; + height: auto; + color: $tertiary-fg-color; + + .mx_RoomSummaryCard_icon_app { + padding: 10px 48px 10px 12px; // based on typical mx_RoomSummaryCard_Button padding + text-overflow: ellipsis; + overflow: hidden; + + .mx_BaseAvatar_image { + vertical-align: top; + margin-right: 12px; + } + + span { + color: $primary-fg-color; + } + } + + .mx_RoomSummaryCard_app_pinToggle, + .mx_RoomSummaryCard_app_options { + position: absolute; + top: 0; + height: 100%; // to give bigger interactive zone + width: 24px; + padding: 12px 4px; + box-sizing: border-box; + min-width: 24px; // prevent flexbox crushing + + &:hover { + &::after { + content: ''; + position: absolute; + height: 24px; + width: 24px; + top: 8px; // equal to padding-top of parent + left: 0; + border-radius: 12px; + background-color: rgba(141, 151, 165, 0.1); + } + } + + &::before { + content: ''; + position: absolute; + height: 16px; + width: 16px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 16px; + background-color: $icon-button-color; + } + } + + .mx_RoomSummaryCard_app_pinToggle { + right: 24px; + + &::before { + mask-image: url('$(res)/img/element-icons/room/pin-upright.svg'); + } + } + + .mx_RoomSummaryCard_app_options { + right: 48px; + display: none; + + &::before { + mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); + } + } + + &.mx_RoomSummaryCard_Button_pinned { + &::after { + opacity: 0.2; + } + + .mx_RoomSummaryCard_app_pinToggle::before { + background-color: $accent-color; + } + } + + &:hover { + .mx_RoomSummaryCard_icon_app { + padding-right: 72px; + } + + .mx_RoomSummaryCard_app_options { + display: unset; + } + } + + &::before { + content: unset; + } + + &::after { + top: 8px; // re-align based on the height change + pointer-events: none; // pass through to the real button + } + } + } + + .mx_AccessibleButton_kind_link { + padding: 0; + margin-top: 12px; + margin-bottom: 12px; + font-size: $font-13px; + font-weight: $font-semi-bold; + } +} + +.mx_RoomSummaryCard_icon_people::before { + mask-image: url("$(res)/img/element-icons/room/members.svg"); +} + +.mx_RoomSummaryCard_icon_files::before { + mask-image: url('$(res)/img/element-icons/room/files.svg'); +} + +.mx_RoomSummaryCard_icon_share::before { + mask-image: url('$(res)/img/element-icons/room/share.svg'); +} + +.mx_RoomSummaryCard_icon_settings::before { + mask-image: url('$(res)/img/element-icons/settings.svg'); +} diff --git a/res/css/views/rooms/_PinnedEventsPanel.scss b/res/css/views/rooms/_PinnedEventsPanel.scss deleted file mode 100644 index 663d5bdf6e7..00000000000 --- a/res/css/views/rooms/_PinnedEventsPanel.scss +++ /dev/null @@ -1,37 +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. -*/ - -.mx_PinnedEventsPanel { - border-top: 1px solid $primary-hairline-color; -} - -.mx_PinnedEventsPanel_body { - max-height: 300px; - overflow-y: auto; - padding-bottom: 15px; -} - -.mx_PinnedEventsPanel_header { - margin: 0; - padding-top: 8px; - padding-bottom: 15px; -} - -.mx_PinnedEventsPanel_cancel { - margin: 12px; - float: right; - display: inline-block; -} diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index a102dfcdd74..4142b0a2efc 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -277,45 +277,6 @@ limitations under the License. margin-top: 18px; } -.mx_RoomHeader_pinnedButton::before { - mask-image: url('$(res)/img/element-icons/room/pin.svg'); -} - -$dot-size: 8px; -$pulse-color: $pinned-unread-color; - -.mx_RoomHeader_pinsIndicatorUnread { - 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_RoomHeader_indicator_pulse 2s infinite; - animation-iteration-count: 1; -} - -@keyframes mx_RoomHeader_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); - } -} - @media only screen and (max-width: 480px) { .mx_RoomHeader_wrapper { padding: 0; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 15fa94e035e..e3237d98a62 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -26,9 +26,9 @@ import dis from '../../dispatcher/dispatcher'; import RateLimitedFunc from '../../ratelimitedfunc'; 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"; @@ -47,6 +47,7 @@ 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 @@ -294,7 +295,13 @@ export default class RightPanel extends React.Component { break; case RightPanelPhases.NotificationPanel: - panel = ; + if (SettingsStore.getValue("feature_pinning")) { + panel = ; + } + break; + + case RightPanelPhases.PinnedMessages: + panel = ; break; case RightPanelPhases.FilePanel: diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index d822b6a8392..e8ad1d7e456 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -155,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 @@ -232,7 +231,6 @@ export default class RoomView extends React.Component { canPeek: false, showApps: false, isPeeking: false, - showingPinned: false, showReadReceipts: true, showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, joining: false, @@ -327,7 +325,6 @@ export default class RoomView extends React.Component { 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(), }; @@ -1375,13 +1372,6 @@ export default class RoomView extends React.Component { 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', @@ -1498,7 +1488,6 @@ export default class RoomView extends React.Component { private onSearchClick = () => { this.setState({ searching: !this.state.searching, - showingPinned: false, }); }; @@ -1825,9 +1814,6 @@ export default class RoomView extends React.Component { } else if (showRoomUpgradeBar) { aux = ; hideCancel = true; - } else if (this.state.showingPinned) { - hideCancel = true; // has own cancel - aux = ; } 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. @@ -2045,7 +2031,6 @@ export default class RoomView extends React.Component { 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} diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx new file mode 100644 index 00000000000..46625b6e076 --- /dev/null +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -0,0 +1,145 @@ +/* +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 { 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([]); + + 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; +}; + +const ReadPinsEventId = "im.vector.room.read_pins"; +const ReadPinsNumIds = 10; + +export const useReadPinnedEvents = (room: Room): Set => { + const [readPinnedEvents, setReadPinnedEvents] = useState>(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 PinnedMessagesCard = ({ room, onClose }: IProps) => { + const cli = useContext(MatrixClientContext); + const pinnedEventIds = usePinnedEvents(room); + const readPinnedEvents = useReadPinnedEvents(room); + + useEffect(() => { + const newlyRead = pinnedEventIds.filter(id => !readPinnedEvents.has(id)); + if (newlyRead.length > 0) { + // Only keep the last N event IDs to avoid infinite growth + cli.setRoomAccountData(room.roomId, ReadPinsEventId, { + event_ids: [ + ...newlyRead.reverse(), + ...readPinnedEvents, + ].splice(0, ReadPinsNumIds), + }); + } + }, [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 = ; + } else if (pinnedEvents.length > 0) { + content = pinnedEvents.filter(Boolean).map(ev => ( + {}} + /> + )); + } else { + content =
+

{_t("You’re all caught up")}

+

{_t("You have no visible notifications.")}

+
; + } + + return + { content } + ; +}; + +export default PinnedMessagesCard; diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 2a7a9e191b8..3d3f33be00b 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -18,7 +18,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; + import {_t} from '../../../languageHandler'; import HeaderButton from './HeaderButton'; import HeaderButtons, {HeaderKind} from './HeaderButtons'; @@ -27,6 +29,8 @@ import {Action} from "../../../dispatcher/actions"; import {ActionPayload} from "../../../dispatcher/payloads"; import RightPanelStore from "../../../stores/RightPanelStore"; 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 =
; + } + + return + { unreadIndicator } + ; +}; + +interface IProps { + room?: Room; +} + @replaceableComponent("views.right_panel.RoomHeaderButtons") -export default class RoomHeaderButtons extends HeaderButtons { - constructor(props) { +export default class RoomHeaderButtons extends HeaderButtons { + constructor(props: IProps) { super(props, HeaderKind.Room); } @@ -80,8 +110,18 @@ 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 <> + { } else { setPowerLevels({}); } - return () => { - setPowerLevels({}); - }; }, [room]); useEventEmitter(cli, "RoomState.events", update); diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 7eeb5875ef1..217b3ea5c21 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -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,41 +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; - } - render() { let searchStatus = null; let cancelButton = null; - let pinnedEventsButton = null; if (this.props.onCancelClick) { cancelButton = ; @@ -174,22 +144,6 @@ export default class RoomHeader extends React.Component { />; } - if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) { - let pinsIndicator = null; - if (this._hasUnreadPins()) { - pinsIndicator = (
); - } - - pinnedEventsButton = - - { pinsIndicator } - ; - } - let forgetButton; if (this.props.onForgetClick) { forgetButton = @@ -239,7 +193,6 @@ export default class RoomHeader extends React.Component {
{ videoCallButton } { voiceCallButton } - { pinnedEventsButton } { forgetButton } { appsButton } { searchButton } @@ -256,7 +209,7 @@ export default class RoomHeader extends React.Component { { topicElement } { cancelButton } { rightRow } - +
); diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 6ff14c16b5a..155d0395728 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -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'), diff --git a/src/stores/RightPanelStorePhases.ts b/src/stores/RightPanelStorePhases.ts index aea78c74605..0d96f929457 100644 --- a/src/stores/RightPanelStorePhases.ts +++ b/src/stores/RightPanelStorePhases.ts @@ -24,6 +24,7 @@ export enum RightPanelPhases { EncryptionPanel = 'EncryptionPanel', RoomSummary = 'RoomSummary', Widget = 'Widget', + PinnedMessages = "PinnedMessages", Room3pidMemberInfo = 'Room3pidMemberInfo', // Group stuff From c1f397dcf7bfaad0e894b4d949b94094108813b6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 16:20:23 +0100 Subject: [PATCH 09/18] delint --- src/components/structures/RoomView.tsx | 2 -- src/components/views/right_panel/HeaderButtons.tsx | 2 +- src/contexts/RoomContext.ts | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index e8ad1d7e456..482f4a45a78 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -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"; diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx index 281d7edd8b0..0c7d9ee2990 100644 --- a/src/components/views/right_panel/HeaderButtons.tsx +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -43,7 +43,7 @@ interface IState { interface IProps {} @replaceableComponent("views.right_panel.HeaderButtons") -export default abstract class HeaderButtons

extends React.Component { +export default abstract class HeaderButtons

extends React.Component { private storeToken: EventSubscription; private dispatcherRef: string; diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index 30ff74b0715..7f2c0bb1570 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -31,7 +31,6 @@ const RoomContext = createContext({ canPeek: false, showApps: false, isPeeking: false, - showingPinned: false, showReadReceipts: true, showRightPanel: true, joining: false, From fd74a946e087b0ae42dbc74cbd4a30ee5386c895 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 25 May 2021 17:24:43 +0100 Subject: [PATCH 10/18] add header --- .../views/right_panel/PinnedMessagesCard.tsx | 6 +- .../views/rooms/PinnedEventsPanel.js | 145 ------------------ src/i18n/strings/en_EN.json | 10 +- 3 files changed, 10 insertions(+), 151 deletions(-) delete mode 100644 src/components/views/rooms/PinnedEventsPanel.js diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index 46625b6e076..579b12c3cf0 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -137,7 +137,11 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => {

; } - return + return { _t("Pinned") }} + className="mx_NotificationPanel" + onClose={onClose} + > { content } ; }; diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js deleted file mode 100644 index 4b310dbbca8..00000000000 --- a/src/components/views/rooms/PinnedEventsPanel.js +++ /dev/null @@ -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 (
{ _t("No pinned messages.") }
); - } - - return this.state.pinned.map((context) => { - return ( - - ); - }); - } - - render() { - let tiles =
{ _t("Loading...") }
; - if (this.state && !this.state.loading) { - tiles = this._getPinnedTiles(); - } - - return ( -
-
- - - -

{ _t("Pinned Messages") }

- { tiles } -
-
- ); - } -} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7ceb039822d..0aebfc48ce8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1508,9 +1508,6 @@ "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 .": "This is the start of .", - "No pinned messages.": "No pinned messages.", - "Loading...": "Loading...", - "Pinned Messages": "Pinned Messages", "Unpin Message": "Unpin Message", "Jump to message": "Jump to message", "%(duration)ss": "%(duration)ss", @@ -1718,6 +1715,10 @@ "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": "Pinned", + "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", @@ -1896,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", @@ -2626,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.", From 27ad90760d117e6c8c203d04669c715d564a01e5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 May 2021 13:51:17 +0100 Subject: [PATCH 11/18] Iterate pinned messages --- .../right_panel/_PinnedMessagesCard.scss | 222 +----------------- res/css/views/rooms/_PinnedEventTile.scss | 122 ++++++---- .../views/right_panel/PinnedMessagesCard.tsx | 61 +++-- .../views/right_panel/RoomHeaderButtons.tsx | 2 +- .../views/rooms/PinnedEventTile.tsx | 130 +++++----- src/i18n/strings/en_EN.json | 6 +- src/stores/RightPanelStorePhases.ts | 1 + 7 files changed, 184 insertions(+), 360 deletions(-) diff --git a/res/css/views/right_panel/_PinnedMessagesCard.scss b/res/css/views/right_panel/_PinnedMessagesCard.scss index 4cbc8b9abc4..b6b8238bed7 100644 --- a/res/css/views/right_panel/_PinnedMessagesCard.scss +++ b/res/css/views/right_panel/_PinnedMessagesCard.scss @@ -15,227 +15,21 @@ limitations under the License. */ .mx_PinnedMessagesCard { + padding-top: 0; + .mx_BaseCard_header { text-align: center; - margin-top: 20px; + margin-top: 0; + border-bottom: 1px solid $menu-border-color; - h2 { + > h2 { font-weight: $font-semi-bold; font-size: $font-18px; - margin: 12px 0 4px; - } - - .mx_RoomSummaryCard_alias { - font-size: $font-13px; - color: $secondary-fg-color; - } - - h2, .mx_RoomSummaryCard_alias { - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; - white-space: pre-wrap; - } - - .mx_RoomSummaryCard_avatar { - display: inline-flex; - - .mx_RoomSummaryCard_e2ee { - display: inline-block; - position: relative; - width: 54px; - height: 54px; - border-radius: 50%; - background-color: #737d8c; - margin-top: -3px; // alignment - margin-left: -10px; // overlap - border: 3px solid $dark-panel-bg-color; - - &::before { - content: ''; - position: absolute; - top: 13px; - left: 13px; - height: 28px; - width: 28px; - mask-size: cover; - mask-repeat: no-repeat; - mask-position: center; - mask-image: url('$(res)/img/e2e/disabled.svg'); - background-color: #ffffff; - } - } - - .mx_RoomSummaryCard_e2ee_normal { - background-color: #424446; - &::before { - mask-image: url('$(res)/img/e2e/normal.svg'); - } - } - - .mx_RoomSummaryCard_e2ee_verified { - background-color: #0dbd8b; - &::before { - mask-image: url('$(res)/img/e2e/verified.svg'); - } - } - - .mx_RoomSummaryCard_e2ee_warning { - background-color: #ff4b55; - &::before { - mask-image: url('$(res)/img/e2e/warning.svg'); - } - } + margin: 8px 0; } - } - - .mx_RoomSummaryCard_aboutGroup { - .mx_RoomSummaryCard_Button { - padding-left: 44px; - &::before { - content: ''; - position: absolute; - top: 8px; - left: 10px; - height: 24px; - width: 24px; - mask-repeat: no-repeat; - mask-position: center; - background-color: $icon-button-color; - } + .mx_BaseCard_close { + margin-right: 6px; } } - - .mx_RoomSummaryCard_appsGroup { - .mx_RoomSummaryCard_Button { - // this button is special so we have to override some of the original styling - // as we will be applying it in its children - padding: 0; - height: auto; - color: $tertiary-fg-color; - - .mx_RoomSummaryCard_icon_app { - padding: 10px 48px 10px 12px; // based on typical mx_RoomSummaryCard_Button padding - text-overflow: ellipsis; - overflow: hidden; - - .mx_BaseAvatar_image { - vertical-align: top; - margin-right: 12px; - } - - span { - color: $primary-fg-color; - } - } - - .mx_RoomSummaryCard_app_pinToggle, - .mx_RoomSummaryCard_app_options { - position: absolute; - top: 0; - height: 100%; // to give bigger interactive zone - width: 24px; - padding: 12px 4px; - box-sizing: border-box; - min-width: 24px; // prevent flexbox crushing - - &:hover { - &::after { - content: ''; - position: absolute; - height: 24px; - width: 24px; - top: 8px; // equal to padding-top of parent - left: 0; - border-radius: 12px; - background-color: rgba(141, 151, 165, 0.1); - } - } - - &::before { - content: ''; - position: absolute; - height: 16px; - width: 16px; - mask-repeat: no-repeat; - mask-position: center; - mask-size: 16px; - background-color: $icon-button-color; - } - } - - .mx_RoomSummaryCard_app_pinToggle { - right: 24px; - - &::before { - mask-image: url('$(res)/img/element-icons/room/pin-upright.svg'); - } - } - - .mx_RoomSummaryCard_app_options { - right: 48px; - display: none; - - &::before { - mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); - } - } - - &.mx_RoomSummaryCard_Button_pinned { - &::after { - opacity: 0.2; - } - - .mx_RoomSummaryCard_app_pinToggle::before { - background-color: $accent-color; - } - } - - &:hover { - .mx_RoomSummaryCard_icon_app { - padding-right: 72px; - } - - .mx_RoomSummaryCard_app_options { - display: unset; - } - } - - &::before { - content: unset; - } - - &::after { - top: 8px; // re-align based on the height change - pointer-events: none; // pass through to the real button - } - } - } - - .mx_AccessibleButton_kind_link { - padding: 0; - margin-top: 12px; - margin-bottom: 12px; - font-size: $font-13px; - font-weight: $font-semi-bold; - } -} - -.mx_RoomSummaryCard_icon_people::before { - mask-image: url("$(res)/img/element-icons/room/members.svg"); -} - -.mx_RoomSummaryCard_icon_files::before { - mask-image: url('$(res)/img/element-icons/room/files.svg'); -} - -.mx_RoomSummaryCard_icon_share::before { - mask-image: url('$(res)/img/element-icons/room/share.svg'); -} - -.mx_RoomSummaryCard_icon_settings::before { - mask-image: url('$(res)/img/element-icons/settings.svg'); } diff --git a/res/css/views/rooms/_PinnedEventTile.scss b/res/css/views/rooms/_PinnedEventTile.scss index 030a76674a9..8e96f3caeb1 100644 --- a/res/css/views/rooms/_PinnedEventTile.scss +++ b/res/css/views/rooms/_PinnedEventTile.scss @@ -16,62 +16,90 @@ 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_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; + } + } -.mx_PinnedEventTile_message { - margin-left: 50px; - position: relative; - top: 0; - left: 0; + &:hover { + .mx_PinnedEventTile_unpinButton { + visibility: visible; + } + } } diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index 579b12c3cf0..48be8cd706f 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -16,6 +16,7 @@ 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'; @@ -42,7 +43,7 @@ export const usePinnedEvents = (room: Room): string[] => { setPinnedEvents(room.currentState.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent()?.pinned || []); }, [room]); - useEventEmitter(room.currentState, "RoomState.events", update); + useEventEmitter(room?.currentState, "RoomState.events", update); useEffect(() => { update(); return () => { @@ -53,7 +54,6 @@ export const usePinnedEvents = (room: Room): string[] => { }; const ReadPinsEventId = "im.vector.room.read_pins"; -const ReadPinsNumIds = 10; export const useReadPinnedEvents = (room: Room): Set => { const [readPinnedEvents, setReadPinnedEvents] = useState>(new Set()); @@ -75,20 +75,36 @@ export const useReadPinnedEvents = (room: Room): Set => { return readPinnedEvents; }; +const useRoomState = (room: Room, mapper: (state: RoomState) => T): T => { + const [value, setValue] = useState(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) { - // Only keep the last N event IDs to avoid infinite growth + // clear out any read pinned events which no longer are pinned cli.setRoomAccountData(room.roomId, ReadPinsEventId, { - event_ids: [ - ...newlyRead.reverse(), - ...readPinnedEvents, - ].splice(0, ReadPinsNumIds), + event_ids: pinnedEventIds, }); } }, [cli, room.roomId, pinnedEventIds, readPinnedEvents]); @@ -122,24 +138,35 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => { if (!pinnedEvents) { content = ; } else if (pinnedEvents.length > 0) { - content = pinnedEvents.filter(Boolean).map(ev => ( - {}} - /> + 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 => ( + )); } else { - content =
+ content =

{_t("You’re all caught up")}

{_t("You have no visible notifications.")}

; } return { _t("Pinned") }} - className="mx_NotificationPanel" + header={

{ _t("Pinned messages") }

} + className="mx_PinnedMessagesCard" onClose={onClose} > { content } diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 3d3f33be00b..7ff7207fb61 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -55,7 +55,7 @@ const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }) => { return { + static contextType = MatrixClientContext; + private onTileClicked = () => { dis.dispatch({ action: 'view_room', - event_id: this.props.mxEvent.getId(), + event_id: this.props.event.getId(), highlighted: true, - room_id: this.props.mxEvent.getRoomId(), + room_id: this.props.event.getRoomId(), }); }; - private 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(); - } - }; - - private 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; + const sender = this.props.event.getSender(); + const senderProfile = this.props.room.getMember(sender); let unpinButton = null; - if (this.canUnpin()) { + if (this.props.onUnpinClicked) { unpinButton = ( - - {_t('Unpin - + ); } - return ( -
-
- - { _t("Jump to message") } - - { unpinButton } -
- - - - - - { senderProfile ? senderProfile.name : sender } - + return
+ + + + { senderProfile?.name || sender } + + + { unpinButton } + +
+ {}} // we need to give this, apparently + /> +
+ +
- { formatFullDate(new Date(this.props.mxEvent.getTs())) } + { formatDate(new Date(this.props.event.getTs())) } -
- {}} // we need to give this, apparently - /> -
+ + + { _t("View message") } +
- ); +
; } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0aebfc48ce8..d5b37a7d2d1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1509,7 +1509,7 @@ "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 .": "This is the start of .", "Unpin Message": "Unpin Message", - "Jump to message": "Jump to message", + "View message": "View message", "%(duration)ss": "%(duration)ss", "%(duration)sm": "%(duration)sm", "%(duration)sh": "%(duration)sh", @@ -1717,8 +1717,7 @@ "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": "Pinned", - "Pinned Messages": "Pinned Messages", + "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", @@ -1952,7 +1951,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", diff --git a/src/stores/RightPanelStorePhases.ts b/src/stores/RightPanelStorePhases.ts index 0d96f929457..d62f6c61103 100644 --- a/src/stores/RightPanelStorePhases.ts +++ b/src/stores/RightPanelStorePhases.ts @@ -44,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, From 54d8953024b1026fd636f2bed4e45cacda12a360 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 May 2021 14:00:53 +0100 Subject: [PATCH 12/18] delint --- res/css/views/rooms/_PinnedEventTile.scss | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/res/css/views/rooms/_PinnedEventTile.scss b/res/css/views/rooms/_PinnedEventTile.scss index 8e96f3caeb1..15b3c16faa7 100644 --- a/res/css/views/rooms/_PinnedEventTile.scss +++ b/res/css/views/rooms/_PinnedEventTile.scss @@ -20,9 +20,10 @@ limitations under the License. padding: 0 4px 12px; display: grid; - grid-template-areas: "avatar name remove" - "content content content" - "footer footer footer"; + 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; From 0758c09d9e9760d3a2bae3b562016ff73af82ec9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 May 2021 14:06:12 +0100 Subject: [PATCH 13/18] i18n --- src/i18n/strings/en_EN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d5b37a7d2d1..7c243647255 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1508,7 +1508,7 @@ "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 .": "This is the start of .", - "Unpin Message": "Unpin Message", + "Unpin": "Unpin", "View message": "View message", "%(duration)ss": "%(duration)ss", "%(duration)sm": "%(duration)sm", @@ -1720,7 +1720,6 @@ "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", @@ -2464,6 +2463,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", From 1ff870927a425145d8676e0e87a97feadc313efa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 26 May 2021 15:40:24 +0100 Subject: [PATCH 14/18] When pinning a message automatically mark it as read --- .../views/context_menus/MessageContextMenu.js | 37 +++++++++---------- .../views/right_panel/PinnedMessagesCard.tsx | 2 +- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 365f2ab1de3..d9f21906fe5 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -31,6 +31,7 @@ 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 { 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(); }; diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index 48be8cd706f..a3f1f2d9df4 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -53,7 +53,7 @@ export const usePinnedEvents = (room: Room): string[] => { return pinnedEvents; }; -const ReadPinsEventId = "im.vector.room.read_pins"; +export const ReadPinsEventId = "im.vector.room.read_pins"; export const useReadPinnedEvents = (room: Room): Set => { const [readPinnedEvents, setReadPinnedEvents] = useState>(new Set()); From 53fc47553906b662d6b27796b3bca6e44a04bfaa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 08:03:31 +0100 Subject: [PATCH 15/18] Update src/components/views/rooms/PinnedEventTile.tsx Co-authored-by: Travis Ralston --- src/components/views/rooms/PinnedEventTile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/PinnedEventTile.tsx b/src/components/views/rooms/PinnedEventTile.tsx index 83eee7d81d0..774dea70c8a 100644 --- a/src/components/views/rooms/PinnedEventTile.tsx +++ b/src/components/views/rooms/PinnedEventTile.tsx @@ -40,7 +40,7 @@ const AVATAR_SIZE = 24; @replaceableComponent("views.rooms.PinnedEventTile") export default class PinnedEventTile extends React.Component { - static contextType = MatrixClientContext; + public static contextType = MatrixClientContext; private onTileClicked = () => { dis.dispatch({ From 42a3ace82af9fec69948e2816346c79b9c472f7f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 08:35:12 +0100 Subject: [PATCH 16/18] Iterate PR based on feedback --- src/components/structures/NotificationPanel.tsx | 4 +--- src/components/structures/RightPanel.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index 7e1566521d2..3bfadf7110f 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -31,7 +31,7 @@ interface IProps { * Component which shows the global notification list using a TimelinePanel */ @replaceableComponent("structures.NotificationPanel") -class NotificationPanel extends React.Component { +export default class NotificationPanel extends React.PureComponent { render() { const emptyState = (

{_t('You’re all caught up')}

@@ -62,5 +62,3 @@ class NotificationPanel extends React.Component { ; } } - -export default NotificationPanel; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index e3237d98a62..353f50ad340 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -295,13 +295,13 @@ export default class RightPanel extends React.Component { break; case RightPanelPhases.NotificationPanel: - if (SettingsStore.getValue("feature_pinning")) { - panel = ; - } + panel = ; break; case RightPanelPhases.PinnedMessages: - panel = ; + if (SettingsStore.getValue("feature_pinning")) { + panel = ; + } break; case RightPanelPhases.FilePanel: From 83d223475bb98af6e08587c701de14b525f25de9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 08:41:22 +0100 Subject: [PATCH 17/18] delint imports --- .../structures/NotificationPanel.tsx | 4 +-- src/components/structures/RightPanel.tsx | 16 ++++----- src/components/structures/RoomView.tsx | 4 +-- .../views/context_menus/MessageContextMenu.js | 10 +++--- .../views/right_panel/GroupHeaderButtons.tsx | 12 +++---- .../views/right_panel/HeaderButton.tsx | 2 +- .../views/right_panel/HeaderButtons.tsx | 8 ++--- .../views/right_panel/RoomHeaderButtons.tsx | 14 ++++---- src/components/views/right_panel/UserInfo.tsx | 36 +++++++++---------- src/components/views/rooms/RoomHeader.js | 8 ++--- src/contexts/RoomContext.ts | 4 +-- src/hooks/useAsyncMemo.ts | 2 +- 12 files changed, 60 insertions(+), 60 deletions(-) diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index 3bfadf7110f..b4f13f6b37b 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -17,9 +17,9 @@ limitations under the License. import React from "react"; import { _t } from '../../languageHandler'; -import {MatrixClientPeg} from "../../MatrixClientPeg"; +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"; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 353f50ad340..294865fe081 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -16,11 +16,11 @@ limitations under the License. */ import React from 'react'; -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 { 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 dis from '../../dispatcher/dispatcher'; import RateLimitedFunc from '../../ratelimitedfunc'; @@ -32,12 +32,12 @@ import { } 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 { ActionPayload } from "../../dispatcher/payloads"; import MemberList from "../views/rooms/MemberList"; import GroupMemberList from "../views/groups/GroupMemberList"; import GroupRoomList from "../views/groups/GroupRoomList"; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 482f4a45a78..fdfc5c2adaa 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -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"; @@ -80,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"; const DEBUG = false; let debuglog = function(msg: string) {}; diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index d9f21906fe5..594b98b1f54 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -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,9 @@ 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) { diff --git a/src/components/views/right_panel/GroupHeaderButtons.tsx b/src/components/views/right_panel/GroupHeaderButtons.tsx index 5ae5709d44e..3c93cf6470f 100644 --- a/src/components/views/right_panel/GroupHeaderButtons.tsx +++ b/src/components/views/right_panel/GroupHeaderButtons.tsx @@ -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, diff --git a/src/components/views/right_panel/HeaderButton.tsx b/src/components/views/right_panel/HeaderButton.tsx index 8451f69fd37..cdf4f44e065 100644 --- a/src/components/views/right_panel/HeaderButton.tsx +++ b/src/components/views/right_panel/HeaderButton.tsx @@ -22,7 +22,7 @@ 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 diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx index 0c7d9ee2990..6d44a081d94 100644 --- a/src/components/views/right_panel/HeaderButtons.tsx +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -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", diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx index 7ff7207fb61..54e18e45297 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.tsx +++ b/src/components/views/right_panel/RoomHeaderButtons.tsx @@ -21,15 +21,15 @@ limitations under the License. import React from "react"; import { Room } from "matrix-js-sdk/src/models/room"; -import {_t} from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import HeaderButton from './HeaderButton'; -import 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 {useSettingValue} from "../../../hooks/useSettings"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { useSettingValue } from "../../../hooks/useSettings"; import { useReadPinnedEvents, usePinnedEvents } from './PinnedMessagesCard'; const ROOM_INFO_PHASES = [ diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 5a40b601111..cdbca59db95 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -17,19 +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 {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +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'; @@ -40,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"; @@ -66,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"; export interface IDevice { deviceId: string; diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 217b3ea5c21..cb6bb0afca8 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -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 { diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index 7f2c0bb1570..e925f8624ba 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -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({ roomLoading: true, diff --git a/src/hooks/useAsyncMemo.ts b/src/hooks/useAsyncMemo.ts index eda44b1ae46..1776ec1d36d 100644 --- a/src/hooks/useAsyncMemo.ts +++ b/src/hooks/useAsyncMemo.ts @@ -14,7 +14,7 @@ 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 = () => Promise; From 5d0d81e79acd06ba9b024e312bfb888844f99a9e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 3 Jun 2021 19:37:26 +0100 Subject: [PATCH 18/18] not sure how I butchered this merge conflict resolution this much. --- src/components/structures/RoomView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 4390d217832..5ffc2fd0da1 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -79,8 +79,8 @@ import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutS import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager'; import { objectHasDiff } from "../../utils/objects"; import SpaceRoomView from "./SpaceRoomView"; -import { IOpts } from "../../createRoom -import { replaceableComponent } from "../../utils/replaceableComponent +import { IOpts } from "../../createRoom"; +import { replaceableComponent } from "../../utils/replaceableComponent"; import { omit } from 'lodash'; import UIStore from "../../stores/UIStore";