[Gekidou MM-45790] Create File Options Menu for Tablet View (#6531)

This commit is contained in:
Jason Frerich
2022-10-11 08:19:13 -05:00
committed by GitHub
parent d201035a89
commit 380b375411
15 changed files with 567 additions and 252 deletions

View File

@@ -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 (
<FileOptionsIcon
onPress={handleOnOptionsPress}
/>
);
}
return null;
};
const optionsButton = (
<FileOptionsIcon
onPress={handleOnOptionsPress}
selected={optionSelected}
/>
);
const fileInfo = (
<FileInfo
@@ -174,7 +172,7 @@ const File = ({
{fileIcon}
</View>
{fileInfo}
{renderOptionsButton()}
{onOptionsPress && optionsButton}
</View>
);
};
@@ -189,7 +187,7 @@ const File = ({
<View style={[style.fileWrapper]}>
{renderDocumentFile}
{fileInfo}
{renderOptionsButton()}
{onOptionsPress && optionsButton}
</View>
);
} else {

View File

@@ -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 (
<TouchableOpacity
onPress={onPress}
style={styles.threeDotContainer}
hitSlop={hitSlop}
style={[styles.threeDotContainer, selected ? styles.selected : null]}
>
<CompassIcon
name='dots-horizontal'

View File

@@ -5,6 +5,7 @@ import React, {useMemo} from 'react';
import {StyleProp, Text, TextStyle, useWindowDimensions, View, ViewStyle} from 'react-native';
import Animated, {AnimatedStyleProp} from 'react-native-reanimated';
import {useIsTablet} from '@app/hooks/device';
import CompassIcon from '@components/compass_icon';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@@ -20,6 +21,9 @@ type ToastProps = {
}
export const TOAST_HEIGHT = 56;
const TOAST_MARGIN = 40;
const WIDTH_TABLET = 484;
const WIDTH_MOBILE = 400;
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
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]);

View File

@@ -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'
/>
);
};

View File

@@ -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 {

View File

@@ -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));

View File

@@ -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 = () => (
<>
<Header fileInfo={fileInfo}/>
<OptionMenus
setAction={setAction}
fileInfo={fileInfo}
/>
</>
);
bottomSheet({
closeButtonId: 'close-search-file-options',
renderContent,
snapPoints: [
bottomSheetSnapPoint(numOptions, ITEM_HEIGHT, insets.bottom) + HEADER_HEIGHT, 10,
],
theme,
title: '',
});
};

View File

@@ -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);

View File

@@ -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<GalleryAction>('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 (
<View>
<Header fileInfo={fileInfo}/>
<>
{canDownloadFiles &&
<OptionItem
key={'download'}
action={handleDownload}
label={intl.formatMessage({id: 'screen.search.results.file_options.download', defaultMessage: 'Download'})}
icon={'download-outline'}
@@ -57,6 +59,7 @@ const FileOptions = ({fileInfo, canDownloadFiles, enablePublicLink}: Props) => {
/>
}
<OptionItem
key={'permalink'}
action={handlePermalink}
label={intl.formatMessage({id: 'screen.search.results.file_options.open_in_channel', defaultMessage: 'Open in channel'})}
icon={'globe'}
@@ -64,29 +67,15 @@ const FileOptions = ({fileInfo, canDownloadFiles, enablePublicLink}: Props) => {
/>
{enablePublicLink &&
<OptionItem
key={'copylink'}
action={handleCopyLink}
label={intl.formatMessage({id: 'screen.search.results.file_options.copy_link', defaultMessage: 'Copy link'})}
icon={'link-variant'}
type='default'
/>
}
<View style={styles.toast} >
{action === 'downloading' &&
<DownloadWithAction
action={action}
item={galleryItem}
setAction={setAction}
/>
}
{action === 'copying' &&
<CopyPublicLink
item={galleryItem}
setAction={setAction}
/>
}
</View>
</View>
</>
);
};
export default FileOptions;
export default OptionMenus;

View File

@@ -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 (
<Overlay
backdropStyle={styles.backDrop}
fullScreen={false}
isVisible={true}
onBackdropPress={toggleOverlay}
overlayStyle={[
styles.tablet,
overlayStyle,
]}
>
<OptionMenus
setAction={setAction}
fileInfo={fileInfo}
/>
</Overlay>
);
};
export default TabletOptions;

View File

@@ -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 (
<DownloadWithAction
action={action}
galleryView={false}
item={galleryItem}
setAction={setAction}
/>
);
case 'copying':
return (
<CopyPublicLink
galleryView={false}
item={galleryItem}
setAction={setAction}
/>
);
default:
return null;
}
};
export default Toasts;

