diff --git a/app/components/files/file.tsx b/app/components/files/file.tsx index 90db548c05..bcd1287958 100644 --- a/app/components/files/file.tsx +++ b/app/components/files/file.tsx @@ -30,7 +30,8 @@ type FileProps = { onPress: (index: number) => void; publicLinkEnabled: boolean; channelName?: string; - onOptionsPress?: (index: number) => void; + onOptionsPress?: (fileInfo: FileInfo) => void; + optionSelected?: boolean; wrapperWidth?: number; showDate?: boolean; updateFileForGallery: (idx: number, file: FileInfo) => void; @@ -74,6 +75,7 @@ const File = ({ nonVisibleImagesCount = 0, onOptionsPress, onPress, + optionSelected, publicLinkEnabled, showDate = false, updateFileForGallery, @@ -94,19 +96,15 @@ const File = ({ const {styles, onGestureEvent, ref} = useGalleryItem(galleryIdentifier, index, handlePreviewPress); const handleOnOptionsPress = useCallback(() => { - onOptionsPress?.(index); - }, [index, onOptionsPress]); + onOptionsPress?.(file); + }, [file, onOptionsPress]); - const renderOptionsButton = () => { - if (onOptionsPress) { - return ( - - ); - } - return null; - }; + const optionsButton = ( + + ); const fileInfo = ( {fileInfo} - {renderOptionsButton()} + {onOptionsPress && optionsButton} ); }; @@ -189,7 +187,7 @@ const File = ({ {renderDocumentFile} {fileInfo} - {renderOptionsButton()} + {onOptionsPress && optionsButton} ); } else { diff --git a/app/components/files/file_options_icon.tsx b/app/components/files/file_options_icon.tsx index 0a77f2bf8b..bf22d49913 100644 --- a/app/components/files/file_options_icon.tsx +++ b/app/components/files/file_options_icon.tsx @@ -2,32 +2,38 @@ // See LICENSE.txt for license information. import React from 'react'; -import {TouchableOpacity, StyleSheet} from 'react-native'; +import {TouchableOpacity} from 'react-native'; import CompassIcon from '@components/compass_icon'; import {useTheme} from '@context/theme'; -import {changeOpacity} from '@utils/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; type Props = { onPress: () => void; + selected?: boolean; } -const styles = StyleSheet.create({ - threeDotContainer: { - alignItems: 'flex-end', - marginHorizontal: 20, - }, +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { + return { + threeDotContainer: { + alignItems: 'flex-end', + borderRadius: 4, + marginHorizontal: 20, + padding: 7, + }, + selected: { + backgroundColor: changeOpacity(theme.buttonBg, 0.08), + }, + }; }); -const hitSlop = {top: 5, bottom: 5, left: 5, right: 5}; - -export default function FileOptionsIcon({onPress}: Props) { +export default function FileOptionsIcon({onPress, selected = false}: Props) { const theme = useTheme(); + const styles = getStyleSheet(theme); return ( ({ center: { @@ -53,10 +57,10 @@ const Toast = ({animatedStyle, children, style, iconName, message, textStyle}: T const theme = useTheme(); const styles = getStyleSheet(theme); const dim = useWindowDimensions(); + const isTablet = useIsTablet(); const containerStyle = useMemo(() => { - const totalMargin = 40; - const width = Math.min(dim.height, dim.width, 400) - totalMargin; - + const toast_width = isTablet ? WIDTH_TABLET : WIDTH_MOBILE; + const width = Math.min(dim.height, dim.width, toast_width) - TOAST_MARGIN; return [styles.container, {width}, style]; }, [dim, styles.container, style]); diff --git a/app/screens/gallery/footer/copy_public_link/index.tsx b/app/screens/gallery/footer/copy_public_link/index.tsx index 3925fcdcff..15816dafa8 100644 --- a/app/screens/gallery/footer/copy_public_link/index.tsx +++ b/app/screens/gallery/footer/copy_public_link/index.tsx @@ -16,6 +16,7 @@ import {useServerUrl} from '@context/server'; import type {GalleryAction, GalleryItemType} from '@typings/screens/gallery'; type Props = { + galleryView?: boolean; item: GalleryItemType; setAction: (action: GalleryAction) => void; } @@ -29,7 +30,7 @@ const styles = StyleSheet.create({ }, }); -const CopyPublicLink = ({item, setAction}: Props) => { +const CopyPublicLink = ({item, galleryView = true, setAction}: Props) => { const {formatMessage} = useIntl(); const serverUrl = useServerUrl(); const insets = useSafeAreaInsets(); @@ -37,11 +38,14 @@ const CopyPublicLink = ({item, setAction}: Props) => { const [error, setError] = useState(''); const mounted = useRef(false); - const animatedStyle = useAnimatedStyle(() => ({ - position: 'absolute', - bottom: GALLERY_FOOTER_HEIGHT + 8 + insets.bottom, - opacity: withTiming(showToast ? 1 : 0, {duration: 300}), - })); + const animatedStyle = useAnimatedStyle(() => { + const marginBottom = galleryView ? GALLERY_FOOTER_HEIGHT + 8 : 0; + return { + position: 'absolute', + bottom: insets.bottom + marginBottom, + opacity: withTiming(showToast ? 1 : 0, {duration: 300}), + }; + }); const copyLink = async () => { try { @@ -87,7 +91,7 @@ const CopyPublicLink = ({item, setAction}: Props) => { animatedStyle={animatedStyle} style={error ? styles.error : styles.toast} message={error || formatMessage({id: 'public_link_copied', defaultMessage: 'Link copied to clipboard'})} - iconName='check' + iconName='link-variant' /> ); }; diff --git a/app/screens/gallery/footer/download_with_action/index.tsx b/app/screens/gallery/footer/download_with_action/index.tsx index cba4d9478f..5e1fdc7af6 100644 --- a/app/screens/gallery/footer/download_with_action/index.tsx +++ b/app/screens/gallery/footer/download_with_action/index.tsx @@ -29,6 +29,7 @@ import type {GalleryAction, GalleryItemType} from '@typings/screens/gallery'; type Props = { action: GalleryAction; + galleryView?: boolean; item: GalleryItemType; setAction: (action: GalleryAction) => void; onDownloadSuccess?: (path: string) => void; @@ -65,7 +66,7 @@ const styles = StyleSheet.create({ }, }); -const DownloadWithAction = ({action, item, onDownloadSuccess, setAction}: Props) => { +const DownloadWithAction = ({action, item, onDownloadSuccess, setAction, galleryView = true}: Props) => { const intl = useIntl(); const serverUrl = useServerUrl(); const insets = useSafeAreaInsets(); @@ -111,11 +112,14 @@ const DownloadWithAction = ({action, item, onDownloadSuccess, setAction}: Props) } } - const animatedStyle = useAnimatedStyle(() => ({ - position: 'absolute', - bottom: GALLERY_FOOTER_HEIGHT + 8 + insets.bottom, - opacity: withTiming(showToast ? 1 : 0, {duration: 300}), - })); + const animatedStyle = useAnimatedStyle(() => { + const marginBottom = galleryView ? GALLERY_FOOTER_HEIGHT + 8 : 0; + return { + position: 'absolute', + bottom: insets.bottom + marginBottom, + opacity: withTiming(showToast ? 1 : 0, {duration: 300}), + }; + }); const cancel = async () => { try { diff --git a/app/screens/home/search/results/file_options/index.ts b/app/screens/home/search/results/file_options/index.ts deleted file mode 100644 index 072bc86aff..0000000000 --- a/app/screens/home/search/results/file_options/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; -import withObservables from '@nozbe/with-observables'; -import {combineLatest, of as of$} from 'rxjs'; -import {switchMap} from 'rxjs/operators'; - -import {observeConfigBooleanValue, observeLicense} from '@queries/servers/system'; - -import FileOptions from './file_options'; - -import type {WithDatabaseArgs} from '@typings/database/database'; - -const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { - const license = observeLicense(database); - const enablePublicLink = observeConfigBooleanValue(database, 'EnablePublicLink'); - const enableMobileFileDownload = observeConfigBooleanValue(database, 'EnableMobileFileDownload'); - - const complianceDisabled = license.pipe(switchMap((l) => of$(l?.IsLicensed === 'false' || l?.Compliance === 'false'))); - const canDownloadFiles = combineLatest([enableMobileFileDownload, complianceDisabled]).pipe( - switchMap(([download, compliance]) => of$(compliance || download)), - ); - return { - canDownloadFiles, - enablePublicLink, - }; -}); - -export default withDatabase(enhanced(FileOptions)); diff --git a/app/screens/home/search/results/file_options/mobile_options.tsx b/app/screens/home/search/results/file_options/mobile_options.tsx new file mode 100644 index 0000000000..a2a0c4f3f0 --- /dev/null +++ b/app/screens/home/search/results/file_options/mobile_options.tsx @@ -0,0 +1,48 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React from 'react'; +import {EdgeInsets} from 'react-native-safe-area-context'; + +import {ITEM_HEIGHT} from '@components/slide_up_panel_item'; +import {bottomSheet} from '@screens/navigation'; +import {GalleryAction} from '@typings/screens/gallery'; +import {bottomSheetSnapPoint} from '@utils/helpers'; + +import Header, {HEADER_HEIGHT} from './header'; +import OptionMenus from './option_menus'; + +type Props = { + fileInfo: FileInfo; + insets: EdgeInsets; + numOptions: number; + setAction: (action: GalleryAction) => void; + theme: Theme; +} + +export const showMobileOptionsBottomSheet = ({ + fileInfo, + insets, + numOptions, + setAction, + theme, +}: Props) => { + const renderContent = () => ( + <> +
+ + + ); + + bottomSheet({ + closeButtonId: 'close-search-file-options', + renderContent, + snapPoints: [ + bottomSheetSnapPoint(numOptions, ITEM_HEIGHT, insets.bottom) + HEADER_HEIGHT, 10, + ], + theme, + title: '', + }); +}; diff --git a/app/screens/home/search/results/file_options/option_menus/index.tsx b/app/screens/home/search/results/file_options/option_menus/index.tsx new file mode 100644 index 0000000000..117a2b9b04 --- /dev/null +++ b/app/screens/home/search/results/file_options/option_menus/index.tsx @@ -0,0 +1,36 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; +import withObservables from '@nozbe/with-observables'; +import compose from 'lodash/fp/compose'; +import {combineLatest, of as of$} from 'rxjs'; +import {map, switchMap} from 'rxjs/operators'; + +import {observeLicense, observeConfigBooleanValue} from '@queries/servers/system'; + +import OptionMenus from './option_menus'; + +import type {WithDatabaseArgs} from '@typings/database/database'; + +const enhance = withObservables([], ({database}: WithDatabaseArgs) => { + const enableMobileFileDownload = observeConfigBooleanValue(database, 'EnableMobileFileDownload'); + + const complianceDisabled = observeLicense(database).pipe( + switchMap((lcs) => of$(lcs?.IsLicensed === 'false' || lcs?.Compliance === 'false')), + ); + + const canDownloadFiles = combineLatest([enableMobileFileDownload, complianceDisabled]).pipe( + map(([download, compliance]) => compliance || download), + ); + + return { + canDownloadFiles, + enablePublicLink: observeConfigBooleanValue(database, 'EnablePublicLink'), + }; +}); + +export default compose( + withDatabase, + enhance, +)(OptionMenus); diff --git a/app/screens/home/search/results/file_options/file_options.tsx b/app/screens/home/search/results/file_options/option_menus/option_menus.tsx similarity index 51% rename from app/screens/home/search/results/file_options/file_options.tsx rename to app/screens/home/search/results/file_options/option_menus/option_menus.tsx index a16f8f198e..2c254273c3 100644 --- a/app/screens/home/search/results/file_options/file_options.tsx +++ b/app/screens/home/search/results/file_options/option_menus/option_menus.tsx @@ -1,55 +1,57 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback, useState} from 'react'; +import React, {useCallback} from 'react'; import {useIntl} from 'react-intl'; -import {View, StyleSheet} from 'react-native'; import {showPermalink} from '@actions/remote/permalink'; import OptionItem from '@components/option_item'; import {useServerUrl} from '@context/server'; -import CopyPublicLink from '@screens/gallery/footer/copy_public_link'; -import DownloadWithAction from '@screens/gallery/footer/download_with_action'; - -import Header from './header'; - -import type {GalleryAction, GalleryItemType} from '@typings/screens/gallery'; - -const styles = StyleSheet.create({ - toast: { - marginTop: 100, - alignItems: 'center', - }, -}); +import {useIsTablet} from '@hooks/device'; +import {dismissBottomSheet} from '@screens/navigation'; +import {GalleryAction} from '@typings/screens/gallery'; type Props = { - canDownloadFiles: boolean; - enablePublicLink: boolean; + canDownloadFiles?: boolean; + enablePublicLink?: boolean; fileInfo: FileInfo; + setAction: (action: GalleryAction) => void; } -const FileOptions = ({fileInfo, canDownloadFiles, enablePublicLink}: Props) => { - const intl = useIntl(); +const OptionMenus = ({ + canDownloadFiles, + enablePublicLink, + fileInfo, + setAction, +}: Props) => { const serverUrl = useServerUrl(); - const [action, setAction] = useState('none'); - - const galleryItem = {...fileInfo, type: 'image'} as GalleryItemType; + const isTablet = useIsTablet(); + const intl = useIntl(); const handleDownload = useCallback(() => { + if (!isTablet) { + dismissBottomSheet(); + } setAction('downloading'); - }, []); + }, [setAction]); const handleCopyLink = useCallback(() => { + if (!isTablet) { + dismissBottomSheet(); + } setAction('copying'); - }, []); + }, [setAction]); const handlePermalink = useCallback(() => { - showPermalink(serverUrl, '', fileInfo.post_id, intl); - }, [serverUrl, fileInfo.post_id, intl]); + if (fileInfo.post_id) { + showPermalink(serverUrl, '', fileInfo.post_id, intl); + setAction('opening'); + } + }, [intl, serverUrl, fileInfo.post_id, setAction]); return ( - -
+ <> {canDownloadFiles && { /> } { /> {enablePublicLink && } - - {action === 'downloading' && - - } - {action === 'copying' && - - } - - + ); }; -export default FileOptions; +export default OptionMenus; diff --git a/app/screens/home/search/results/file_options/tablet_options.tsx b/app/screens/home/search/results/file_options/tablet_options.tsx new file mode 100644 index 0000000000..9dc4641c76 --- /dev/null +++ b/app/screens/home/search/results/file_options/tablet_options.tsx @@ -0,0 +1,87 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React, {useCallback, useMemo} from 'react'; +import {Overlay} from 'react-native-elements'; + +import {ITEM_HEIGHT} from '@components/option_item'; +import {useTheme} from '@context/theme'; +import {GalleryAction} from '@typings/screens/gallery'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +import {XyOffset} from '../file_result'; + +import OptionMenus from './option_menus'; + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ + tablet: { + backgroundColor: theme.centerChannelBg, + borderColor: changeOpacity(theme.centerChannelColor, 0.16), + borderRadius: 8, + borderWidth: 1, + paddingLeft: 20, + position: 'absolute', + right: 20, + width: 252, + marginRight: 20, + shadowColor: theme.centerChannelColor, + shadowOffset: { + width: 0, + height: 8, + }, + shadowOpacity: 0.12, + shadowRadius: 24, + }, + backDrop: {opacity: 0}, +})); + +const openDownMargin = 64; + +type Props = { + fileInfo: FileInfo; + numOptions: number; + openUp?: boolean; + setAction: (action: GalleryAction) => void; + setShowOptions: (show: boolean) => void; + xyOffset: XyOffset; +} +const TabletOptions = ({ + fileInfo, + numOptions, + openUp = false, + setAction, + setShowOptions, + xyOffset, +}: Props) => { + const theme = useTheme(); + const styles = getStyleSheet(theme); + + const toggleOverlay = useCallback(() => { + setShowOptions(false); + }, []); + + const overlayStyle = useMemo(() => ({ + marginTop: openUp ? 0 : openDownMargin, + top: xyOffset?.y ? xyOffset.y - (openUp ? ITEM_HEIGHT * numOptions : 0) : 0, + right: xyOffset?.x, + }), [numOptions, openUp, xyOffset]); + + return ( + + + + ); +}; + +export default TabletOptions; diff --git a/app/screens/home/search/results/file_options/toasts.tsx b/app/screens/home/search/results/file_options/toasts.tsx new file mode 100644 index 0000000000..7aad769a3d --- /dev/null +++ b/app/screens/home/search/results/file_options/toasts.tsx @@ -0,0 +1,46 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React from 'react'; + +import CopyPublicLink from '@screens/gallery/footer/copy_public_link'; +import DownloadWithAction from '@screens/gallery/footer/download_with_action'; + +import type {GalleryAction, GalleryItemType} from '@typings/screens/gallery'; + +type Props = { + action: GalleryAction; + fileInfo: FileInfo | undefined; + setAction: (action: GalleryAction) => void; +} +const Toasts = ({ + action, + fileInfo, + setAction, +}: Props) => { + const galleryItem = {...fileInfo, type: 'image'} as GalleryItemType; + + switch (action) { + case 'downloading': + return ( + + ); + case 'copying': + return ( + + ); + + default: + return null; + } +}; + +export default Toasts; diff --git a/app/screens/home/search/results/file_result.tsx b/app/screens/home/search/results/file_result.tsx new file mode 100644 index 0000000000..9125c504d0 --- /dev/null +++ b/app/screens/home/search/results/file_result.tsx @@ -0,0 +1,122 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useRef, useState} from 'react'; +import {Dimensions, StyleSheet, View} from 'react-native'; + +import File from '@components/files/file'; +import {useIsTablet} from '@hooks/device'; +import {GalleryAction} from '@typings/screens/gallery'; +import {getViewPortWidth} from '@utils/images'; + +import TabletOptions from './file_options/tablet_options'; + +export type XyOffset = {x: number; y: number} | undefined; + +const styles = StyleSheet.create({ + container: { + flex: 1, + marginHorizontal: 20, + }, +}); + +type Props = { + canDownloadFiles: boolean; + channelName: string | undefined; + fileInfo: FileInfo; + index: number; + isSingleImage: boolean; + numOptions: number; + onOptionsPress: (finfo: FileInfo) => void; + onPress: (idx: number) => void; + publicLinkEnabled: boolean; + setAction: (action: GalleryAction) => void; + updateFileForGallery: (idx: number, file: FileInfo) => void; +} + +const galleryIdentifier = 'search-files-location'; + +const FileResult = ({ + canDownloadFiles, + channelName, + fileInfo, + index, + isSingleImage, + numOptions, + onOptionsPress, + onPress, + publicLinkEnabled, + setAction, + updateFileForGallery, +}: Props) => { + const elementsRef = useRef(null); + const isTablet = useIsTablet(); + const isReplyPost = false; + + const [showOptions, setShowOptions] = useState(false); + const [openUp, setOpenUp] = useState(false); + const [xyOffset, setXYoffset] = useState(undefined); + const {height} = Dimensions.get('window'); + + const fileRef = useCallback((element: View) => { + if (showOptions) { + elementsRef.current = element; + elementsRef?.current?.measureInWindow((x, y) => { + setOpenUp((y > height / 2)); + setXYoffset({x, y}); + }); + } + }, [elementsRef, showOptions]); + + const handleOptionsPress = useCallback((fInfo: FileInfo) => { + setShowOptions(true); + onOptionsPress(fInfo); + }, []); + + const handleSetAction = useCallback((action: GalleryAction) => { + setAction(action); + if (showOptions && action !== 'none') { + setShowOptions(false); + } + }, [setAction, showOptions]); + + return ( + <> + + + + {isTablet && showOptions && xyOffset && + + } + + ); +}; + +export default FileResult; diff --git a/app/screens/home/search/results/file_results.tsx b/app/screens/home/search/results/file_results.tsx index ec01da4b2d..5e22bf3d26 100644 --- a/app/screens/home/search/results/file_results.tsx +++ b/app/screens/home/search/results/file_results.tsx @@ -1,44 +1,39 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {StyleSheet, FlatList, ListRenderItemInfo, StyleProp, View, ViewStyle} from 'react-native'; +import React, {useCallback, useMemo, useState} from 'react'; +import {FlatList, ListRenderItemInfo, StyleProp, ViewStyle} from 'react-native'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; -import File from '@components/files/file'; +import {useIsTablet} from '@app/hooks/device'; +import {useImageAttachments} from '@app/hooks/files'; import NoResultsWithTerm from '@components/no_results_with_term'; -import {ITEM_HEIGHT} from '@components/option_item'; -import {Screens} from '@constants'; import {useTheme} from '@context/theme'; -import {useIsTablet} from '@hooks/device'; -import {useImageAttachments} from '@hooks/files'; -import {bottomSheet, dismissBottomSheet} from '@screens/navigation'; -import NavigationStore from '@store/navigation_store'; +import {GalleryAction} from '@typings/screens/gallery'; import {isImage, isVideo} from '@utils/file'; -import {fileToGalleryItem, openGalleryAtIndex} from '@utils/gallery'; -import {bottomSheetSnapPoint} from '@utils/helpers'; -import {getViewPortWidth} from '@utils/images'; +import { + getChannelNamesWithID, + getFileInfosIndexes, + getNumberFileMenuOptions, + getOrderedFileInfos, + getOrderedGalleryItems, +} from '@utils/files'; +import {openGalleryAtIndex} from '@utils/gallery'; import {TabTypes} from '@utils/search'; import {preventDoubleTap} from '@utils/tap'; -import FileOptions from './file_options'; -import {HEADER_HEIGHT} from './file_options/header'; +import {showMobileOptionsBottomSheet} from './file_options/mobile_options'; +import Toasts from './file_options/toasts'; +import FileResult from './file_result'; import type ChannelModel from '@typings/database/models/servers/channel'; -const styles = StyleSheet.create({ - container: { - flex: 1, - marginHorizontal: 20, - }, -}); - type Props = { canDownloadFiles: boolean; fileChannels: ChannelModel[]; fileInfos: FileInfo[]; - publicLinkEnabled: boolean; paddingTop: StyleProp; + publicLinkEnabled: boolean; searchValue: string; } @@ -48,121 +43,76 @@ const FileResults = ({ canDownloadFiles, fileChannels, fileInfos, - publicLinkEnabled, paddingTop, + publicLinkEnabled, searchValue, }: Props) => { const theme = useTheme(); - const isTablet = useIsTablet(); const insets = useSafeAreaInsets(); - const [lastViewedIndex, setLastViewedIndex] = useState(undefined); - const containerStyle = useMemo(() => ({top: fileInfos.length ? 8 : 0}), [fileInfos]); + const isTablet = useIsTablet(); + + const [action, setAction] = useState('none'); + const [lastViewedFileInfo, setLastViewedFileInfo] = useState(undefined); + + const containerStyle = useMemo(() => ([paddingTop, {top: fileInfos.length ? 8 : 0}]), [fileInfos, paddingTop]); + const numOptions = getNumberFileMenuOptions(canDownloadFiles, publicLinkEnabled); const {images: imageAttachments, nonImages: nonImageAttachments} = useImageAttachments(fileInfos, publicLinkEnabled); - const channelNames = useMemo(() => fileChannels.reduce<{[id: string]: string | undefined}>((acc, v) => { - acc[v.id] = v.displayName; - return acc; - }, {}), [fileChannels]); + const filesForGallery = imageAttachments.concat(nonImageAttachments); - const orderedFilesForGallery = useMemo(() => { - const filesForGallery = imageAttachments.concat(nonImageAttachments); - return filesForGallery.sort((a: FileInfo, b: FileInfo) => { - return (b.create_at || 0) - (a.create_at || 0); - }); - }, [imageAttachments, nonImageAttachments]); + const channelNames = useMemo(() => getChannelNamesWithID(fileChannels), [fileChannels]); + const orderedFileInfos = useMemo(() => getOrderedFileInfos(filesForGallery), []); + const fileInfosIndexes = useMemo(() => getFileInfosIndexes(orderedFileInfos), []); + const orderedGalleryItems = useMemo(() => getOrderedGalleryItems(orderedFileInfos), []); - const filesForGalleryIndexes = useMemo(() => orderedFilesForGallery.reduce<{[id: string]: number | undefined}>((acc, v, idx) => { - if (v.id) { - acc[v.id] = idx; - } - return acc; - }, {}), [orderedFilesForGallery]); - - const handlePreviewPress = useCallback(preventDoubleTap((idx: number) => { - const items = orderedFilesForGallery.map((f) => fileToGalleryItem(f, f.user_id)); - openGalleryAtIndex(galleryIdentifier, idx, items); - }), [orderedFilesForGallery]); - - const handleOptionsPress = useCallback((item: number) => { - setLastViewedIndex(item); - let numberOptions = 1; - numberOptions += canDownloadFiles ? 1 : 0; - numberOptions += publicLinkEnabled ? 1 : 0; - const renderContent = () => ( - - ); - bottomSheet({ - closeButtonId: 'close-search-file-options', - renderContent, - snapPoints: [bottomSheetSnapPoint(numberOptions, ITEM_HEIGHT, insets.bottom) + HEADER_HEIGHT, 10], - theme, - title: '', - }); - }, [canDownloadFiles, publicLinkEnabled, orderedFilesForGallery, theme]); - - // This effect handles the case where a user has the FileOptions Modal - // open and the server changes the ability to download files or copy public - // links. Reopen the Bottom Sheet again so the new options are added or - // removed. - useEffect(() => { - if (lastViewedIndex === undefined) { - return; - } - if (NavigationStore.getNavigationTopComponentId() === Screens.BOTTOM_SHEET) { - dismissBottomSheet().then(() => { - handleOptionsPress(lastViewedIndex); - }); - } - }, [canDownloadFiles, publicLinkEnabled]); + const onPreviewPress = useCallback(preventDoubleTap((idx: number) => { + openGalleryAtIndex(galleryIdentifier, idx, orderedGalleryItems); + }), [orderedGalleryItems]); const updateFileForGallery = (idx: number, file: FileInfo) => { 'worklet'; - orderedFilesForGallery[idx] = file; + orderedFileInfos[idx] = file; }; - const renderItem = useCallback(({item}: ListRenderItemInfo) => { - const container: StyleProp = fileInfos.length > 1 ? styles.container : undefined; - const isSingleImage = orderedFilesForGallery.length === 1 && (isImage(orderedFilesForGallery[0]) || isVideo(orderedFilesForGallery[0])); - const isReplyPost = false; + const onOptionsPress = useCallback((fInfo: FileInfo) => { + setLastViewedFileInfo(fInfo); + if (!isTablet) { + showMobileOptionsBottomSheet({ + fileInfo: fInfo, + insets, + numOptions, + setAction, + theme, + }); + } + }, [insets, isTablet, numOptions, theme]); + + const renderItem = useCallback(({item}: ListRenderItemInfo) => { + const isSingleImage = orderedFileInfos.length === 1 && (isImage(orderedFileInfos[0]) || isVideo(orderedFileInfos[0])); return ( - - - + ); }, [ - (orderedFilesForGallery.length === 1) && orderedFilesForGallery[0].mime_type, + (orderedFileInfos.length === 1) && orderedFileInfos[0].mime_type, canDownloadFiles, channelNames, - fileInfos.length > 1, - filesForGalleryIndexes, - handleOptionsPress, - handlePreviewPress, - isTablet, + fileInfosIndexes, + onPreviewPress, + setAction, publicLinkEnabled, - theme, ]); const noResults = useMemo(() => ( @@ -173,23 +123,30 @@ const FileResults = ({ ), [searchValue]); return ( - + <> + + + ); }; diff --git a/app/screens/home/search/results/results.tsx b/app/screens/home/search/results/results.tsx index 6bf6fbba82..5971ec699c 100644 --- a/app/screens/home/search/results/results.tsx +++ b/app/screens/home/search/results/results.tsx @@ -33,7 +33,6 @@ const getStyles = (dimensions: ScaledSize) => { flex: 1, width: dimensions.width, }, - }); }; @@ -106,8 +105,8 @@ const Results = ({ canDownloadFiles={canDownloadFiles} fileChannels={fileChannels} fileInfos={fileInfos} - publicLinkEnabled={publicLinkEnabled} paddingTop={paddingTop} + publicLinkEnabled={publicLinkEnabled} searchValue={searchValue} /> diff --git a/app/utils/files.tsx b/app/utils/files.tsx new file mode 100644 index 0000000000..8c625ee6d5 --- /dev/null +++ b/app/utils/files.tsx @@ -0,0 +1,44 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {ChannelModel} from '@app/database/models/server'; +import {fileToGalleryItem} from '@utils/gallery'; + +export const getNumberFileMenuOptions = (canDownloadFiles: boolean, publicLinkEnabled: boolean) => { + let numberItems = 1; + numberItems += canDownloadFiles ? 1 : 0; + numberItems += publicLinkEnabled ? 1 : 0; + return numberItems; +}; + +// return an object with key:value of channelID:channelDisplayName +export const getChannelNamesWithID = (fileChannels: ChannelModel[]) => { + return fileChannels.reduce<{[id: string]: string | undefined}>((acc, v) => { + acc[v.id] = v.displayName; + return acc; + }, {}); +}; + +// return array of fileInfos (image and non-image) sorted by create_at date +export const getOrderedFileInfos = (fileInfos: FileInfo[]) => { + return fileInfos.sort((a: FileInfo, b: FileInfo) => { + return (b.create_at || 0) - (a.create_at || 0); + }); +}; + +// returns object with keys of fileInfo.id and value of the ordered index from +// orderedFilesForGallery +export const getFileInfosIndexes = (orderedFilesForGallery: FileInfo[]) => { + return orderedFilesForGallery.reduce<{[id: string]: number | undefined}>((acc, v, idx) => { + if (v.id) { + acc[v.id] = idx; + } + return acc; + }, {}); +}; + +// return ordered FileInfo[] converted to GalleryItemType[] +export const getOrderedGalleryItems = (orderedFileInfos: FileInfo[]) => { + return orderedFileInfos.map((f) => fileToGalleryItem(f, f.user_id)); +}; +