mirror of
https://github.com/vector-im/element-web.git
synced 2026-05-05 12:16:53 +02:00
More WIP
This commit is contained in:
parent
044687a3af
commit
bcfcf7a7d2
@ -164,18 +164,15 @@ export interface MessagePanelMethods {
|
||||
|
||||
scrollToEventIfNeeded: (eventId: string) => void;
|
||||
|
||||
showHiddenEvents: boolean;
|
||||
|
||||
// Once dynamic content in the events load, make the scrollPanel check the scroll offsets.
|
||||
onHeightChanged: () => void;
|
||||
updateTimelineMinHeight: () => void;
|
||||
onTimelineReset: () => void;
|
||||
getTileForEventId: (eventId?: string) => UnwrappedEventTile | undefined;
|
||||
// A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination.
|
||||
grouperKeyMap: WeakMap<MatrixEvent, string>;
|
||||
shouldShowEvent: (mxEv: MatrixEvent, forceHideEvents: boolean) => boolean;
|
||||
readMarkerForEvent: (eventId: string, isLastEvent: boolean) => ReactNode;
|
||||
wantsSeparator: (prevEvent: MatrixEvent | null, mxEvent: MatrixEvent) => SeparatorKind;
|
||||
scrollPanel: RefObject<ScrollPanel | null>;
|
||||
getNodeForEventId: (eventId: string) => HTMLElement | undefined;
|
||||
props: IProps;
|
||||
}
|
||||
|
||||
export interface GrouperPanel {
|
||||
getTilesForEvent: (
|
||||
prevEvent: MatrixEvent | null,
|
||||
wrappedEvent: WrappedEvent,
|
||||
@ -185,8 +182,18 @@ export interface MessagePanelMethods {
|
||||
nextEventWithTile?: MatrixEvent | null,
|
||||
) => ReactNode[];
|
||||
layout?: Layout;
|
||||
scrollPanel: RefObject<ScrollPanel | null>;
|
||||
getNodeForEventId: (eventId: string) => HTMLElement | undefined;
|
||||
// A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination.
|
||||
grouperKeyMap: WeakMap<MatrixEvent, string>;
|
||||
showHiddenEvents: boolean;
|
||||
// Once dynamic content in the events load, make the scrollPanel check the scroll offsets.
|
||||
onHeightChanged: () => void;
|
||||
shouldShowEvent: (mxEv: MatrixEvent, forceHideEvents: boolean) => boolean;
|
||||
readMarkerForEvent: (eventId: string, isLastEvent: boolean) => ReactNode;
|
||||
wantsSeparator: (prevEvent: MatrixEvent | null, mxEvent: MatrixEvent) => SeparatorKind;
|
||||
// ID of an event to highlight. If undefined, no event will be highlighted.
|
||||
highlightedEventId?: string;
|
||||
// whether the timeline can visually go back any further
|
||||
canBackPaginate?: boolean;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
@ -270,7 +277,6 @@ interface IProps {
|
||||
|
||||
callEventGroupers: Map<string, LegacyCallEventGrouper>;
|
||||
ref: RefObject<MessagePanelMethods | null>;
|
||||
props: IProps;
|
||||
}
|
||||
|
||||
interface IReadReceiptForUser {
|
||||
@ -530,17 +536,40 @@ export const MessagePanelNew: React.FC<IProps> = (props: IProps) => {
|
||||
|
||||
// Once dynamic content in the events load, make the scrollPanel check the scroll offsets.
|
||||
const onHeightChanged = useCallback((): void => scrollPanel.current?.checkScroll(), []);
|
||||
const resizeObserver = useRef(new ResizeObserver(onHeightChanged));
|
||||
const resizeObserverRef = useRef<null | ResizeObserver>(null);
|
||||
|
||||
const getResizeObserver = useCallback((): ResizeObserver => {
|
||||
if (resizeObserverRef.current !== null) return resizeObserverRef.current;
|
||||
const observer = new ResizeObserver(onHeightChanged);
|
||||
resizeObserverRef.current = observer;
|
||||
return observer;
|
||||
}, [onHeightChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
unmounted.current = true;
|
||||
const observer = resizeObserver.current;
|
||||
const observer = getResizeObserver();
|
||||
return () => {
|
||||
unmounted.current = false;
|
||||
readReceiptMap.current = {};
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
}, [getResizeObserver]);
|
||||
|
||||
useEffect(() => {
|
||||
const room = props.room;
|
||||
if (!room) return;
|
||||
const pendingEditItem = getPendingEditItem(room, context.timelineRenderingType);
|
||||
if (!props.editState && room && pendingEditItem) {
|
||||
const event = room.findEventById(pendingEditItem);
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: !event?.isRedacted() ? event : null,
|
||||
timelineRenderingType: context.timelineRenderingType,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.room]);
|
||||
|
||||
const isUnmounting = (): boolean => unmounted.current;
|
||||
|
||||
@ -558,86 +587,103 @@ export const MessagePanelNew: React.FC<IProps> = (props: IProps) => {
|
||||
};
|
||||
|
||||
// TODO: Implement granular (per-room) hide options
|
||||
const shouldShowEvent = (mxEv: MatrixEvent, forceHideEvents = false): boolean => {
|
||||
if (props.hideThreadedMessages && props.room) {
|
||||
const { shouldLiveInRoom } = props.room.eventShouldLiveIn(mxEv, props.events);
|
||||
if (!shouldLiveInRoom) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (MatrixClientPeg.safeGet().isUserIgnored(mxEv.getSender()!)) {
|
||||
return false; // ignored = no show (only happens if the ignore happens after an event was received)
|
||||
}
|
||||
|
||||
if (showHiddenEvents && !forceHideEvents) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!haveRendererForEvent(mxEv, MatrixClientPeg.safeGet(), showHiddenEvents.current)) {
|
||||
return false; // no tile = no show
|
||||
}
|
||||
|
||||
// Always show highlighted event
|
||||
if (props.highlightedEventId === mxEv.getId()) return true;
|
||||
|
||||
return !shouldHideEvent(mxEv, context);
|
||||
};
|
||||
|
||||
const readMarkerForEvent = (eventId: string, isLastEvent: boolean): JSX.Element | null => {
|
||||
if (context.timelineRenderingType === TimelineRenderingType.File) return null;
|
||||
|
||||
const visible = !isLastEvent && props.readMarkerVisible;
|
||||
|
||||
if (props.readMarkerEventId === eventId) {
|
||||
let hr;
|
||||
// if the read marker comes at the end of the timeline (except
|
||||
// for local echoes, which are excluded from RMs, because they
|
||||
// don't have useful event ids), we don't want to show it, but
|
||||
// we still want to create the <li/> for it so that the
|
||||
// algorithms which depend on its position on the screen aren't
|
||||
// confused.
|
||||
if (visible) {
|
||||
hr = <hr style={{ opacity: 1, width: "99%" }} />;
|
||||
const shouldShowEvent = useCallback(
|
||||
(mxEv: MatrixEvent, forceHideEvents = false): boolean => {
|
||||
if (props.hideThreadedMessages && props.room) {
|
||||
const { shouldLiveInRoom } = props.room.eventShouldLiveIn(mxEv, props.events);
|
||||
if (!shouldLiveInRoom) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
key={"readMarker_" + eventId}
|
||||
ref={readMarkerNode}
|
||||
className="mx_MessagePanel_myReadMarker"
|
||||
data-scroll-tokens={eventId}
|
||||
>
|
||||
{hr}
|
||||
</li>
|
||||
);
|
||||
} else if (ghostReadMarkers.includes(eventId)) {
|
||||
// We render 'ghost' read markers in the DOM while they
|
||||
// transition away. This allows the actual read marker
|
||||
// to be in the right place straight away without having
|
||||
// to wait for the transition to finish.
|
||||
// There are probably much simpler ways to do this transition,
|
||||
// possibly using react-transition-group which handles keeping
|
||||
// elements in the DOM whilst they transition out, although our
|
||||
// case is a little more complex because only some of the items
|
||||
// transition (ie. the read markers do but the event tiles do not)
|
||||
// and TransitionGroup requires that all its children are Transitions.
|
||||
const hr = (
|
||||
<hr ref={collectGhostReadMarker} onTransitionEnd={onGhostTransitionEnd} data-eventid={eventId} />
|
||||
);
|
||||
if (MatrixClientPeg.safeGet().isUserIgnored(mxEv.getSender()!)) {
|
||||
return false; // ignored = no show (only happens if the ignore happens after an event was received)
|
||||
}
|
||||
|
||||
// give it a key which depends on the event id. That will ensure that
|
||||
// we get a new DOM node (restarting the animation) when the ghost
|
||||
// moves to a different event.
|
||||
return (
|
||||
<li key={"_readuptoghost_" + eventId} className="mx_MessagePanel_myReadMarker">
|
||||
{hr}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
if (showHiddenEvents.current && !forceHideEvents) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
if (!haveRendererForEvent(mxEv, MatrixClientPeg.safeGet(), showHiddenEvents.current)) {
|
||||
return false; // no tile = no show
|
||||
}
|
||||
|
||||
// Always show highlighted event
|
||||
if (props.highlightedEventId === mxEv.getId()) return true;
|
||||
|
||||
return !shouldHideEvent(mxEv, context);
|
||||
},
|
||||
[context, props.events, props.hideThreadedMessages, props.highlightedEventId, props.room],
|
||||
);
|
||||
|
||||
const readMarkerForEvent = useCallback(
|
||||
(eventId: string, isLastEvent: boolean): JSX.Element | null => {
|
||||
if (context.timelineRenderingType === TimelineRenderingType.File) return null;
|
||||
|
||||
const visible = !isLastEvent && props.readMarkerVisible;
|
||||
|
||||
if (props.readMarkerEventId === eventId) {
|
||||
let hr;
|
||||
// if the read marker comes at the end of the timeline (except
|
||||
// for local echoes, which are excluded from RMs, because they
|
||||
// don't have useful event ids), we don't want to show it, but
|
||||
// we still want to create the <li/> for it so that the
|
||||
// algorithms which depend on its position on the screen aren't
|
||||
// confused.
|
||||
if (visible) {
|
||||
hr = <hr style={{ opacity: 1, width: "99%" }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
key={"readMarker_" + eventId}
|
||||
ref={readMarkerNode}
|
||||
className="mx_MessagePanel_myReadMarker"
|
||||
data-scroll-tokens={eventId}
|
||||
>
|
||||
{hr}
|
||||
</li>
|
||||
);
|
||||
} else if (ghostReadMarkers.includes(eventId)) {
|
||||
// We render 'ghost' read markers in the DOM while they
|
||||
// transition away. This allows the actual read marker
|
||||
// to be in the right place straight away without having
|
||||
// to wait for the transition to finish.
|
||||
// There are probably much simpler ways to do this transition,
|
||||
// possibly using react-transition-group which handles keeping
|
||||
// elements in the DOM whilst they transition out, although our
|
||||
// case is a little more complex because only some of the items
|
||||
// transition (ie. the read markers do but the event tiles do not)
|
||||
// and TransitionGroup requires that all its children are Transitions.
|
||||
const hr = (
|
||||
<hr ref={collectGhostReadMarker} onTransitionEnd={onGhostTransitionEnd} data-eventid={eventId} />
|
||||
);
|
||||
|
||||
// give it a key which depends on the event id. That will ensure that
|
||||
// we get a new DOM node (restarting the animation) when the ghost
|
||||
// moves to a different event.
|
||||
return (
|
||||
<li key={"_readuptoghost_" + eventId} className="mx_MessagePanel_myReadMarker">
|
||||
{hr}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[
|
||||
collectGhostReadMarker,
|
||||
context.timelineRenderingType,
|
||||
ghostReadMarkers,
|
||||
onGhostTransitionEnd,
|
||||
props.readMarkerEventId,
|
||||
props.readMarkerVisible,
|
||||
],
|
||||
);
|
||||
|
||||
const collectEventTile = useCallback((eventId: string, node: UnwrappedEventTile): void => {
|
||||
eventTiles.current[eventId] = node;
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(props.ref, () => {
|
||||
return {
|
||||
@ -685,11 +731,6 @@ export const MessagePanelNew: React.FC<IProps> = (props: IProps) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
showHiddenEvents: context?.showHiddenEvents ?? showHiddenEvents.current,
|
||||
// TODO: Implement granular (per-room) hide options
|
||||
shouldShowEvent,
|
||||
readMarkerForEvent,
|
||||
onHeightChanged,
|
||||
updateTimelineMinHeight: (): void => {
|
||||
const scrollPanelCurrent = scrollPanel.current;
|
||||
|
||||
@ -718,19 +759,206 @@ export const MessagePanelNew: React.FC<IProps> = (props: IProps) => {
|
||||
}
|
||||
return eventTiles.current[eventId];
|
||||
},
|
||||
grouperKeyMap: grouperKeyMapRef.current,
|
||||
wantsSeparator: (prevEvent: MatrixEvent | null, mxEvent: MatrixEvent) =>
|
||||
getWantsSeparator(prevEvent, mxEvent, context.timelineRenderingType, props.canBackPaginate),
|
||||
getTilesForEvent,
|
||||
layout: props.layout,
|
||||
scrollPanel,
|
||||
getNodeForEventId,
|
||||
props,
|
||||
};
|
||||
});
|
||||
|
||||
const getEventTiles = (): ReactNode[] => {
|
||||
if (!props.ref.current) return [];
|
||||
const getTilesForEvent = useCallback(
|
||||
(
|
||||
prevEvent: MatrixEvent | null,
|
||||
wrappedEvent: WrappedEvent,
|
||||
last = false,
|
||||
isGrouped = false,
|
||||
nextEvent: WrappedEvent | null = null,
|
||||
nextEventWithTile: MatrixEvent | null = null,
|
||||
): ReactNode[] => {
|
||||
const mxEv = wrappedEvent.event;
|
||||
const ret: ReactNode[] = [];
|
||||
|
||||
const isEditing = props.editState?.getEvent().getId() === mxEv.getId();
|
||||
// local echoes have a fake date, which could even be yesterday. Treat them as 'today' for the date separators.
|
||||
const ts1 = mxEv.getTs() ?? Date.now();
|
||||
|
||||
// do we need a separator since the last event?
|
||||
const wantsSeparator = getWantsSeparator(
|
||||
prevEvent,
|
||||
mxEv,
|
||||
context.timelineRenderingType,
|
||||
props.canBackPaginate,
|
||||
);
|
||||
if (!isGrouped && props.room) {
|
||||
if (wantsSeparator === SeparatorKind.Date) {
|
||||
ret.push(
|
||||
<li key={ts1}>
|
||||
<DateSeparator key={ts1} roomId={props.room.roomId} ts={ts1} />
|
||||
</li>,
|
||||
);
|
||||
} else if (wantsSeparator === SeparatorKind.LateEvent) {
|
||||
const text = _t("timeline|late_event_separator", {
|
||||
dateTime: formatDate(mxEv.getDate() ?? new Date()),
|
||||
});
|
||||
ret.push(
|
||||
<li key={ts1}>
|
||||
<TimelineSeparator key={ts1} label={text}>
|
||||
{text}
|
||||
</TimelineSeparator>
|
||||
</li>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
let lastInSection = true;
|
||||
if (nextEventWithTile) {
|
||||
const nextEv = nextEventWithTile;
|
||||
const willWantSeparator = getWantsSeparator(
|
||||
mxEv,
|
||||
nextEv,
|
||||
context.timelineRenderingType,
|
||||
props.canBackPaginate,
|
||||
);
|
||||
lastInSection =
|
||||
willWantSeparator === SeparatorKind.Date ||
|
||||
mxEv.getSender() !== nextEv.getSender() ||
|
||||
getEventDisplayInfo(cli, nextEv, showHiddenEvents.current).isInfoMessage ||
|
||||
!shouldFormContinuation(mxEv, nextEv, cli, showHiddenEvents.current, context.timelineRenderingType);
|
||||
}
|
||||
|
||||
// is this a continuation of the previous message?
|
||||
const continuation =
|
||||
wantsSeparator === SeparatorKind.None &&
|
||||
shouldFormContinuation(prevEvent, mxEv, cli, showHiddenEvents.current, context.timelineRenderingType);
|
||||
|
||||
const eventId = mxEv.getId()!;
|
||||
const highlight = eventId === props.highlightedEventId;
|
||||
|
||||
const readReceipts = readReceiptsByEvent.current.get(eventId);
|
||||
|
||||
const callEventGrouper = props.callEventGroupers.get(mxEv.getContent().call_id);
|
||||
// use txnId as key if available so that we don't remount during sending
|
||||
ret.push(
|
||||
<EventTile
|
||||
key={mxEv.getTxnId() || eventId}
|
||||
as="li"
|
||||
ref={(tile) => {
|
||||
if (tile) collectEventTile(eventId, tile);
|
||||
}}
|
||||
alwaysShowTimestamps={props.alwaysShowTimestamps}
|
||||
mxEvent={mxEv}
|
||||
continuation={continuation}
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
replacingEventId={mxEv.replacingEventId()}
|
||||
editState={isEditing ? props.editState : undefined}
|
||||
resizeObserver={getResizeObserver()}
|
||||
readReceipts={readReceipts}
|
||||
readReceiptMap={readReceiptMap.current}
|
||||
showUrlPreview={props.showUrlPreview}
|
||||
checkUnmounting={isUnmounting}
|
||||
eventSendStatus={mxEv.getAssociatedStatus() ?? undefined}
|
||||
isTwelveHour={props.isTwelveHour}
|
||||
permalinkCreator={props.permalinkCreator}
|
||||
last={last}
|
||||
lastInSection={lastInSection}
|
||||
lastSuccessful={wrappedEvent.lastSuccessfulWeSent}
|
||||
isSelectedEvent={highlight}
|
||||
getRelationsForEvent={props.getRelationsForEvent}
|
||||
showReactions={props.showReactions}
|
||||
layout={props.layout}
|
||||
showReadReceipts={props.showReadReceipts}
|
||||
callEventGrouper={callEventGrouper}
|
||||
hideSender={hideSender}
|
||||
/>,
|
||||
);
|
||||
|
||||
return ret;
|
||||
},
|
||||
[
|
||||
collectEventTile,
|
||||
context.timelineRenderingType,
|
||||
getResizeObserver,
|
||||
hideSender,
|
||||
props.alwaysShowTimestamps,
|
||||
props.callEventGroupers,
|
||||
props.canBackPaginate,
|
||||
props.editState,
|
||||
props.getRelationsForEvent,
|
||||
props.highlightedEventId,
|
||||
props.isTwelveHour,
|
||||
props.layout,
|
||||
props.permalinkCreator,
|
||||
props.room,
|
||||
props.showReactions,
|
||||
props.showReadReceipts,
|
||||
props.showUrlPreview,
|
||||
],
|
||||
);
|
||||
|
||||
// Get an object that maps from event ID to a list of read receipts that
|
||||
// should be shown next to that event. If a hidden event has read receipts,
|
||||
// they are folded into the receipts of the last shown event.
|
||||
const getReadReceiptsByShownEvent = useCallback(
|
||||
(events: WrappedEvent[]): Map<string, IReadReceiptProps[]> => {
|
||||
const receiptsByEvent: Map<string, IReadReceiptProps[]> = new Map();
|
||||
const receiptsByUserId: Map<string, IReadReceiptForUser> = new Map();
|
||||
|
||||
let lastShownEventId: string | undefined;
|
||||
for (const event of props.events) {
|
||||
if (shouldShowEvent(event)) {
|
||||
lastShownEventId = event.getId();
|
||||
}
|
||||
if (!lastShownEventId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingReceipts = receiptsByEvent.get(lastShownEventId) || [];
|
||||
const newReceipts = getReadReceiptsForEvent(props.room, event, context.threadId);
|
||||
if (!newReceipts) continue;
|
||||
receiptsByEvent.set(lastShownEventId, existingReceipts.concat(newReceipts));
|
||||
|
||||
// Record these receipts along with their last shown event ID for
|
||||
// each associated user ID.
|
||||
for (const receipt of newReceipts) {
|
||||
receiptsByUserId.set(receipt.userId, {
|
||||
lastShownEventId,
|
||||
receipt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// It's possible in some cases (for example, when a read receipt
|
||||
// advances before we have paginated in the new event that it's marking
|
||||
// received) that we can temporarily not have a matching event for
|
||||
// someone which had one in the last. By looking through our previous
|
||||
// mapping of receipts by user ID, we can cover recover any receipts
|
||||
// that would have been lost by using the same event ID from last time.
|
||||
for (const userId of readReceiptsByUserId.current.keys()) {
|
||||
if (receiptsByUserId.get(userId)) {
|
||||
continue;
|
||||
}
|
||||
const { lastShownEventId, receipt } = readReceiptsByUserId.current.get(userId)!;
|
||||
const existingReceipts = receiptsByEvent.get(lastShownEventId) || [];
|
||||
receiptsByEvent.set(lastShownEventId, existingReceipts.concat(receipt));
|
||||
receiptsByUserId.set(userId, { lastShownEventId, receipt });
|
||||
}
|
||||
readReceiptsByUserId.current = receiptsByUserId;
|
||||
|
||||
// After grouping receipts by shown events, do another pass to sort each
|
||||
// receipt list.
|
||||
for (const receipts of receiptsByEvent.values()) {
|
||||
receipts.sort((r1, r2) => {
|
||||
return r2.ts - r1.ts;
|
||||
});
|
||||
}
|
||||
|
||||
return receiptsByEvent;
|
||||
},
|
||||
[context.threadId, props.events, props.room, shouldShowEvent],
|
||||
);
|
||||
|
||||
const getEventTiles = useCallback((): ReactNode[] => {
|
||||
if (!grouperPanelRef.current) return [];
|
||||
// first figure out which is the last event in the list which we're
|
||||
// actually going to show; this allows us to behave slightly
|
||||
// differently for the last event in the list. (eg show timestamp)
|
||||
@ -810,9 +1038,9 @@ export const MessagePanelNew: React.FC<IProps> = (props: IProps) => {
|
||||
}
|
||||
|
||||
for (const Grouper of groupers) {
|
||||
if (Grouper.canStartGroup(props.ref.current, wrappedEvent) && !props.disableGrouping) {
|
||||
if (Grouper.canStartGroup(grouperPanelRef.current, wrappedEvent) && !props.disableGrouping) {
|
||||
grouper = new Grouper(
|
||||
props.ref.current,
|
||||
grouperPanelRef.current,
|
||||
wrappedEvent,
|
||||
prevEvent,
|
||||
lastShownEvent,
|
||||
@ -844,185 +1072,54 @@ export const MessagePanelNew: React.FC<IProps> = (props: IProps) => {
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
}, [
|
||||
getReadReceiptsByShownEvent,
|
||||
getTilesForEvent,
|
||||
props.disableGrouping,
|
||||
props.events,
|
||||
props.showReadReceipts,
|
||||
readMarkerForEvent,
|
||||
shouldShowEvent,
|
||||
]);
|
||||
|
||||
const getTilesForEvent = (
|
||||
prevEvent: MatrixEvent | null,
|
||||
wrappedEvent: WrappedEvent,
|
||||
last = false,
|
||||
isGrouped = false,
|
||||
nextEvent: WrappedEvent | null = null,
|
||||
nextEventWithTile: MatrixEvent | null = null,
|
||||
): ReactNode[] => {
|
||||
const mxEv = wrappedEvent.event;
|
||||
const ret: ReactNode[] = [];
|
||||
const grouperPanelRef = useRef<GrouperPanel | null>(null);
|
||||
useImperativeHandle(grouperPanelRef, () => {
|
||||
return {
|
||||
grouperKeyMap: grouperKeyMapRef.current,
|
||||
wantsSeparator: (prevEvent: MatrixEvent | null, mxEvent: MatrixEvent) =>
|
||||
getWantsSeparator(prevEvent, mxEvent, context.timelineRenderingType, props.canBackPaginate),
|
||||
getTilesForEvent,
|
||||
layout: props.layout,
|
||||
canBackPaginate: props.canBackPaginate,
|
||||
highlightedEventId: props.highlightedEventId,
|
||||
showHiddenEvents: context?.showHiddenEvents ?? showHiddenEvents.current,
|
||||
// TODO: Implement granular (per-room) hide options
|
||||
shouldShowEvent,
|
||||
readMarkerForEvent,
|
||||
onHeightChanged,
|
||||
};
|
||||
}, [
|
||||
context.showHiddenEvents,
|
||||
context.timelineRenderingType,
|
||||
getTilesForEvent,
|
||||
onHeightChanged,
|
||||
props.canBackPaginate,
|
||||
props.highlightedEventId,
|
||||
props.layout,
|
||||
readMarkerForEvent,
|
||||
shouldShowEvent,
|
||||
]);
|
||||
|
||||
const isEditing = props.editState?.getEvent().getId() === mxEv.getId();
|
||||
// local echoes have a fake date, which could even be yesterday. Treat them as 'today' for the date separators.
|
||||
const ts1 = mxEv.getTs() ?? Date.now();
|
||||
|
||||
// do we need a separator since the last event?
|
||||
const wantsSeparator = getWantsSeparator(prevEvent, mxEv, context.timelineRenderingType, props.canBackPaginate);
|
||||
if (!isGrouped && props.room) {
|
||||
if (wantsSeparator === SeparatorKind.Date) {
|
||||
ret.push(
|
||||
<li key={ts1}>
|
||||
<DateSeparator key={ts1} roomId={props.room.roomId} ts={ts1} />
|
||||
</li>,
|
||||
);
|
||||
} else if (wantsSeparator === SeparatorKind.LateEvent) {
|
||||
const text = _t("timeline|late_event_separator", {
|
||||
dateTime: formatDate(mxEv.getDate() ?? new Date()),
|
||||
});
|
||||
ret.push(
|
||||
<li key={ts1}>
|
||||
<TimelineSeparator key={ts1} label={text}>
|
||||
{text}
|
||||
</TimelineSeparator>
|
||||
</li>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
let lastInSection = true;
|
||||
if (nextEventWithTile) {
|
||||
const nextEv = nextEventWithTile;
|
||||
const willWantSeparator = getWantsSeparator(
|
||||
mxEv,
|
||||
nextEv,
|
||||
context.timelineRenderingType,
|
||||
props.canBackPaginate,
|
||||
);
|
||||
lastInSection =
|
||||
willWantSeparator === SeparatorKind.Date ||
|
||||
mxEv.getSender() !== nextEv.getSender() ||
|
||||
getEventDisplayInfo(cli, nextEv, showHiddenEvents.current).isInfoMessage ||
|
||||
!shouldFormContinuation(mxEv, nextEv, cli, showHiddenEvents.current, context.timelineRenderingType);
|
||||
}
|
||||
|
||||
// is this a continuation of the previous message?
|
||||
const continuation =
|
||||
wantsSeparator === SeparatorKind.None &&
|
||||
shouldFormContinuation(prevEvent, mxEv, cli, showHiddenEvents.current, context.timelineRenderingType);
|
||||
|
||||
const eventId = mxEv.getId()!;
|
||||
const highlight = eventId === props.highlightedEventId;
|
||||
|
||||
const readReceipts = readReceiptsByEvent.current.get(eventId);
|
||||
|
||||
const callEventGrouper = props.callEventGroupers.get(mxEv.getContent().call_id);
|
||||
// use txnId as key if available so that we don't remount during sending
|
||||
ret.push(
|
||||
<EventTile
|
||||
key={mxEv.getTxnId() || eventId}
|
||||
as="li"
|
||||
ref={(tile) => {
|
||||
if (tile) collectEventTile(eventId, tile);
|
||||
}}
|
||||
alwaysShowTimestamps={props.alwaysShowTimestamps}
|
||||
mxEvent={mxEv}
|
||||
continuation={continuation}
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
replacingEventId={mxEv.replacingEventId()}
|
||||
editState={isEditing ? props.editState : undefined}
|
||||
resizeObserver={resizeObserver.current}
|
||||
readReceipts={readReceipts}
|
||||
readReceiptMap={readReceiptMap.current}
|
||||
showUrlPreview={props.showUrlPreview}
|
||||
checkUnmounting={isUnmounting}
|
||||
eventSendStatus={mxEv.getAssociatedStatus() ?? undefined}
|
||||
isTwelveHour={props.isTwelveHour}
|
||||
permalinkCreator={props.permalinkCreator}
|
||||
last={last}
|
||||
lastInSection={lastInSection}
|
||||
lastSuccessful={wrappedEvent.lastSuccessfulWeSent}
|
||||
isSelectedEvent={highlight}
|
||||
getRelationsForEvent={props.getRelationsForEvent}
|
||||
showReactions={props.showReactions}
|
||||
layout={props.layout}
|
||||
showReadReceipts={props.showReadReceipts}
|
||||
callEventGrouper={callEventGrouper}
|
||||
hideSender={hideSender}
|
||||
/>,
|
||||
);
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Get an object that maps from event ID to a list of read receipts that
|
||||
// should be shown next to that event. If a hidden event has read receipts,
|
||||
// they are folded into the receipts of the last shown event.
|
||||
const getReadReceiptsByShownEvent = (events: WrappedEvent[]): Map<string, IReadReceiptProps[]> => {
|
||||
const receiptsByEvent: Map<string, IReadReceiptProps[]> = new Map();
|
||||
const receiptsByUserId: Map<string, IReadReceiptForUser> = new Map();
|
||||
|
||||
let lastShownEventId: string | undefined;
|
||||
for (const event of props.events) {
|
||||
if (shouldShowEvent(event)) {
|
||||
lastShownEventId = event.getId();
|
||||
}
|
||||
if (!lastShownEventId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingReceipts = receiptsByEvent.get(lastShownEventId) || [];
|
||||
const newReceipts = getReadReceiptsForEvent(props.room, event, context.threadId);
|
||||
if (!newReceipts) continue;
|
||||
receiptsByEvent.set(lastShownEventId, existingReceipts.concat(newReceipts));
|
||||
|
||||
// Record these receipts along with their last shown event ID for
|
||||
// each associated user ID.
|
||||
for (const receipt of newReceipts) {
|
||||
receiptsByUserId.set(receipt.userId, {
|
||||
lastShownEventId,
|
||||
receipt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// It's possible in some cases (for example, when a read receipt
|
||||
// advances before we have paginated in the new event that it's marking
|
||||
// received) that we can temporarily not have a matching event for
|
||||
// someone which had one in the last. By looking through our previous
|
||||
// mapping of receipts by user ID, we can cover recover any receipts
|
||||
// that would have been lost by using the same event ID from last time.
|
||||
for (const userId of readReceiptsByUserId.current.keys()) {
|
||||
if (receiptsByUserId.get(userId)) {
|
||||
continue;
|
||||
}
|
||||
const { lastShownEventId, receipt } = readReceiptsByUserId.current.get(userId)!;
|
||||
const existingReceipts = receiptsByEvent.get(lastShownEventId) || [];
|
||||
receiptsByEvent.set(lastShownEventId, existingReceipts.concat(receipt));
|
||||
receiptsByUserId.set(userId, { lastShownEventId, receipt });
|
||||
}
|
||||
readReceiptsByUserId.current = receiptsByUserId;
|
||||
|
||||
// After grouping receipts by shown events, do another pass to sort each
|
||||
// receipt list.
|
||||
for (const receipts of receiptsByEvent.values()) {
|
||||
receipts.sort((r1, r2) => {
|
||||
return r2.ts - r1.ts;
|
||||
});
|
||||
}
|
||||
|
||||
return receiptsByEvent;
|
||||
};
|
||||
|
||||
const collectEventTile = (eventId: string, node: UnwrappedEventTile): void => {
|
||||
eventTiles.current[eventId] = node;
|
||||
};
|
||||
|
||||
const onTypingShown = (): void => {
|
||||
const onTypingShown = useCallback((): void => {
|
||||
const scrollPanelCurrent = scrollPanel.current;
|
||||
// this will make the timeline grow, so checkScroll
|
||||
scrollPanelCurrent?.checkScroll();
|
||||
if (scrollPanelCurrent && scrollPanelCurrent.getScrollState().stuckAtBottom) {
|
||||
scrollPanelCurrent.preventShrinking();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onTypingHidden = (): void => {
|
||||
const onTypingHidden = useCallback((): void => {
|
||||
const scrollPanelCurrent = scrollPanel.current;
|
||||
if (scrollPanelCurrent) {
|
||||
// as hiding the typing notifications doesn't
|
||||
@ -1033,7 +1130,7 @@ export const MessagePanelNew: React.FC<IProps> = (props: IProps) => {
|
||||
// reveal added padding to balance the notifs disappearing.
|
||||
scrollPanelCurrent.checkScroll();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
let topSpinner;
|
||||
let bottomSpinner;
|
||||
@ -1084,7 +1181,11 @@ export const MessagePanelNew: React.FC<IProps> = (props: IProps) => {
|
||||
fixedChildren={ircResizer}
|
||||
>
|
||||
{topSpinner}
|
||||
{getEventTiles()}
|
||||
|
||||
{
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
getEventTiles()
|
||||
}
|
||||
{whoIsTypingDom}
|
||||
{bottomSpinner}
|
||||
</ScrollPanel>
|
||||
|
||||
@ -10,8 +10,7 @@ import { type ReactNode } from "react";
|
||||
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { type WrappedEvent } from "../MessagePanel";
|
||||
import type MessagePanel from "../MessagePanel";
|
||||
import type { MessagePanelMethods } from "../MessagePanel-functional";
|
||||
import type { GrouperPanel } from "../MessagePanel-functional";
|
||||
|
||||
/* Grouper classes determine when events can be grouped together in a summary.
|
||||
* Groupers should have the following methods:
|
||||
@ -25,7 +24,7 @@ import type { MessagePanelMethods } from "../MessagePanel-functional";
|
||||
* when determining things such as whether a date separator is necessary
|
||||
*/
|
||||
export abstract class BaseGrouper {
|
||||
public static canStartGroup = (_panel: MessagePanel | MessagePanelMethods, _ev: WrappedEvent): boolean => true;
|
||||
public static canStartGroup = (_panel: GrouperPanel, _ev: WrappedEvent): boolean => true;
|
||||
|
||||
public events: WrappedEvent[] = [];
|
||||
// events that we include in the group but then eject out and place above the group.
|
||||
@ -33,7 +32,7 @@ export abstract class BaseGrouper {
|
||||
public readMarker: ReactNode;
|
||||
|
||||
public constructor(
|
||||
public readonly panel: MessagePanel | MessagePanelMethods,
|
||||
public readonly panel: GrouperPanel,
|
||||
public readonly firstEventAndShouldShow: WrappedEvent,
|
||||
public readonly prevEvent: MatrixEvent | null,
|
||||
public readonly lastShownEvent: MatrixEvent | undefined,
|
||||
|
||||
@ -12,24 +12,20 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { BaseGrouper } from "./BaseGrouper";
|
||||
import { type WrappedEvent } from "../MessagePanel";
|
||||
import type MessagePanel from "../MessagePanel";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import DateSeparator from "../../views/messages/DateSeparator";
|
||||
import NewRoomIntro from "../../views/rooms/NewRoomIntro";
|
||||
import GenericEventListSummary from "../../views/elements/GenericEventListSummary";
|
||||
import { SeparatorKind } from "../../views/messages/TimelineSeparator";
|
||||
import type { MessagePanelMethods } from "../MessagePanel-functional";
|
||||
import type { GrouperPanel } from "../MessagePanel-functional";
|
||||
|
||||
// Wrap initial room creation events into a GenericEventListSummary
|
||||
// Grouping only events sent by the same user that sent the `m.room.create` and only until
|
||||
// the first non-state event, beacon_info event or membership event which is not regarding the sender of the `m.room.create` event
|
||||
|
||||
export class CreationGrouper extends BaseGrouper {
|
||||
public static canStartGroup = function (
|
||||
_panel: MessagePanel | MessagePanelMethods,
|
||||
{ event }: WrappedEvent,
|
||||
): boolean {
|
||||
public static canStartGroup = function (_panel: GrouperPanel, { event }: WrappedEvent): boolean {
|
||||
return event.getType() === EventType.RoomCreate;
|
||||
};
|
||||
|
||||
@ -142,7 +138,7 @@ export class CreationGrouper extends BaseGrouper {
|
||||
onToggle={panel.onHeightChanged} // Update scroll state
|
||||
summaryMembers={ev.sender ? [ev.sender] : undefined}
|
||||
summaryText={summaryText}
|
||||
layout={(this.panel as MessagePanelMethods).layout ?? (this.panel as MessagePanel).props.layout}
|
||||
layout={this.panel.layout}
|
||||
>
|
||||
{eventTiles}
|
||||
</GenericEventListSummary>,
|
||||
|
||||
@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React, { type ReactNode } from "react";
|
||||
import { EventType, type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type MessagePanel from "../MessagePanel";
|
||||
import type { WrappedEvent } from "../MessagePanel";
|
||||
import { BaseGrouper } from "./BaseGrouper";
|
||||
import { hasText } from "../../../TextForEvent";
|
||||
@ -18,7 +17,7 @@ import DateSeparator from "../../views/messages/DateSeparator";
|
||||
import HistoryTile from "../../views/rooms/HistoryTile";
|
||||
import EventListSummary from "../../views/elements/EventListSummary";
|
||||
import { SeparatorKind } from "../../views/messages/TimelineSeparator";
|
||||
import type { MessagePanelMethods } from "../MessagePanel-functional";
|
||||
import type { GrouperPanel } from "../MessagePanel-functional";
|
||||
|
||||
const groupedStateEvents = [
|
||||
EventType.RoomMember,
|
||||
@ -29,10 +28,7 @@ const groupedStateEvents = [
|
||||
|
||||
// Wrap consecutive grouped events in a ListSummary
|
||||
export class MainGrouper extends BaseGrouper {
|
||||
public static canStartGroup = function (
|
||||
panel: MessagePanel | MessagePanelMethods,
|
||||
{ event: ev, shouldShow }: WrappedEvent,
|
||||
): boolean {
|
||||
public static canStartGroup = function (panel: GrouperPanel, { event: ev, shouldShow }: WrappedEvent): boolean {
|
||||
if (!shouldShow) return false;
|
||||
|
||||
if (ev.isState() && groupedStateEvents.includes(ev.getType() as EventType)) {
|
||||
@ -51,7 +47,7 @@ export class MainGrouper extends BaseGrouper {
|
||||
};
|
||||
|
||||
public constructor(
|
||||
public readonly panel: MessagePanel,
|
||||
public readonly panel: GrouperPanel,
|
||||
public readonly firstEventAndShouldShow: WrappedEvent,
|
||||
public readonly prevEvent: MatrixEvent | null,
|
||||
public readonly lastShownEvent: MatrixEvent | undefined,
|
||||
@ -145,7 +141,7 @@ export class MainGrouper extends BaseGrouper {
|
||||
let highlightInSummary = false;
|
||||
let eventTiles: ReactNode[] | null = this.events
|
||||
.map((e, i) => {
|
||||
if (e.event.getId() === panel.props.highlightedEventId) {
|
||||
if (e.event.getId() === panel.highlightedEventId) {
|
||||
highlightInSummary = true;
|
||||
}
|
||||
return panel.getTilesForEvent(
|
||||
@ -165,7 +161,7 @@ export class MainGrouper extends BaseGrouper {
|
||||
|
||||
// If a membership event is the start of visible history, tell the user
|
||||
// why they can't see earlier messages
|
||||
if (!this.panel.props.canBackPaginate && !this.prevEvent) {
|
||||
if (!this.panel.canBackPaginate && !this.prevEvent) {
|
||||
ret.push(<HistoryTile key="historytile" />);
|
||||
}
|
||||
|
||||
@ -176,7 +172,7 @@ export class MainGrouper extends BaseGrouper {
|
||||
events={this.events.map((e) => e.event)}
|
||||
onToggle={panel.onHeightChanged} // Update scroll state
|
||||
startExpanded={highlightInSummary}
|
||||
layout={this.panel.props.layout}
|
||||
layout={this.panel.layout}
|
||||
>
|
||||
{eventTiles}
|
||||
</EventListSummary>,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user