Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 1331e96

Browse files
PalidGermain
andauthored
Add ability to properly edit messages in Threads. (#6877)
* Fix infinite rerender loop when editing message * Refactor "edit_event" to Action.EditEvent * Make up-arrow edit working in Threads * Properly handle timeline events edit state * Properly traverse messages to be edited * Add MatrixClientContextHOC * Refactor RoomContext to use AppRenderingContext * Typescriptify test Co-authored-by: Germain <[email protected]>
1 parent 5dede23 commit 1331e96

File tree

15 files changed

+403
-189
lines changed

15 files changed

+403
-189
lines changed

src/components/structures/MessagePanel.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ import Spinner from "../views/elements/Spinner";
4848
import TileErrorBoundary from '../views/messages/TileErrorBoundary';
4949
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
5050
import EditorStateTransfer from "../../utils/EditorStateTransfer";
51+
import { logger } from 'matrix-js-sdk/src/logger';
52+
import { Action } from '../../dispatcher/actions';
5153

5254
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
5355
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
@@ -287,6 +289,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
287289
ghostReadMarkers,
288290
});
289291
}
292+
293+
const pendingEditItem = this.pendingEditItem;
294+
if (!this.props.editState && this.props.room && pendingEditItem) {
295+
defaultDispatcher.dispatch({
296+
action: Action.EditEvent,
297+
event: this.props.room.findEventById(pendingEditItem),
298+
timelineRenderingType: this.context.timelineRenderingType,
299+
});
300+
}
290301
}
291302

