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, {
timelineSupport: true,
pendingEvents: false,
});
// XXX: what is our initial pagination token?! it somehow needs to be synchronised with /sync.
notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS);

View File

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

View File

@ -92,6 +92,8 @@ import SpaceStore from "../../stores/SpaceStore";
import { logger } from "matrix-js-sdk/src/logger";
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;
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) {
return;
}
@ -349,8 +351,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
roomLoading: RoomViewStore.isRoomLoading(),
roomLoadError: RoomViewStore.getRoomLoadError(),
joining: RoomViewStore.isJoining(),
initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
replyToEvent: RoomViewStore.getQuotingEvent(),
// we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
@ -362,6 +362,39 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
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
this.settingWatchers = this.settingWatchers.concat([
SettingsStore.watchSetting("showReadReceipts", roomId, (...[,,, value]) =>

View File

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

View File

@ -45,6 +45,8 @@ interface IProps {
mxEvent: MatrixEvent;
permalinkCreator?: RoomPermalinkCreator;
e2eStatus?: E2EStatus;
initialEvent?: MatrixEvent;
initialEventHighlighted?: boolean;
}
interface IState {
@ -102,19 +104,19 @@ export default class ThreadView extends React.Component<IProps, IState> {
}
}
switch (payload.action) {
case Action.EditEvent: {
case Action.EditEvent:
// Quit early if it's not a thread context
if (payload.timelineRenderingType !== TimelineRenderingType.Thread) return;
// Quit early if that's not a thread event
if (payload.event && !payload.event.getThread()) return;
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
this.setState({ editState }, () => {
this.setState({
editState: payload.event ? new EditorStateTransfer(payload.event) : null,
}, () => {
if (payload.event) {
this.timelinePanelRef.current?.scrollToEventIfNeeded(payload.event.getId());
}
});
break;
}
case 'reply_to_event':
if (payload.context === TimelineRenderingType.Thread) {
this.setState({
@ -131,7 +133,11 @@ export default class ThreadView extends React.Component<IProps, IState> {
let thread = mxEv.getThread();
if (!thread) {
const client = MatrixClientPeg.get();
thread = new Thread([mxEv], this.props.room, client);
thread = new Thread(
[mxEv],
this.props.room,
client,
);
mxEv.setThread(thread);
}
thread.on(ThreadEvent.Update, this.updateThread);
@ -163,7 +169,22 @@ export default class ThreadView extends React.Component<IProps, IState> {
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 {
const highlightedEventId = this.props.initialEventHighlighted
? this.props.initialEvent?.getId()
: null;
return (
<RoomContext.Provider value={{
...this.context,
@ -197,6 +218,9 @@ export default class ThreadView extends React.Component<IProps, IState> {
permalinkCreator={this.props.permalinkCreator}
membersLoaded={true}
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
className="mx_ThreadInfo"
onClick={() => {
dispatchShowThreadEvent(this.props.mxEvent);
dispatchShowThreadEvent(
this.props.mxEvent,
);
}}
>
<span className="mx_ThreadInfo_thread-icon" />

View File

@ -19,12 +19,18 @@ import { Action } from "../actions";
import dis from '../dispatcher';
import { SetRightPanelPhasePayload } from "../payloads/SetRightPanelPhasePayload";
export const dispatchShowThreadEvent = (event: MatrixEvent) => {
export const dispatchShowThreadEvent = (
rootEvent: MatrixEvent,
initialEvent?: MatrixEvent,
highlighted?: boolean,
) => {
dis.dispatch({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.ThreadView,
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 SettingsStore from "../settings/SettingsStore";
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.
@ -158,3 +161,37 @@ export function isVoiceMessage(mxEvent: MatrixEvent): boolean {
!!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;
}