chore: migrate AttachmentView to hooks (#5003)

* migrate AttachmentView to hooks

* fix types

* fix types

* fix lint
This commit is contained in:
Gleidson Daniel Silva 2023-06-29 18:10:49 -03:00 committed by GitHub
parent 01577d9534
commit 47fbde566d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 189 additions and 150 deletions

View File

@ -0,0 +1,43 @@
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { TNavigation } from '../../stacks/stackType';
import {
AdminPanelStackParamList,
ChatsStackParamList,
DisplayPrefStackParamList,
DrawerParamList,
E2EEnterYourPasswordStackParamList,
E2ESaveYourPasswordStackParamList,
InsideStackParamList,
NewMessageStackParamList,
OutsideModalParamList,
OutsideParamList,
ProfileStackParamList,
SettingsStackParamList
} from '../../stacks/types';
type TRoutes =
| ChatsStackParamList
| ProfileStackParamList
| SettingsStackParamList
| AdminPanelStackParamList
| DisplayPrefStackParamList
| DrawerParamList
| NewMessageStackParamList
| E2ESaveYourPasswordStackParamList
| E2EEnterYourPasswordStackParamList
| InsideStackParamList
| OutsideParamList
| OutsideModalParamList
| TNavigation;
export function useAppNavigation<ParamList extends TRoutes, RouteName extends keyof ParamList = keyof ParamList>() {
const navigation = useNavigation<StackNavigationProp<ParamList, RouteName>>();
return navigation;
}
export function useAppRoute<ParamList extends TRoutes, RouteName extends keyof ParamList = keyof ParamList>() {
const route = useRoute<RouteProp<ParamList, RouteName>>();
return route;
}

View File

@ -82,6 +82,7 @@ import {
SettingsStackParamList SettingsStackParamList
} from './types'; } from './types';
import { isIOS } from '../lib/methods/helpers'; import { isIOS } from '../lib/methods/helpers';
import { TNavigation } from './stackType';
// ChatsStackNavigator // ChatsStackNavigator
const ChatsStack = createStackNavigator<ChatsStackParamList>(); const ChatsStack = createStackNavigator<ChatsStackParamList>();
@ -306,7 +307,7 @@ const E2EEnterYourPasswordStackNavigator = () => {
}; };
// InsideStackNavigator // InsideStackNavigator
const InsideStack = createStackNavigator<InsideStackParamList>(); const InsideStack = createStackNavigator<InsideStackParamList & TNavigation>();
const InsideStackNavigator = () => { const InsideStackNavigator = () => {
const { theme } = React.useContext(ThemeContext); const { theme } = React.useContext(ThemeContext);

View File

@ -73,6 +73,7 @@ import {
ModalStackParamList ModalStackParamList
} from './types'; } from './types';
import { isIOS } from '../../lib/methods/helpers'; import { isIOS } from '../../lib/methods/helpers';
import { TNavigation } from '../stackType';
// ChatsStackNavigator // ChatsStackNavigator
const ChatsStack = createStackNavigator<MasterDetailChatsStackParamList>(); const ChatsStack = createStackNavigator<MasterDetailChatsStackParamList>();
@ -206,7 +207,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
}); });
// InsideStackNavigator // InsideStackNavigator
const InsideStack = createStackNavigator<MasterDetailInsideStackParamList>(); const InsideStack = createStackNavigator<MasterDetailInsideStackParamList & TNavigation>();
const InsideStackNavigator = React.memo(() => { const InsideStackNavigator = React.memo(() => {
const { theme } = React.useContext(ThemeContext); const { theme } = React.useContext(ThemeContext);
return ( return (

View File

@ -207,9 +207,6 @@ export type ModalStackParamList = {
export type MasterDetailInsideStackParamList = { export type MasterDetailInsideStackParamList = {
DrawerNavigator: NavigatorScreenParams<Partial<MasterDetailDrawerParamList>>; // TODO: Change DrawerNavigator: NavigatorScreenParams<Partial<MasterDetailDrawerParamList>>; // TODO: Change
ModalStackNavigator: NavigatorScreenParams<ModalStackParamList>; ModalStackNavigator: NavigatorScreenParams<ModalStackParamList>;
AttachmentView: {
attachment: IAttachment;
};
ModalBlockView: { ModalBlockView: {
data: any; // TODO: Change data: any; // TODO: Change
}; };

20
app/stacks/stackType.ts Normal file
View File

@ -0,0 +1,20 @@
import { IAttachment } from '../definitions';
import { IOptionsField } from '../views/NotificationPreferencesView/options';
export type TNavigation = {
PickerView: {
title: string;
data: IOptionsField[];
value?: string;
onSearch: (text?: string, offset?: number) => Promise<any>;
onEndReached?: (text?: string, offset?: number) => Promise<any>;
total?: number;
onChangeValue: Function;
};
ForwardLivechatView: {
rid: string;
};
AttachmentView: {
attachment: IAttachment;
};
};

View File

@ -288,9 +288,6 @@ export type InsideStackParamList = {
NewMessageStackNavigator: NavigatorScreenParams<NewMessageStackParamList>; NewMessageStackNavigator: NavigatorScreenParams<NewMessageStackParamList>;
E2ESaveYourPasswordStackNavigator: NavigatorScreenParams<E2ESaveYourPasswordStackParamList>; E2ESaveYourPasswordStackNavigator: NavigatorScreenParams<E2ESaveYourPasswordStackParamList>;
E2EEnterYourPasswordStackNavigator: NavigatorScreenParams<E2EEnterYourPasswordStackParamList>; E2EEnterYourPasswordStackNavigator: NavigatorScreenParams<E2EEnterYourPasswordStackParamList>;
AttachmentView: {
attachment: IAttachment;
};
StatusView: undefined; StatusView: undefined;
ShareView: { ShareView: {
attachments: IAttachment[]; attachments: IAttachment[];

View File

@ -1,83 +1,114 @@
import React from 'react';
import { PermissionsAndroid, StyleSheet, View } from 'react-native';
import { connect } from 'react-redux';
import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/native';
import CameraRoll from '@react-native-community/cameraroll'; import CameraRoll from '@react-native-community/cameraroll';
import * as mime from 'react-native-mime-types'; import { HeaderBackground, useHeaderHeight } from '@react-navigation/elements';
import RNFetchBlob from 'rn-fetch-blob'; import { StackNavigationOptions } from '@react-navigation/stack';
import { Video, ResizeMode } from 'expo-av'; import { ResizeMode, Video } from 'expo-av';
import { sha256 } from 'js-sha256'; import { sha256 } from 'js-sha256';
import { withSafeAreaInsets } from 'react-native-safe-area-context'; import React from 'react';
import { HeaderBackground, HeaderHeightContext } from '@react-navigation/elements'; import { PermissionsAndroid, useWindowDimensions, View } from 'react-native';
import * as mime from 'react-native-mime-types';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { shallowEqual } from 'react-redux';
import RNFetchBlob from 'rn-fetch-blob';
import { LISTENER } from '../containers/Toast';
import EventEmitter from '../lib/methods/helpers/events';
import I18n from '../i18n';
import { TSupportedThemes, withTheme } from '../theme';
import { ImageViewer } from '../containers/ImageViewer';
import { themes } from '../lib/constants';
import RCActivityIndicator from '../containers/ActivityIndicator'; import RCActivityIndicator from '../containers/ActivityIndicator';
import * as HeaderButton from '../containers/HeaderButton'; import * as HeaderButton from '../containers/HeaderButton';
import { isAndroid, formatAttachmentUrl } from '../lib/methods/helpers'; import { ImageViewer } from '../containers/ImageViewer';
import { getUserSelector } from '../selectors/login';
import { withDimensions } from '../dimensions';
import StatusBar from '../containers/StatusBar'; import StatusBar from '../containers/StatusBar';
import { InsideStackParamList } from '../stacks/types'; import { LISTENER } from '../containers/Toast';
import { IApplicationState, IUser, IAttachment } from '../definitions'; 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';
const styles = StyleSheet.create({ const RenderContent = ({
container: { setLoading,
flex: 1 attachment
} }: {
}); setLoading: React.Dispatch<React.SetStateAction<boolean>>;
interface IAttachmentViewState {
attachment: IAttachment; attachment: IAttachment;
loading: boolean; }) => {
} 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
);
interface IAttachmentViewProps { React.useLayoutEffect(() => {
navigation: StackNavigationProp<InsideStackParamList, 'AttachmentView'>; const blurSub = navigation.addListener('blur', () => {
route: RouteProp<InsideStackParamList, 'AttachmentView'>; if (videoRef.current && videoRef.current.stopAsync) {
theme?: TSupportedThemes; videoRef.current.stopAsync();
baseUrl: string;
width?: number;
height?: number;
insets?: { left: number; bottom: number; right: number; top: number };
user: IUser;
Allow_Save_Media_to_Gallery: boolean;
}
class AttachmentView extends React.Component<IAttachmentViewProps, IAttachmentViewState> {
private unsubscribeBlur: (() => void) | undefined;
private videoRef: any;
constructor(props: IAttachmentViewProps) {
super(props);
const attachment = props.route.params?.attachment;
this.state = { attachment, loading: true };
this.setHeader();
}
componentDidMount() {
const { navigation } = this.props;
this.unsubscribeBlur = navigation.addListener('blur', () => {
if (this.videoRef && this.videoRef.stopAsync) {
this.videoRef.stopAsync();
} }
}); });
} return () => {
blurSub();
};
}, [navigation]);
componentWillUnmount() { if (attachment.image_url) {
if (this.unsubscribeBlur) { const url = formatAttachmentUrl(attachment.title_link || attachment.image_url, user.id, user.token, baseUrl);
this.unsubscribeBlur(); 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;
};
setHeader = () => { const AttachmentView = (): React.ReactElement => {
const { route, navigation, theme, Allow_Save_Media_to_Gallery } = this.props; const navigation = useAppNavigation<TNavigation, 'AttachmentView'>();
const attachment = route.params?.attachment; 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; let { title } = attachment;
try { try {
if (title) { if (title) {
@ -89,30 +120,30 @@ class AttachmentView extends React.Component<IAttachmentViewProps, IAttachmentVi
const options: StackNavigationOptions = { const options: StackNavigationOptions = {
title: title || '', title: title || '',
headerTitleAlign: 'center', headerTitleAlign: 'center',
headerTitleStyle: { color: themes[theme!].previewTintColor }, headerTitleStyle: { color: colors.previewTintColor },
headerTintColor: themes[theme!].previewTintColor, headerTintColor: colors.previewTintColor,
headerTitleContainerStyle: { flex: 1, maxWidth: undefined }, headerTitleContainerStyle: { flex: 1, maxWidth: undefined },
headerLeftContainerStyle: { flexGrow: undefined, flexBasis: undefined }, headerLeftContainerStyle: { flexGrow: undefined, flexBasis: undefined },
headerRightContainerStyle: { flexGrow: undefined, flexBasis: undefined }, headerRightContainerStyle: { flexGrow: undefined, flexBasis: undefined },
headerLeft: () => ( headerLeft: () => (
<HeaderButton.CloseModal testID='close-attachment-view' navigation={navigation} color={themes[theme!].previewTintColor} /> <HeaderButton.CloseModal testID='close-attachment-view' navigation={navigation} color={colors.previewTintColor} />
), ),
headerRight: () => headerRight: () =>
Allow_Save_Media_to_Gallery ? ( Allow_Save_Media_to_Gallery ? (
<HeaderButton.Download testID='save-image' onPress={this.handleSave} color={themes[theme!].previewTintColor} /> <HeaderButton.Download testID='save-image' onPress={handleSave} color={colors.previewTintColor} />
) : null, ) : null,
headerBackground: () => ( headerBackground: () => (
<HeaderBackground style={{ backgroundColor: themes[theme!].previewBackground, shadowOpacity: 0, elevation: 0 }} /> <HeaderBackground style={{ backgroundColor: colors.previewBackground, shadowOpacity: 0, elevation: 0 }} />
) )
}; };
navigation.setOptions(options); navigation.setOptions(options);
}; };
getVideoRef = (ref: Video) => (this.videoRef = ref); React.useLayoutEffect(() => {
setHeader();
}, [navigation]);
handleSave = async () => { const handleSave = async () => {
const { attachment } = this.state;
const { user, baseUrl } = this.props;
const { title_link, image_url, image_type, video_url, video_type } = attachment; const { title_link, image_url, image_type, video_url, video_type } = attachment;
const url = title_link || image_url || video_url; const url = title_link || image_url || video_url;
@ -133,7 +164,7 @@ class AttachmentView extends React.Component<IAttachmentViewProps, IAttachmentVi
} }
} }
this.setState({ loading: true }); setLoading(true);
try { try {
const extension = image_url const extension = image_url
? `.${mime.extension(image_type) || 'jpg'}` ? `.${mime.extension(image_type) || 'jpg'}`
@ -141,77 +172,24 @@ class AttachmentView extends React.Component<IAttachmentViewProps, IAttachmentVi
// The return of mime.extension('video/quicktime') is .qt, // The return of mime.extension('video/quicktime') is .qt,
// this format the iOS isn't recognize and can't save on gallery // this format the iOS isn't recognize and can't save on gallery
const documentDir = `${RNFetchBlob.fs.dirs.DocumentDir}/`; const documentDir = `${RNFetchBlob.fs.dirs.DocumentDir}/`;
const path = `${documentDir + sha256(url!) + extension}`; const path = `${documentDir + sha256(url) + extension}`;
const file = await RNFetchBlob.config({ path }).fetch('GET', mediaAttachment); const file = await RNFetchBlob.config({ path }).fetch('GET', mediaAttachment);
await CameraRoll.save(path, { album: 'Rocket.Chat' }); await CameraRoll.save(path, { album: 'Rocket.Chat' });
await file.flush(); file.flush();
EventEmitter.emit(LISTENER, { message: I18n.t('saved_to_gallery') }); EventEmitter.emit(LISTENER, { message: I18n.t('saved_to_gallery') });
} catch (e) { } catch (e) {
EventEmitter.emit(LISTENER, { message: I18n.t(image_url ? 'error-save-image' : 'error-save-video') }); EventEmitter.emit(LISTENER, { message: I18n.t(image_url ? 'error-save-image' : 'error-save-video') });
} }
this.setState({ loading: false }); setLoading(false);
}; };
renderImage = (uri: string) => { return (
const { width, height, insets } = this.props; <View style={{ backgroundColor: colors.backgroundColor, flex: 1 }}>
return ( <StatusBar barStyle='light-content' backgroundColor={colors.previewBackground} />
<HeaderHeightContext.Consumer> <RenderContent attachment={attachment} setLoading={setLoading} />
{headerHeight => ( {loading ? <RCActivityIndicator absolute size='large' /> : null}
<ImageViewer </View>
uri={uri}
onLoadEnd={() => this.setState({ loading: false })}
width={width!}
height={height! - insets!.top - insets!.bottom - (headerHeight || 0)}
/>
)}
</HeaderHeightContext.Consumer>
);
};
renderVideo = (uri: string) => (
<Video
source={{ uri }}
rate={1.0}
volume={1.0}
isMuted={false}
resizeMode={ResizeMode.CONTAIN}
shouldPlay
isLooping={false}
style={styles.container}
useNativeControls
onLoad={() => this.setState({ loading: false })}
onError={console.log}
ref={this.getVideoRef}
/>
); );
};
render() { export default AttachmentView;
const { loading, attachment } = this.state;
const { theme, user, baseUrl } = this.props;
let content = null;
if (attachment && attachment.image_url) {
const uri = formatAttachmentUrl(attachment.title_link || attachment.image_url, user.id, user.token, baseUrl);
content = this.renderImage(encodeURI(uri));
} else if (attachment && attachment.video_url) {
const uri = formatAttachmentUrl(attachment.video_url, user.id, user.token, baseUrl);
content = this.renderVideo(encodeURI(uri));
}
return (
<View style={[styles.container, { backgroundColor: themes[theme!].backgroundColor }]}>
<StatusBar barStyle='light-content' backgroundColor={themes[theme!].previewBackground} />
{content}
{loading ? <RCActivityIndicator absolute size='large' /> : null}
</View>
);
}
}
const mapStateToProps = (state: IApplicationState) => ({
baseUrl: state.server.server,
user: getUserSelector(state),
Allow_Save_Media_to_Gallery: (state.settings.Allow_Save_Media_to_Gallery as boolean) ?? true
});
export default connect(mapStateToProps)(withTheme(withDimensions(withSafeAreaInsets(AttachmentView))));

View File

@ -33,6 +33,7 @@ import {
ICustomEmoji ICustomEmoji
} from '../../definitions'; } from '../../definitions';
import { Services } from '../../lib/services'; import { Services } from '../../lib/services';
import { TNavigation } from '../../stacks/stackType';
interface IMessagesViewProps { interface IMessagesViewProps {
user: { user: {
@ -43,7 +44,7 @@ interface IMessagesViewProps {
baseUrl: string; baseUrl: string;
navigation: CompositeNavigationProp< navigation: CompositeNavigationProp<
StackNavigationProp<ChatsStackParamList, 'MessagesView'>, StackNavigationProp<ChatsStackParamList, 'MessagesView'>,
StackNavigationProp<MasterDetailInsideStackParamList> StackNavigationProp<MasterDetailInsideStackParamList & TNavigation>
>; >;
route: RouteProp<ChatsStackParamList, 'MessagesView'>; route: RouteProp<ChatsStackParamList, 'MessagesView'>;
customEmojis: { [key: string]: ICustomEmoji }; customEmojis: { [key: string]: ICustomEmoji };

View File

@ -39,6 +39,7 @@ import {
ICustomEmoji ICustomEmoji
} from '../../definitions'; } from '../../definitions';
import { Services } from '../../lib/services'; import { Services } from '../../lib/services';
import { TNavigation } from '../../stacks/stackType';
const QUERY_SIZE = 50; const QUERY_SIZE = 50;
@ -59,7 +60,7 @@ export interface IRoomInfoParam {
interface INavigationOption { interface INavigationOption {
navigation: CompositeNavigationProp< navigation: CompositeNavigationProp<
StackNavigationProp<ChatsStackParamList, 'SearchMessagesView'>, StackNavigationProp<ChatsStackParamList, 'SearchMessagesView'>,
StackNavigationProp<InsideStackParamList> StackNavigationProp<InsideStackParamList & TNavigation>
>; >;
route: RouteProp<ChatsStackParamList, 'SearchMessagesView'>; route: RouteProp<ChatsStackParamList, 'SearchMessagesView'>;
} }