292303
private calculateRoomMembersCount = (): void => {
@@ -550,10 +561,14 @@ export default class MessagePanel extends React.Component<IProps, IState> {
550561
return { nextEvent, nextTile };
551562
}
552563

553-
private get roomHasPendingEdit(): string {
554-
return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
564+
private get pendingEditItem(): string | undefined {
565+
try {
566+
return localStorage.getItem(`mx_edit_room_${this.props.room.roomId}_${this.context.timelineRenderingType}`);
567+
} catch (err) {
568+
logger.error(err);
569+
return undefined;
570+
}
555571
}
556-
557572
private getEventTiles(): ReactNode[] {
558573
this.eventNodes = {};
559574

@@ -663,13 +678,6 @@ export default class MessagePanel extends React.Component<IProps, IState> {
663678
}
664679
}
665680

666-
if (!this.props.editState && this.roomHasPendingEdit) {
667-
defaultDispatcher.dispatch({
668-
action: "edit_event",
669-
event: this.props.room.findEventById(this.roomHasPendingEdit),
670-
});
671-
}
672-
673681
if (grouper) {
674682
ret.push(...grouper.getTiles());
675683
}

src/components/structures/RoomView.tsx

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ import { Layout } from "../../settings/Layout";
4848
import AccessibleButton from "../views/elements/AccessibleButton";
4949
import RightPanelStore from "../../stores/RightPanelStore";
5050
import { haveTileForEvent } from "../views/rooms/EventTile";
51-
import RoomContext from "../../contexts/RoomContext";
52-
import MatrixClientContext from "../../contexts/MatrixClientContext";
51+
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
52+
import MatrixClientContext, { withMatrixClientHOC, MatrixClientProps } from "../../contexts/MatrixClientContext";
5353
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
5454
import { Action } from "../../dispatcher/actions";
5555
import { IMatrixClientCreds } from "../../MatrixClientPeg";
@@ -91,6 +91,7 @@ import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
9191
import SpaceStore from "../../stores/SpaceStore";
9292

9393
import { logger } from "matrix-js-sdk/src/logger";
94+
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
9495

9596
const DEBUG = false;
9697
let debuglog = function(msg: string) {};
@@ -102,7 +103,7 @@ if (DEBUG) {
102103
debuglog = logger.log.bind(console);
103104
}
104105

105-
interface IProps {
106+
interface IRoomProps extends MatrixClientProps {
106107
threepidInvite: IThreepidInvite;
107108
oobData?: IOOBData;
108109

@@ -113,7 +114,7 @@ interface IProps {
113114
onRegistered?(credentials: IMatrixClientCreds): void;
114115
}
115116

116-
export interface IState {
117+
export interface IRoomState {
117118
room?: Room;
118119
roomId?: string;
119120
roomAlias?: string;
@@ -187,10 +188,12 @@ export interface IState {
187188
// if it did we don't want the room to be marked as read as soon as it is loaded.
188189
wasContextSwitch?: boolean;
189190
editState?: EditorStateTransfer;
191+
timelineRenderingType: TimelineRenderingType;
192+
liveTimeline?: EventTimeline;
190193
}
191194

192195
@replaceableComponent("structures.RoomView")
193-
export default class RoomView extends React.Component<IProps, IState> {
196+
export class RoomView extends React.Component<IRoomProps, IRoomState> {
194197
private readonly dispatcherRef: string;
195198
private readonly roomStoreToken: EventSubscription;
196199
private readonly rightPanelStoreToken: EventSubscription;
@@ -247,6 +250,8 @@ export default class RoomView extends React.Component<IProps, IState> {
247250
showDisplaynameChanges: true,
248251
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
249252
dragCounter: 0,
253+
timelineRenderingType: TimelineRenderingType.Room,
254+
liveTimeline: undefined,
250255
};
251256

252257
this.dispatcherRef = dis.register(this.onAction);
@@ -336,7 +341,7 @@ export default class RoomView extends React.Component<IProps, IState> {
336341

337342
const roomId = RoomViewStore.getRoomId();
338343

339-
const newState: Pick<IState, any> = {
344+
const newState: Pick<IRoomState, any> = {
340345
roomId,
341346
roomAlias: RoomViewStore.getRoomAlias(),
342347
roomLoading: RoomViewStore.isRoomLoading(),
@@ -808,7 +813,9 @@ export default class RoomView extends React.Component<IProps, IState> {
808813
this.onSearchClick();
809814
break;
810815

811-
case "edit_event": {
816+
case Action.EditEvent: {
817+
// Quit early if we're trying to edit events in wrong rendering context
818+
if (payload.timelineRenderingType !== this.state.timelineRenderingType) return;
812819
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
813820
this.setState({ editState }, () => {
814821
if (payload.event) {
@@ -932,6 +939,10 @@ export default class RoomView extends React.Component<IProps, IState> {
932939
this.updateE2EStatus(room);
933940
this.updatePermissions(room);
934941
this.checkWidgets(room);
942+
943+
this.setState({
944+
liveTimeline: room.getLiveTimeline(),
945+
});
935946
};
936947

937948
private async calculateRecommendedVersion(room: Room) {
@@ -2086,3 +2097,6 @@ export default class RoomView extends React.Component<IProps, IState> {
20862097
);
20872098
}
20882099
}
2100+
2101+
const RoomViewWithMatrixClient = withMatrixClientHOC(RoomView);
2102+
export default RoomViewWithMatrixClient;

src/components/structures/ThreadView.tsx

Lines changed: 69 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPan
3434
import { Action } from '../../dispatcher/actions';
3535
import { MatrixClientPeg } from '../../MatrixClientPeg';
3636
import { E2EStatus } from '../../utils/ShieldUtils';
37+
import EditorStateTransfer from '../../utils/EditorStateTransfer';
38+
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
3739

3840
interface IProps {
3941
room: Room;
@@ -47,10 +49,14 @@ interface IProps {
4749
interface IState {
4850
replyToEvent?: MatrixEvent;
4951
thread?: Thread;
52+
editState?: EditorStateTransfer;
53+
5054
}
5155

5256
@replaceableComponent("structures.ThreadView")
5357
export default class ThreadView extends React.Component<IProps, IState> {
58+
static contextType = RoomContext;
59+
5460
private dispatcherRef: string;
5561
private timelinePanelRef: React.RefObject<TimelinePanel> = React.createRef();
5662

@@ -90,6 +96,23 @@ export default class ThreadView extends React.Component<IProps, IState> {
9096
this.setupThread(payload.event);
9197
}
9298
}
99+
switch (payload.action) {
100+
case Action.EditEvent: {
101+
// Quit early if it's not a thread context
102+
if (payload.timelineRenderingType !== TimelineRenderingType.Thread) return;
103+
// Quit early if that's not a thread event
104+
if (payload.event && !payload.event.getThread()) return;
105+
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
106+
this.setState({ editState }, () => {
107+
if (payload.event) {
108+
this.timelinePanelRef.current?.scrollToEventIfNeeded(payload.event.getId());
109+
}
110+
});
111+
break;
112+
}
113+
default:
114+
break;
115+
}
93116
};
94117

95118
private setupThread = (mxEv: MatrixEvent) => {
@@ -124,44 +147,53 @@ export default class ThreadView extends React.Component<IProps, IState> {
124147

125148
public render(): JSX.Element {
126149
return (
127-
<BaseCard
128-
className="mx_ThreadView"
129-
onClose={this.props.onClose}
130-
previousPhase={RightPanelPhases.RoomSummary}
131-
withoutScrollContainer={true}
132-
>
133-
{ this.state.thread && (
134-
<TimelinePanel
135-
ref={this.timelinePanelRef}
136-
showReadReceipts={false} // No RR support in thread's MVP
137-
manageReadReceipts={false} // No RR support in thread's MVP
138-
manageReadMarkers={false} // No RM support in thread's MVP
139-
sendReadReceiptOnLoad={false} // No RR support in thread's MVP
140-
timelineSet={this.state?.thread?.timelineSet}
141-
showUrlPreview={true}
142-
tileShape={TileShape.Thread}
143-
empty={<div>empty</div>}
144-
alwaysShowTimestamps={true}
145-
layout={Layout.Group}
146-
hideThreadedMessages={false}
147-
hidden={false}
148-
showReactions={true}
149-
className="mx_RoomView_messagePanel mx_GroupLayout"
150+
<RoomContext.Provider value={{
151+
...this.context,
152+
timelineRenderingType: TimelineRenderingType.Thread,
153+
liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(),
154+
}}>
155+
156+
<BaseCard
157+
className="mx_ThreadView"
158+
onClose={this.props.onClose}
159+
previousPhase={RightPanelPhases.RoomSummary}
160+
withoutScrollContainer={true}
161+
>
162+
{ this.state.thread && (
163+
<TimelinePanel
164+
ref={this.timelinePanelRef}
165+
showReadReceipts={false} // No RR support in thread's MVP
166+
manageReadReceipts={false} // No RR support in thread's MVP
167+
manageReadMarkers={false} // No RM support in thread's MVP
168+
sendReadReceiptOnLoad={false} // No RR support in thread's MVP
169+
timelineSet={this.state?.thread?.timelineSet}
170+
showUrlPreview={true}
171+
tileShape={TileShape.Thread}
172+
empty={<div>empty</div>}
173+
alwaysShowTimestamps={true}
174+
layout={Layout.Group}
175+
hideThreadedMessages={false}
176+
hidden={false}
177+
showReactions={true}
178+
className="mx_RoomView_messagePanel mx_GroupLayout"
179+
permalinkCreator={this.props.permalinkCreator}
180+
membersLoaded={true}
181+
editState={this.state.editState}
182+
/>
183+
) }
184+
185+
{ this.state?.thread?.timelineSet && (<MessageComposer
186+
room={this.props.room}
187+
resizeNotifier={this.props.resizeNotifier}
188+
replyInThread={true}
189+
replyToEvent={this.state?.thread?.replyToEvent}
190+
showReplyPreview={false}
150191
permalinkCreator={this.props.permalinkCreator}
151-
membersLoaded={true}
152-
/>
153-
) }
154-
<MessageComposer
155-
room={this.props.room}
156-
resizeNotifier={this.props.resizeNotifier}
157-
replyInThread={true}
158-
replyToEvent={this.state?.thread?.replyToEvent}
159-
showReplyPreview={false}
160-
permalinkCreator={this.props.permalinkCreator}
161-
e2eStatus={this.props.e2eStatus}
162-
compact={true}
163-
/>
164-
</BaseCard>
192+
e2eStatus={this.props.e2eStatus}
193+
compact={true}
194+
/>) }
195+
</BaseCard>
196+
</RoomContext.Provider>
165197
);
166198
}
167199
}

src/components/views/messages/MessageActionBar.tsx

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { Action } from '../../../dispatcher/actions';
2727
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
2828
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
2929
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
30-
import RoomContext from "../../../contexts/RoomContext";
30+
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
3131
import Toolbar from "../../../accessibility/Toolbar";
3232
import { RovingAccessibleTooltipButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
3333
import { replaceableComponent } from "../../../utils/replaceableComponent";
@@ -128,11 +128,6 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
128128
</React.Fragment>;
129129
};
130130

131-
export enum ActionBarRenderingContext {
132-
Room,
133-
Thread
134-
}
135-
136131
interface IMessageActionBarProps {
137132
mxEvent: MatrixEvent;
138133
reactions?: Relations;
@@ -142,18 +137,13 @@ interface IMessageActionBarProps {
142137
permalinkCreator?: RoomPermalinkCreator;
143138
onFocusChange?: (menuDisplayed: boolean) => void;
144139
toggleThreadExpanded: () => void;
145-
renderingContext?: ActionBarRenderingContext;
146140
isQuoteExpanded?: boolean;
147141
}
148142

149143
@replaceableComponent("views.messages.MessageActionBar")
150144
export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> {
151145
public static contextType = RoomContext;
152146

153-
public static defaultProps = {
154-
renderingContext: ActionBarRenderingContext.Room,
155-
};
156-
157147
public componentDidMount(): void {
158148
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
159149
this.props.mxEvent.on("Event.status", this.onSent);
@@ -217,8 +207,9 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
217207

218208
private onEditClick = (ev: React.MouseEvent): void => {
219209
dis.dispatch({
220-
action: 'edit_event',
210+
action: Action.EditEvent,
221211
event: this.props.mxEvent,
212+
timelineRenderingType: this.context.timelineRenderingType,
222213
});
223214
};
224215

@@ -298,7 +289,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
298289
// Like the resend button, the react and reply buttons need to appear before the edit.
299290
// The only catch is we do the reply button first so that we can make sure the react
300291
// button is the very first button without having to do length checks for `splice()`.
301-
if (this.context.canReply && this.props.renderingContext === ActionBarRenderingContext.Room) {
292+
if (this.context.canReply && this.context.timelineRenderingType === TimelineRenderingType.Room) {
302293
toolbarOpts.splice(0, 0, <>
303294
<RovingAccessibleTooltipButton
304295
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"

0 commit comments

Comments
 (0)