Reuse media content/info types from the js-sdk (#12308)

This commit is contained in:
Michael Telatynski 2024-03-11 09:30:00 +00:00 committed by GitHub
parent 89eb884821
commit 431ae32304
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 74 additions and 258 deletions

View file

@ -76,6 +76,7 @@ module.exports = {
group: [
"matrix-js-sdk/src/**",
"!matrix-js-sdk/src/matrix",
"!matrix-js-sdk/src/types",
"matrix-js-sdk/lib",
"matrix-js-sdk/lib/",
"matrix-js-sdk/lib/**",

27
src/@types/matrix-js-sdk.d.ts vendored Normal file
View file

@ -0,0 +1,27 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { BLURHASH_FIELD } from "../utils/image-media";
// Matrix JS SDK extensions
declare module "matrix-js-sdk" {
export interface FileInfo {
/**
* @see https://github.com/matrix-org/matrix-spec-proposals/pull/2448
*/
[BLURHASH_FIELD]?: string;
}
}

View file

@ -28,19 +28,19 @@ import {
UploadProgress,
THREAD_RELATION_TYPE,
} from "matrix-js-sdk/src/matrix";
import {
ImageInfo,
AudioInfo,
VideoInfo,
EncryptedFile,
MediaEventContent,
MediaEventInfo,
} from "matrix-js-sdk/src/types";
import encrypt from "matrix-encrypt-attachment";
import extractPngChunks from "png-chunks-extract";
import { logger } from "matrix-js-sdk/src/logger";
import { removeElement } from "matrix-js-sdk/src/utils";
import {
AudioInfo,
EncryptedFile,
ImageInfo,
IMediaEventContent,
IMediaEventInfo,
VideoInfo,
} from "./customisations/models/IMediaEventContent";
import dis from "./dispatcher/dispatcher";
import { _t } from "./languageHandler";
import Modal from "./Modal";
@ -537,7 +537,7 @@ export default class ContentMessages {
promBefore?: Promise<any>,
): Promise<void> {
const fileName = file.name || _t("common|attachment");
const content: Omit<IMediaEventContent, "info"> & { info: Partial<IMediaEventInfo> } = {
const content: Omit<MediaEventContent, "info"> & { info: Partial<MediaEventInfo> } = {
body: fileName,
info: {
size: file.size,

View file

@ -17,12 +17,12 @@ limitations under the License.
import React from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { IContent } from "matrix-js-sdk/src/matrix";
import { MediaEventContent } from "matrix-js-sdk/src/types";
import { Playback } from "../../../audio/Playback";
import InlineSpinner from "../elements/InlineSpinner";
import { _t } from "../../../languageHandler";
import AudioPlayer from "../audio_messages/AudioPlayer";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
import MFileBody from "./MFileBody";
import { IBodyProps } from "./IBodyProps";
import { PlaybackManager } from "../../../audio/PlaybackManager";
@ -67,7 +67,7 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
// We should have a buffer to work with now: let's set it up
// Note: we don't actually need a waveform to render an audio event, but voice messages do.
const content = this.props.mxEvent.getContent<IMediaEventContent & IContent>();
const content = this.props.mxEvent.getContent<MediaEventContent & IContent>();
const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map((p: number) => p / 1024);
// We should have a buffer to work with now: let's set it up

View file

@ -16,6 +16,7 @@ limitations under the License.
import React, { AllHTMLAttributes, createRef } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { MediaEventContent } from "matrix-js-sdk/src/types";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
@ -23,7 +24,6 @@ import AccessibleButton from "../elements/AccessibleButton";
import { mediaFromContent } from "../../../customisations/Media";
import ErrorDialog from "../dialogs/ErrorDialog";
import { fileSize, presentableTextForFile } from "../../../utils/FileUtils";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
import { IBodyProps } from "./IBodyProps";
import { FileDownloader } from "../../../utils/FileDownloader";
import TextWithTooltip from "../elements/TextWithTooltip";
@ -128,8 +128,8 @@ export default class MFileBody extends React.Component<IProps, IState> {
const media = mediaFromContent(this.props.mxEvent.getContent());
return media.srcHttp;
}
private get content(): IMediaEventContent {
return this.props.mxEvent.getContent<IMediaEventContent>();
private get content(): MediaEventContent {
return this.props.mxEvent.getContent<MediaEventContent>();
}
private get fileName(): string {

View file

@ -21,6 +21,7 @@ import classNames from "classnames";
import { CSSTransition, SwitchTransition } from "react-transition-group";
import { logger } from "matrix-js-sdk/src/logger";
import { ClientEvent, ClientEventHandlerMap } from "matrix-js-sdk/src/matrix";
import { ImageContent } from "matrix-js-sdk/src/types";
import { Tooltip } from "@vector-im/compound-web";
import MFileBody from "./MFileBody";
@ -30,7 +31,6 @@ import SettingsStore from "../../../settings/SettingsStore";
import Spinner from "../elements/Spinner";
import { Media, mediaFromContent } from "../../../customisations/Media";
import { BLURHASH_FIELD, createThumbnail } from "../../../utils/image-media";
import { ImageContent } from "../../../customisations/models/IMediaEventContent";
import ImageView from "../elements/ImageView";
import { IBodyProps } from "./IBodyProps";
import { ImageSize, suggestedSize as suggestedImageSize } from "../../../settings/enums/ImageSize";

View file

@ -15,9 +15,9 @@ limitations under the License.
*/
import React from "react";
import { ImageContent } from "matrix-js-sdk/src/types";
import MImageBody from "./MImageBody";
import { ImageContent } from "../../../customisations/models/IMediaEventContent";
const FORCED_IMAGE_HEIGHT = 44;

View file

@ -16,10 +16,10 @@ limitations under the License.
import React, { ComponentProps, ReactNode } from "react";
import { Tooltip } from "@vector-im/compound-web";
import { MediaEventContent } from "matrix-js-sdk/src/types";
import MImageBody from "./MImageBody";
import { BLURHASH_FIELD } from "../../../utils/image-media";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
export default class MStickerBody extends MImageBody {
// Mostly empty to prevent default behaviour of MImageBody
@ -80,7 +80,7 @@ export default class MStickerBody extends MImageBody {
return null;
}
protected getBanner(content: IMediaEventContent): ReactNode {
protected getBanner(content: MediaEventContent): ReactNode {
return null; // we don't need a banner, we have a tooltip
}
}

View file

@ -16,6 +16,7 @@ limitations under the License.
import React, { ReactNode } from "react";
import { decode } from "blurhash";
import { MediaEventContent } from "matrix-js-sdk/src/types";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler";
@ -23,7 +24,6 @@ import SettingsStore from "../../../settings/SettingsStore";
import InlineSpinner from "../elements/InlineSpinner";
import { mediaFromContent } from "../../../customisations/Media";
import { BLURHASH_FIELD } from "../../../utils/image-media";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
import { IBodyProps } from "./IBodyProps";
import MFileBody from "./MFileBody";
import { ImageSize, suggestedSize as suggestedVideoSize } from "../../../settings/enums/ImageSize";
@ -62,7 +62,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
}
private getContentUrl(): string | undefined {
const content = this.props.mxEvent.getContent<IMediaEventContent>();
const content = this.props.mxEvent.getContent<MediaEventContent>();
// During export, the content url will point to the MSC, which will later point to a local url
if (this.props.forExport) return content.file?.url ?? content.url;
const media = mediaFromContent(content);
@ -82,7 +82,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
// there's no need of thumbnail when the content is local
if (this.props.forExport) return null;
const content = this.props.mxEvent.getContent<IMediaEventContent>();
const content = this.props.mxEvent.getContent<MediaEventContent>();
const media = mediaFromContent(content);
if (media.isEncrypted && this.state.decryptedThumbnailUrl) {
@ -121,7 +121,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
posterLoading: true,
});
const content = this.props.mxEvent.getContent<IMediaEventContent>();
const content = this.props.mxEvent.getContent<MediaEventContent>();
const media = mediaFromContent(content);
if (media.hasThumbnail) {
const image = new Image();
@ -157,7 +157,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
this.props.onHeightChanged?.();
} else {
logger.log("NOT preloading video");
const content = this.props.mxEvent.getContent<IMediaEventContent>();
const content = this.props.mxEvent.getContent<MediaEventContent>();
let mimetype = content?.info?.mimetype;

View file

@ -15,10 +15,11 @@
*/
import { MatrixClient, ResizeMethod } from "matrix-js-sdk/src/matrix";
import { MediaEventContent } from "matrix-js-sdk/src/types";
import { Optional } from "matrix-events-sdk";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { IMediaEventContent, IPreparedMedia, prepEventContentAsMedia } from "./models/IMediaEventContent";
import { IPreparedMedia, prepEventContentAsMedia } from "./models/IMediaEventContent";
import { UserFriendlyError } from "../languageHandler";
// Populate this class with the details of your customisations when copying it.
@ -154,11 +155,11 @@ export class Media {
/**
* Creates a media object from event content.
* @param {IMediaEventContent} content The event content.
* @param {MediaEventContent} content The event content.
* @param {MatrixClient} client? Optional client to use.
* @returns {Media} The media object.
*/
export function mediaFromContent(content: Partial<IMediaEventContent>, client?: MatrixClient): Media {
export function mediaFromContent(content: Partial<MediaEventContent>, client?: MatrixClient): Media {
return new Media(prepEventContentAsMedia(content), client);
}

View file

@ -14,220 +14,7 @@
* limitations under the License.
*/
// TODO: These types should be elsewhere.
import { MsgType } from "matrix-js-sdk/src/matrix";
import { BLURHASH_FIELD } from "../../utils/image-media";
/**
* @see https://spec.matrix.org/v1.7/client-server-api/#extensions-to-mroommessage-msgtypes
*/
export interface EncryptedFile {
/**
* The URL to the file.
*/
url: string;
/**
* A JSON Web Key object.
*/
key: {
alg: string;
key_ops: string[]; // eslint-disable-line camelcase
kty: string;
k: string;
ext: boolean;
};
/**
* The 128-bit unique counter block used by AES-CTR, encoded as unpadded base64.
*/
iv: string;
/**
* A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64.
* Clients should support the SHA-256 hash, which uses the key sha256.
*/
hashes: { [alg: string]: string };
/**
* Version of the encrypted attachment's protocol. Must be v2.
*/
v: string;
}
interface ThumbnailInfo {
/**
* The mimetype of the image, e.g. image/jpeg.
*/
mimetype?: string;
/**
* The intended display width of the image in pixels.
* This may differ from the intrinsic dimensions of the image file.
*/
w?: number;
/**
* The intended display height of the image in pixels.
* This may differ from the intrinsic dimensions of the image file.
*/
h?: number;
/**
* Size of the image in bytes.
*/
size?: number;
}
interface BaseInfo {
mimetype?: string;
size?: number;
}
/**
* @see https://spec.matrix.org/v1.7/client-server-api/#mfile
*/
export interface FileInfo extends BaseInfo {
/**
* @see https://github.com/matrix-org/matrix-spec-proposals/pull/2448
*/
[BLURHASH_FIELD]?: string;
/**
* Information on the encrypted thumbnail file, as specified in End-to-end encryption.
* Only present if the thumbnail is encrypted.
* @see https://spec.matrix.org/v1.7/client-server-api/#sending-encrypted-attachments
*/
thumbnail_file?: EncryptedFile;
/**
* Metadata about the image referred to in thumbnail_url.
*/
thumbnail_info?: ThumbnailInfo;
/**
* The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted.
*/
thumbnail_url?: string;
}
/**
* @see https://spec.matrix.org/v1.7/client-server-api/#mimage
*
*/
export interface ImageInfo extends FileInfo, ThumbnailInfo {}
/**
* @see https://spec.matrix.org/v1.7/client-server-api/#mimage
*/
export interface AudioInfo extends BaseInfo {
/**
* The duration of the audio in milliseconds.
*/
duration?: number;
}
/**
* @see https://spec.matrix.org/v1.7/client-server-api/#mvideo
*/
export interface VideoInfo extends AudioInfo, ImageInfo {
/**
* The duration of the video in milliseconds.
*/
duration?: number;
}
export type IMediaEventInfo = FileInfo | ImageInfo | AudioInfo | VideoInfo;
interface BaseContent {
/**
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
* @see https://spec.matrix.org/v1.7/client-server-api/#sending-encrypted-attachments
*/
file?: EncryptedFile;
/**
* Required if the file is unencrypted. The URL (typically mxc:// URI) to the file.
*/
url?: string;
}
/**
* @see https://spec.matrix.org/v1.7/client-server-api/#mfile
*/
export interface FileContent extends BaseContent {
/**
* A human-readable description of the file.
* This is recommended to be the filename of the original upload.
*/
body: string;
/**
* The original filename of the uploaded file.
*/
filename?: string;
/**
* Information about the file referred to in url.
*/
info?: FileInfo;
/**
* One of: [m.file].
*/
msgtype: MsgType.File;
}
/**
* @see https://spec.matrix.org/v1.7/client-server-api/#mimage
*/
export interface ImageContent extends BaseContent {
/**
* A textual representation of the image.
* This could be the alt text of the image, the filename of the image,
* or some kind of content description for accessibility e.g. image attachment.
*/
body: string;
/**
* Metadata about the image referred to in url.
*/
info?: ImageInfo;
/**
* One of: [m.image].
*/
msgtype: MsgType.Image;
}
/**
* @see https://spec.matrix.org/v1.7/client-server-api/#maudio
*/
export interface AudioContent extends BaseContent {
/**
* A description of the audio e.g. Bee Gees - Stayin Alive,
* or some kind of content description for accessibility e.g. audio attachment.
*/
body: string;
/**
* Metadata for the audio clip referred to in url.
*/
info?: AudioInfo;
/**
* One of: [m.audio].
*/
msgtype: MsgType.Audio;
}
/**
* @see https://spec.matrix.org/v1.7/client-server-api/#mvideo
*/
export interface VideoContent extends BaseContent {
/**
* A description of the video e.g. Gangnam style,
* or some kind of content description for accessibility e.g. video attachment.
*/
body: string;
/**
* Metadata about the video clip referred to in url.
*/
info?: VideoInfo;
/**
* One of: [m.video].
*/
msgtype: MsgType.Video;
}
/**
* Type representing media event contents for `m.room.message` events listed in the Matrix specification
*/
export type IMediaEventContent = FileContent | ImageContent | AudioContent | VideoContent;
import { EncryptedFile, MediaEventContent } from "matrix-js-sdk/src/types";
export interface IPreparedMedia extends IMediaObject {
thumbnail?: IMediaObject;
@ -241,11 +28,11 @@ export interface IMediaObject {
/**
* Parses an event content body into a prepared media object. This prepared media object
* can be used with other functions to manipulate the media.
* @param {IMediaEventContent} content Unredacted media event content. See interface.
* @param {MediaEventContent} content Unredacted media event content. See interface.
* @returns {IPreparedMedia} A prepared media object.
* @throws Throws if the given content cannot be packaged into a prepared media object.
*/
export function prepEventContentAsMedia(content: Partial<IMediaEventContent>): IPreparedMedia {
export function prepEventContentAsMedia(content: Partial<MediaEventContent>): IPreparedMedia {
let thumbnail: IMediaObject | undefined;
if (typeof content?.info === "object" && "thumbnail_url" in content.info && content.info.thumbnail_url) {
thumbnail = {

View file

@ -15,8 +15,7 @@ limitations under the License.
*/
import { IEventRelation, UploadProgress } from "matrix-js-sdk/src/matrix";
import { EncryptedFile } from "../customisations/models/IMediaEventContent";
import { EncryptedFile } from "matrix-js-sdk/src/types";
export class RoomUpload {
public readonly abortController = new AbortController();

View file

@ -17,9 +17,9 @@ limitations under the License.
// Pull in the encryption lib so that we can decrypt attachments.
import encrypt from "matrix-encrypt-attachment";
import { parseErrorResponse } from "matrix-js-sdk/src/matrix";
import { EncryptedFile, MediaEventInfo } from "matrix-js-sdk/src/types";
import { mediaFromContent } from "../customisations/Media";
import { EncryptedFile, IMediaEventInfo } from "../customisations/models/IMediaEventContent";
import { getBlobSafeMimeType } from "./blobs";
export class DownloadError extends Error {
@ -44,10 +44,10 @@ export class DecryptError extends Error {
* This passed to [link]{@link https://github.com/matrix-org/matrix-encrypt-attachment}
* as the encryption info object, so will also have the those keys in addition to
* the keys below.
* @param {IMediaEventInfo} info The info parameter taken from the matrix event.
* @param {MediaEventInfo} info The info parameter taken from the matrix event.
* @returns {Promise<Blob>} Resolves to a Blob of the file.
*/
export async function decryptFile(file?: EncryptedFile, info?: IMediaEventInfo): Promise<Blob> {
export async function decryptFile(file?: EncryptedFile, info?: MediaEventInfo): Promise<Blob> {
// throws if file is falsy
const media = mediaFromContent({ file });

View file

@ -25,22 +25,22 @@ import {
FileSizeReturnArray,
FileSizeReturnObject,
} from "filesize";
import { MediaEventContent } from "matrix-js-sdk/src/types";
import { IMediaEventContent } from "../customisations/models/IMediaEventContent";
import { _t } from "../languageHandler";
/**
* Extracts a human-readable label for the file attachment to use as
* link text.
*
* @param {IMediaEventContent} content The "content" key of the matrix event.
* @param {MediaEventContent} content The "content" key of the matrix event.
* @param {string} fallbackText The fallback text
* @param {boolean} withSize Whether to include size information. Default true.
* @param {boolean} shortened Ensure the extension of the file name is visible. Default false.
* @return {string} the human-readable link text for the attachment.
*/
export function presentableTextForFile(
content: IMediaEventContent,
content: MediaEventContent,
fallbackText = _t("common|attachment"),
withSize = true,
shortened = false,

View file

@ -15,12 +15,12 @@ limitations under the License.
*/
import { MatrixEvent, EventType, MsgType } from "matrix-js-sdk/src/matrix";
import { FileContent, ImageContent, MediaEventContent } from "matrix-js-sdk/src/types";
import { logger } from "matrix-js-sdk/src/logger";
import { LazyValue } from "./LazyValue";
import { Media, mediaFromContent } from "../customisations/Media";
import { decryptFile } from "./DecryptFile";
import { FileContent, ImageContent, IMediaEventContent } from "../customisations/models/IMediaEventContent";
import { IDestroyable } from "./IDestroyable";
// TODO: We should consider caching the blobs. https://github.com/vector-im/element-web/issues/17192
@ -48,7 +48,7 @@ export class MediaEventHelper implements IDestroyable {
public get fileName(): string {
return (
this.event.getContent<FileContent>().filename ||
this.event.getContent<IMediaEventContent>().body ||
this.event.getContent<MediaEventContent>().body ||
"download"
);
}
@ -81,7 +81,7 @@ export class MediaEventHelper implements IDestroyable {
private fetchSource = (): Promise<Blob> => {
if (this.media.isEncrypted) {
const content = this.event.getContent<IMediaEventContent>();
const content = this.event.getContent<MediaEventContent>();
return decryptFile(content.file!, content.info);
}
return this.media.downloadSource().then((r) => r.blob());

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import { Direction, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { MediaEventContent } from "matrix-js-sdk/src/types";
import { saveAs } from "file-saver";
import { logger } from "matrix-js-sdk/src/logger";
import sanitizeFilename from "sanitize-filename";
@ -24,7 +25,6 @@ import { decryptFile } from "../DecryptFile";
import { mediaFromContent } from "../../customisations/Media";
import { formatFullDateNoDay, formatFullDateNoDayISO } from "../../DateUtils";
import { isVoiceMessage } from "../EventUtils";
import { IMediaEventContent } from "../../customisations/models/IMediaEventContent";
import { _t } from "../../languageHandler";
import SdkConfig from "../../SdkConfig";
@ -225,7 +225,7 @@ export default abstract class Exporter {
let blob: Blob | undefined = undefined;
try {
const isEncrypted = event.isEncrypted();
const content = event.getContent<IMediaEventContent>();
const content = event.getContent<MediaEventContent>();
const shouldDecrypt = isEncrypted && content.hasOwnProperty("file") && event.getType() !== "m.sticker";
if (shouldDecrypt) {
blob = await decryptFile(content.file);

View file

@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EncryptedFile } from "matrix-js-sdk/src/types";
import { BlurhashEncoder } from "../BlurhashEncoder";
import { EncryptedFile } from "../customisations/models/IMediaEventContent";
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;

View file

@ -26,6 +26,7 @@ import {
RelationType,
TypedEventEmitter,
} from "matrix-js-sdk/src/matrix";
import { EncryptedFile } from "matrix-js-sdk/src/types";
import {
ChunkRecordedPayload,
@ -38,7 +39,6 @@ import {
VoiceBroadcastRecorderEvent,
} from "..";
import { uploadFile } from "../../ContentMessages";
import { EncryptedFile } from "../../customisations/models/IMediaEventContent";
import { createVoiceMessageContent } from "../../utils/createVoiceMessageContent";
import { IDestroyable } from "../../utils/IDestroyable";
import dis from "../../dispatcher/dispatcher";

View file

@ -29,9 +29,9 @@ import {
Relations,
SyncState,
} from "matrix-js-sdk/src/matrix";
import { EncryptedFile } from "matrix-js-sdk/src/types";
import { uploadFile } from "../../../src/ContentMessages";
import { EncryptedFile } from "../../../src/customisations/models/IMediaEventContent";
import { createVoiceMessageContent } from "../../../src/utils/createVoiceMessageContent";
import {
createVoiceBroadcastRecorder,