Files
element-web/src/utils/MediaEventHelper.ts
Will Hunt 13c4ab2cf4 Add support for hiding videos (#29496)
* start hide

* Move useSettingsValueWithSetter to useSettings

* Add new setting showMediaEventIds

* Add a migration path

* Add an action button to hide settings.

* Tweaks to MImageBody to support new setting.

* Fixup and add tests

* add description for migration

* docs fixes

* add type

* i18n

* appese prettier

* Add tests for HideActionButton

* lint

* lint

* First pass at support for previewing/hiding images.

* Add a test for video files.

* First pass at supporting hiding video files.

* Use a hook for media visibility.

* Drop setting hook usage.

* Fixup MImageBody test

* Fixup tests

* Support functional components for message body rendering.

* Add a comment

* Move props into IProps

* Use new wrapping logic

* lint

* fixup

* allow for a delay for the image to render

* remove .only

* lint

* Fix jest test

* Fixup tests.

* make tests happy

* Improve comments

* review fixes

* unbreak test
2025-03-24 14:38:34 +00:00

131 lines
4.9 KiB
TypeScript

/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { type MatrixEvent, EventType, MsgType } from "matrix-js-sdk/src/matrix";
import { type FileContent, type ImageContent, type MediaEventContent } from "matrix-js-sdk/src/types";
import { logger } from "matrix-js-sdk/src/logger";
import { LazyValue } from "./LazyValue";
import { type Media, mediaFromContent } from "../customisations/Media";
import { decryptFile } from "./DecryptFile";
import { type IDestroyable } from "./IDestroyable";
// TODO: We should consider caching the blobs. https://github.com/vector-im/element-web/issues/17192
export class MediaEventHelper implements IDestroyable {
// Either an HTTP or Object URL (when encrypted) to the media.
public readonly sourceUrl: LazyValue<string | null>;
public readonly thumbnailUrl: LazyValue<string | null>;
// Either the raw or decrypted (when encrypted) contents of the file.
public readonly sourceBlob: LazyValue<Blob>;
public readonly thumbnailBlob: LazyValue<Blob | null>;
public readonly media: Media;
public constructor(private event: MatrixEvent) {
this.sourceUrl = new LazyValue(this.prepareSourceUrl);
this.thumbnailUrl = new LazyValue(this.prepareThumbnailUrl);
this.sourceBlob = new LazyValue(this.fetchSource);
this.thumbnailBlob = new LazyValue(this.fetchThumbnail);
this.media = mediaFromContent(this.event.getContent());
}
public get fileName(): string {
return (
this.event.getContent<FileContent>().filename ||
this.event.getContent<MediaEventContent>().body ||
"download"
);
}
public destroy(): void {
if (this.media.isEncrypted) {
if (this.sourceUrl.cachedValue) URL.revokeObjectURL(this.sourceUrl.cachedValue);
if (this.thumbnailUrl.cachedValue) URL.revokeObjectURL(this.thumbnailUrl.cachedValue);
}
}
private prepareSourceUrl = async (): Promise<string | null> => {
if (this.media.isEncrypted) {
const blob = await this.sourceBlob.value;
return URL.createObjectURL(blob);
} else {
return this.media.srcHttp;
}
};
private prepareThumbnailUrl = async (): Promise<string | null> => {
if (this.media.isEncrypted) {
const blob = await this.thumbnailBlob.value;
if (blob === null) return null;
return URL.createObjectURL(blob);
} else {
return this.media.thumbnailHttp;
}
};
private fetchSource = (): Promise<Blob> => {
if (this.media.isEncrypted) {
const content = this.event.getContent<MediaEventContent>();
return decryptFile(content.file!, content.info);
}
return this.media.downloadSource().then((r) => r.blob());
};
private fetchThumbnail = (): Promise<Blob | null> => {
if (!this.media.hasThumbnail) return Promise.resolve(null);
if (this.media.isEncrypted) {
const content = this.event.getContent<ImageContent>();
if (content.info?.thumbnail_file) {
return decryptFile(content.info.thumbnail_file, content.info.thumbnail_info);
} else {
// "Should never happen"
logger.warn("Media claims to have thumbnail and is encrypted, but no thumbnail_file found");
return Promise.resolve(null);
}
}
const thumbnailHttp = this.media.thumbnailHttp;
if (!thumbnailHttp) return Promise.resolve(null);
return fetch(thumbnailHttp).then((r) => r.blob());
};
public static isEligible(event: MatrixEvent): boolean {
if (!event) return false;
if (event.isRedacted()) return false;
if (event.getType() === EventType.Sticker) return true;
if (event.getType() !== EventType.RoomMessage) return false;
const content = event.getContent();
const mediaMsgTypes: string[] = [MsgType.Video, MsgType.Audio, MsgType.Image, MsgType.File];
if (mediaMsgTypes.includes(content.msgtype!)) return true;
if (typeof content.url === "string") return true;
// Finally, it's probably not media
return false;
}
/**
* Determine if the media event in question supports being hidden in the timeline.
* @param event Any matrix event.
* @returns `true` if the media can be hidden, otherwise false.
*/
public static canHide(event: MatrixEvent): boolean {
if (!event) return false;
if (event.isRedacted()) return false;
const content = event.getContent();
const hideTypes: string[] = [MsgType.Video, MsgType.Image];
if (hideTypes.includes(content.msgtype!)) return true;
return false;
}
}