mattermost-mobile-upstream/app/components/expo_image/index.tsx
Elias Nahum b6fc85385e
Migrate from RNN to Expo router (#9402)
* Expo-Router partial migration

* Full migration to ExpoRouter

* i18n

* add tests

* Merge branch 'main' into expo-router

* align package-lock.json

* i18n

* feedback review

* feedback review

* android 16kb patch

* Refactored emoji picker keyboard using a state machine

* set android launch_screen in drawables instead of layout

* no conditional hooks

* remove duplicate call in test

* address feedback

* fix tests and address more feedback

* small corrections

* small corrections

* small corrections

* fix draft test

* fix draft test 2

* remove navigateToScreen in draff test

* fix nit in draft test

* remove doc file

* Feedback review

* fix snapshots

* improve test coverage

* fix i18n

* Upgrade t o RN 0.83, other dependencies and new architecture (#9737)

* library hardware-keyboard android new arch support fix

* library rnshare android new arch support fix

* library rnutils android new arch support fix

* library secure-pdf-viewer add new arch support

* Enable new arch on iOS and Android

* update emm and network client with new-arch

* update intune to use new-arch

* performance improvements

* fix tests

* fixes post input on new-arch

* fix android emoji picker

* remove doc

* run tests in parallel

* upgrade to React Native 0.83.9, Expo 55, React 19, and New Architecture

Bumps core dependencies to their latest versions:
- React Native 0.77.3 → 0.83.9 (New Architecture enabled)
- React 18.3.1 → 19.2.5
- Expo SDK 52 → 55
- TypeScript 5.8.3 → 6.0.3
- Node.js 22.16.0 → 24.15.0
- react-native-reanimated 3.17.3 → 4.3.0
- Various other dependency upgrades

Migrates iOS AppDelegate from Objective-C to Swift, updates Android
build tooling and Gradle wrapper, refreshes all patches and font
assets, and adapts app code, tests, and CI workflows throughout for
API changes introduced by these upgrades.

* remove unecessary dismissKeyboard in notifications

* feedback review

* update intune to use new-arch

* update @mattermost deps

* missing i18n string

* additional fixes

* update fastlane
2026-05-13 17:44:55 +08:00

184 lines
6.1 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(() => {
const src = props.source;
if (typeof src === 'number' || Array.isArray(src) || src == null) {
return src as ImageSource;
}
const objSource = src as ImageSource & {uri?: string; headers?: Record<string, string>};
const sourceHeaders = shouldAttachServerAuthHeaders(objSource.uri, serverUrl) && requestHeaders ? {...requestHeaders, ...objSource.headers} : objSource.headers;
delete sourceHeaders?.Accept;
// Only add cacheKey and cachePath if id is provided (i.e., not memory-only caching)
if (id) {
return {
...objSource,
headers: sourceHeaders,
cacheKey: id,
cachePath,
};
}
return {
...objSource,
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(() => {
const ph = props.placeholder;
if (!ph || typeof ph === 'number' || typeof ph === 'string' || Array.isArray(ph)) {
return ph as ImageSource | undefined;
}
const objPh = ph as ImageSource & {uri?: string; headers?: Record<string, string>};
const placeholderHeaders = shouldAttachServerAuthHeaders(objPh.uri, serverUrl) && requestHeaders ? {...requestHeaders, ...objPh.headers} : objPh.headers;
delete placeholderHeaders?.Accept;
// If placeholder has a uri and id is provided, add cachePath and cacheKey
if (objPh.uri && id) {
return {
...objPh,
headers: placeholderHeaders,
cacheKey: `${id}-thumb`,
cachePath,
};
}
return {
...objPh,
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(() => {
const src = props.source;
if (typeof src === 'number' || Array.isArray(src) || src == null) {
return src as ImageSource;
}
const objSource = src as ImageSource;
// Only add cacheKey and cachePath if id is provided (i.e., not memory-only caching)
if (id) {
return {
...objSource,
cacheKey: id,
cachePath,
};
}
return objSource;
}, [id, props.source, cachePath]);
// Process placeholder to add cachePath and cacheKey if it has a uri
const placeholder: ImageSource | undefined = useMemo(() => {
const ph = props.placeholder;
if (!ph || typeof ph === 'number' || typeof ph === 'string' || Array.isArray(ph)) {
return ph as ImageSource | undefined;
}
const objPh = ph as ImageSource & {uri?: string};
// If placeholder has a uri and id is provided, add cachePath and cacheKey
if (objPh.uri && id) {
return {
...objPh,
cacheKey: `${id}-thumb`,
cachePath,
};
}
return objPh;
}, [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;