Make threads use 'm.thread' relation

This commit is contained in:
Germain Souquet 2021-10-14 16:57:02 +01:00
parent 562a880c7d
commit d315641056
7 changed files with 52 additions and 58 deletions

View file

@ -271,9 +271,6 @@ export default class MessagePanel extends React.Component<IProps, IState> {
componentDidMount() { componentDidMount() {
this.calculateRoomMembersCount(); this.calculateRoomMembersCount();
this.props.room?.on("RoomState.members", this.calculateRoomMembersCount); this.props.room?.on("RoomState.members", this.calculateRoomMembersCount);
if (SettingsStore.getValue("feature_thread")) {
this.props.room?.getThreads().forEach(thread => thread.fetchReplyChain());
}
this.isMounted = true; this.isMounted = true;
} }
@ -463,8 +460,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
// Checking if the message has a "parentEventId" as we do not // Checking if the message has a "parentEventId" as we do not
// want to hide the root event of the thread // want to hide the root event of the thread
if (mxEv.replyInThread && mxEv.parentEventId if (mxEv.isThreadRoot && this.props.hideThreadedMessages
&& this.props.hideThreadedMessages
&& SettingsStore.getValue("feature_thread")) { && SettingsStore.getValue("feature_thread")) {
return false; return false;
} }

View file

@ -71,7 +71,7 @@ const useFilteredThreadsTimelinePanel = ({
userId, userId,
updateTimeline, updateTimeline,
}: { }: {
threads: Set<Thread>; threads: Map<string, Thread>;
room: Room; room: Room;
userId: string; userId: string;
filterOption: ThreadFilterType; filterOption: ThreadFilterType;
@ -85,13 +85,13 @@ const useFilteredThreadsTimelinePanel = ({
useEffect(() => { useEffect(() => {
let filteredThreads = Array.from(threads); let filteredThreads = Array.from(threads);
if (filterOption === ThreadFilterType.My) { if (filterOption === ThreadFilterType.My) {
filteredThreads = filteredThreads.filter(thread => { filteredThreads = filteredThreads.filter(([id, thread]) => {
return thread.rootEvent.getSender() === userId; return thread.rootEvent.getSender() === userId;
}); });
} }
// NOTE: Temporarily reverse the list until https://github.com/vector-im/element-web/issues/19393 gets properly resolved // NOTE: Temporarily reverse the list until https://github.com/vector-im/element-web/issues/19393 gets properly resolved
// The proper list order should be top-to-bottom, like in social-media newsfeeds. // The proper list order should be top-to-bottom, like in social-media newsfeeds.
filteredThreads.reverse().forEach(thread => { filteredThreads.reverse().forEach(([id, thread]) => {
const event = thread.rootEvent; const event = thread.rootEvent;
if (timelineSet.findEventById(event.getId()) || event.status !== null) return; if (timelineSet.findEventById(event.getId()) || event.status !== null) return;
timelineSet.addEventToTimeline( timelineSet.addEventToTimeline(

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { MatrixEvent, Room } from 'matrix-js-sdk/src'; import { MatrixEvent, Room } from 'matrix-js-sdk/src';
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread'; import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
import { RelationType } from 'matrix-js-sdk/src/@types/event';
import BaseCard from "../views/right_panel/BaseCard"; import BaseCard from "../views/right_panel/BaseCard";
import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
@ -185,8 +186,10 @@ export default class ThreadView extends React.Component<IProps, IState> {
{ this.state?.thread?.timelineSet && (<MessageComposer { this.state?.thread?.timelineSet && (<MessageComposer
room={this.props.room} room={this.props.room}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
replyInThread={true} relation={{
replyToEvent={this.state?.thread?.replyToEvent} rel_type: RelationType.Thread,
event_id: this.state.thread.id,
}}
showReplyPreview={false} showReplyPreview={false}
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
e2eStatus={this.props.e2eStatus} e2eStatus={this.props.e2eStatus}

View file

@ -21,7 +21,6 @@ import classNames from 'classnames';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { UNSTABLE_ELEMENT_REPLY_IN_THREAD } from "matrix-js-sdk/src/@types/event";
import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { Layout } from "../../../settings/Layout"; import { Layout } from "../../../settings/Layout";
@ -225,28 +224,15 @@ export default class ReplyThread extends React.Component<IProps, IState> {
return { body, html }; return { body, html };
} }
public static makeReplyMixIn(ev: MatrixEvent, replyInThread: boolean) { public static makeReplyMixIn(ev: MatrixEvent) {
if (!ev) return {}; if (!ev) return {};
return {
const replyMixin = {
'm.relates_to': { 'm.relates_to': {
'm.in_reply_to': { 'm.in_reply_to': {
'event_id': ev.getId(), 'event_id': ev.getId(),
}, },
}, },
}; };
/**
* @experimental
* Rendering hint for threads, only attached if true to make
* sure that Element does not start sending that property for all events
*/
if (replyInThread) {
const inReplyTo = replyMixin['m.relates_to']['m.in_reply_to'];
inReplyTo[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name] = replyInThread;
}
return replyMixin;
} }
public static hasThreadReply(event: MatrixEvent) { public static hasThreadReply(event: MatrixEvent) {

View file

@ -35,7 +35,7 @@ import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindin
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import SendHistoryManager from '../../../SendHistoryManager'; import SendHistoryManager from '../../../SendHistoryManager';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { MsgType, UNSTABLE_ELEMENT_REPLY_IN_THREAD } from 'matrix-js-sdk/src/@types/event'; import { MsgType } from 'matrix-js-sdk/src/@types/event';
import { Room } from 'matrix-js-sdk/src/models/room'; import { Room } from 'matrix-js-sdk/src/models/room';
import ErrorDialog from "../dialogs/ErrorDialog"; import ErrorDialog from "../dialogs/ErrorDialog";
import QuestionDialog from "../dialogs/QuestionDialog"; import QuestionDialog from "../dialogs/QuestionDialog";
@ -46,7 +46,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { withMatrixClientHOC, MatrixClientProps } from '../../../contexts/MatrixClientContext'; import { withMatrixClientHOC, MatrixClientProps } from '../../../contexts/MatrixClientContext';
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext'; import RoomContext from '../../../contexts/RoomContext';
function getHtmlReplyFallback(mxEvent: MatrixEvent): string { function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
const html = mxEvent.getContent().formatted_body; const html = mxEvent.getContent().formatted_body;
@ -70,7 +70,6 @@ function getTextReplyFallback(mxEvent: MatrixEvent): string {
function createEditContent( function createEditContent(
model: EditorModel, model: EditorModel,
editedEvent: MatrixEvent, editedEvent: MatrixEvent,
renderingContext?: TimelineRenderingType,
): IContent { ): IContent {
const isEmote = containsEmote(model); const isEmote = containsEmote(model);
if (isEmote) { if (isEmote) {
@ -112,10 +111,6 @@ function createEditContent(
}, },
}; };
if (renderingContext === TimelineRenderingType.Thread) {
relation['m.relates_to'][UNSTABLE_ELEMENT_REPLY_IN_THREAD.name] = true;
}
return Object.assign(relation, contentBody); return Object.assign(relation, contentBody);
} }
@ -143,8 +138,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
const isRestored = this.createEditorModel(); const isRestored = this.createEditorModel();
const ev = this.props.editState.getEvent(); const ev = this.props.editState.getEvent();
const renderingContext = this.context.timelineRenderingType; const editContent = createEditContent(this.model, ev);
const editContent = createEditContent(this.model, ev, renderingContext);
this.state = { this.state = {
saveDisabled: !isRestored || !this.isContentModified(editContent["m.new_content"]), saveDisabled: !isRestored || !this.isContentModified(editContent["m.new_content"]),
}; };
@ -369,8 +363,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd); const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON); this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
} }
const renderingContext = this.context.timelineRenderingType; const editContent = createEditContent(this.model, editedEvent);
const editContent = createEditContent(this.model, editedEvent, renderingContext);
const newContent = editContent["m.new_content"]; const newContent = editContent["m.new_content"];
let shouldSend = true; let shouldSend = true;

View file

@ -17,7 +17,7 @@ import React, { createRef } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent, IEventRelation } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
@ -54,6 +54,7 @@ import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
import UIStore, { UI_EVENTS } from '../../../stores/UIStore'; import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import InfoDialog from "../dialogs/InfoDialog"; import InfoDialog from "../dialogs/InfoDialog";
import { RelationType } from 'matrix-js-sdk/src/@types/event';
let instanceCount = 0; let instanceCount = 0;
const NARROW_MODE_BREAKPOINT = 500; const NARROW_MODE_BREAKPOINT = 500;
@ -225,7 +226,7 @@ interface IProps {
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
permalinkCreator: RoomPermalinkCreator; permalinkCreator: RoomPermalinkCreator;
replyToEvent?: MatrixEvent; replyToEvent?: MatrixEvent;
replyInThread?: boolean; relation?: IEventRelation;
showReplyPreview?: boolean; showReplyPreview?: boolean;
e2eStatus?: E2EStatus; e2eStatus?: E2EStatus;
compact?: boolean; compact?: boolean;
@ -252,7 +253,6 @@ export default class MessageComposer extends React.Component<IProps, IState> {
private instanceId: number; private instanceId: number;
static defaultProps = { static defaultProps = {
replyInThread: false,
showReplyPreview: true, showReplyPreview: true,
compact: false, compact: false,
}; };
@ -378,9 +378,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
private renderPlaceholderText = () => { private renderPlaceholderText = () => {
if (this.props.replyToEvent) { if (this.props.replyToEvent) {
if (this.props.replyInThread && this.props.e2eStatus) { const replyingToThread = this.props.relation?.rel_type === RelationType.Thread;
if (replyingToThread && this.props.e2eStatus) {
return _t('Reply to encrypted thread…'); return _t('Reply to encrypted thread…');
} else if (this.props.replyInThread) { } else if (replyingToThread) {
return _t('Reply to thread…'); return _t('Reply to thread…');
} else if (this.props.e2eStatus) { } else if (this.props.e2eStatus) {
return _t('Send an encrypted reply…'); return _t('Send an encrypted reply…');
@ -558,7 +559,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
room={this.props.room} room={this.props.room}
placeholder={this.renderPlaceholderText()} placeholder={this.renderPlaceholderText()}
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
replyInThread={this.props.replyInThread} relation={this.props.relation}
replyToEvent={this.props.replyToEvent} replyToEvent={this.props.replyToEvent}
onChange={this.onChange} onChange={this.onChange}
disabled={this.state.haveRecording} disabled={this.state.haveRecording}

View file

@ -16,7 +16,7 @@ limitations under the License.
import React, { ClipboardEvent, createRef, KeyboardEvent } from 'react'; import React, { ClipboardEvent, createRef, KeyboardEvent } from 'react';
import EMOJI_REGEX from 'emojibase-regex'; import EMOJI_REGEX from 'emojibase-regex';
import { IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { IContent, MatrixEvent, IEventRelation } from 'matrix-js-sdk/src/models/event';
import { DebouncedFunc, throttle } from 'lodash'; import { DebouncedFunc, throttle } from 'lodash';
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event"; import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@ -61,10 +61,10 @@ import RoomContext from '../../../contexts/RoomContext';
function addReplyToMessageContent( function addReplyToMessageContent(
content: IContent, content: IContent,
replyToEvent: MatrixEvent, replyToEvent: MatrixEvent,
replyInThread: boolean,
permalinkCreator: RoomPermalinkCreator, permalinkCreator: RoomPermalinkCreator,
relation?: IEventRelation,
): void { ): void {
const replyContent = ReplyThread.makeReplyMixIn(replyToEvent, replyInThread); const replyContent = ReplyThread.makeReplyMixIn(replyToEvent);
Object.assign(content, replyContent); Object.assign(content, replyContent);
// Part of Replies fallback support - prepend the text we're sending // Part of Replies fallback support - prepend the text we're sending
@ -76,13 +76,20 @@ function addReplyToMessageContent(
} }
content.body = nestedReply.body + content.body; content.body = nestedReply.body + content.body;
} }
if (relation) {
content['m.relates_to'] = {
...relation, // the composer can have a default
...content['m.relates_to'],
};
}
} }
// exported for tests // exported for tests
export function createMessageContent( export function createMessageContent(
model: EditorModel, model: EditorModel,
replyToEvent: MatrixEvent, replyToEvent: MatrixEvent,
replyInThread: boolean, relation: IEventRelation,
permalinkCreator: RoomPermalinkCreator, permalinkCreator: RoomPermalinkCreator,
): IContent { ): IContent {
const isEmote = containsEmote(model); const isEmote = containsEmote(model);
@ -106,7 +113,14 @@ export function createMessageContent(
} }
if (replyToEvent) { if (replyToEvent) {
addReplyToMessageContent(content, replyToEvent, replyInThread, permalinkCreator); addReplyToMessageContent(content, replyToEvent, permalinkCreator);
}
if (relation) {
content['m.relates_to'] = {
...relation,
...content['m.relates_to'],
};
} }
return content; return content;
@ -134,7 +148,7 @@ interface ISendMessageComposerProps extends MatrixClientProps {
room: Room; room: Room;
placeholder?: string; placeholder?: string;
permalinkCreator: RoomPermalinkCreator; permalinkCreator: RoomPermalinkCreator;
replyInThread?: boolean; relation?: IEventRelation;
replyToEvent?: MatrixEvent; replyToEvent?: MatrixEvent;
disabled?: boolean; disabled?: boolean;
onChange?(model: EditorModel): void; onChange?(model: EditorModel): void;
@ -162,12 +176,11 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
} }
public componentDidUpdate(prevProps: ISendMessageComposerProps): void { public componentDidUpdate(prevProps: ISendMessageComposerProps): void {
const replyToEventChanged = this.props.replyInThread && (this.props.replyToEvent !== prevProps.replyToEvent); const replyingToThread = this.props.relation?.key === RelationType.Thread;
if (replyToEventChanged) { const differentEventTarget = this.props.relation?.event_id !== prevProps.relation?.event_id;
this.model.reset([]);
}
if (this.props.replyInThread && this.props.replyToEvent && (!prevProps.replyToEvent || replyToEventChanged)) { const threadChanged = replyingToThread && (differentEventTarget);
if (threadChanged) {
const partCreator = new CommandPartCreator(this.props.room, this.props.mxClient); const partCreator = new CommandPartCreator(this.props.room, this.props.mxClient);
const parts = this.restoreStoredEditorState(partCreator) || []; const parts = this.restoreStoredEditorState(partCreator) || [];
this.model.reset(parts); this.model.reset(parts);
@ -180,6 +193,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
if (this.editorRef.current?.isComposing(event)) { if (this.editorRef.current?.isComposing(event)) {
return; return;
} }
const replyingToThread = this.props.relation?.key === RelationType.Thread;
const action = getKeyBindingsManager().getMessageComposerAction(event); const action = getKeyBindingsManager().getMessageComposerAction(event);
switch (action) { switch (action) {
case MessageComposerAction.Send: case MessageComposerAction.Send:
@ -201,7 +215,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
if (this.editorRef.current?.isSelectionCollapsed() && this.editorRef.current?.isCaretAtStart()) { if (this.editorRef.current?.isSelectionCollapsed() && this.editorRef.current?.isCaretAtStart()) {
const events = const events =
this.context.liveTimeline.getEvents() this.context.liveTimeline.getEvents()
.concat(this.props.replyInThread ? [] : this.props.room.getPendingEvents()); .concat(replyingToThread ? [] : this.props.room.getPendingEvents());
const editEvent = findEditableEvent({ const editEvent = findEditableEvent({
events, events,
isForward: false, isForward: false,
@ -393,8 +407,8 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
addReplyToMessageContent( addReplyToMessageContent(
content, content,
replyToEvent, replyToEvent,
this.props.replyInThread,
this.props.permalinkCreator, this.props.permalinkCreator,
this.props.relation,
); );
} }
} else { } else {
@ -441,7 +455,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
content = createMessageContent( content = createMessageContent(
model, model,
replyToEvent, replyToEvent,
this.props.replyInThread, this.props.relation,
this.props.permalinkCreator, this.props.permalinkCreator,
); );
} }
@ -515,7 +529,8 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
} }
private restoreStoredEditorState(partCreator: PartCreator): Part[] { private restoreStoredEditorState(partCreator: PartCreator): Part[] {
if (this.props.replyInThread && !this.props.replyToEvent) { const replyingToThread = this.props.relation?.key === RelationType.Thread;
if (replyingToThread) {
return null; return null;
} }