mattermost-mobile/app/components/expo_image/index.tsx
Nick Misasi 46d46790b5
Fix Android image loads by attaching auth headers (#9554)
* Fix images not loading on android"

* Update app/components/expo_image/index.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update snapshots and remove accept

* Fix 16KB page size patch failing due to stale diff context (#9555)

The expo_image/index.tsx hunks in 9325-full.diff were generated before
the NetworkManager/header-handling code was added to ExpoImage. The stale
context lines caused patch's fuzzy matching to apply ExpoImage hunks to
ExpoImageBackground instead, then ExpoImageBackground hunks failed
because those sections were already modified.

Regenerated the expo_image diff hunks to match the current file state
with proper context lines for both ExpoImage (with header handling) and
ExpoImageBackground components.

https://claude.ai/code/session_01SGXWQzxrcPpWsq2YrjoUwd

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Your Name <larkox@gmail.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-26 21:36:03 -05:00

173 lines
5.8 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Image, ImageBackground, type ImageBackgroundProps, type ImageProps, type ImageSource} from 'expo-image';
import React, {forwardRef, useMemo} from 'react';
import Animated from 'react-native-reanimated';
import {useServerUrl} from '@context/server';
import NetworkManager from '@managers/network_manager';
import {urlSafeBase64Encode} from '@utils/security';
type ExpoImagePropsWithId = ImageProps & {id: string};
type ExpoImagePropsMemoryOnly = ImageProps & {cachePolicy: 'memory'; id?: string};
type ExpoImageProps = ExpoImagePropsWithId | ExpoImagePropsMemoryOnly;
type ExpoImageBackgroundPropsWithId = ImageBackgroundProps & {id: string};
type ExpoImageBackgroundPropsMemoryOnly = ImageBackgroundProps & {cachePolicy: 'memory'; id?: string};
type ExpoImageBackgroundProps = ExpoImageBackgroundPropsWithId | ExpoImageBackgroundPropsMemoryOnly;
function shouldAttachServerAuthHeaders(uri: string | undefined, serverUrl: string) {
if (!uri) {
return false;
}
try {
const requestUrl = new URL(uri);
const serverBaseUrl = new URL(serverUrl);
if (requestUrl.origin !== serverBaseUrl.origin) {
return false;
}
return requestUrl.pathname.startsWith('/api/v4/');
} catch {
// On any parsing error, do not attach auth headers
return false;
}
}
const ExpoImage = forwardRef<Image, ExpoImageProps>(({id, ...props}, ref) => {
const serverUrl = useServerUrl();
const requestHeaders = useMemo(() => {
try {
const client = NetworkManager.getClient(serverUrl);
return client.getRequestHeaders('GET');
} catch {
return undefined;
}
}, [serverUrl]);
/**
* SECURITY NOTE: cachePath uses base64 encoding for URL safety, NOT encryption.
* Server URLs are not considered sensitive information, and this encoding is purely
* for filesystem path compatibility (avoiding special characters in directory names).
*/
const cachePath = useMemo(() => urlSafeBase64Encode(serverUrl), [serverUrl]);
const source: ImageSource = useMemo(() => {
if (typeof props.source === 'number') {
return props.source;
}
const sourceHeaders = shouldAttachServerAuthHeaders(props.source?.uri, serverUrl) && requestHeaders ? {...requestHeaders, ...props.source?.headers} : props.source?.headers;
delete sourceHeaders?.Accept;
// Only add cacheKey and cachePath if id is provided (i.e., not memory-only caching)
if (id) {
return {
...props.source,
headers: sourceHeaders,
cacheKey: id,
cachePath,
};
}
return {
...props.source,
headers: sourceHeaders,
};
}, [id, props.source, cachePath, requestHeaders, serverUrl]);
// Process placeholder to add cachePath and cacheKey if it has a uri
const placeholder: ImageSource | undefined = useMemo(() => {
if (!props.placeholder || typeof props.placeholder === 'number' || typeof props.placeholder === 'string') {
return props.placeholder;
}
const placeholderHeaders = shouldAttachServerAuthHeaders(props.placeholder?.uri, serverUrl) && requestHeaders ? {...requestHeaders, ...props.placeholder?.headers} : props.placeholder?.headers;
delete placeholderHeaders?.Accept;
// If placeholder has a uri and id is provided, add cachePath and cacheKey
if (props.placeholder.uri && id) {
return {
...props.placeholder,
headers: placeholderHeaders,
cacheKey: `${id}-thumb`,
cachePath,
};
}
return {
...props.placeholder,
headers: placeholderHeaders,
};
}, [props.placeholder, id, cachePath, requestHeaders, serverUrl]);
return (
<Image
ref={ref}
{...props}
source={source}
placeholder={placeholder}
/>
);
});
ExpoImage.displayName = 'ExpoImage';
const ExpoImageBackground = ({id, ...props}: ExpoImageBackgroundProps) => {
const serverUrl = useServerUrl();
const cachePath = useMemo(() => urlSafeBase64Encode(serverUrl), [serverUrl]);
const source: ImageSource = useMemo(() => {
if (typeof props.source === 'number') {
return props.source;
}
// Only add cacheKey and cachePath if id is provided (i.e., not memory-only caching)
if (id) {
return {
...props.source,
cacheKey: id,
cachePath,
};
}
return props.source;
}, [id, props.source, cachePath]);
// Process placeholder to add cachePath and cacheKey if it has a uri
const placeholder: ImageSource | undefined = useMemo(() => {
if (!props.placeholder || typeof props.placeholder === 'number' || typeof props.placeholder === 'string') {
return props.placeholder;
}
// If placeholder has a uri and id is provided, add cachePath and cacheKey
if (props.placeholder.uri && id) {
return {
...props.placeholder,
cacheKey: `${id}-thumb`,
cachePath,
};
}
return props.placeholder;
}, [props.placeholder, id, cachePath]);
return (
<ImageBackground
{...props}
source={source}
placeholder={placeholder}
>
{props.children}
</ImageBackground>
);
};
const ExpoImageAnimated = Animated.createAnimatedComponent(ExpoImage);
export {
ExpoImageAnimated,
ExpoImageBackground,
};
export default ExpoImage;