mirror of https://github.com/vector-im/riot-web
Implement deep-linking for threads (matrix.to) (#7003)
parent
bc32f05fcb
commit
e20ac7bf1e
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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]) =>
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
|
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue