Files
ThreadNet-Web/src/components/structures/FilePanel.tsx
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

323 lines
12 KiB
TypeScript
Raw Normal View History

2016-08-31 11:58:06 +01:00
/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
Copyright 2019-2022 The Matrix.org Foundation C.I.C.
2016-08-31 11:58:06 +01:00
Copyright 2016 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
2024-09-09 14:57:16 +01:00
Please see LICENSE files in the repository root for full details.
2016-08-31 11:58:06 +01:00
*/
import React, { createRef } from "react";
import {
Filter,
EventTimelineSet,
IRoomTimelineData,
Direction,
MatrixEvent,
MatrixEventEvent,
Room,
RoomEvent,
TimelineWindow,
} from "matrix-js-sdk/src/matrix";
2021-10-22 17:23:32 -05:00
import { logger } from "matrix-js-sdk/src/logger";
import FilesIcon from "@vector-im/compound-design-tokens/assets/web/icons/files";
2021-06-29 11:38:19 +01:00
2021-06-29 13:26:09 +01:00
import { MatrixClientPeg } from "../../MatrixClientPeg";
import EventIndexPeg from "../../indexing/EventIndexPeg";
2017-11-13 20:19:33 +01:00
import { _t } from "../../languageHandler";
import SearchWarning, { WarningKind } from "../views/elements/SearchWarning";
2022-01-05 16:14:44 +01:00
import BaseCard from "../views/right_panel/BaseCard";
2021-06-29 11:38:19 +01:00
import ResizeNotifier from "../../utils/ResizeNotifier";
2021-07-02 17:08:27 +02:00
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
import { Layout } from "../../settings/enums/Layout";
2021-10-11 11:26:05 +02:00
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import Measured from "../views/elements/Measured";
import EmptyState from "../views/right_panel/EmptyState";
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
2021-06-29 11:38:19 +01:00
interface IProps {
roomId: string;
onClose: () => void;
2021-07-01 23:23:03 +01:00
resizeNotifier: ResizeNotifier;
2021-06-29 11:38:19 +01:00
}
interface IState {
2023-02-13 11:39:16 +00:00
timelineSet: EventTimelineSet | null;
narrow: boolean;
2021-06-29 11:38:19 +01:00
}
2016-08-31 11:58:06 +01:00
/*
* Component which shows the filtered file using a TimelinePanel
*/
2021-06-29 11:38:19 +01:00
class FilePanel extends React.Component<IProps, IState> {
public static contextType = RoomContext;
2024-12-02 09:39:36 +00:00
declare public context: React.ContextType<typeof RoomContext>;
// This is used to track if a decrypted event was a live event and should be
// added to the timeline.
2021-06-29 11:38:19 +01:00
private decryptingEvents = new Set<string>();
public noRoom = false;
private card = createRef<HTMLDivElement>();
2016-08-31 11:58:06 +01:00
2023-02-13 11:39:16 +00:00
public state: IState = {
2020-08-29 12:14:16 +01:00
timelineSet: null,
narrow: false,
2020-08-29 12:14:16 +01:00
};
private onRoomTimeline = (
ev: MatrixEvent,
room: Room | undefined,
toStartOfTimeline: boolean | undefined,
removed: boolean,
data: IRoomTimelineData,
): void => {
if (room?.roomId !== this.props.roomId) return;
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
const client = MatrixClientPeg.safeGet();
2021-05-18 13:01:38 +01:00
client.decryptEventIfNeeded(ev);
if (ev.isBeingDecrypted()) {
this.decryptingEvents.add(ev.getId()!);
} else {
this.addEncryptedLiveEvent(ev);
}
2020-08-29 12:14:16 +01:00
};
2021-06-29 12:18:41 +01:00
private onEventDecrypted = (ev: MatrixEvent, err?: any): void => {
if (ev.getRoomId() !== this.props.roomId) return;
const eventId = ev.getId()!;
if (!this.decryptingEvents.delete(eventId)) return;
if (err) return;
this.addEncryptedLiveEvent(ev);
2020-08-29 12:14:16 +01:00
};
2021-06-29 12:18:41 +01:00
public addEncryptedLiveEvent(ev: MatrixEvent): void {
if (!this.state.timelineSet) return;
const timeline = this.state.timelineSet.getLiveTimeline();
if (ev.getType() !== "m.room.message") return;
if (!["m.file", "m.image", "m.video", "m.audio"].includes(ev.getContent().msgtype!)) {
return;
}
if (!this.state.timelineSet.eventIdToTimeline(ev.getId()!)) {
this.state.timelineSet.addEventToTimeline(ev, timeline, {
fromCache: false,
addToState: false,
toStartOfTimeline: false,
});
}
2020-08-29 12:14:16 +01:00
}
2021-06-29 12:18:41 +01:00
public async componentDidMount(): Promise<void> {
const client = MatrixClientPeg.safeGet();
await this.updateTimelineSet(this.props.roomId);
if (!client.isRoomEncrypted(this.props.roomId)) return;
// The timelineSets filter makes sure that encrypted events that contain
// URLs never get added to the timeline, even if they are live events.
// These methods are here to manually listen for such events and add
// them despite the filter's best efforts.
//
// We do this only for encrypted rooms and if an event index exists,
// this could be made more general in the future or the filter logic
// could be fixed.
if (EventIndexPeg.get() !== null) {
client.on(RoomEvent.Timeline, this.onRoomTimeline);
client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
}
2020-08-29 12:14:16 +01:00
}
2021-06-29 12:18:41 +01:00
public componentWillUnmount(): void {
const client = MatrixClientPeg.get();
if (client === null) return;
if (!client.isRoomEncrypted(this.props.roomId)) return;
if (EventIndexPeg.get() !== null) {
client.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
client.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
}
2020-08-29 12:14:16 +01:00
}
2016-09-07 15:43:14 +01:00
public async fetchFileEventsServer(room: Room): Promise<EventTimelineSet> {
const client = MatrixClientPeg.safeGet();
const filter = new Filter(client.getSafeUserId());
filter.setDefinition({
room: {
timeline: {
contains_url: true,
types: ["m.room.message"],
},
},
});
filter.filterId = await client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter);
return room.getOrCreateFilteredTimelineSet(filter);
2020-08-29 12:14:16 +01:00
}
2021-07-01 10:01:48 +01:00
private onPaginationRequest = (
timelineWindow: TimelineWindow,
direction: Direction,
limit: number,
): Promise<boolean> => {
const client = MatrixClientPeg.safeGet();
const eventIndex = EventIndexPeg.get();
const roomId = this.props.roomId;
const room = client.getRoom(roomId);
// We override the pagination request for encrypted rooms so that we ask
// the event index to fulfill the pagination request. Asking the server
// to paginate won't ever work since the server can't correctly filter
// out events containing URLs
if (room && client.isRoomEncrypted(roomId) && eventIndex !== null) {
return eventIndex.paginateTimelineWindow(room, timelineWindow, direction, limit);
} else {
return timelineWindow.paginate(direction, limit);
}
2020-08-29 12:14:16 +01:00
};
private onMeasurement = (narrow: boolean): void => {
this.setState({ narrow });
};
2021-06-29 12:18:41 +01:00
public async updateTimelineSet(roomId: string): Promise<void> {
const client = MatrixClientPeg.safeGet();
2017-10-11 17:56:17 +01:00
const room = client.getRoom(roomId);
const eventIndex = EventIndexPeg.get();
2016-09-07 15:43:14 +01:00
2017-05-12 22:38:57 +01:00
this.noRoom = !room;
2016-09-07 15:43:14 +01:00
if (room) {
let timelineSet;
try {
2020-01-17 11:52:20 +01:00
timelineSet = await this.fetchFileEventsServer(room);
// If this room is encrypted the file panel won't be populated
// correctly since the defined filter doesn't support encrypted
// events and the server can't check if encrypted events contain
// URLs.
//
// This is where our event index comes into place, we ask the
// event index to populate the timelineSet for us. This call
// will add 10 events to the live timeline of the set. More can
// be requested using pagination.
if (client.isRoomEncrypted(roomId) && eventIndex !== null) {
const timeline = timelineSet.getLiveTimeline();
await eventIndex.populateFileTimeline(timelineSet, timeline, room, 10);
}
this.setState({ timelineSet: timelineSet });
} catch (error) {
2021-10-15 16:30:53 +02:00
logger.error("Failed to get or create file panel filter", error);
}
2017-05-12 22:38:57 +01:00
} else {
2021-10-15 16:30:53 +02:00
logger.error("Failed to add filtered timelineSet for FilePanel as no room!");
}
2020-08-29 12:14:16 +01:00
}
public render(): React.ReactNode {
if (MatrixClientPeg.safeGet().isGuest()) {
2020-09-08 10:19:51 +01:00
return (
<BaseCard
className="mx_FilePanel mx_RoomView_messageListWrapper"
onClose={this.props.onClose}
header={_t("right_panel|files_button")}
>
2017-06-07 11:40:46 +01:00
<div className="mx_RoomView_empty">
2021-04-27 17:23:27 +02:00
{_t(
"file_panel|guest_note",
2021-04-27 17:23:27 +02:00
{},
{
a: (sub) => (
<a href="#/register" key="sub">
{sub}
</a>
2022-12-12 12:24:14 +01:00
),
2021-04-27 17:23:27 +02:00
},
)}
2017-06-07 11:40:46 +01:00
</div>
2020-09-08 10:19:51 +01:00
</BaseCard>
);
} else if (this.noRoom) {
2020-09-08 10:19:51 +01:00
return (
<BaseCard
className="mx_FilePanel mx_RoomView_messageListWrapper"
onClose={this.props.onClose}
header={_t("right_panel|files_button")}
>
<div className="mx_RoomView_empty">{_t("file_panel|peek_note")}</div>
2020-09-08 10:19:51 +01:00
</BaseCard>
);
2017-05-12 22:38:57 +01:00
}
2016-08-31 11:58:06 +01:00
// wrap a TimelinePanel with the jump-to-event bits turned off.
const emptyState = (
<EmptyState
Icon={FilesIcon}
title={_t("file_panel|empty_heading")}
description={_t("file_panel|empty_description")}
/>
);
const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.safeGet().isRoomEncrypted(this.props.roomId);
2016-09-06 01:44:55 +01:00
if (this.state.timelineSet) {
return (
<ScopedRoomContextProvider
{...this.context}
timelineRenderingType={TimelineRenderingType.File}
narrow={this.state.narrow}
2021-10-11 11:26:05 +02:00
>
<BaseCard
className="mx_FilePanel"
onClose={this.props.onClose}
withoutScrollContainer
ref={this.card}
header={_t("right_panel|files_button")}
2021-10-11 11:26:05 +02:00
>
{this.card.current && (
<Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />
)}
<SearchWarning isRoomEncrypted={isRoomEncrypted} kind={WarningKind.Files} />
2021-10-11 11:26:05 +02:00
<TimelinePanel
manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={this.state.timelineSet}
showUrlPreview={false}
onPaginationRequest={this.onPaginationRequest}
resizeNotifier={this.props.resizeNotifier}
empty={emptyState}
layout={Layout.Group}
/>
</BaseCard>
</ScopedRoomContextProvider>
2016-09-06 01:44:55 +01:00
);
2017-10-11 17:56:17 +01:00
} else {
2016-09-07 15:43:14 +01:00
return (
<ScopedRoomContextProvider {...this.context} timelineRenderingType={TimelineRenderingType.File}>
<BaseCard
className="mx_FilePanel"
onClose={this.props.onClose}
header={_t("right_panel|files_button")}
>
2021-10-11 11:26:05 +02:00
<Spinner />
</BaseCard>
</ScopedRoomContextProvider>
2016-09-07 15:43:14 +01:00
);
2016-09-06 01:44:55 +01:00
}
2020-08-29 12:14:16 +01:00
}
}
2016-08-31 11:58:06 +01:00
export default FilePanel;