mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
perf(desktop): cache display media and retain shown images (#3264)
This commit is contained in:
1 parent
6a24ac3f4e
commit
ee1c861eb2
14 files changed
+172
-18
No files matched your search
@@ -22,6 +22,7 @@ import {useDeleteAttachment} from '@app/features/messaging/hooks/useDeleteAttach
|
|||||||
import {useMatureMedia} from '@app/features/messaging/hooks/useMatureMedia';
|
import {useMatureMedia} from '@app/features/messaging/hooks/useMatureMedia';
|
||||||
import {useMediaFavorite} from '@app/features/messaging/hooks/useMediaFavorite';
|
import {useMediaFavorite} from '@app/features/messaging/hooks/useMediaFavorite';
|
||||||
import {useNearViewport} from '@app/features/messaging/hooks/useNearViewport';
|
import {useNearViewport} from '@app/features/messaging/hooks/useNearViewport';
|
||||||
|
import {resolveDesktopCrossOriginMediaURL} from '@app/features/messaging/utils/DesktopResourceUrl';
|
||||||
import {createDownloadHandler} from '@app/features/messaging/utils/FileDownloadUtils';
|
import {createDownloadHandler} from '@app/features/messaging/utils/FileDownloadUtils';
|
||||||
import * as ImageCacheUtils from '@app/features/messaging/utils/ImageCacheUtils';
|
import * as ImageCacheUtils from '@app/features/messaging/utils/ImageCacheUtils';
|
||||||
import {buildMediaProxyURL, mediaDevicePixelRatio} from '@app/features/messaging/utils/MediaProxyUtils';
|
import {buildMediaProxyURL, mediaDevicePixelRatio} from '@app/features/messaging/utils/MediaProxyUtils';
|
||||||
@@ -575,7 +576,7 @@ const EmbedVideo: FC<EmbedVideoProps> = observer(
|
|||||||
muted
|
muted
|
||||||
playsInline
|
playsInline
|
||||||
preload="metadata"
|
preload="metadata"
|
||||||
src={effectiveSrc}
|
src={resolveDesktopCrossOriginMediaURL(effectiveSrc)}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onLoadedMetadata={handleMetadataProbeLoaded}
|
onLoadedMetadata={handleMetadataProbeLoaded}
|
||||||
data-flx="channel.embeds.media.embed-video.metadata-probe"
|
data-flx="channel.embeds.media.embed-video.metadata-probe"
|
||||||
@@ -609,7 +610,7 @@ const EmbedVideo: FC<EmbedVideoProps> = observer(
|
|||||||
<video
|
<video
|
||||||
ref={inlineVideoRef}
|
ref={inlineVideoRef}
|
||||||
className={styles.inlineVideo}
|
className={styles.inlineVideo}
|
||||||
src={effectiveSrc}
|
src={resolveDesktopCrossOriginMediaURL(effectiveSrc)}
|
||||||
poster={presentedPosterSrc ?? undefined}
|
poster={presentedPosterSrc ?? undefined}
|
||||||
autoPlay
|
autoPlay
|
||||||
playsInline
|
playsInline
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {useMaybeMessageViewContext} from '@app/features/channel/components/Messa
|
|||||||
import {PAUSE_DESCRIPTOR, PLAY_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
import {PAUSE_DESCRIPTOR, PLAY_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||||
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
|
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
|
||||||
import {useAttachmentRefreshOnError} from '@app/features/messaging/hooks/useAttachmentRefreshOnError';
|
import {useAttachmentRefreshOnError} from '@app/features/messaging/hooks/useAttachmentRefreshOnError';
|
||||||
|
import {resolveDesktopCrossOriginMediaURL} from '@app/features/messaging/utils/DesktopResourceUrl';
|
||||||
import {buildMediaProxyURL} from '@app/features/messaging/utils/MediaProxyUtils';
|
import {buildMediaProxyURL} from '@app/features/messaging/utils/MediaProxyUtils';
|
||||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||||
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
||||||
@@ -324,7 +325,7 @@ const VoiceMessagePlayer: React.FC<VoiceMessagePlayerProps> = observer(
|
|||||||
>
|
>
|
||||||
<audio
|
<audio
|
||||||
ref={mediaRef as React.RefObject<HTMLAudioElement>}
|
ref={mediaRef as React.RefObject<HTMLAudioElement>}
|
||||||
src={hasStarted || wantsMetadata ? effectiveSrc : undefined}
|
src={hasStarted || wantsMetadata ? resolveDesktopCrossOriginMediaURL(effectiveSrc) : undefined}
|
||||||
preload={wantsMetadata ? 'metadata' : 'none'}
|
preload={wantsMetadata ? 'metadata' : 'none'}
|
||||||
onError={handleMediaError}
|
onError={handleMediaError}
|
||||||
data-flx="channel.embeds.media.voice-message-player.audio"
|
data-flx="channel.embeds.media.voice-message-player.audio"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {AnimatedCropSourceFormat, CropParams} from '@app/features/expressio
|
|||||||
import {CropPipelineError, cropAnimatedImage} from '@app/features/expressions/workers/AnimatedImageCropWorkerManager';
|
import {CropPipelineError, cropAnimatedImage} from '@app/features/expressions/workers/AnimatedImageCropWorkerManager';
|
||||||
import {showMessagingErrorModal} from '@app/features/messaging/components/alerts/MessagingErrorModalUtils';
|
import {showMessagingErrorModal} from '@app/features/messaging/components/alerts/MessagingErrorModalUtils';
|
||||||
import styles from '@app/features/messaging/components/modals/ImageCropModal.module.css';
|
import styles from '@app/features/messaging/components/modals/ImageCropModal.module.css';
|
||||||
|
import {resolveDesktopCrossOriginMediaURL} from '@app/features/messaging/utils/DesktopResourceUrl';
|
||||||
import {formatFileSize} from '@app/features/messaging/utils/FileUtils';
|
import {formatFileSize} from '@app/features/messaging/utils/FileUtils';
|
||||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||||
import {Button} from '@app/features/ui/button/Button';
|
import {Button} from '@app/features/ui/button/Button';
|
||||||
@@ -720,7 +721,7 @@ export const ImageCropModal: React.FC<ImageCropModalProps> = observer(
|
|||||||
width: displayDimensions.width * zoomRatio,
|
width: displayDimensions.width * zoomRatio,
|
||||||
height: displayDimensions.height * zoomRatio,
|
height: displayDimensions.height * zoomRatio,
|
||||||
};
|
};
|
||||||
const response = await fetch(imageUrl);
|
const response = await fetch(resolveDesktopCrossOriginMediaURL(imageUrl) ?? imageUrl);
|
||||||
if (controller.signal.aborted) return;
|
if (controller.signal.aborted) return;
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch image data');
|
throw new Error('Failed to fetch image data');
|
||||||
@@ -869,7 +870,7 @@ export const ImageCropModal: React.FC<ImageCropModalProps> = observer(
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
ref={imageRef}
|
ref={imageRef}
|
||||||
src={imageUrl}
|
src={resolveDesktopCrossOriginMediaURL(imageUrl)}
|
||||||
alt={i18n._(CROP_PREVIEW_DESCRIPTOR)}
|
alt={i18n._(CROP_PREVIEW_DESCRIPTOR)}
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const UPLOAD_RELAY_MULTIPART_PARAMETERS: ReadonlySet<string> = new Set(['uploadI
|
|||||||
const UPLOAD_RELAY_BASE_PATH_PATTERN = /(?:^|\/)relay(?:\/|$)/u;
|
const UPLOAD_RELAY_BASE_PATH_PATTERN = /(?:^|\/)relay(?:\/|$)/u;
|
||||||
const UPLOAD_RELAY_BASE_SUFFIX_PATTERN = /\/v1\/relay$/u;
|
const UPLOAD_RELAY_BASE_SUFFIX_PATTERN = /\/v1\/relay$/u;
|
||||||
const TRAILING_SLASHES_PATTERN = /\/+$/u;
|
const TRAILING_SLASHES_PATTERN = /\/+$/u;
|
||||||
|
const LOOPBACK_IPV4_PATTERN = /^127(?:\.\d{1,3}){3}$/u;
|
||||||
|
|
||||||
class DesktopUploadRelayUrlError extends Error {
|
class DesktopUploadRelayUrlError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
@@ -56,6 +57,44 @@ export function wrapDesktopLocalResourceURLForInstance(value: string, instanceKe
|
|||||||
return buildDesktopLocalResourceProxyURL(target, instanceKey);
|
return buildDesktopLocalResourceProxyURL(target, instanceKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveDesktopDisplayResourceURL(value: string): string {
|
||||||
|
if (!isDesktopLocalAppDocument()) return value;
|
||||||
|
const target = parseDisplayTarget(value);
|
||||||
|
if (target == null) return value;
|
||||||
|
if (isDirectlyDisplayableTarget(target)) return target.toString();
|
||||||
|
return wrapResolvedDesktopLocalResourceURL(target.toString(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDesktopDisplayResourceURLForInstance(value: string, instanceKey: string): string {
|
||||||
|
if (!isDesktopLocalAppDocument()) return value;
|
||||||
|
const target = parseDisplayTarget(value);
|
||||||
|
if (target != null && isDirectlyDisplayableTarget(target)) return target.toString();
|
||||||
|
return wrapDesktopLocalResourceURLForInstance(value, instanceKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDesktopCrossOriginMediaURL(value: string | undefined): string | undefined {
|
||||||
|
if (value == null || value.length === 0) return value;
|
||||||
|
return wrapDesktopLocalResourceURL(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDisplayTarget(value: string): URL | null {
|
||||||
|
try {
|
||||||
|
return new URL(parseDesktopLocalResourceProxyTarget(value));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDirectlyDisplayableTarget(target: URL): boolean {
|
||||||
|
if (target.protocol === 'https:') return true;
|
||||||
|
return target.protocol === 'http:' && isLoopbackHostname(target.hostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLoopbackHostname(hostname: string): boolean {
|
||||||
|
if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '[::1]') return true;
|
||||||
|
return LOOPBACK_IPV4_PATTERN.test(hostname);
|
||||||
|
}
|
||||||
|
|
||||||
export function updateDesktopLocalResourceURLTarget(value: string, updateTarget: (target: URL) => boolean): string {
|
export function updateDesktopLocalResourceURLTarget(value: string, updateTarget: (target: URL) => boolean): string {
|
||||||
const targetValue = parseDesktopLocalResourceProxyTarget(value);
|
const targetValue = parseDesktopLocalResourceProxyTarget(value);
|
||||||
let target: URL;
|
let target: URL;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import {retainImage} from '@app/features/messaging/utils/ImageRetention';
|
||||||
import {LRUCache} from 'lru-cache';
|
import {LRUCache} from 'lru-cache';
|
||||||
|
|
||||||
export interface CachedImageSize {
|
export interface CachedImageSize {
|
||||||
@@ -127,6 +128,7 @@ function failEntry(entry: ImageCacheEntry): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function settleLoaded(entry: ImageCacheEntry, image: HTMLImageElement): void {
|
function settleLoaded(entry: ImageCacheEntry, image: HTMLImageElement): void {
|
||||||
|
retainImage(image);
|
||||||
clearRetryState(entry);
|
clearRetryState(entry);
|
||||||
detachImageLoad(entry);
|
detachImageLoad(entry);
|
||||||
entry.loaded = true;
|
entry.loaded = true;
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import {LRUCache} from 'lru-cache';
|
||||||
|
|
||||||
|
const RETAINED_IMAGE_BYTE_BUDGET = 192 * 1024 * 1024;
|
||||||
|
const RETAINED_IMAGE_MAX_ENTRIES = 2000;
|
||||||
|
const RETAINED_IMAGE_MIN_BYTES = 1024;
|
||||||
|
const BYTES_PER_PIXEL = 4;
|
||||||
|
const MAX_RETAINED_SOURCE_LENGTH = 16 * 1024;
|
||||||
|
|
||||||
|
const retainedImages = new LRUCache<string, HTMLImageElement>({
|
||||||
|
max: RETAINED_IMAGE_MAX_ENTRIES,
|
||||||
|
maxSize: RETAINED_IMAGE_BYTE_BUDGET,
|
||||||
|
sizeCalculation: (image) => estimateDecodedBytes(image.naturalWidth, image.naturalHeight),
|
||||||
|
});
|
||||||
|
|
||||||
|
function estimateDecodedBytes(width: number, height: number): number {
|
||||||
|
return Math.min(RETAINED_IMAGE_BYTE_BUDGET, Math.max(RETAINED_IMAGE_MIN_BYTES, width * height * BYTES_PER_PIXEL));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRetainableSource(src: string): boolean {
|
||||||
|
return (
|
||||||
|
src.length > 0 && src.length <= MAX_RETAINED_SOURCE_LENGTH && !src.startsWith('data:') && !src.startsWith('blob:')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function retainImage(image: HTMLImageElement): void {
|
||||||
|
if (!image.complete || image.naturalWidth <= 0) return;
|
||||||
|
const src = image.currentSrc || image.src;
|
||||||
|
if (!isRetainableSource(src)) return;
|
||||||
|
if (retainedImages.get(src) != null) return;
|
||||||
|
const holder = new Image();
|
||||||
|
holder.decoding = 'async';
|
||||||
|
holder.src = src;
|
||||||
|
retainedImages.set(src, holder, {size: estimateDecodedBytes(image.naturalWidth, image.naturalHeight)});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installImageRetention(target: Document): () => void {
|
||||||
|
const handleLoad = (event: Event): void => {
|
||||||
|
if (event.target instanceof HTMLImageElement) retainImage(event.target);
|
||||||
|
};
|
||||||
|
target.addEventListener('load', handleLoad, true);
|
||||||
|
return () => target.removeEventListener('load', handleLoad, true);
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// @vitest-environment happy-dom
|
||||||
|
// @vitest-environment-options {"url":"fluxer-app://app/channels/@me"}
|
||||||
|
|
||||||
|
import {beforeEach, describe, expect, test, vi} from 'vitest';
|
||||||
|
|
||||||
|
const runtime = vi.hoisted(() => ({
|
||||||
|
mediaEndpoint: 'https://media.example.com',
|
||||||
|
staticCdnEndpoint: null as string | null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@app/features/platform/DesktopLocalAppRuntime', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<object>()),
|
||||||
|
isDesktopLocalAppDocument: () => true,
|
||||||
|
}));
|
||||||
|
vi.mock('@app/features/app/state/InstanceSnapshotStore', () => ({runtimeInstanceKey: () => 'instance-key'}));
|
||||||
|
vi.mock('@app/features/app/state/RuntimeConfig', () => ({
|
||||||
|
default: {
|
||||||
|
get mediaEndpoint() {
|
||||||
|
return runtime.mediaEndpoint;
|
||||||
|
},
|
||||||
|
get staticCdnEndpoint() {
|
||||||
|
return runtime.staticCdnEndpoint;
|
||||||
|
},
|
||||||
|
uploadRelayEndpoint: null,
|
||||||
|
getSnapshot: () => ({}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {buildMediaProxyURL} = await import('@app/features/messaging/utils/MediaProxyUtils');
|
||||||
|
const {resolveDesktopCrossOriginMediaURL} = await import('@app/features/messaging/utils/DesktopResourceUrl');
|
||||||
|
|
||||||
|
describe('desktop media URLs', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
runtime.mediaEndpoint = 'https://media.example.com';
|
||||||
|
});
|
||||||
|
|
||||||
|
test('images on https media load directly so the HTTP cache serves revisits', () => {
|
||||||
|
const url = buildMediaProxyURL('https://media.example.com/attachments/1/2/a.png', {format: 'webp', width: 320});
|
||||||
|
expect(url).toBe('https://media.example.com/attachments/1/2/a.png?format=webp&width=320');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('images on loopback http media load directly', () => {
|
||||||
|
runtime.mediaEndpoint = 'http://localhost:48090/media';
|
||||||
|
expect(buildMediaProxyURL('http://localhost:48090/media/avatars/1/a.webp')).toBe(
|
||||||
|
'http://localhost:48090/media/avatars/1/a.webp',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('images on cleartext non-loopback media stay behind the local proxy', () => {
|
||||||
|
runtime.mediaEndpoint = 'http://192.168.1.20/media';
|
||||||
|
expect(buildMediaProxyURL('http://192.168.1.20/media/avatars/1/a.webp')).toMatch(/^fluxer-app:\/\/app\/proxy\//);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cross-origin media elements keep a same-origin source', () => {
|
||||||
|
expect(resolveDesktopCrossOriginMediaURL('https://media.example.com/attachments/1/2/v.mp4')).toMatch(
|
||||||
|
/^fluxer-app:\/\/app\/proxy\/instance-key\?/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
resolveDesktopDisplayResourceURL,
|
||||||
|
resolveDesktopDisplayResourceURLForInstance,
|
||||||
unwrapDesktopLocalResourceURL,
|
unwrapDesktopLocalResourceURL,
|
||||||
wrapDesktopLocalResourceURL,
|
|
||||||
wrapDesktopLocalResourceURLForInstance,
|
|
||||||
} from '@app/features/messaging/utils/DesktopResourceUrl';
|
} from '@app/features/messaging/utils/DesktopResourceUrl';
|
||||||
import {MEDIA_PROXY_IMAGE_SIZES, type MediaProxyImageSize} from '@fluxer/constants/src/MediaProxyImageSizes';
|
import {MEDIA_PROXY_IMAGE_SIZES, type MediaProxyImageSize} from '@fluxer/constants/src/MediaProxyImageSizes';
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ export function buildMediaProxyURL(originalUrl: string, options: MediaProxyOptio
|
|||||||
if (!originalUrl) return originalUrl;
|
if (!originalUrl) return originalUrl;
|
||||||
const url = new URL(unwrapDesktopLocalResourceURL(originalUrl));
|
const url = new URL(unwrapDesktopLocalResourceURL(originalUrl));
|
||||||
appendMediaProxyParams(url, options);
|
appendMediaProxyParams(url, options);
|
||||||
return wrapDesktopLocalResourceURL(url.toString());
|
return resolveDesktopDisplayResourceURL(url.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildMediaProxyURLForInstance(
|
export function buildMediaProxyURLForInstance(
|
||||||
@@ -110,14 +110,14 @@ export function buildMediaProxyURLForInstance(
|
|||||||
if (!originalUrl) return originalUrl;
|
if (!originalUrl) return originalUrl;
|
||||||
const url = new URL(unwrapDesktopLocalResourceURL(originalUrl));
|
const url = new URL(unwrapDesktopLocalResourceURL(originalUrl));
|
||||||
appendMediaProxyParams(url, options);
|
appendMediaProxyParams(url, options);
|
||||||
return wrapDesktopLocalResourceURLForInstance(url.toString(), instanceKey);
|
return resolveDesktopDisplayResourceURLForInstance(url.toString(), instanceKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildFitInsideMediaProxyURL(originalUrl: string, options: FitInsideMediaProxyOptions = {}): string {
|
export function buildFitInsideMediaProxyURL(originalUrl: string, options: FitInsideMediaProxyOptions = {}): string {
|
||||||
if (!originalUrl) return originalUrl;
|
if (!originalUrl) return originalUrl;
|
||||||
const url = new URL(unwrapDesktopLocalResourceURL(originalUrl));
|
const url = new URL(unwrapDesktopLocalResourceURL(originalUrl));
|
||||||
appendMediaProxyParams(url, getFitInsideProxyOptions(options));
|
appendMediaProxyParams(url, getFitInsideProxyOptions(options));
|
||||||
return wrapDesktopLocalResourceURL(url.toString());
|
return resolveDesktopDisplayResourceURL(url.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
function readProxyDimensionParam(url: URL, key: 'width' | 'height'): number | undefined {
|
function readProxyDimensionParam(url: URL, key: 'width' | 'height'): number | undefined {
|
||||||
@@ -150,7 +150,7 @@ export function stripMediaProxyParams(proxyURL: string): string {
|
|||||||
url.searchParams.delete('format');
|
url.searchParams.delete('format');
|
||||||
url.searchParams.delete('quality');
|
url.searchParams.delete('quality');
|
||||||
url.searchParams.delete('animated');
|
url.searchParams.delete('animated');
|
||||||
return wrapDesktopLocalResourceURL(url.toString());
|
return resolveDesktopDisplayResourceURL(url.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildAnimatedImageProxyURL(proxyURL: string, width?: number, height?: number): string {
|
export function buildAnimatedImageProxyURL(proxyURL: string, width?: number, height?: number): string {
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ async function fetchFirstPngBlob(urls: Array<string>): Promise<Blob> {
|
|||||||
let lastError: unknown = null;
|
let lastError: unknown = null;
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url);
|
const response = await fetch(wrapDesktopLocalResourceURL(url));
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Unexpected response status ${response.status} for ${url}`);
|
throw new Error(`Unexpected response status ${response.status} for ${url}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import {PAUSE_DESCRIPTOR, PLAY_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
import {PAUSE_DESCRIPTOR, PLAY_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||||
import {useAttachmentRefreshOnError} from '@app/features/messaging/hooks/useAttachmentRefreshOnError';
|
import {useAttachmentRefreshOnError} from '@app/features/messaging/hooks/useAttachmentRefreshOnError';
|
||||||
|
import {resolveDesktopCrossOriginMediaURL} from '@app/features/messaging/utils/DesktopResourceUrl';
|
||||||
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
|
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
|
||||||
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
|
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
|
||||||
import styles from '@app/features/voice/components/media_player/AudioPlayer.module.css';
|
import styles from '@app/features/voice/components/media_player/AudioPlayer.module.css';
|
||||||
@@ -210,7 +211,7 @@ export function AudioPlayer({
|
|||||||
{/* biome-ignore lint/a11y/useMediaCaption: voice/audio attachments have no caption track source */}
|
{/* biome-ignore lint/a11y/useMediaCaption: voice/audio attachments have no caption track source */}
|
||||||
<audio
|
<audio
|
||||||
ref={mediaRef as React.RefObject<HTMLAudioElement>}
|
ref={mediaRef as React.RefObject<HTMLAudioElement>}
|
||||||
src={sourceAttribute}
|
src={resolveDesktopCrossOriginMediaURL(sourceAttribute)}
|
||||||
preload={preloadAttribute}
|
preload={preloadAttribute}
|
||||||
onError={handleMediaError}
|
onError={handleMediaError}
|
||||||
data-flx="voice.media-player.audio-player.audio"
|
data-flx="voice.media-player.audio-player.audio"
|
||||||
|
|||||||
+3
-2
@@ -9,6 +9,7 @@ import {
|
|||||||
REMOVE_FROM_FAVORITES_DESCRIPTOR,
|
REMOVE_FROM_FAVORITES_DESCRIPTOR,
|
||||||
} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||||
import {useAttachmentRefreshOnError} from '@app/features/messaging/hooks/useAttachmentRefreshOnError';
|
import {useAttachmentRefreshOnError} from '@app/features/messaging/hooks/useAttachmentRefreshOnError';
|
||||||
|
import {resolveDesktopCrossOriginMediaURL} from '@app/features/messaging/utils/DesktopResourceUrl';
|
||||||
import {formatFileSize} from '@app/features/messaging/utils/FileUtils';
|
import {formatFileSize} from '@app/features/messaging/utils/FileUtils';
|
||||||
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
||||||
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
|
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
|
||||||
@@ -205,7 +206,7 @@ export function InlineAudioPlayer({
|
|||||||
>
|
>
|
||||||
<audio
|
<audio
|
||||||
ref={mediaRef as React.RefObject<HTMLAudioElement>}
|
ref={mediaRef as React.RefObject<HTMLAudioElement>}
|
||||||
src={sourceAttribute}
|
src={resolveDesktopCrossOriginMediaURL(sourceAttribute)}
|
||||||
preload={preloadAttribute}
|
preload={preloadAttribute}
|
||||||
onError={handleMediaError}
|
onError={handleMediaError}
|
||||||
data-flx="voice.media-player.inline-audio-player.audio"
|
data-flx="voice.media-player.inline-audio-player.audio"
|
||||||
@@ -333,7 +334,7 @@ export function InlineAudioPlayer({
|
|||||||
>
|
>
|
||||||
<audio
|
<audio
|
||||||
ref={mediaRef as React.RefObject<HTMLAudioElement>}
|
ref={mediaRef as React.RefObject<HTMLAudioElement>}
|
||||||
src={sourceAttribute}
|
src={resolveDesktopCrossOriginMediaURL(sourceAttribute)}
|
||||||
preload={preloadAttribute}
|
preload={preloadAttribute}
|
||||||
onError={handleMediaError}
|
onError={handleMediaError}
|
||||||
data-flx="voice.media-player.inline-audio-player.audio--2"
|
data-flx="voice.media-player.inline-audio-player.audio--2"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import Accessibility from '@app/features/accessibility/state/Accessibility';
|
import Accessibility from '@app/features/accessibility/state/Accessibility';
|
||||||
|
import {resolveDesktopCrossOriginMediaURL} from '@app/features/messaging/utils/DesktopResourceUrl';
|
||||||
import * as ImageCacheUtils from '@app/features/messaging/utils/ImageCacheUtils';
|
import * as ImageCacheUtils from '@app/features/messaging/utils/ImageCacheUtils';
|
||||||
import {decodeThumbHashDataURL} from '@app/features/messaging/utils/ThumbHashUtils';
|
import {decodeThumbHashDataURL} from '@app/features/messaging/utils/ThumbHashUtils';
|
||||||
import {observeResize} from '@app/features/platform/utils/SharedResizeObserver';
|
import {observeResize} from '@app/features/platform/utils/SharedResizeObserver';
|
||||||
@@ -155,7 +156,7 @@ function VideoSeekPreviewThumbnail({src, poster, time}: VideoSeekPreviewThumbnai
|
|||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
className={styles.seekPreviewVideo}
|
className={styles.seekPreviewVideo}
|
||||||
src={src}
|
src={resolveDesktopCrossOriginMediaURL(src)}
|
||||||
poster={poster}
|
poster={poster}
|
||||||
preload="metadata"
|
preload="metadata"
|
||||||
muted
|
muted
|
||||||
@@ -606,7 +607,7 @@ export const VideoPlayer = observer(function VideoPlayer({
|
|||||||
<video
|
<video
|
||||||
ref={mediaRef as React.RefObject<HTMLVideoElement>}
|
ref={mediaRef as React.RefObject<HTMLVideoElement>}
|
||||||
className={clsx(styles.video, renderModel.shouldHideVideo && styles.videoHidden)}
|
className={clsx(styles.video, renderModel.shouldHideVideo && styles.videoHidden)}
|
||||||
src={renderModel.shouldAttachSource ? src : undefined}
|
src={renderModel.shouldAttachSource ? resolveDesktopCrossOriginMediaURL(src) : undefined}
|
||||||
preload={renderModel.preloadAttribute}
|
preload={renderModel.preloadAttribute}
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
playsInline
|
playsInline
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
|
|||||||
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
|
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
|
||||||
import {PanZoomSurface} from '@app/features/messaging/components/modals/media_modal/pan_zoom/PanZoomSurface';
|
import {PanZoomSurface} from '@app/features/messaging/components/modals/media_modal/pan_zoom/PanZoomSurface';
|
||||||
import type {ZoomState} from '@app/features/messaging/components/modals/media_modal/shared';
|
import type {ZoomState} from '@app/features/messaging/components/modals/media_modal/shared';
|
||||||
|
import {resolveDesktopCrossOriginMediaURL} from '@app/features/messaging/utils/DesktopResourceUrl';
|
||||||
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
||||||
import {
|
import {
|
||||||
clampMediaTime,
|
clampMediaTime,
|
||||||
@@ -404,7 +405,7 @@ export const MobileVideoViewer = observer(function MobileVideoViewer({
|
|||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
className={styles.video}
|
className={styles.video}
|
||||||
src={src}
|
src={resolveDesktopCrossOriginMediaURL(src)}
|
||||||
autoPlay={initialTime === undefined}
|
autoPlay={initialTime === undefined}
|
||||||
playsInline
|
playsInline
|
||||||
loop={loop}
|
loop={loop}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import '@app/features/theme/styles/preflight.css';
|
|||||||
import {resolveDomainMigrationSide} from '@app/features/app/domain_migration/DomainMigrationCore';
|
import {resolveDomainMigrationSide} from '@app/features/app/domain_migration/DomainMigrationCore';
|
||||||
import {runDomainMigrationPreMount} from '@app/features/app/domain_migration/DomainMigrationPreMount';
|
import {runDomainMigrationPreMount} from '@app/features/app/domain_migration/DomainMigrationPreMount';
|
||||||
import {installRuntimeConfigEffects} from '@app/features/app/state/RuntimeConfigEffects';
|
import {installRuntimeConfigEffects} from '@app/features/app/state/RuntimeConfigEffects';
|
||||||
|
import {installImageRetention} from '@app/features/messaging/utils/ImageRetention';
|
||||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||||
import {loadLazyModule} from '@app/features/platform/utils/LazyModuleLoader';
|
import {loadLazyModule} from '@app/features/platform/utils/LazyModuleLoader';
|
||||||
import {installUnhandledRejectionReporter} from '@app/features/platform/utils/UnhandledRejectionReporter';
|
import {installUnhandledRejectionReporter} from '@app/features/platform/utils/UnhandledRejectionReporter';
|
||||||
@@ -35,6 +36,7 @@ function loadAppBootstrap() {
|
|||||||
|
|
||||||
async function bootstrap(): Promise<void> {
|
async function bootstrap(): Promise<void> {
|
||||||
installRuntimeConfigEffects();
|
installRuntimeConfigEffects();
|
||||||
|
installImageRetention(document);
|
||||||
const passkeyBridgeSide =
|
const passkeyBridgeSide =
|
||||||
window.location.pathname === PASSKEY_BRIDGE_PATH ? resolveDomainMigrationSide(window.location.origin) : null;
|
window.location.pathname === PASSKEY_BRIDGE_PATH ? resolveDomainMigrationSide(window.location.origin) : null;
|
||||||
if (passkeyBridgeSide !== null) {
|
if (passkeyBridgeSide !== null) {
|
||||||
|
|||||||
Reference in new issue
Block a user