Move groupers into their own directory (#11738)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
479ed55ad0
commit
a01a67eb8e
4 changed files with 422 additions and 360 deletions
|
@ -17,15 +17,7 @@ limitations under the License.
|
||||||
import React, { createRef, ReactNode, TransitionEvent } from "react";
|
import React, { createRef, ReactNode, TransitionEvent } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {
|
import { Room, MatrixClient, RoomStateEvent, EventStatus, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
|
||||||
Room,
|
|
||||||
MatrixClient,
|
|
||||||
RoomStateEvent,
|
|
||||||
EventStatus,
|
|
||||||
MatrixEvent,
|
|
||||||
EventType,
|
|
||||||
M_BEACON_INFO,
|
|
||||||
} from "matrix-js-sdk/src/matrix";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { isSupportedReceiptType } from "matrix-js-sdk/src/utils";
|
import { isSupportedReceiptType } from "matrix-js-sdk/src/utils";
|
||||||
import { Optional } from "matrix-events-sdk";
|
import { Optional } from "matrix-events-sdk";
|
||||||
|
@ -36,24 +28,17 @@ import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||||
import { Layout } from "../../settings/enums/Layout";
|
import { Layout } from "../../settings/enums/Layout";
|
||||||
import { _t } from "../../languageHandler";
|
|
||||||
import EventTile, {
|
import EventTile, {
|
||||||
GetRelationsForEvent,
|
GetRelationsForEvent,
|
||||||
IReadReceiptProps,
|
IReadReceiptProps,
|
||||||
isEligibleForSpecialReceipt,
|
isEligibleForSpecialReceipt,
|
||||||
UnwrappedEventTile,
|
UnwrappedEventTile,
|
||||||
} from "../views/rooms/EventTile";
|
} from "../views/rooms/EventTile";
|
||||||
import { hasText } from "../../TextForEvent";
|
|
||||||
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
|
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
|
||||||
import DMRoomMap from "../../utils/DMRoomMap";
|
|
||||||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
|
||||||
import HistoryTile from "../views/rooms/HistoryTile";
|
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import LegacyCallEventGrouper from "./LegacyCallEventGrouper";
|
import LegacyCallEventGrouper from "./LegacyCallEventGrouper";
|
||||||
import WhoIsTypingTile from "../views/rooms/WhoIsTypingTile";
|
import WhoIsTypingTile from "../views/rooms/WhoIsTypingTile";
|
||||||
import ScrollPanel, { IScrollState } from "./ScrollPanel";
|
import ScrollPanel, { IScrollState } from "./ScrollPanel";
|
||||||
import GenericEventListSummary from "../views/elements/GenericEventListSummary";
|
|
||||||
import EventListSummary from "../views/elements/EventListSummary";
|
|
||||||
import DateSeparator from "../views/messages/DateSeparator";
|
import DateSeparator from "../views/messages/DateSeparator";
|
||||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
|
@ -66,16 +51,12 @@ import { IReadReceiptInfo } from "../views/rooms/ReadReceiptMarker";
|
||||||
import { haveRendererForEvent } from "../../events/EventTileFactory";
|
import { haveRendererForEvent } from "../../events/EventTileFactory";
|
||||||
import { editorRoomKey } from "../../Editing";
|
import { editorRoomKey } from "../../Editing";
|
||||||
import { hasThreadSummary } from "../../utils/EventUtils";
|
import { hasThreadSummary } from "../../utils/EventUtils";
|
||||||
import { VoiceBroadcastInfoEventType } from "../../voice-broadcast";
|
import { BaseGrouper } from "./grouper/BaseGrouper";
|
||||||
|
import { MainGrouper } from "./grouper/MainGrouper";
|
||||||
|
import { CreationGrouper } from "./grouper/CreationGrouper";
|
||||||
|
|
||||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||||
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
||||||
const groupedStateEvents = [
|
|
||||||
EventType.RoomMember,
|
|
||||||
EventType.RoomThirdPartyInvite,
|
|
||||||
EventType.RoomServerAcl,
|
|
||||||
EventType.RoomPinnedEvents,
|
|
||||||
];
|
|
||||||
|
|
||||||
// check if there is a previous event and it has the same sender as this event
|
// check if there is a previous event and it has the same sender as this event
|
||||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||||
|
@ -1080,348 +1061,12 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
* Simplifies threading of event context like whether it's the last successful event we sent which cannot be determined
|
* Simplifies threading of event context like whether it's the last successful event we sent which cannot be determined
|
||||||
* by a consumer from the event alone, so has to be done by the event list processing code earlier.
|
* by a consumer from the event alone, so has to be done by the event list processing code earlier.
|
||||||
*/
|
*/
|
||||||
interface WrappedEvent {
|
export interface WrappedEvent {
|
||||||
event: MatrixEvent;
|
event: MatrixEvent;
|
||||||
shouldShow?: boolean;
|
shouldShow?: boolean;
|
||||||
lastSuccessfulWeSent?: boolean;
|
lastSuccessfulWeSent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class BaseGrouper {
|
|
||||||
public static canStartGroup = (_panel: MessagePanel, _ev: WrappedEvent): boolean => true;
|
|
||||||
|
|
||||||
public events: WrappedEvent[] = [];
|
|
||||||
// events that we include in the group but then eject out and place above the group.
|
|
||||||
public ejectedEvents: WrappedEvent[] = [];
|
|
||||||
public readMarker: ReactNode;
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
public readonly panel: MessagePanel,
|
|
||||||
public readonly firstEventAndShouldShow: WrappedEvent,
|
|
||||||
public readonly prevEvent: MatrixEvent | null,
|
|
||||||
public readonly lastShownEvent: MatrixEvent | undefined,
|
|
||||||
public readonly nextEvent: WrappedEvent | null,
|
|
||||||
public readonly nextEventTile?: MatrixEvent | null,
|
|
||||||
) {
|
|
||||||
this.readMarker = panel.readMarkerForEvent(
|
|
||||||
firstEventAndShouldShow.event.getId()!,
|
|
||||||
firstEventAndShouldShow.event === lastShownEvent,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract shouldGroup(ev: WrappedEvent): boolean;
|
|
||||||
public abstract add(ev: WrappedEvent): void;
|
|
||||||
public abstract getTiles(): ReactNode[];
|
|
||||||
public abstract getNewPrevEvent(): MatrixEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Grouper classes determine when events can be grouped together in a summary.
|
|
||||||
* Groupers should have the following methods:
|
|
||||||
* - canStartGroup (static): determines if a new group should be started with the
|
|
||||||
* given event
|
|
||||||
* - shouldGroup: determines if the given event should be added to an existing group
|
|
||||||
* - add: adds an event to an existing group (should only be called if shouldGroup
|
|
||||||
* return true)
|
|
||||||
* - getTiles: returns the tiles that represent the group
|
|
||||||
* - getNewPrevEvent: returns the event that should be used as the new prevEvent
|
|
||||||
* when determining things such as whether a date separator is necessary
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 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
|
|
||||||
class CreationGrouper extends BaseGrouper {
|
|
||||||
public static canStartGroup = function (_panel: MessagePanel, { event }: WrappedEvent): boolean {
|
|
||||||
return event.getType() === EventType.RoomCreate;
|
|
||||||
};
|
|
||||||
|
|
||||||
public shouldGroup({ event, shouldShow }: WrappedEvent): boolean {
|
|
||||||
const panel = this.panel;
|
|
||||||
const createEvent = this.firstEventAndShouldShow.event;
|
|
||||||
if (!shouldShow) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (panel.wantsDateSeparator(this.firstEventAndShouldShow.event, event.getDate())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const eventType = event.getType();
|
|
||||||
if (
|
|
||||||
eventType === EventType.RoomMember &&
|
|
||||||
(event.getStateKey() !== createEvent.getSender() || event.getContent()["membership"] !== "join")
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// beacons are not part of room creation configuration
|
|
||||||
// should be shown in timeline
|
|
||||||
if (M_BEACON_INFO.matches(eventType)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (VoiceBroadcastInfoEventType === eventType) {
|
|
||||||
// always show voice broadcast info events in timeline
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.isState() && event.getSender() === createEvent.getSender()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public add(wrappedEvent: WrappedEvent): void {
|
|
||||||
const { event: ev, shouldShow } = wrappedEvent;
|
|
||||||
const panel = this.panel;
|
|
||||||
this.readMarker = this.readMarker || panel.readMarkerForEvent(ev.getId()!, ev === this.lastShownEvent);
|
|
||||||
if (!shouldShow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ev.getType() === EventType.RoomEncryption) {
|
|
||||||
this.ejectedEvents.push(wrappedEvent);
|
|
||||||
} else {
|
|
||||||
this.events.push(wrappedEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getTiles(): ReactNode[] {
|
|
||||||
// If we don't have any events to group, don't even try to group them. The logic
|
|
||||||
// below assumes that we have a group of events to deal with, but we might not if
|
|
||||||
// the events we were supposed to group were redacted.
|
|
||||||
if (!this.events || !this.events.length) return [];
|
|
||||||
|
|
||||||
const panel = this.panel;
|
|
||||||
const ret: ReactNode[] = [];
|
|
||||||
const isGrouped = true;
|
|
||||||
const createEvent = this.firstEventAndShouldShow;
|
|
||||||
const lastShownEvent = this.lastShownEvent;
|
|
||||||
|
|
||||||
if (panel.wantsDateSeparator(this.prevEvent, createEvent.event.getDate())) {
|
|
||||||
const ts = createEvent.event.getTs();
|
|
||||||
ret.push(
|
|
||||||
<li key={ts + "~"}>
|
|
||||||
<DateSeparator roomId={createEvent.event.getRoomId()!} ts={ts} />
|
|
||||||
</li>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this m.room.create event should be shown (room upgrade) then show it before the summary
|
|
||||||
if (createEvent.shouldShow) {
|
|
||||||
// pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered
|
|
||||||
ret.push(...panel.getTilesForEvent(createEvent.event, createEvent));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const ejected of this.ejectedEvents) {
|
|
||||||
ret.push(
|
|
||||||
...panel.getTilesForEvent(createEvent.event, ejected, createEvent.event === lastShownEvent, isGrouped),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventTiles = this.events
|
|
||||||
.map((e) => {
|
|
||||||
// In order to prevent DateSeparators from appearing in the expanded form
|
|
||||||
// of GenericEventListSummary, render each member event as if the previous
|
|
||||||
// one was itself. This way, the timestamp of the previous event === the
|
|
||||||
// timestamp of the current event, and no DateSeparator is inserted.
|
|
||||||
return panel.getTilesForEvent(e.event, e, e.event === lastShownEvent, isGrouped);
|
|
||||||
})
|
|
||||||
.reduce((a, b) => a.concat(b), []);
|
|
||||||
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
|
|
||||||
const ev = this.events[this.events.length - 1].event;
|
|
||||||
|
|
||||||
let summaryText: string;
|
|
||||||
const roomId = ev.getRoomId();
|
|
||||||
const creator = ev.sender?.name ?? ev.getSender();
|
|
||||||
if (roomId && DMRoomMap.shared().getUserIdForRoomId(roomId)) {
|
|
||||||
summaryText = _t("timeline|creation_summary_dm", { creator });
|
|
||||||
} else {
|
|
||||||
summaryText = _t("timeline|creation_summary_room", { creator });
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.push(<NewRoomIntro key="newroomintro" />);
|
|
||||||
|
|
||||||
ret.push(
|
|
||||||
<GenericEventListSummary
|
|
||||||
key="roomcreationsummary"
|
|
||||||
events={this.events.map((e) => e.event)}
|
|
||||||
onToggle={panel.onHeightChanged} // Update scroll state
|
|
||||||
summaryMembers={ev.sender ? [ev.sender] : undefined}
|
|
||||||
summaryText={summaryText}
|
|
||||||
layout={this.panel.props.layout}
|
|
||||||
>
|
|
||||||
{eventTiles}
|
|
||||||
</GenericEventListSummary>,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.readMarker) {
|
|
||||||
ret.push(this.readMarker);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getNewPrevEvent(): MatrixEvent {
|
|
||||||
return this.firstEventAndShouldShow.event;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap consecutive grouped events in a ListSummary
|
|
||||||
class MainGrouper extends BaseGrouper {
|
|
||||||
public static canStartGroup = function (panel: MessagePanel, { event: ev, shouldShow }: WrappedEvent): boolean {
|
|
||||||
if (!shouldShow) return false;
|
|
||||||
|
|
||||||
if (ev.isState() && groupedStateEvents.includes(ev.getType() as EventType)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ev.isRedacted()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (panel.showHiddenEvents && !panel.shouldShowEvent(ev, true)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
public readonly panel: MessagePanel,
|
|
||||||
public readonly firstEventAndShouldShow: WrappedEvent,
|
|
||||||
public readonly prevEvent: MatrixEvent | null,
|
|
||||||
public readonly lastShownEvent: MatrixEvent | undefined,
|
|
||||||
nextEvent: WrappedEvent | null,
|
|
||||||
nextEventTile: MatrixEvent | null,
|
|
||||||
) {
|
|
||||||
super(panel, firstEventAndShouldShow, prevEvent, lastShownEvent, nextEvent, nextEventTile);
|
|
||||||
this.events = [firstEventAndShouldShow];
|
|
||||||
}
|
|
||||||
|
|
||||||
public shouldGroup({ event: ev, shouldShow }: WrappedEvent): boolean {
|
|
||||||
if (!shouldShow) {
|
|
||||||
// absorb hidden events so that they do not break up streams of messages & redaction events being grouped
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (this.panel.wantsDateSeparator(this.events[0].event, ev.getDate())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (ev.isState() && groupedStateEvents.includes(ev.getType() as EventType)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (ev.isRedacted()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (this.panel.showHiddenEvents && !this.panel.shouldShowEvent(ev, true)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public add(wrappedEvent: WrappedEvent): void {
|
|
||||||
const { event: ev, shouldShow } = wrappedEvent;
|
|
||||||
if (ev.getType() === EventType.RoomMember) {
|
|
||||||
// We can ignore any events that don't actually have a message to display
|
|
||||||
if (!hasText(ev, MatrixClientPeg.safeGet(), this.panel.showHiddenEvents)) return;
|
|
||||||
}
|
|
||||||
this.readMarker = this.readMarker || this.panel.readMarkerForEvent(ev.getId()!, ev === this.lastShownEvent);
|
|
||||||
if (!this.panel.showHiddenEvents && !shouldShow) {
|
|
||||||
// absorb hidden events to not split the summary
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.events.push(wrappedEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateKey(): string {
|
|
||||||
return "eventlistsummary-" + this.events[0].event.getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getTiles(): ReactNode[] {
|
|
||||||
// If we don't have any events to group, don't even try to group them. The logic
|
|
||||||
// below assumes that we have a group of events to deal with, but we might not if
|
|
||||||
// the events we were supposed to group were redacted.
|
|
||||||
if (!this.events?.length) return [];
|
|
||||||
|
|
||||||
const isGrouped = true;
|
|
||||||
const panel = this.panel;
|
|
||||||
const lastShownEvent = this.lastShownEvent;
|
|
||||||
const ret: ReactNode[] = [];
|
|
||||||
|
|
||||||
if (panel.wantsDateSeparator(this.prevEvent, this.events[0].event.getDate())) {
|
|
||||||
const ts = this.events[0].event.getTs();
|
|
||||||
ret.push(
|
|
||||||
<li key={ts + "~"}>
|
|
||||||
<DateSeparator roomId={this.events[0].event.getRoomId()!} ts={ts} />
|
|
||||||
</li>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that the key of the EventListSummary does not change with new events in either direction.
|
|
||||||
// This will prevent it from being re-created unnecessarily, and instead will allow new props to be provided.
|
|
||||||
// In turn, the shouldComponentUpdate method on ELS can be used to prevent unnecessary renderings.
|
|
||||||
const keyEvent = this.events.find((e) => this.panel.grouperKeyMap.get(e.event));
|
|
||||||
const key =
|
|
||||||
keyEvent && this.panel.grouperKeyMap.has(keyEvent.event)
|
|
||||||
? this.panel.grouperKeyMap.get(keyEvent.event)!
|
|
||||||
: this.generateKey();
|
|
||||||
if (!keyEvent) {
|
|
||||||
// Populate the weak map with the key.
|
|
||||||
// Note that we only set the key on the specific event it refers to, since this group might get
|
|
||||||
// split up in the future by other intervening events. If we were to set the key on all events
|
|
||||||
// currently in the group, we would risk later giving the same key to multiple groups.
|
|
||||||
this.panel.grouperKeyMap.set(this.events[0].event, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
let highlightInSummary = false;
|
|
||||||
let eventTiles: ReactNode[] | null = this.events
|
|
||||||
.map((e, i) => {
|
|
||||||
if (e.event.getId() === panel.props.highlightedEventId) {
|
|
||||||
highlightInSummary = true;
|
|
||||||
}
|
|
||||||
return panel.getTilesForEvent(
|
|
||||||
i === 0 ? this.prevEvent : this.events[i - 1].event,
|
|
||||||
e,
|
|
||||||
e.event === lastShownEvent,
|
|
||||||
isGrouped,
|
|
||||||
this.nextEvent,
|
|
||||||
this.nextEventTile,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.reduce((a, b) => a.concat(b), []);
|
|
||||||
|
|
||||||
if (eventTiles.length === 0) {
|
|
||||||
eventTiles = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
ret.push(<HistoryTile key="historytile" />);
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.push(
|
|
||||||
<EventListSummary
|
|
||||||
key={key}
|
|
||||||
data-testid={key}
|
|
||||||
events={this.events.map((e) => e.event)}
|
|
||||||
onToggle={panel.onHeightChanged} // Update scroll state
|
|
||||||
startExpanded={highlightInSummary}
|
|
||||||
layout={this.panel.props.layout}
|
|
||||||
>
|
|
||||||
{eventTiles}
|
|
||||||
</EventListSummary>,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.readMarker) {
|
|
||||||
ret.push(this.readMarker);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getNewPrevEvent(): MatrixEvent {
|
|
||||||
return this.events[this.events.length - 1].event;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// all the grouper classes that we use, ordered by priority
|
// all the grouper classes that we use, ordered by priority
|
||||||
const groupers = [CreationGrouper, MainGrouper];
|
const groupers = [CreationGrouper, MainGrouper];
|
||||||
|
|
||||||
|
|
59
src/components/structures/grouper/BaseGrouper.ts
Normal file
59
src/components/structures/grouper/BaseGrouper.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import MessagePanel, { WrappedEvent } from "../MessagePanel";
|
||||||
|
|
||||||
|
/* Grouper classes determine when events can be grouped together in a summary.
|
||||||
|
* Groupers should have the following methods:
|
||||||
|
* - canStartGroup (static): determines if a new group should be started with the
|
||||||
|
* given event
|
||||||
|
* - shouldGroup: determines if the given event should be added to an existing group
|
||||||
|
* - add: adds an event to an existing group (should only be called if shouldGroup
|
||||||
|
* return true)
|
||||||
|
* - getTiles: returns the tiles that represent the group
|
||||||
|
* - getNewPrevEvent: returns the event that should be used as the new prevEvent
|
||||||
|
* when determining things such as whether a date separator is necessary
|
||||||
|
*/
|
||||||
|
export abstract class BaseGrouper {
|
||||||
|
public static canStartGroup = (_panel: MessagePanel, _ev: WrappedEvent): boolean => true;
|
||||||
|
|
||||||
|
public events: WrappedEvent[] = [];
|
||||||
|
// events that we include in the group but then eject out and place above the group.
|
||||||
|
public ejectedEvents: WrappedEvent[] = [];
|
||||||
|
public readMarker: ReactNode;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
public readonly panel: MessagePanel,
|
||||||
|
public readonly firstEventAndShouldShow: WrappedEvent,
|
||||||
|
public readonly prevEvent: MatrixEvent | null,
|
||||||
|
public readonly lastShownEvent: MatrixEvent | undefined,
|
||||||
|
public readonly nextEvent: WrappedEvent | null,
|
||||||
|
public readonly nextEventTile?: MatrixEvent | null,
|
||||||
|
) {
|
||||||
|
this.readMarker = panel.readMarkerForEvent(
|
||||||
|
firstEventAndShouldShow.event.getId()!,
|
||||||
|
firstEventAndShouldShow.event === lastShownEvent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract shouldGroup(ev: WrappedEvent): boolean;
|
||||||
|
public abstract add(ev: WrappedEvent): void;
|
||||||
|
public abstract getTiles(): ReactNode[];
|
||||||
|
public abstract getNewPrevEvent(): MatrixEvent;
|
||||||
|
}
|
166
src/components/structures/grouper/CreationGrouper.tsx
Normal file
166
src/components/structures/grouper/CreationGrouper.tsx
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { EventType, M_BEACON_INFO, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { BaseGrouper } from "./BaseGrouper";
|
||||||
|
import MessagePanel, { WrappedEvent } from "../MessagePanel";
|
||||||
|
import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast";
|
||||||
|
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";
|
||||||
|
|
||||||
|
// 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, { event }: WrappedEvent): boolean {
|
||||||
|
return event.getType() === EventType.RoomCreate;
|
||||||
|
};
|
||||||
|
|
||||||
|
public shouldGroup({ event, shouldShow }: WrappedEvent): boolean {
|
||||||
|
const panel = this.panel;
|
||||||
|
const createEvent = this.firstEventAndShouldShow.event;
|
||||||
|
if (!shouldShow) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (panel.wantsDateSeparator(this.firstEventAndShouldShow.event, event.getDate())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const eventType = event.getType();
|
||||||
|
if (
|
||||||
|
eventType === EventType.RoomMember &&
|
||||||
|
(event.getStateKey() !== createEvent.getSender() || event.getContent()["membership"] !== "join")
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// beacons are not part of room creation configuration
|
||||||
|
// should be shown in timeline
|
||||||
|
if (M_BEACON_INFO.matches(eventType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (VoiceBroadcastInfoEventType === eventType) {
|
||||||
|
// always show voice broadcast info events in timeline
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.isState() && event.getSender() === createEvent.getSender()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public add(wrappedEvent: WrappedEvent): void {
|
||||||
|
const { event: ev, shouldShow } = wrappedEvent;
|
||||||
|
const panel = this.panel;
|
||||||
|
this.readMarker = this.readMarker || panel.readMarkerForEvent(ev.getId()!, ev === this.lastShownEvent);
|
||||||
|
if (!shouldShow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ev.getType() === EventType.RoomEncryption) {
|
||||||
|
this.ejectedEvents.push(wrappedEvent);
|
||||||
|
} else {
|
||||||
|
this.events.push(wrappedEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTiles(): ReactNode[] {
|
||||||
|
// If we don't have any events to group, don't even try to group them. The logic
|
||||||
|
// below assumes that we have a group of events to deal with, but we might not if
|
||||||
|
// the events we were supposed to group were redacted.
|
||||||
|
if (!this.events || !this.events.length) return [];
|
||||||
|
|
||||||
|
const panel = this.panel;
|
||||||
|
const ret: ReactNode[] = [];
|
||||||
|
const isGrouped = true;
|
||||||
|
const createEvent = this.firstEventAndShouldShow;
|
||||||
|
const lastShownEvent = this.lastShownEvent;
|
||||||
|
|
||||||
|
if (panel.wantsDateSeparator(this.prevEvent, createEvent.event.getDate())) {
|
||||||
|
const ts = createEvent.event.getTs();
|
||||||
|
ret.push(
|
||||||
|
<li key={ts + "~"}>
|
||||||
|
<DateSeparator roomId={createEvent.event.getRoomId()!} ts={ts} />
|
||||||
|
</li>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this m.room.create event should be shown (room upgrade) then show it before the summary
|
||||||
|
if (createEvent.shouldShow) {
|
||||||
|
// pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered
|
||||||
|
ret.push(...panel.getTilesForEvent(createEvent.event, createEvent));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ejected of this.ejectedEvents) {
|
||||||
|
ret.push(
|
||||||
|
...panel.getTilesForEvent(createEvent.event, ejected, createEvent.event === lastShownEvent, isGrouped),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventTiles = this.events
|
||||||
|
.map((e) => {
|
||||||
|
// In order to prevent DateSeparators from appearing in the expanded form
|
||||||
|
// of GenericEventListSummary, render each member event as if the previous
|
||||||
|
// one was itself. This way, the timestamp of the previous event === the
|
||||||
|
// timestamp of the current event, and no DateSeparator is inserted.
|
||||||
|
return panel.getTilesForEvent(e.event, e, e.event === lastShownEvent, isGrouped);
|
||||||
|
})
|
||||||
|
.reduce((a, b) => a.concat(b), []);
|
||||||
|
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
|
||||||
|
const ev = this.events[this.events.length - 1].event;
|
||||||
|
|
||||||
|
let summaryText: string;
|
||||||
|
const roomId = ev.getRoomId();
|
||||||
|
const creator = ev.sender?.name ?? ev.getSender();
|
||||||
|
if (roomId && DMRoomMap.shared().getUserIdForRoomId(roomId)) {
|
||||||
|
summaryText = _t("timeline|creation_summary_dm", { creator });
|
||||||
|
} else {
|
||||||
|
summaryText = _t("timeline|creation_summary_room", { creator });
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.push(<NewRoomIntro key="newroomintro" />);
|
||||||
|
|
||||||
|
ret.push(
|
||||||
|
<GenericEventListSummary
|
||||||
|
key="roomcreationsummary"
|
||||||
|
events={this.events.map((e) => e.event)}
|
||||||
|
onToggle={panel.onHeightChanged} // Update scroll state
|
||||||
|
summaryMembers={ev.sender ? [ev.sender] : undefined}
|
||||||
|
summaryText={summaryText}
|
||||||
|
layout={this.panel.props.layout}
|
||||||
|
>
|
||||||
|
{eventTiles}
|
||||||
|
</GenericEventListSummary>,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.readMarker) {
|
||||||
|
ret.push(this.readMarker);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getNewPrevEvent(): MatrixEvent {
|
||||||
|
return this.firstEventAndShouldShow.event;
|
||||||
|
}
|
||||||
|
}
|
192
src/components/structures/grouper/MainGrouper.tsx
Normal file
192
src/components/structures/grouper/MainGrouper.tsx
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { EventType, 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";
|
||||||
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
import DateSeparator from "../../views/messages/DateSeparator";
|
||||||
|
import HistoryTile from "../../views/rooms/HistoryTile";
|
||||||
|
import EventListSummary from "../../views/elements/EventListSummary";
|
||||||
|
|
||||||
|
const groupedStateEvents = [
|
||||||
|
EventType.RoomMember,
|
||||||
|
EventType.RoomThirdPartyInvite,
|
||||||
|
EventType.RoomServerAcl,
|
||||||
|
EventType.RoomPinnedEvents,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Wrap consecutive grouped events in a ListSummary
|
||||||
|
export class MainGrouper extends BaseGrouper {
|
||||||
|
public static canStartGroup = function (panel: MessagePanel, { event: ev, shouldShow }: WrappedEvent): boolean {
|
||||||
|
if (!shouldShow) return false;
|
||||||
|
|
||||||
|
if (ev.isState() && groupedStateEvents.includes(ev.getType() as EventType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.isRedacted()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (panel.showHiddenEvents && !panel.shouldShowEvent(ev, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
public readonly panel: MessagePanel,
|
||||||
|
public readonly firstEventAndShouldShow: WrappedEvent,
|
||||||
|
public readonly prevEvent: MatrixEvent | null,
|
||||||
|
public readonly lastShownEvent: MatrixEvent | undefined,
|
||||||
|
nextEvent: WrappedEvent | null,
|
||||||
|
nextEventTile: MatrixEvent | null,
|
||||||
|
) {
|
||||||
|
super(panel, firstEventAndShouldShow, prevEvent, lastShownEvent, nextEvent, nextEventTile);
|
||||||
|
this.events = [firstEventAndShouldShow];
|
||||||
|
}
|
||||||
|
|
||||||
|
public shouldGroup({ event: ev, shouldShow }: WrappedEvent): boolean {
|
||||||
|
if (!shouldShow) {
|
||||||
|
// absorb hidden events so that they do not break up streams of messages & redaction events being grouped
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.panel.wantsDateSeparator(this.events[0].event, ev.getDate())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ev.isState() && groupedStateEvents.includes(ev.getType() as EventType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (ev.isRedacted()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.panel.showHiddenEvents && !this.panel.shouldShowEvent(ev, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public add(wrappedEvent: WrappedEvent): void {
|
||||||
|
const { event: ev, shouldShow } = wrappedEvent;
|
||||||
|
if (ev.getType() === EventType.RoomMember) {
|
||||||
|
// We can ignore any events that don't actually have a message to display
|
||||||
|
if (!hasText(ev, MatrixClientPeg.safeGet(), this.panel.showHiddenEvents)) return;
|
||||||
|
}
|
||||||
|
this.readMarker = this.readMarker || this.panel.readMarkerForEvent(ev.getId()!, ev === this.lastShownEvent);
|
||||||
|
if (!this.panel.showHiddenEvents && !shouldShow) {
|
||||||
|
// absorb hidden events to not split the summary
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.events.push(wrappedEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateKey(): string {
|
||||||
|
return "eventlistsummary-" + this.events[0].event.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTiles(): ReactNode[] {
|
||||||
|
// If we don't have any events to group, don't even try to group them. The logic
|
||||||
|
// below assumes that we have a group of events to deal with, but we might not if
|
||||||
|
// the events we were supposed to group were redacted.
|
||||||
|
if (!this.events?.length) return [];
|
||||||
|
|
||||||
|
const isGrouped = true;
|
||||||
|
const panel = this.panel;
|
||||||
|
const lastShownEvent = this.lastShownEvent;
|
||||||
|
const ret: ReactNode[] = [];
|
||||||
|
|
||||||
|
if (panel.wantsDateSeparator(this.prevEvent, this.events[0].event.getDate())) {
|
||||||
|
const ts = this.events[0].event.getTs();
|
||||||
|
ret.push(
|
||||||
|
<li key={ts + "~"}>
|
||||||
|
<DateSeparator roomId={this.events[0].event.getRoomId()!} ts={ts} />
|
||||||
|
</li>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the key of the EventListSummary does not change with new events in either direction.
|
||||||
|
// This will prevent it from being re-created unnecessarily, and instead will allow new props to be provided.
|
||||||
|
// In turn, the shouldComponentUpdate method on ELS can be used to prevent unnecessary renderings.
|
||||||
|
const keyEvent = this.events.find((e) => this.panel.grouperKeyMap.get(e.event));
|
||||||
|
const key =
|
||||||
|
keyEvent && this.panel.grouperKeyMap.has(keyEvent.event)
|
||||||
|
? this.panel.grouperKeyMap.get(keyEvent.event)!
|
||||||
|
: this.generateKey();
|
||||||
|
if (!keyEvent) {
|
||||||
|
// Populate the weak map with the key.
|
||||||
|
// Note that we only set the key on the specific event it refers to, since this group might get
|
||||||
|
// split up in the future by other intervening events. If we were to set the key on all events
|
||||||
|
// currently in the group, we would risk later giving the same key to multiple groups.
|
||||||
|
this.panel.grouperKeyMap.set(this.events[0].event, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
let highlightInSummary = false;
|
||||||
|
let eventTiles: ReactNode[] | null = this.events
|
||||||
|
.map((e, i) => {
|
||||||
|
if (e.event.getId() === panel.props.highlightedEventId) {
|
||||||
|
highlightInSummary = true;
|
||||||
|
}
|
||||||
|
return panel.getTilesForEvent(
|
||||||
|
i === 0 ? this.prevEvent : this.events[i - 1].event,
|
||||||
|
e,
|
||||||
|
e.event === lastShownEvent,
|
||||||
|
isGrouped,
|
||||||
|
this.nextEvent,
|
||||||
|
this.nextEventTile,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.reduce((a, b) => a.concat(b), []);
|
||||||
|
|
||||||
|
if (eventTiles.length === 0) {
|
||||||
|
eventTiles = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
ret.push(<HistoryTile key="historytile" />);
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.push(
|
||||||
|
<EventListSummary
|
||||||
|
key={key}
|
||||||
|
data-testid={key}
|
||||||
|
events={this.events.map((e) => e.event)}
|
||||||
|
onToggle={panel.onHeightChanged} // Update scroll state
|
||||||
|
startExpanded={highlightInSummary}
|
||||||
|
layout={this.panel.props.layout}
|
||||||
|
>
|
||||||
|
{eventTiles}
|
||||||
|
</EventListSummary>,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.readMarker) {
|
||||||
|
ret.push(this.readMarker);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getNewPrevEvent(): MatrixEvent {
|
||||||
|
return this.events[this.events.length - 1].event;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue