diff --git a/app/screens/image_preview/image_preview.js b/app/screens/image_preview/image_preview.js index 68ff8f408..024b42204 100644 --- a/app/screens/image_preview/image_preview.js +++ b/app/screens/image_preview/image_preview.js @@ -13,6 +13,7 @@ import { Text, TouchableOpacity, View, + findNodeHandle, } from 'react-native'; import RNFetchBlob from 'rn-fetch-blob'; import Icon from 'react-native-vector-icons/Ionicons'; @@ -32,8 +33,8 @@ import {getLocalFilePathFromFile, isDocument, isVideo} from 'app/utils/file'; import {emptyFunction} from 'app/utils/general'; import {calculateDimensions} from 'app/utils/images'; import {t} from 'app/utils/i18n'; +import BottomSheet from 'app/utils/bottom_sheet'; import { - showModalOverCurrentContext, dismissModal, mergeNavigationOptions, } from 'app/actions/navigation'; @@ -89,6 +90,8 @@ export default class ImagePreview extends PureComponent { showDownloader: false, target: props.target, }; + + this.headerRef = React.createRef(); } componentDidMount() { @@ -267,6 +270,7 @@ export default class ImagePreview extends PureComponent { return ( @@ -434,7 +438,9 @@ export default class ImagePreview extends PureComponent { } this.setState({showHeaderFooter: show}); - StatusBar.setHidden(!show, 'slide'); + if (Platform.OS === 'ios') { + StatusBar.setHidden(!show, 'slide'); + } Animated.timing(this.headerFooterAnim, { ...ANIM_CONFIG, @@ -465,7 +471,8 @@ export default class ImagePreview extends PureComponent { showDownloadOptionsIOS = async () => { const {formatMessage} = this.context.intl; const file = this.getCurrentFile(); - const items = []; + const options = []; + const actions = []; let permissionRequest; const hasPermissionToStorage = await Permissions.check('photo'); @@ -510,37 +517,37 @@ export default class ImagePreview extends PureComponent { const path = getLocalFilePathFromFile(VIDEOS_PATH, file); const exist = await RNFetchBlob.fs.exists(path); if (exist) { - items.push({ - action: this.saveVideoIOS, - text: { - id: t('mobile.image_preview.save_video'), - defaultMessage: 'Save Video', - }, - }); + options.push(formatMessage({ + id: t('mobile.image_preview.save_video'), + defaultMessage: 'Save Video', + })); + actions.push(this.saveVideoIOS); } else { this.showVideoDownloadRequiredAlertIOS(); } } else { - items.push({ - action: this.showDownloader, - text: { - id: t('mobile.image_preview.save'), - defaultMessage: 'Save Image', - }, - }); + options.push(formatMessage({ + id: t('mobile.image_preview.save'), + defaultMessage: 'Save Image', + })); + actions.push(this.showDownloader); } - if (items.length) { - this.setHeaderAndFooterVisible(false); + if (options.length) { + options.push(formatMessage({ + id: 'mobile.post.cancel', + defaultMessage: 'Cancel', + })); + actions.push(emptyFunction); - const screen = 'OptionsModal'; - const passProps = { + BottomSheet.showBottomSheetWithOptions({ + options, + cancelButtonIndex: options.length - 1, title: file.caption, - items, - onCancelPress: () => this.setHeaderAndFooterVisible(true), - }; - - showModalOverCurrentContext(screen, passProps); + anchor: this.headerRef.current ? findNodeHandle(this.headerRef.current) : null, + }, (buttonIndex) => { + actions[buttonIndex](); + }); } }; diff --git a/app/screens/image_preview/image_preview.test.js b/app/screens/image_preview/image_preview.test.js index b02aa3352..7753643f9 100644 --- a/app/screens/image_preview/image_preview.test.js +++ b/app/screens/image_preview/image_preview.test.js @@ -6,10 +6,12 @@ import {shallow} from 'enzyme'; import { TouchableOpacity, } from 'react-native'; +import RNFetchBlob from 'rn-fetch-blob'; import Preferences from 'mattermost-redux/constants/preferences'; import * as NavigationActions from 'app/actions/navigation'; +import BottomSheet from 'app/utils/bottom_sheet'; import ImagePreview from './image_preview'; @@ -20,6 +22,11 @@ jest.mock('react-native-doc-viewer', () => { OpenFile: jest.fn(), }; }); +jest.mock('react-native-permissions', () => { + return { + check: jest.fn(), + }; +}); describe('ImagePreview', () => { const baseProps = { @@ -108,4 +115,111 @@ describe('ImagePreview', () => { }, ); }); + + test('should show bottom sheet when showDownloadOptionsIOS is called for existing video', async () => { + BottomSheet.showBottomSheetWithOptions = jest.fn(); + RNFetchBlob.fs.exists = jest.fn().mockReturnValue(true); + const formatMessage = jest.fn(({defaultMessage}) => { + return defaultMessage; + }); + + const index = 0; + const files = [{ + caption: 'caption', + data: { + mime_type: 'video/mp4', + }, + }]; + const props = { + ...baseProps, + index, + files, + }; + const wrapper = shallow( + , + {context: {intl: {formatMessage}}}, + ); + + const instance = wrapper.instance(); + await instance.showDownloadOptionsIOS(); + + const expectedOptions = { + options: ['Save Video', 'Cancel'], + cancelButtonIndex: 1, + anchor: null, + title: files[index].caption, + }; + expect(BottomSheet.showBottomSheetWithOptions). + toHaveBeenCalledWith(expectedOptions, expect.any(Function)); + }); + + test('should not show bottom sheet when showDownloadOptionsIOS is called for non-existing video', async () => { + BottomSheet.showBottomSheetWithOptions = jest.fn(); + RNFetchBlob.fs.exists = jest.fn().mockReturnValue(false); + + const index = 0; + const files = [{ + caption: 'caption', + data: { + mime_type: 'video/mp4', + }, + }]; + const props = { + ...baseProps, + index, + files, + }; + const wrapper = shallow( + , + {context: {intl: {formatMessage: jest.fn()}}}, + ); + + const instance = wrapper.instance(); + await instance.showDownloadOptionsIOS(); + + expect(BottomSheet.showBottomSheetWithOptions).not.toHaveBeenCalled(); + }); + + test('should show bottom sheet when showDownloadOptionsIOS is called for image', async () => { + BottomSheet.showBottomSheetWithOptions = jest.fn(); + RNFetchBlob.fs.exists = jest.fn().mockReturnValue(true); + const formatMessage = jest.fn(({defaultMessage}) => { + return defaultMessage; + }); + + const index = 0; + const files = [{ + caption: 'caption', + data: { + mime_type: 'image/jpeg', + }, + }]; + const props = { + ...baseProps, + index, + files, + }; + const wrapper = shallow( + , + {context: {intl: {formatMessage}}}, + ); + + const instance = wrapper.instance(); + await instance.showDownloadOptionsIOS(); + + const expectedOptions = { + options: ['Save Image', 'Cancel'], + cancelButtonIndex: 1, + anchor: null, + title: files[index].caption, + }; + expect(BottomSheet.showBottomSheetWithOptions). + toHaveBeenCalledWith(expectedOptions, expect.any(Function)); + }); });