Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 5cdd706

Browse files
committed
Demonstrative example of MSC3916 using blobs/async auth
1 parent 906c9dd commit 5cdd706

File tree

9 files changed

+128
-9
lines changed

9 files changed

+128
-9
lines changed

src/components/structures/BackdropPanel.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ limitations under the License.
1616

1717
import React, { CSSProperties } from "react";
1818

19+
import AuthedImage from "../views/elements/AuthedImage";
20+
1921
interface IProps {
2022
backgroundImage?: string;
2123
blurMultiplier?: number;
@@ -36,7 +38,7 @@ export const BackdropPanel: React.FC<IProps> = ({ backgroundImage, blurMultiplie
3638
}
3739
return (
3840
<div className="mx_BackdropPanel">
39-
<img role="presentation" alt="" style={styles} className="mx_BackdropPanel--image" src={backgroundImage} />
41+
<AuthedImage role="presentation" alt="" style={styles} className="mx_BackdropPanel--image" src={backgroundImage} />
4042
</div>
4143
);
4244
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
Copyright 2024 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React, {useEffect} from "react";
18+
19+
import {canAddAuthToMediaUrl, getMediaByUrl} from "../../../utils/media";
20+
21+
interface IProps extends React.HTMLProps<HTMLImageElement> {
22+
}
23+
24+
const AuthedImage: React.FC<IProps> = (props) => {
25+
const [src, setSrc] = React.useState<string | undefined>("");
26+
27+
useEffect(() => {
28+
let blobUrl: string | undefined;
29+
async function getImage(): Promise<void> {
30+
if (props.src) {
31+
if (await canAddAuthToMediaUrl(props.src)) {
32+
const response = await getMediaByUrl(props.src);
33+
blobUrl = URL.createObjectURL(await response.blob());
34+
setSrc(blobUrl);
35+
} else {
36+
// Skip blob caching if we're just doing a plain http(s) request.
37+
setSrc(props.src);
38+
}
39+
}
40+
}
41+
42+
// noinspection JSIgnoredPromiseFromCall
43+
getImage();
44+
45+
return () => {
46+
// Cleanup
47+
if (blobUrl) {
48+
URL.revokeObjectURL(blobUrl);
49+
}
50+
};
51+
}, [props.src]);
52+
53+
const props2 = {...props};
54+
props2.src = src;
55+
56+
return (
57+
// eslint-disable-next-line jsx-a11y/alt-text
58+
<img {...props2} />
59+
);
60+
};
61+
62+
export default AuthedImage;

src/components/views/elements/ImageView.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
3737
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
3838
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
3939
import { presentableTextForFile } from "../../../utils/FileUtils";
40+
import AuthedImage from "./AuthedImage";
4041
import AccessibleButton from "./AccessibleButton";
4142

4243
// Max scale to keep gaps around the image
@@ -585,7 +586,7 @@ export default class ImageView extends React.Component<IProps, IState> {
585586
onMouseUp={this.onEndMoving}
586587
onMouseLeave={this.onEndMoving}
587588
>
588-
<img
589+
<AuthedImage
589590
src={this.props.src}
590591
style={style}
591592
alt={this.props.name}

src/components/views/messages/MImageBody.tsx

+17-3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import { presentableTextForFile } from "../../../utils/FileUtils";
4141
import { createReconnectedListener } from "../../../utils/connection";
4242
import MediaProcessingError from "./shared/MediaProcessingError";
4343
import { DecryptError, DownloadError } from "../../../utils/DecryptFile";
44+
import AuthedImage from "../elements/AuthedImage";
45+
import {getMediaByUrl} from "../../../utils/media";
4446

4547
enum Placeholder {
4648
NoImage,
@@ -301,7 +303,19 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
301303
img.onerror = reject;
302304
});
303305
img.crossOrigin = "Anonymous"; // CORS allow canvas access
304-
img.src = contentUrl ?? "";
306+
307+
if (contentUrl) {
308+
const response = await getMediaByUrl(contentUrl);
309+
const blob = await response.blob();
310+
const blobUrl = URL.createObjectURL(blob);
311+
img.src = blobUrl;
312+
313+
loadPromise.then(() => {
314+
URL.revokeObjectURL(blobUrl);
315+
});
316+
} else {
317+
img.src = "";
318+
}
305319

306320
try {
307321
await loadPromise;
@@ -435,7 +449,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
435449
imageElement = <HiddenImagePlaceholder />;
436450
} else {
437451
imageElement = (
438-
<img
452+
<AuthedImage
439453
style={{ display: "none" }}
440454
src={thumbUrl}
441455
ref={this.image}
@@ -478,7 +492,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
478492
// which has the same width as the timeline
479493
// mx_MImageBody_thumbnail resizes img to exactly container size
480494
img = (
481-
<img
495+
<AuthedImage
482496
className="mx_MImageBody_thumbnail"
483497
src={thumbUrl}
484498
ref={this.image}

src/components/views/messages/ReactionsRowButton.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import ReactionsRowButtonTooltip from "./ReactionsRowButtonTooltip";
2626
import AccessibleButton from "../elements/AccessibleButton";
2727
import MatrixClientContext from "../../../contexts/MatrixClientContext";
2828
import { REACTION_SHORTCODE_KEY } from "./ReactionsRow";
29+
import AuthedImage from "../elements/AuthedImage";
2930

3031
export interface IProps {
3132
// The event we're displaying reactions for
@@ -144,7 +145,7 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
144145
const imageSrc = mediaFromMxc(content).srcHttp;
145146
if (imageSrc) {
146147
reactionContent = (
147-
<img
148+
<AuthedImage
148149
className="mx_ReactionsRowButton_content"
149150
alt={customReactionName || _t("timeline|reactions|custom_reaction_fallback_label")}
150151
src={imageSrc}

src/components/views/rooms/LinkPreviewWidget.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { mediaFromMxc } from "../../../customisations/Media";
2626
import ImageView from "../elements/ImageView";
2727
import LinkWithTooltip from "../elements/LinkWithTooltip";
2828
import PlatformPeg from "../../../PlatformPeg";
29+
import AuthedImage from "../elements/AuthedImage";
2930

3031
interface IProps {
3132
link: string;
@@ -95,7 +96,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
9596
if (image) {
9697
img = (
9798
<div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
98-
<img
99+
<AuthedImage
99100
ref={this.image}
100101
style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }}
101102
src={image}

src/components/views/spaces/SpaceBasicSettings.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { _t } from "../../../languageHandler";
2020
import AccessibleButton from "../elements/AccessibleButton";
2121
import Field from "../elements/Field";
2222
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
23+
import AuthedImage from "../elements/AuthedImage";
2324

2425
interface IProps {
2526
avatarUrl?: string;
@@ -44,7 +45,7 @@ export const SpaceAvatar: React.FC<Pick<IProps, "avatarUrl" | "avatarDisabled" |
4445
let avatarSection;
4546
if (avatarDisabled) {
4647
if (avatar) {
47-
avatarSection = <img className="mx_SpaceBasicSettings_avatar" src={avatar} alt="" />;
48+
avatarSection = <AuthedImage className="mx_SpaceBasicSettings_avatar" src={avatar} alt="" />;
4849
} else {
4950
avatarSection = <div className="mx_SpaceBasicSettings_avatar" />;
5051
}

src/customisations/Media.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { Optional } from "matrix-events-sdk";
2121
import { MatrixClientPeg } from "../MatrixClientPeg";
2222
import { IPreparedMedia, prepEventContentAsMedia } from "./models/IMediaEventContent";
2323
import { UserFriendlyError } from "../languageHandler";
24+
import {getMediaByUrl} from "../utils/media";
2425

2526
// Populate this class with the details of your customisations when copying it.
2627

@@ -149,7 +150,7 @@ export class Media {
149150
if (!src) {
150151
throw new UserFriendlyError("error|download_media");
151152
}
152-
return fetch(src);
153+
return getMediaByUrl(src);
153154
}
154155
}
155156

src/utils/media.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
Copyright 2024 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import {MatrixClientPeg} from "../MatrixClientPeg";
18+
19+
export async function canAddAuthToMediaUrl(url: string): Promise<boolean> {
20+
return url.includes("/_matrix/media/v3") && Boolean(await MatrixClientPeg.get()?.doesServerSupportUnstableFeature("org.matrix.msc3916"));
21+
}
22+
23+
export async function getMediaByUrl(url: string): Promise<Response> {
24+
// If the server doesn't support unstable auth, don't use it :)
25+
if (!(await canAddAuthToMediaUrl(url))) {
26+
return fetch(url);
27+
}
28+
29+
// We can rewrite the URL to support auth now, and request accordingly.
30+
url = url.replace(/\/media\/v3\/(.*)\//, "/client/unstable/org.matrix.msc3916/media/$1/");
31+
return fetch(url, {
32+
headers: {
33+
'Authorization': `Bearer ${MatrixClientPeg.get()?.getAccessToken()}`,
34+
},
35+
});
36+
}

0 commit comments

Comments
 (0)