mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-06 10:50:13 +00:00
213 lines
7.0 KiB
TypeScript
213 lines
7.0 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {type ImageLoadEventData} from 'expo-image';
|
|
import React, {useCallback, useContext, useMemo, useState} from 'react';
|
|
import {Pressable, View} from 'react-native';
|
|
import Animated from 'react-native-reanimated';
|
|
import {SvgUri} from 'react-native-svg';
|
|
|
|
import ExpoImage from '@components/expo_image';
|
|
import ExternalImage, {isSVGImage} from '@components/external_image';
|
|
import FileIcon from '@components/files/file_icon';
|
|
import {GalleryInit} from '@context/gallery';
|
|
import {useIsTablet} from '@hooks/device';
|
|
import {useGalleryItem} from '@hooks/gallery';
|
|
import {lookupMimeType} from '@utils/file';
|
|
import {openGalleryAtIndex} from '@utils/gallery';
|
|
import {getViewPortWidth, isGifTooLarge} from '@utils/images';
|
|
import {urlSafeBase64Encode} from '@utils/security';
|
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
|
import {extractFilenameFromUrl, isValidUrl} from '@utils/url';
|
|
|
|
import {MmBlocksLayoutWidthContext, MmBlocksRenderContext} from './context';
|
|
import {
|
|
computeMmBlocksImageLayout,
|
|
refineMmBlocksImageLayoutFromIntrinsic,
|
|
type MmBlocksImageLayout,
|
|
} from './utils/image';
|
|
|
|
import type {GalleryItemType} from '@typings/screens/gallery';
|
|
|
|
export type MmBlocksImageProps = {
|
|
altText?: string;
|
|
caps: {maxWidth?: number; maxHeight?: number};
|
|
imageMetadata?: PostImage;
|
|
imageStyle?: 'default' | 'person';
|
|
imageUrl: string;
|
|
theme: Theme;
|
|
};
|
|
|
|
type MmBlocksImageContentProps = MmBlocksImageProps & {
|
|
displaySrc: string;
|
|
};
|
|
|
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|
errorFrame: {
|
|
alignItems: 'center',
|
|
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
|
borderRadius: 4,
|
|
borderWidth: 1,
|
|
justifyContent: 'center',
|
|
},
|
|
}));
|
|
|
|
const MmBlocksImageContent = ({
|
|
altText,
|
|
caps,
|
|
displaySrc,
|
|
imageMetadata,
|
|
imageStyle = 'default',
|
|
imageUrl,
|
|
theme,
|
|
}: MmBlocksImageContentProps) => {
|
|
const layoutWidth = useContext(MmBlocksLayoutWidthContext);
|
|
const {location, postId} = useContext(MmBlocksRenderContext)!;
|
|
const galleryIdentifier = `${postId}-MmBlocksImage-${location}`;
|
|
const isTablet = useIsTablet();
|
|
const viewPortWidth = getViewPortWidth(false, isTablet);
|
|
const style = getStyleSheet(theme);
|
|
const [loadError, setLoadError] = useState(false);
|
|
const [intrinsicSize, setIntrinsicSize] = useState<{width: number; height: number} | undefined>(undefined);
|
|
|
|
const isSvg = isSVGImage(imageMetadata, imageUrl);
|
|
|
|
const layout = useMemo((): MmBlocksImageLayout => {
|
|
if (intrinsicSize && !imageMetadata?.width && !imageMetadata?.height) {
|
|
return refineMmBlocksImageLayoutFromIntrinsic(
|
|
intrinsicSize.width,
|
|
intrinsicSize.height,
|
|
caps,
|
|
layoutWidth,
|
|
viewPortWidth,
|
|
);
|
|
}
|
|
|
|
return computeMmBlocksImageLayout(caps, layoutWidth, imageMetadata, viewPortWidth);
|
|
}, [caps, imageMetadata, intrinsicSize, layoutWidth, viewPortWidth]);
|
|
|
|
const fileId = useMemo(() => `uid-mm-blocks-image-${urlSafeBase64Encode(imageUrl)}`, [imageUrl]);
|
|
|
|
const dimensionsStyle = useMemo(() => ({
|
|
width: layout.width,
|
|
height: layout.height,
|
|
}), [layout.height, layout.width]);
|
|
|
|
const imageBorderRadius = imageStyle === 'person' ? layout.width / 2 : 3;
|
|
|
|
const onPress = useCallback(() => {
|
|
const item: GalleryItemType = {
|
|
id: fileId,
|
|
postId,
|
|
uri: displaySrc,
|
|
width: layout.galleryWidth,
|
|
height: layout.galleryHeight,
|
|
name: altText || extractFilenameFromUrl(imageUrl) || 'image.png',
|
|
mime_type: lookupMimeType(imageUrl) || 'image/png',
|
|
type: 'image',
|
|
lastPictureUpdate: 0,
|
|
cacheKey: fileId,
|
|
};
|
|
openGalleryAtIndex(galleryIdentifier, 0, [item]);
|
|
}, [altText, displaySrc, fileId, galleryIdentifier, imageUrl, layout.galleryHeight, layout.galleryWidth, postId]);
|
|
|
|
const {ref, onGestureEvent, styles: galleryStyles} = useGalleryItem(
|
|
galleryIdentifier,
|
|
0,
|
|
onPress,
|
|
);
|
|
|
|
const onError = useCallback(() => {
|
|
setLoadError(true);
|
|
}, []);
|
|
|
|
const onLoad = useCallback((event: ImageLoadEventData) => {
|
|
const intrinsicWidth = event.source.width;
|
|
const intrinsicHeight = event.source.height;
|
|
if (!intrinsicWidth || !intrinsicHeight) {
|
|
return;
|
|
}
|
|
if (imageMetadata?.width && imageMetadata?.height) {
|
|
return;
|
|
}
|
|
setIntrinsicSize({width: intrinsicWidth, height: intrinsicHeight});
|
|
}, [imageMetadata?.height, imageMetadata?.width]);
|
|
|
|
if (!isValidUrl(imageUrl) || !displaySrc || loadError || (imageMetadata && isGifTooLarge(imageMetadata))) {
|
|
return (
|
|
<View style={[style.errorFrame, dimensionsStyle]}>
|
|
<FileIcon
|
|
failed={true}
|
|
testID='mm_blocks_image.error'
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const renderImage = () => {
|
|
if (isSvg) {
|
|
return (
|
|
<SvgUri
|
|
uri={displaySrc}
|
|
width={layout.width}
|
|
height={layout.height}
|
|
style={[dimensionsStyle, {borderRadius: imageBorderRadius}]}
|
|
onError={onError}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ExpoImage
|
|
id={fileId}
|
|
ref={ref}
|
|
nativeID={`MmBlocksImage-${fileId}`}
|
|
source={{uri: displaySrc}}
|
|
style={[dimensionsStyle, {borderRadius: imageBorderRadius}]}
|
|
contentFit={imageStyle === 'person' ? 'cover' : 'contain'}
|
|
onError={onError}
|
|
onLoad={onLoad}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const imageContent = (
|
|
<Pressable
|
|
onPress={onGestureEvent}
|
|
style={({pressed}) => pressed && {opacity: 0.72}}
|
|
accessibilityRole='imagebutton'
|
|
accessibilityLabel={altText}
|
|
>
|
|
{renderImage()}
|
|
</Pressable>
|
|
);
|
|
|
|
return (
|
|
<GalleryInit galleryIdentifier={galleryIdentifier}>
|
|
<Animated.View style={[galleryStyles, dimensionsStyle]}>
|
|
{imageContent}
|
|
</Animated.View>
|
|
</GalleryInit>
|
|
);
|
|
};
|
|
|
|
const MmBlocksImage = ({imageUrl, imageMetadata, ...contentProps}: MmBlocksImageProps) => {
|
|
return (
|
|
<ExternalImage
|
|
src={imageUrl}
|
|
imageMetadata={imageMetadata}
|
|
>
|
|
{(displaySrc: string) => (
|
|
<MmBlocksImageContent
|
|
{...contentProps}
|
|
displaySrc={displaySrc}
|
|
imageMetadata={imageMetadata}
|
|
imageUrl={imageUrl}
|
|
/>
|
|
)}
|
|
</ExternalImage>
|
|
);
|
|
};
|
|
|
|
export default MmBlocksImage;
|