element-portable/src/hooks/usePinnedEvents.ts
Florian Duros 6b384fe9c1
Fix huge usage bandwidth and performance issue of pinned message banner. (#37)
* Return only the first 100 pinned messages

* Execute pinned message 10 by 10
2024-09-13 07:47:22 +00:00

206 lines
6.4 KiB
TypeScript

/*
* Copyright 2024 New Vector Ltd.
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*/
import { useCallback, useEffect, useMemo, useState } from "react";
import {
Room,
RoomEvent,
RoomStateEvent,
MatrixEvent,
EventType,
RelationType,
EventTimeline,
MatrixClient,
} from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { useTypedEventEmitter } from "./useEventEmitter";
import { ReadPinsEventId } from "../components/views/right_panel/types";
import { useMatrixClientContext } from "../contexts/MatrixClientContext";
import { useAsyncMemo } from "./useAsyncMemo";
import PinningUtils from "../utils/PinningUtils";
import { batch } from "../utils/promise.ts";
/**
* Get the pinned event IDs from a room.
* The number of pinned events is limited to 100.
* @param room
*/
function getPinnedEventIds(room?: Room): string[] {
const eventIds: string[] =
room
?.getLiveTimeline()
.getState(EventTimeline.FORWARDS)
?.getStateEvents(EventType.RoomPinnedEvents, "")
?.getContent()?.pinned ?? [];
// Limit the number of pinned events to 100
return eventIds.slice(0, 100);
}
/**
* Get the pinned event IDs from a room.
* @param room
*/
export const usePinnedEvents = (room?: Room): string[] => {
const [pinnedEvents, setPinnedEvents] = useState<string[]>(getPinnedEventIds(room));
// Update the pinned events when the room state changes
// Filter out events that are not pinned events
const update = useCallback(
(ev?: MatrixEvent) => {
if (ev && ev.getType() !== EventType.RoomPinnedEvents) return;
setPinnedEvents(getPinnedEventIds(room));
},
[room],
);
useTypedEventEmitter(room?.getLiveTimeline().getState(EventTimeline.FORWARDS), RoomStateEvent.Events, update);
useEffect(() => {
setPinnedEvents(getPinnedEventIds(room));
return () => {
setPinnedEvents([]);
};
}, [room]);
return pinnedEvents;
};
/**
* Get the read pinned event IDs from a room.
* @param room
*/
function getReadPinnedEventIds(room?: Room): Set<string> {
return new Set(room?.getAccountData(ReadPinsEventId)?.getContent()?.event_ids ?? []);
}
/**
* Get the read pinned event IDs from a room.
* @param room
*/
export const useReadPinnedEvents = (room?: Room): Set<string> => {
const [readPinnedEvents, setReadPinnedEvents] = useState<Set<string>>(new Set());
// Update the read pinned events when the room state changes
// Filter out events that are not read pinned events
const update = useCallback(
(ev?: MatrixEvent) => {
if (ev && ev.getType() !== ReadPinsEventId) return;
setReadPinnedEvents(getReadPinnedEventIds(room));
},
[room],
);
useTypedEventEmitter(room, RoomEvent.AccountData, update);
useEffect(() => {
setReadPinnedEvents(getReadPinnedEventIds(room));
return () => {
setReadPinnedEvents(new Set());
};
}, [room]);
return readPinnedEvents;
};
/**
* Fetch the pinned event
* @param room
* @param pinnedEventId
* @param cli
*/
async function fetchPinnedEvent(room: Room, pinnedEventId: string, cli: MatrixClient): Promise<MatrixEvent | null> {
const timelineSet = room.getUnfilteredTimelineSet();
// Get the event from the local timeline
const localEvent = timelineSet
?.getTimelineForEvent(pinnedEventId)
?.getEvents()
.find((e) => e.getId() === pinnedEventId);
// Decrypt the event if it's encrypted
// Can happen when the tab is refreshed and the pinned events card is opened directly
if (localEvent?.isEncrypted()) {
await cli.decryptEventIfNeeded(localEvent, { emit: false });
}
// If the event is available locally, return it if it's pinnable
// or if it's redacted (to show the redacted event and to be able to unpin it)
// Otherwise, return null
if (localEvent) return PinningUtils.isUnpinnable(localEvent) ? localEvent : null;
try {
// The event is not available locally, so we fetch the event and latest edit in parallel
const [
evJson,
{
events: [edit],
},
] = await Promise.all([
cli.fetchRoomEvent(room.roomId, pinnedEventId),
cli.relations(room.roomId, pinnedEventId, RelationType.Replace, null, { limit: 1 }),
]);
const event = new MatrixEvent(evJson);
// Decrypt the event if it's encrypted
if (event.isEncrypted()) {
await cli.decryptEventIfNeeded(event, { emit: false });
}
// Handle poll events
await room.processPollEvents([event]);
const senderUserId = event.getSender();
if (senderUserId && PinningUtils.isUnpinnable(event)) {
// Inject sender information
event.sender = room.getMember(senderUserId);
// Also inject any edits we've found
if (edit) event.makeReplaced(edit);
return event;
}
} catch (err) {
logger.error(`Error looking up pinned event ${pinnedEventId} in room ${room.roomId}`);
logger.error(err);
}
return null;
}
/**
* Fetch the pinned events
* @param room
* @param pinnedEventIds
*/
export function useFetchedPinnedEvents(room: Room, pinnedEventIds: string[]): Array<MatrixEvent | null> | null {
const cli = useMatrixClientContext();
return useAsyncMemo(
() => {
const fetchPromises = pinnedEventIds.map((eventId) => () => fetchPinnedEvent(room, eventId, cli));
// Fetch the pinned events in batches of 10
return batch(fetchPromises, 10);
},
[cli, room, pinnedEventIds],
null,
);
}
/**
* Fetch the pinned events and sort them by from the oldest to the newest
* The order is determined by the event timestamp
* @param room
* @param pinnedEventIds
*/
export function useSortedFetchedPinnedEvents(room: Room, pinnedEventIds: string[]): Array<MatrixEvent | null> {
const pinnedEvents = useFetchedPinnedEvents(room, pinnedEventIds);
return useMemo(() => {
if (!pinnedEvents) return [];
return pinnedEvents.sort((a, b) => {
if (!a) return -1;
if (!b) return 1;
return a.getTs() - b.getTs();
});
}, [pinnedEvents]);
}