View File

@@ -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<View | null>(null);
const isTablet = useIsTablet();
const isReplyPost = false;
const [showOptions, setShowOptions] = useState<boolean>(false);
const [openUp, setOpenUp] = useState<boolean>(false);
const [xyOffset, setXYoffset] = useState<XyOffset>(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 (
<>
<View
ref={fileRef}
style={styles.container}
>
<File
asCard={true}
canDownloadFiles={canDownloadFiles}
channelName={channelName}
file={fileInfo}
galleryIdentifier={galleryIdentifier}
inViewPort={true}
index={index}
isSingleImage={isSingleImage}
nonVisibleImagesCount={0}
onOptionsPress={handleOptionsPress}
onPress={onPress}
optionSelected={isTablet && showOptions}
publicLinkEnabled={publicLinkEnabled}
showDate={true}
updateFileForGallery={updateFileForGallery}
wrapperWidth={(getViewPortWidth(isReplyPost, isTablet) - 6)}
/>
</View>
{isTablet && showOptions && xyOffset &&
<TabletOptions
fileInfo={fileInfo}
numOptions={numOptions}
openUp={openUp}
setAction={handleSetAction}
setShowOptions={setShowOptions}
xyOffset={xyOffset}
/>
}
</>
);
};
export default FileResult;

View File

@@ -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<ViewStyle>;
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<number | undefined>(undefined);
const containerStyle = useMemo(() => ({top: fileInfos.length ? 8 : 0}), [fileInfos]);
const isTablet = useIsTablet();
const [action, setAction] = useState<GalleryAction>('none');
const [lastViewedFileInfo, setLastViewedFileInfo] = useState<FileInfo | undefined>(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 = () => (
<FileOptions
fileInfo={orderedFilesForGallery[item]}
/>
);
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<FileInfo>) => {
const container: StyleProp<ViewStyle> = 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<FileInfo>) => {
const isSingleImage = orderedFileInfos.length === 1 && (isImage(orderedFileInfos[0]) || isVideo(orderedFileInfos[0]));
return (
<View
style={container}
key={item.id}
>
<File
asCard={true}
canDownloadFiles={canDownloadFiles}
channelName={channelNames[item.channel_id!]}
file={item}
galleryIdentifier={galleryIdentifier}
inViewPort={true}
index={filesForGalleryIndexes[item.id!] || 0}
isSingleImage={isSingleImage}
key={item.id}
nonVisibleImagesCount={0}
onOptionsPress={handleOptionsPress}
onPress={handlePreviewPress}
publicLinkEnabled={publicLinkEnabled}
showDate={true}
updateFileForGallery={updateFileForGallery}
wrapperWidth={(getViewPortWidth(isReplyPost, isTablet) - 6)}
/>
</View>
<FileResult
canDownloadFiles={canDownloadFiles}
channelName={channelNames[item.channel_id!]}
fileInfo={item}
index={fileInfosIndexes[item.id!] || 0}
isSingleImage={isSingleImage}
numOptions={numOptions}
onOptionsPress={onOptionsPress}
onPress={onPreviewPress}
publicLinkEnabled={publicLinkEnabled}
setAction={setAction}
updateFileForGallery={updateFileForGallery}
/>
);
}, [
(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 (
<FlatList
ListEmptyComponent={noResults}
contentContainerStyle={[paddingTop, containerStyle]}
data={orderedFilesForGallery}
indicatorStyle='black'
initialNumToRender={10}
listKey={'files'}
maxToRenderPerBatch={5}
nestedScrollEnabled={true}
refreshing={false}
removeClippedSubviews={true}
renderItem={renderItem}
scrollEventThrottle={16}
scrollToOverflowEnabled={true}
showsVerticalScrollIndicator={true}
testID='search_results.post_list.flat_list'
/>
<>
<FlatList
ListEmptyComponent={noResults}
contentContainerStyle={containerStyle}
data={orderedFileInfos}
indicatorStyle='black'
initialNumToRender={10}
listKey={'files'}
maxToRenderPerBatch={5}
nestedScrollEnabled={true}
refreshing={false}
removeClippedSubviews={true}
renderItem={renderItem}
scrollEventThrottle={16}
scrollToOverflowEnabled={true}
showsVerticalScrollIndicator={true}
testID='search_results.post_list.flat_list'
/>
<Toasts
action={action}
fileInfo={lastViewedFileInfo}
setAction={setAction}
/>
</>
);
};

View File

@@ -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}
/>
</View>

44
app/utils/files.tsx Normal file
View File

@@ -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));
};