Implement deep-linking for threads (matrix.to) (#7003)

pull/21833/head
Germain 2021-10-22 09:30:36 +01:00 committed by GitHub
parent bc32f05fcb
commit e20ac7bf1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 122 additions and 12 deletions

View File

@ -295,6 +295,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
const notifTimelineSet = new EventTimelineSet(null, { const notifTimelineSet = new EventTimelineSet(null, {
timelineSupport: true, timelineSupport: true,
pendingEvents: false,
}); });
// XXX: what is our initial pagination token?! it somehow needs to be synchronised with /sync. // XXX: what is our initial pagination token?! it somehow needs to be synchronised with /sync.
notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS); notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS);

View File

@ -75,6 +75,8 @@ interface IState {
groupRoomId?: string; groupRoomId?: string;
groupId?: string; groupId?: string;
event: MatrixEvent; event: MatrixEvent;
initialEvent?: MatrixEvent;
initialEventHighlighted?: boolean;
} }
@replaceableComponent("structures.RightPanel") @replaceableComponent("structures.RightPanel")
@ -209,6 +211,8 @@ export default class RightPanel extends React.Component<IProps, IState> {
groupId: payload.groupId, groupId: payload.groupId,
member: payload.member, member: payload.member,
event: payload.event, event: payload.event,
initialEvent: payload.initialEvent,
initialEventHighlighted: payload.highlighted,
verificationRequest: payload.verificationRequest, verificationRequest: payload.verificationRequest,
verificationRequestPromise: payload.verificationRequestPromise, verificationRequestPromise: payload.verificationRequestPromise,
widgetId: payload.widgetId, widgetId: payload.widgetId,
@ -244,7 +248,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
} }
}; };
render() { public render(): JSX.Element {
let panel = <div />; let panel = <div />;
const roomId = this.props.room ? this.props.room.roomId : undefined; const roomId = this.props.room ? this.props.room.roomId : undefined;
@ -327,6 +331,8 @@ export default class RightPanel extends React.Component<IProps, IState> {
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
onClose={this.onClose} onClose={this.onClose}
mxEvent={this.state.event} mxEvent={this.state.event}
initialEvent={this.state.initialEvent}
initialEventHighlighted={this.state.initialEventHighlighted}
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
e2eStatus={this.props.e2eStatus} />; e2eStatus={this.props.e2eStatus} />;
break; break;

View File

@ -92,6 +92,8 @@ import SpaceStore from "../../stores/SpaceStore";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
import { dispatchShowThreadEvent } from '../../dispatcher/dispatch-actions/threads';
import { fetchInitialEvent } from "../../utils/EventUtils";
const DEBUG = false; const DEBUG = false;
let debuglog = function(msg: string) {}; let debuglog = function(msg: string) {};
@ -321,7 +323,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}); });
}; };
private onRoomViewStoreUpdate = (initial?: boolean) => { private onRoomViewStoreUpdate = async (initial?: boolean): Promise<void> => {
if (this.unmounted) { if (this.unmounted) {
return; return;
} }
@ -349,8 +351,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
roomLoading: RoomViewStore.isRoomLoading(), roomLoading: RoomViewStore.isRoomLoading(),
roomLoadError: RoomViewStore.getRoomLoadError(), roomLoadError: RoomViewStore.getRoomLoadError(),
joining: RoomViewStore.isJoining(), joining: RoomViewStore.isJoining(),
initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
replyToEvent: RoomViewStore.getQuotingEvent(), replyToEvent: RoomViewStore.getQuotingEvent(),
// we should only peek once we have a ready client // we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(), shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
@ -362,6 +362,39 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
wasContextSwitch: RoomViewStore.getWasContextSwitch(), wasContextSwitch: RoomViewStore.getWasContextSwitch(),
}; };
const initialEventId = RoomViewStore.getInitialEventId();
if (initialEventId) {
const room = this.context.getRoom(roomId);
let initialEvent = room?.findEventById(initialEventId);
// The event does not exist in the current sync data
// We need to fetch it to know whether to route this request
// to the main timeline or to a threaded one
// In the current state, if a thread does not exist in the sync data
// We will only display the event targeted by the `matrix.to` link
// and the root event.
// The rest will be lost for now, until the aggregation API on the server
// becomes available to fetch a whole thread
if (!initialEvent) {
initialEvent = await fetchInitialEvent(
this.context,
roomId,
initialEventId,
);
}
const thread = initialEvent?.getThread();
if (thread && !initialEvent?.isThreadRoot) {
dispatchShowThreadEvent(
thread.rootEvent,
initialEvent,
RoomViewStore.isInitialEventHighlighted(),
);
} else {
newState.initialEventId = initialEventId;
newState.isInitialEventHighlighted = RoomViewStore.isInitialEventHighlighted();
}
}
// Add watchers for each of the settings we just looked up // Add watchers for each of the settings we just looked up
this.settingWatchers = this.settingWatchers.concat([ this.settingWatchers = this.settingWatchers.concat([
SettingsStore.watchSetting("showReadReceipts", roomId, (...[,,, value]) => SettingsStore.watchSetting("showReadReceipts", roomId, (...[,,, value]) =>

View File

@ -67,6 +67,7 @@ const useFilteredThreadsTimelinePanel = ({
const timelineSet = useMemo(() => new EventTimelineSet(null, { const timelineSet = useMemo(() => new EventTimelineSet(null, {
timelineSupport: true, timelineSupport: true,
unstableClientRelationAggregation: true, unstableClientRelationAggregation: true,
pendingEvents: false,
}), []); }), []);
useEffect(() => { useEffect(() => {

View File

@ -45,6 +45,8 @@ interface IProps {
mxEvent: MatrixEvent; mxEvent: MatrixEvent;
permalinkCreator?: RoomPermalinkCreator; permalinkCreator?: RoomPermalinkCreator;
e2eStatus?: E2EStatus; e2eStatus?: E2EStatus;
initialEvent?: MatrixEvent;
initialEventHighlighted?: boolean;
} }
interface IState { interface IState {
@ -102,19 +104,19 @@ export default class ThreadView extends React.Component<IProps, IState> {
} }
} }
switch (payload.action) { switch (payload.action) {
case Action.EditEvent: { case Action.EditEvent:
// Quit early if it's not a thread context // Quit early if it's not a thread context
if (payload.timelineRenderingType !== TimelineRenderingType.Thread) return; if (payload.timelineRenderingType !== TimelineRenderingType.Thread) return;
// Quit early if that's not a thread event // Quit early if that's not a thread event
if (payload.event && !payload.event.getThread()) return; if (payload.event && !payload.event.getThread()) return;
const editState = payload.event ? new EditorStateTransfer(payload.event) : null; this.setState({
this.setState({ editState }, () => { editState: payload.event ? new EditorStateTransfer(payload.event) : null,
}, () => {
if (payload.event) { if (payload.event) {
this.timelinePanelRef.current?.scrollToEventIfNeeded(payload.event.getId()); this.timelinePanelRef.current?.scrollToEventIfNeeded(payload.event.getId());
} }
}); });
break; break;
}
case 'reply_to_event': case 'reply_to_event':
if (payload.context === TimelineRenderingType.Thread) { if (payload.context === TimelineRenderingType.Thread) {
this.setState({ this.setState({
@ -131,7 +133,11 @@ export default class ThreadView extends React.Component<IProps, IState> {
let thread = mxEv.getThread(); let thread = mxEv.getThread();
if (!thread) { if (!thread) {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
thread = new Thread([mxEv], this.props.room, client); thread = new Thread(
[mxEv],
this.props.room,
client,
);
mxEv.setThread(thread); mxEv.setThread(thread);
} }
thread.on(ThreadEvent.Update, this.updateThread); thread.on(ThreadEvent.Update, this.updateThread);
@ -163,7 +169,22 @@ export default class ThreadView extends React.Component<IProps, IState> {
this.timelinePanelRef.current?.refreshTimeline(); this.timelinePanelRef.current?.refreshTimeline();
}; };
private onScroll = (): void => {
if (this.props.initialEvent && this.props.initialEventHighlighted) {
dis.dispatch({
action: 'view_room',
room_id: this.props.room.roomId,
event_id: this.props.initialEvent?.getId(),
highlighted: false,
replyingToEvent: this.state.replyToEvent,
});
}
};
public render(): JSX.Element { public render(): JSX.Element {
const highlightedEventId = this.props.initialEventHighlighted
? this.props.initialEvent?.getId()
: null;
return ( return (
<RoomContext.Provider value={{ <RoomContext.Provider value={{
...this.context, ...this.context,
@ -197,6 +218,9 @@ export default class ThreadView extends React.Component<IProps, IState> {
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
membersLoaded={true} membersLoaded={true}
editState={this.state.editState} editState={this.state.editState}
eventId={this.props.initialEvent?.getId()}
highlightedEventId={highlightedEventId}
onUserScroll={this.onScroll}
/> />
) } ) }

View File

@ -574,7 +574,9 @@ export default class EventTile extends React.Component<IProps, IState> {
<div <div
className="mx_ThreadInfo" className="mx_ThreadInfo"
onClick={() => { onClick={() => {
dispatchShowThreadEvent(this.props.mxEvent); dispatchShowThreadEvent(
this.props.mxEvent,
);
}} }}
> >
<span className="mx_ThreadInfo_thread-icon" /> <span className="mx_ThreadInfo_thread-icon" />

View File

@ -19,12 +19,18 @@ import { Action } from "../actions";
import dis from '../dispatcher'; import dis from '../dispatcher';
import { SetRightPanelPhasePayload } from "../payloads/SetRightPanelPhasePayload"; import { SetRightPanelPhasePayload } from "../payloads/SetRightPanelPhasePayload";
export const dispatchShowThreadEvent = (event: MatrixEvent) => { export const dispatchShowThreadEvent = (
rootEvent: MatrixEvent,
initialEvent?: MatrixEvent,
highlighted?: boolean,
) => {
dis.dispatch({ dis.dispatch({
action: Action.SetRightPanelPhase, action: Action.SetRightPanelPhase,
phase: RightPanelPhases.ThreadView, phase: RightPanelPhases.ThreadView,
refireParams: { refireParams: {
event, event: rootEvent,
initialEvent,
highlighted,
}, },
}); });
}; };

View File

@ -21,6 +21,9 @@ import shouldHideEvent from "../shouldHideEvent";
import { getHandlerTile, haveTileForEvent } from "../components/views/rooms/EventTile"; import { getHandlerTile, haveTileForEvent } from "../components/views/rooms/EventTile";
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { Thread } from 'matrix-js-sdk/src/models/thread';
import { logger } from 'matrix-js-sdk/src/logger';
/** /**
* Returns whether an event should allow actions like reply, reactions, edit, etc. * Returns whether an event should allow actions like reply, reactions, edit, etc.
@ -158,3 +161,37 @@ export function isVoiceMessage(mxEvent: MatrixEvent): boolean {
!!content['org.matrix.msc3245.voice'] !!content['org.matrix.msc3245.voice']
); );
} }
export async function fetchInitialEvent(
client: MatrixClient,
roomId: string,
eventId: string): Promise<MatrixEvent | null> {
let initialEvent: MatrixEvent;
try {
const eventData = await client.fetchRoomEvent(roomId, eventId);
initialEvent = new MatrixEvent(eventData);
} catch (e) {
logger.warn("Could not find initial event: " + initialEvent.threadRootId);
initialEvent = null;
}
if (initialEvent?.isThreadRelation) {
try {
const rootEventData = await client.fetchRoomEvent(roomId, initialEvent.threadRootId);
const rootEvent = new MatrixEvent(rootEventData);
const room = client.getRoom(roomId);
const thread = new Thread(
[rootEvent],
room,
client,
);
thread.addEvent(initialEvent);
room.threads.set(thread.id, thread);
} catch (e) {
logger.warn("Could not find root event: " + initialEvent.threadRootId);
}
}
return initialEvent;
}