forked from Ivasoft/mattermost-mobile
[Gekidou MM-45790] Create File Options Menu for Tablet View (#6531)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
@@ -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: '',
|
||||
});
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
46
app/screens/home/search/results/file_options/toasts.tsx
Normal file
46
app/screens/home/search/results/file_options/toasts.tsx
Normal 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;
|
||||
122
app/screens/home/search/results/file_result.tsx
Normal file
122
app/screens/home/search/results/file_result.tsx
Normal 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;
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
44
app/utils/files.tsx
Normal 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));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user