Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React to MatrixEvent sender/target being updated for rendering state events #28947

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 123 additions & 55 deletions src/components/views/elements/EventListSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ Please see LICENSE files in the repository root for full details.
*/

import React, { ComponentProps, ReactNode } from "react";
import { MatrixEvent, RoomMember, EventType } from "matrix-js-sdk/src/matrix";
import { EventType, MatrixEvent, MatrixEventEvent, RoomMember } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { throttle } from "lodash";

import { _t } from "../../../languageHandler";
import { formatList } from "../../../utils/FormattingUtils";
Expand All @@ -22,6 +23,8 @@ import { Layout } from "../../../settings/enums/Layout";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import AccessibleButton from "./AccessibleButton";
import RoomContext from "../../../contexts/RoomContext";
import { arrayHasDiff } from "../../../utils/arrays.ts";
import { objectHasDiff } from "../../../utils/objects.ts";

const onPinnedMessagesClick = (): void => {
RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false);
Expand Down Expand Up @@ -69,9 +72,14 @@ enum TransitionType {

const SEP = ",";

export default class EventListSummary extends React.Component<
IProps & Required<Pick<IProps, "summaryLength" | "threshold" | "avatarsMaxLength" | "layout">>
> {
type Props = IProps & Required<Pick<IProps, "summaryLength" | "threshold" | "avatarsMaxLength" | "layout">>;

interface State {
userEvents: Record<string, IUserEvents[]>;
summaryMembers: RoomMember[];
}

export default class EventListSummary extends React.Component<Props, State> {
public static contextType = RoomContext;
declare public context: React.ContextType<typeof RoomContext>;

Expand All @@ -82,15 +90,122 @@ export default class EventListSummary extends React.Component<
layout: Layout.Group,
};

public shouldComponentUpdate(nextProps: IProps): boolean {
public constructor(props: Props) {
super(props);

this.state = this.generateState();
}

private generateState(): State {
const eventsToRender = this.props.events;

// Map user IDs to latest Avatar Member. ES6 Maps are ordered by when the key was created,
// so this works perfectly for us to match event order whilst storing the latest Avatar Member
const latestUserAvatarMember = new Map<string, RoomMember>();

// Object mapping user IDs to an array of IUserEvents
const userEvents: Record<string, IUserEvents[]> = {};
eventsToRender.forEach((e, index) => {
const type = e.getType();

let userKey = e.getSender()!;
if (e.isState() && type === EventType.RoomThirdPartyInvite) {
userKey = e.getContent().display_name;
} else if (e.isState() && type === EventType.RoomMember) {
userKey = e.getStateKey()!;
} else if (e.isRedacted() && e.getUnsigned()?.redacted_because) {
userKey = e.getUnsigned().redacted_because!.sender;
}

// Initialise a user's events
if (!userEvents[userKey]) {
userEvents[userKey] = [];
}

let displayName = userKey;
if (e.isRedacted()) {
const sender = this.context?.room?.getMember(userKey);
if (sender) {
displayName = sender.name;
latestUserAvatarMember.set(userKey, sender);
}
} else if (e.target && TARGET_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
displayName = e.target.name;
latestUserAvatarMember.set(userKey, e.target);
} else if (e.sender && type !== EventType.RoomThirdPartyInvite) {
displayName = e.sender.name;
latestUserAvatarMember.set(userKey, e.sender);
}

userEvents[userKey].push({
mxEvent: e,
displayName,
index: index,
});
});

return {
userEvents,
summaryMembers: Array.from(latestUserAvatarMember.values()),
};
}

public componentDidMount(): void {
this.bindSentinelListeners(this.props.events);
}

public componentDidUpdate(prevProps: Readonly<Props>): void {
if (prevProps.events !== this.props.events) {
this.unbindSentinelListeners(prevProps.events);
this.bindSentinelListeners(this.props.events);
this.setState(this.generateState());
}
}

public componentWillUnmount(): void {
this.unbindSentinelListeners(this.props.events);
}

private bindSentinelListeners(events: MatrixEvent[]): void {
for (const event of events) {
event.on(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated);
}
}

private unbindSentinelListeners(events: MatrixEvent[]): void {
for (const event of events) {
event.on(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated);
}
}

private onEventSentinelUpdated = throttle(
(): void => {
console.log("@@ SENTINEL UPDATED");
this.setState(this.generateState());
},
500,
{ leading: true, trailing: true },
);

public shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
// Update if
// - The number of summarised events has changed
// - or if the summary is about to toggle to become collapsed
// - or if there are fewEvents, meaning the child eventTiles are shown as-is
// - or if the summary members have changed
// - or if the one of IUserEvents within userEvents have changed
return (
nextProps.events.length !== this.props.events.length ||
nextProps.events.length < this.props.threshold ||
nextProps.layout !== this.props.layout
nextProps.layout !== this.props.layout ||
arrayHasDiff(nextState.summaryMembers, this.state.summaryMembers) ||
arrayHasDiff(Object.values(nextState.userEvents), Object.values(this.state.userEvents)) ||
Object.keys(nextState.userEvents).length !== Object.keys(this.state.userEvents).length ||
Object.keys(nextState.userEvents).some((userId) =>
nextState.userEvents[userId].some((event, i) =>
objectHasDiff(event, this.state.userEvents[userId]?.[i] ?? {}),
),
)
);
}

Expand Down Expand Up @@ -492,54 +607,7 @@ export default class EventListSummary extends React.Component<
}

public render(): React.ReactNode {
const eventsToRender = this.props.events;

// Map user IDs to latest Avatar Member. ES6 Maps are ordered by when the key was created,
// so this works perfectly for us to match event order whilst storing the latest Avatar Member
const latestUserAvatarMember = new Map<string, RoomMember>();

// Object mapping user IDs to an array of IUserEvents
const userEvents: Record<string, IUserEvents[]> = {};
eventsToRender.forEach((e, index) => {
const type = e.getType();

let userKey = e.getSender()!;
if (e.isState() && type === EventType.RoomThirdPartyInvite) {
userKey = e.getContent().display_name;
} else if (e.isState() && type === EventType.RoomMember) {
userKey = e.getStateKey()!;
} else if (e.isRedacted() && e.getUnsigned()?.redacted_because) {
userKey = e.getUnsigned().redacted_because!.sender;
}

// Initialise a user's events
if (!userEvents[userKey]) {
userEvents[userKey] = [];
}

let displayName = userKey;
if (e.isRedacted()) {
const sender = this.context?.room?.getMember(userKey);
if (sender) {
displayName = sender.name;
latestUserAvatarMember.set(userKey, sender);
}
} else if (e.target && TARGET_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
displayName = e.target.name;
latestUserAvatarMember.set(userKey, e.target);
} else if (e.sender && type !== EventType.RoomThirdPartyInvite) {
displayName = e.sender.name;
latestUserAvatarMember.set(userKey, e.sender);
}

userEvents[userKey].push({
mxEvent: e,
displayName,
index: index,
});
});

const aggregate = this.getAggregate(userEvents);
const aggregate = this.getAggregate(this.state.userEvents);

// Sort types by order of lowest event index within sequence
const orderedTransitionSequences = Object.keys(aggregate.names).sort(
Expand All @@ -554,7 +622,7 @@ export default class EventListSummary extends React.Component<
onToggle={this.props.onToggle}
startExpanded={this.props.startExpanded}
children={this.props.children}
summaryMembers={[...latestUserAvatarMember.values()]}
summaryMembers={this.state.summaryMembers}
layout={this.props.layout}
summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)}
/>
Expand Down
15 changes: 14 additions & 1 deletion src/components/views/messages/TextualEvent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/

import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix";

import RoomContext from "../../../contexts/RoomContext";
import * as TextForEvent from "../../../TextForEvent";
Expand All @@ -21,6 +21,19 @@ export default class TextualEvent extends React.Component<IProps> {
public static contextType = RoomContext;
declare public context: React.ContextType<typeof RoomContext>;

public componentDidMount(): void {
this.props.mxEvent.on(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated);
}
public componentWillUnmount(): void {
this.props.mxEvent.off(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated);
}

private onEventSentinelUpdated = (): void => {
// XXX: this is crap, but we don't have a better way to force a re-render
// Many TextForEvent handlers render parts of `event.sender` and `event.target` so ensure they are updated
this.forceUpdate();
};

public render(): React.ReactNode {
const text = TextForEvent.textForEvent(
this.props.mxEvent,
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/usePinnedEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ async function fetchPinnedEvent(room: Room, pinnedEventId: string, cli: MatrixCl
const senderUserId = event.getSender();
if (senderUserId && PinningUtils.isUnpinnable(event)) {
// Inject sender information
event.sender = room.getMember(senderUserId);
event.setMetadata(room.currentState, false);
// Also inject any edits we've found
if (edit) event.makeReplaced(edit);

Expand Down
7 changes: 1 addition & 6 deletions src/utils/exportUtils/Exporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,7 @@ export default abstract class Exporter {
}

protected setEventMetadata(event: MatrixEvent): MatrixEvent {
const roomState = this.room.currentState;
const sender = event.getSender();
event.sender = (!!sender && roomState?.getSentinelMember(sender)) || null;
if (event.getType() === "m.room.member") {
event.target = roomState?.getSentinelMember(event.getStateKey()!) ?? null;
}
event.setMetadata(this.room.currentState, false);
return event;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ exports[`<PinnedMessagesCard /> should show two pinned messages 1`] = `
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
title="@alice:example.org"
>
a
</span>
Expand Down Expand Up @@ -222,6 +223,7 @@ exports[`<PinnedMessagesCard /> should show two pinned messages 1`] = `
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
title="@alice:example.org"
>
a
</span>
Expand Down Expand Up @@ -364,6 +366,7 @@ exports[`<PinnedMessagesCard /> unpin all should not allow to unpinall 1`] = `
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
title="@alice:example.org"
>
a
</span>
Expand Down Expand Up @@ -441,6 +444,7 @@ exports[`<PinnedMessagesCard /> unpin all should not allow to unpinall 1`] = `
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
title="@alice:example.org"
>
a
</span>
Expand Down
Loading