mirror of
https://github.com/vector-im/element-web.git
synced 2025-08-11 16:57:05 +02:00
React to MatrixEvent sender/target being updated for rendering state events (#28947)
* React to MatrixEvent sender/target sentinels being updated for rendering state events Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * React to sentinel changes in EventListSummary Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
585aa75525
commit
f99d7ce2bb
@ -9,8 +9,9 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ComponentProps, ReactNode } from "react";
|
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 { KnownMembership } from "matrix-js-sdk/src/types";
|
||||||
|
import { throttle } from "lodash";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { formatList } from "../../../utils/FormattingUtils";
|
import { formatList } from "../../../utils/FormattingUtils";
|
||||||
@ -22,6 +23,8 @@ import { Layout } from "../../../settings/enums/Layout";
|
|||||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||||
import AccessibleButton from "./AccessibleButton";
|
import AccessibleButton from "./AccessibleButton";
|
||||||
import RoomContext from "../../../contexts/RoomContext";
|
import RoomContext from "../../../contexts/RoomContext";
|
||||||
|
import { arrayHasDiff } from "../../../utils/arrays.ts";
|
||||||
|
import { objectHasDiff } from "../../../utils/objects.ts";
|
||||||
|
|
||||||
const onPinnedMessagesClick = (): void => {
|
const onPinnedMessagesClick = (): void => {
|
||||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false);
|
RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false);
|
||||||
@ -69,9 +72,14 @@ enum TransitionType {
|
|||||||
|
|
||||||
const SEP = ",";
|
const SEP = ",";
|
||||||
|
|
||||||
export default class EventListSummary extends React.Component<
|
type Props = IProps & Required<Pick<IProps, "summaryLength" | "threshold" | "avatarsMaxLength" | "layout">>;
|
||||||
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;
|
public static contextType = RoomContext;
|
||||||
declare public context: React.ContextType<typeof RoomContext>;
|
declare public context: React.ContextType<typeof RoomContext>;
|
||||||
|
|
||||||
@ -82,15 +90,122 @@ export default class EventListSummary extends React.Component<
|
|||||||
layout: Layout.Group,
|
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
|
// Update if
|
||||||
// - The number of summarised events has changed
|
// - The number of summarised events has changed
|
||||||
// - or if the summary is about to toggle to become collapsed
|
// - 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 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 (
|
return (
|
||||||
nextProps.events.length !== this.props.events.length ||
|
nextProps.events.length !== this.props.events.length ||
|
||||||
nextProps.events.length < this.props.threshold ||
|
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] ?? {}),
|
||||||
|
),
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -492,54 +607,7 @@ export default class EventListSummary extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const eventsToRender = this.props.events;
|
const aggregate = this.getAggregate(this.state.userEvents);
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Sort types by order of lowest event index within sequence
|
// Sort types by order of lowest event index within sequence
|
||||||
const orderedTransitionSequences = Object.keys(aggregate.names).sort(
|
const orderedTransitionSequences = Object.keys(aggregate.names).sort(
|
||||||
@ -554,7 +622,7 @@ export default class EventListSummary extends React.Component<
|
|||||||
onToggle={this.props.onToggle}
|
onToggle={this.props.onToggle}
|
||||||
startExpanded={this.props.startExpanded}
|
startExpanded={this.props.startExpanded}
|
||||||
children={this.props.children}
|
children={this.props.children}
|
||||||
summaryMembers={[...latestUserAvatarMember.values()]}
|
summaryMembers={this.state.summaryMembers}
|
||||||
layout={this.props.layout}
|
layout={this.props.layout}
|
||||||
summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)}
|
summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)}
|
||||||
/>
|
/>
|
||||||
|
@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
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 RoomContext from "../../../contexts/RoomContext";
|
||||||
import * as TextForEvent from "../../../TextForEvent";
|
import * as TextForEvent from "../../../TextForEvent";
|
||||||
@ -21,6 +21,19 @@ export default class TextualEvent extends React.Component<IProps> {
|
|||||||
public static contextType = RoomContext;
|
public static contextType = RoomContext;
|
||||||
declare public context: React.ContextType<typeof 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 {
|
public render(): React.ReactNode {
|
||||||
const text = TextForEvent.textForEvent(
|
const text = TextForEvent.textForEvent(
|
||||||
this.props.mxEvent,
|
this.props.mxEvent,
|
||||||
|
@ -154,7 +154,7 @@ async function fetchPinnedEvent(room: Room, pinnedEventId: string, cli: MatrixCl
|
|||||||
const senderUserId = event.getSender();
|
const senderUserId = event.getSender();
|
||||||
if (senderUserId && PinningUtils.isUnpinnable(event)) {
|
if (senderUserId && PinningUtils.isUnpinnable(event)) {
|
||||||
// Inject sender information
|
// Inject sender information
|
||||||
event.sender = room.getMember(senderUserId);
|
event.setMetadata(room.currentState, false);
|
||||||
// Also inject any edits we've found
|
// Also inject any edits we've found
|
||||||
if (edit) event.makeReplaced(edit);
|
if (edit) event.makeReplaced(edit);
|
||||||
|
|
||||||
|
@ -110,12 +110,7 @@ export default abstract class Exporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected setEventMetadata(event: MatrixEvent): MatrixEvent {
|
protected setEventMetadata(event: MatrixEvent): MatrixEvent {
|
||||||
const roomState = this.room.currentState;
|
event.setMetadata(this.room.currentState, false);
|
||||||
const sender = event.getSender();
|
|
||||||
event.sender = (!!sender && roomState?.getSentinelMember(sender)) || null;
|
|
||||||
if (event.getType() === "m.room.member") {
|
|
||||||
event.target = roomState?.getSentinelMember(event.getStateKey()!) ?? null;
|
|
||||||
}
|
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,6 +145,7 @@ exports[`<PinnedMessagesCard /> should show two pinned messages 1`] = `
|
|||||||
data-type="round"
|
data-type="round"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
style="--cpd-avatar-size: 32px;"
|
style="--cpd-avatar-size: 32px;"
|
||||||
|
title="@alice:example.org"
|
||||||
>
|
>
|
||||||
a
|
a
|
||||||
</span>
|
</span>
|
||||||
@ -222,6 +223,7 @@ exports[`<PinnedMessagesCard /> should show two pinned messages 1`] = `
|
|||||||
data-type="round"
|
data-type="round"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
style="--cpd-avatar-size: 32px;"
|
style="--cpd-avatar-size: 32px;"
|
||||||
|
title="@alice:example.org"
|
||||||
>
|
>
|
||||||
a
|
a
|
||||||
</span>
|
</span>
|
||||||
@ -364,6 +366,7 @@ exports[`<PinnedMessagesCard /> unpin all should not allow to unpinall 1`] = `
|
|||||||
data-type="round"
|
data-type="round"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
style="--cpd-avatar-size: 32px;"
|
style="--cpd-avatar-size: 32px;"
|
||||||
|
title="@alice:example.org"
|
||||||
>
|
>
|
||||||
a
|
a
|
||||||
</span>
|
</span>
|
||||||
@ -441,6 +444,7 @@ exports[`<PinnedMessagesCard /> unpin all should not allow to unpinall 1`] = `
|
|||||||
data-type="round"
|
data-type="round"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
style="--cpd-avatar-size: 32px;"
|
style="--cpd-avatar-size: 32px;"
|
||||||
|
title="@alice:example.org"
|
||||||
>
|
>
|
||||||
a
|
a
|
||||||
</span>
|
</span>
|
||||||
|
Loading…
Reference in New Issue
Block a user