Rocket.Chat.ReactNative/app/views/AttachmentView.tsx

203 lines
6.8 KiB
TypeScript

import { CameraRoll } from '@react-native-camera-roll/camera-roll';
import { HeaderBackground, useHeaderHeight } from '@react-navigation/elements';
import { StackNavigationOptions } from '@react-navigation/stack';
import { ResizeMode, Video } from 'expo-av';
import React from 'react';
import { PermissionsAndroid, useWindowDimensions, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { shallowEqual } from 'react-redux';
import RNFetchBlob from 'rn-fetch-blob';
import { isImageBase64 } from '../lib/methods';
import RCActivityIndicator from '../containers/ActivityIndicator';
import * as HeaderButton from '../containers/HeaderButton';
import { ImageViewer } from '../containers/ImageViewer';
import StatusBar from '../containers/StatusBar';
import { LISTENER } from '../containers/Toast';
import { IAttachment } from '../definitions';
import I18n from '../i18n';
import { useAppSelector } from '../lib/hooks';
import { useAppNavigation, useAppRoute } from '../lib/hooks/navigation';
import { formatAttachmentUrl, isAndroid } from '../lib/methods/helpers';
import EventEmitter from '../lib/methods/helpers/events';
import { getUserSelector } from '../selectors/login';
import { TNavigation } from '../stacks/stackType';
import { useTheme } from '../theme';
import { LOCAL_DOCUMENT_DIRECTORY, getFilename } from '../lib/methods/handleMediaDownload';
const RenderContent = ({
setLoading,
attachment
}: {
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
attachment: IAttachment;
}) => {
const videoRef = React.useRef<Video>(null);
const insets = useSafeAreaInsets();
const { width, height } = useWindowDimensions();
const headerHeight = useHeaderHeight();
const navigation = useAppNavigation<TNavigation, 'AttachmentView'>();
const { baseUrl, user } = useAppSelector(
state => ({
baseUrl: state.server.server,
user: { id: getUserSelector(state).id, token: getUserSelector(state).token }
}),
shallowEqual
);
React.useLayoutEffect(() => {
const blurSub = navigation.addListener('blur', () => {
if (videoRef.current && videoRef.current.stopAsync) {
videoRef.current.stopAsync();
}
});
return () => {
blurSub();
};
}, [navigation]);
if (attachment.image_url) {
const url = formatAttachmentUrl(attachment.title_link || attachment.image_url, user.id, user.token, baseUrl);
const uri = encodeURI(url);
return (
<ImageViewer
uri={uri}
onLoadEnd={() => setLoading(false)}
width={width}
height={height - insets.top - insets.bottom - (headerHeight || 0)}
/>
);
}
if (attachment.video_url) {
const url = formatAttachmentUrl(attachment.video_url, user.id, user.token, baseUrl);
const uri = encodeURI(url);
return (
<Video
source={{ uri }}
rate={1.0}
volume={1.0}
isMuted={false}
resizeMode={ResizeMode.CONTAIN}
shouldPlay
isLooping={false}
style={{ flex: 1 }}
useNativeControls
onLoad={() => setLoading(false)}
onError={console.log}
ref={videoRef}
/>
);
}
return null;
};
const AttachmentView = (): React.ReactElement => {
const navigation = useAppNavigation<TNavigation, 'AttachmentView'>();
const {
params: { attachment }
} = useAppRoute<TNavigation, 'AttachmentView'>();
const [loading, setLoading] = React.useState(true);
const { colors } = useTheme();
const { baseUrl, user, Allow_Save_Media_to_Gallery } = useAppSelector(
state => ({
baseUrl: state.server.server,
user: { id: getUserSelector(state).id, token: getUserSelector(state).token },
Allow_Save_Media_to_Gallery: (state.settings.Allow_Save_Media_to_Gallery as boolean) ?? true
}),
shallowEqual
);
const setHeader = () => {
let { title } = attachment;
try {
if (title) {
title = decodeURI(title);
}
} catch {
// Do nothing
}
const options: StackNavigationOptions = {
title: title || '',
headerTitleAlign: 'center',
headerTitleStyle: { color: colors.previewTintColor },
headerTintColor: colors.previewTintColor,
headerTitleContainerStyle: { flex: 1, maxWidth: undefined },
headerLeftContainerStyle: { flexGrow: undefined, flexBasis: undefined },
headerRightContainerStyle: { flexGrow: undefined, flexBasis: undefined },
headerLeft: () => (
<HeaderButton.CloseModal testID='close-attachment-view' navigation={navigation} color={colors.previewTintColor} />
),
headerRight: () =>
Allow_Save_Media_to_Gallery && !isImageBase64(attachment.image_url) ? (
<HeaderButton.Download testID='save-image' onPress={handleSave} color={colors.previewTintColor} />
) : null,
headerBackground: () => (
<HeaderBackground style={{ backgroundColor: colors.previewBackground, shadowOpacity: 0, elevation: 0 }} />
)
};
navigation.setOptions(options);
};
React.useLayoutEffect(() => {
setHeader();
}, [navigation]);
const handleSave = async () => {
const { title_link, image_url, image_type, video_url, video_type } = attachment;
// When the attachment is a video, the video_url refers to local file and the title_link to the link
const url = video_url || title_link || image_url;
if (!url) {
return;
}
if (isAndroid) {
const rationale = {
title: I18n.t('Write_External_Permission'),
message: I18n.t('Write_External_Permission_Message'),
buttonPositive: 'Ok'
};
const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, rationale);
if (!(result || result === PermissionsAndroid.RESULTS.GRANTED)) {
return;
}
}
setLoading(true);
try {
if (LOCAL_DOCUMENT_DIRECTORY && url.startsWith(LOCAL_DOCUMENT_DIRECTORY)) {
await CameraRoll.save(url, { album: 'Rocket.Chat' });
} else {
const mediaAttachment = formatAttachmentUrl(url, user.id, user.token, baseUrl);
let filename = '';
if (image_url) {
filename = getFilename({ title: attachment.title, type: 'image', mimeType: image_type, url });
} else {
filename = getFilename({ title: attachment.title, type: 'video', mimeType: video_type, url });
}
const documentDir = `${RNFetchBlob.fs.dirs.DocumentDir}/`;
const path = `${documentDir + filename}`;
const file = await RNFetchBlob.config({ path }).fetch('GET', mediaAttachment);
await CameraRoll.save(path, { album: 'Rocket.Chat' });
file.flush();
}
EventEmitter.emit(LISTENER, { message: I18n.t('saved_to_gallery') });
} catch (e) {
EventEmitter.emit(LISTENER, { message: I18n.t(image_url ? 'error-save-image' : 'error-save-video') });
}
setLoading(false);
};
return (
<View style={{ backgroundColor: colors.backgroundColor, flex: 1 }}>
<StatusBar barStyle='light-content' backgroundColor={colors.previewBackground} />
<RenderContent attachment={attachment} setLoading={setLoading} />
{loading ? <RCActivityIndicator absolute size='large' /> : null}
</View>
);
};
export default AttachmentView;