From 5cdd706ef9e34ffc8a31a820c2c9fedf1694a767 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Apr 2024 15:43:24 -0600 Subject: [PATCH] Demonstrative example of MSC3916 using blobs/async auth --- src/components/structures/BackdropPanel.tsx | 4 +- src/components/views/elements/AuthedImage.tsx | 62 +++++++++++++++++++ src/components/views/elements/ImageView.tsx | 3 +- src/components/views/messages/MImageBody.tsx | 20 +++++- .../views/messages/ReactionsRowButton.tsx | 3 +- .../views/rooms/LinkPreviewWidget.tsx | 3 +- .../views/spaces/SpaceBasicSettings.tsx | 3 +- src/customisations/Media.ts | 3 +- src/utils/media.ts | 36 +++++++++++ 9 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 src/components/views/elements/AuthedImage.tsx create mode 100644 src/utils/media.ts diff --git a/src/components/structures/BackdropPanel.tsx b/src/components/structures/BackdropPanel.tsx index d0374805990..2a07cca574d 100644 --- a/src/components/structures/BackdropPanel.tsx +++ b/src/components/structures/BackdropPanel.tsx @@ -16,6 +16,8 @@ limitations under the License. import React, { CSSProperties } from "react"; +import AuthedImage from "../views/elements/AuthedImage"; + interface IProps { backgroundImage?: string; blurMultiplier?: number; @@ -36,7 +38,7 @@ export const BackdropPanel: React.FC = ({ backgroundImage, blurMultiplie } return (
- +
); }; diff --git a/src/components/views/elements/AuthedImage.tsx b/src/components/views/elements/AuthedImage.tsx new file mode 100644 index 00000000000..37ab249d5d9 --- /dev/null +++ b/src/components/views/elements/AuthedImage.tsx @@ -0,0 +1,62 @@ +/* +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 React, {useEffect} from "react"; + +import {canAddAuthToMediaUrl, getMediaByUrl} from "../../../utils/media"; + +interface IProps extends React.HTMLProps { +} + +const AuthedImage: React.FC = (props) => { + const [src, setSrc] = React.useState(""); + + useEffect(() => { + let blobUrl: string | undefined; + async function getImage(): Promise { + if (props.src) { + if (await canAddAuthToMediaUrl(props.src)) { + const response = await getMediaByUrl(props.src); + blobUrl = URL.createObjectURL(await response.blob()); + setSrc(blobUrl); + } else { + // Skip blob caching if we're just doing a plain http(s) request. + setSrc(props.src); + } + } + } + + // noinspection JSIgnoredPromiseFromCall + getImage(); + + return () => { + // Cleanup + if (blobUrl) { + URL.revokeObjectURL(blobUrl); + } + }; + }, [props.src]); + + const props2 = {...props}; + props2.src = src; + + return ( + // eslint-disable-next-line jsx-a11y/alt-text + + ); +}; + +export default AuthedImage; diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index ddc2769c48d..d9a824535aa 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -37,6 +37,7 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { presentableTextForFile } from "../../../utils/FileUtils"; +import AuthedImage from "./AuthedImage"; import AccessibleButton from "./AccessibleButton"; // Max scale to keep gaps around the image @@ -585,7 +586,7 @@ export default class ImageView extends React.Component { onMouseUp={this.onEndMoving} onMouseLeave={this.onEndMoving} > - {this.props.name} { img.onerror = reject; }); img.crossOrigin = "Anonymous"; // CORS allow canvas access - img.src = contentUrl ?? ""; + + if (contentUrl) { + const response = await getMediaByUrl(contentUrl); + const blob = await response.blob(); + const blobUrl = URL.createObjectURL(blob); + img.src = blobUrl; + + loadPromise.then(() => { + URL.revokeObjectURL(blobUrl); + }); + } else { + img.src = ""; + } try { await loadPromise; @@ -435,7 +449,7 @@ export default class MImageBody extends React.Component { imageElement = ; } else { imageElement = ( - { // which has the same width as the timeline // mx_MImageBody_thumbnail resizes img to exactly container size img = ( - {customReactionName { if (image) { img = (
- ; + avatarSection = ; } else { avatarSection =
; } diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts index 25e8489658d..6d253348b6d 100644 --- a/src/customisations/Media.ts +++ b/src/customisations/Media.ts @@ -21,6 +21,7 @@ import { Optional } from "matrix-events-sdk"; import { MatrixClientPeg } from "../MatrixClientPeg"; import { IPreparedMedia, prepEventContentAsMedia } from "./models/IMediaEventContent"; import { UserFriendlyError } from "../languageHandler"; +import {getMediaByUrl} from "../utils/media"; // Populate this class with the details of your customisations when copying it. @@ -149,7 +150,7 @@ export class Media { if (!src) { throw new UserFriendlyError("error|download_media"); } - return fetch(src); + return getMediaByUrl(src); } } diff --git a/src/utils/media.ts b/src/utils/media.ts new file mode 100644 index 00000000000..497414db6c9 --- /dev/null +++ b/src/utils/media.ts @@ -0,0 +1,36 @@ +/* +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 {MatrixClientPeg} from "../MatrixClientPeg"; + +export async function canAddAuthToMediaUrl(url: string): Promise { + return url.includes("/_matrix/media/v3") && Boolean(await MatrixClientPeg.get()?.doesServerSupportUnstableFeature("org.matrix.msc3916")); +} + +export async function getMediaByUrl(url: string): Promise { + // If the server doesn't support unstable auth, don't use it :) + if (!(await canAddAuthToMediaUrl(url))) { + return fetch(url); + } + + // We can rewrite the URL to support auth now, and request accordingly. + url = url.replace(/\/media\/v3\/(.*)\//, "/client/unstable/org.matrix.msc3916/media/$1/"); + return fetch(url, { + headers: { + 'Authorization': `Bearer ${MatrixClientPeg.get()?.getAccessToken()}`, + }, + }); +}