[MM-44655] : Add channel files screen (#7223)
* add channel files screen * fix filtering * refactor code and fix style * add search and some refactoring * fix lint * refactoring * move file filters to files directory * fix dependecy * UX fixes and minor memo dependency changes * fix empty state on Ipad * fix search issues * fix lint error * fix search issue * fix loading on filter changes * show different text if no files found when filter is applied * feedback review --------- Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
parent
265c4fe8a6
commit
100a760502
23 changed files with 570 additions and 32 deletions
|
|
@ -30,43 +30,51 @@ type FilterItem = {
|
|||
filterType: FileFilter;
|
||||
separator?: boolean;
|
||||
}
|
||||
|
||||
const data: FilterItem[] = [
|
||||
{
|
||||
export const FilterData = {
|
||||
[FileFilters.ALL]: {
|
||||
id: t('screen.search.results.filter.all_file_types'),
|
||||
defaultMessage: 'All file types',
|
||||
filterType: FileFilters.ALL,
|
||||
}, {
|
||||
},
|
||||
[FileFilters.DOCUMENTS]: {
|
||||
id: t('screen.search.results.filter.documents'),
|
||||
defaultMessage: 'Documents',
|
||||
filterType: FileFilters.DOCUMENTS,
|
||||
}, {
|
||||
},
|
||||
[FileFilters.SPREADSHEETS]: {
|
||||
id: t('screen.search.results.filter.spreadsheets'),
|
||||
defaultMessage: 'Spreadsheets',
|
||||
filterType: FileFilters.SPREADSHEETS,
|
||||
}, {
|
||||
},
|
||||
[FileFilters.PRESENTATIONS]: {
|
||||
id: t('screen.search.results.filter.presentations'),
|
||||
defaultMessage: 'Presentations',
|
||||
filterType: FileFilters.PRESENTATIONS,
|
||||
}, {
|
||||
},
|
||||
[FileFilters.CODE]: {
|
||||
id: t('screen.search.results.filter.code'),
|
||||
defaultMessage: 'Code',
|
||||
filterType: FileFilters.CODE,
|
||||
}, {
|
||||
},
|
||||
[FileFilters.IMAGES]: {
|
||||
id: t('screen.search.results.filter.images'),
|
||||
defaultMessage: 'Images',
|
||||
filterType: FileFilters.IMAGES,
|
||||
}, {
|
||||
},
|
||||
[FileFilters.AUDIO]: {
|
||||
id: t('screen.search.results.filter.audio'),
|
||||
defaultMessage: 'Audio',
|
||||
filterType: FileFilters.AUDIO,
|
||||
}, {
|
||||
},
|
||||
[FileFilters.VIDEOS]: {
|
||||
id: t('screen.search.results.filter.videos'),
|
||||
defaultMessage: 'Videos',
|
||||
filterType: FileFilters.VIDEOS,
|
||||
separator: false,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const data: FilterItem[] = Object.values(FilterData);
|
||||
|
||||
export const NUMBER_FILTER_ITEMS = data.length;
|
||||
export const FILTER_ITEM_HEIGHT = ITEM_HEIGHT;
|
||||
|
|
@ -78,7 +86,7 @@ type FilterProps = {
|
|||
title: string;
|
||||
}
|
||||
|
||||
const Filter = ({initialFilter, setFilter, title}: FilterProps) => {
|
||||
const File_filter = ({initialFilter, setFilter, title}: FilterProps) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
|
|
@ -122,4 +130,4 @@ const Filter = ({initialFilter, setFilter, title}: FilterProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default Filter;
|
||||
export default File_filter;
|
||||
|
|
@ -23,7 +23,7 @@ const styles = StyleSheet.create({
|
|||
|
||||
type Props = {
|
||||
canDownloadFiles: boolean;
|
||||
channelName: string | undefined;
|
||||
channelName?: string;
|
||||
fileInfo: FileInfo;
|
||||
index: number;
|
||||
numOptions: number;
|
||||
|
|
@ -5,6 +5,7 @@ import React, {useCallback, useMemo, useState} from 'react';
|
|||
import {FlatList, ListRenderItemInfo, StyleProp, ViewStyle} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import NoResults from '@components/files_search/no_results';
|
||||
import NoResultsWithTerm from '@components/no_results_with_term';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
|
|
@ -34,6 +35,8 @@ type Props = {
|
|||
paddingTop: StyleProp<ViewStyle>;
|
||||
publicLinkEnabled: boolean;
|
||||
searchValue: string;
|
||||
isChannelFiles?: boolean;
|
||||
isFilterEnabled?: boolean;
|
||||
}
|
||||
|
||||
const galleryIdentifier = 'search-files-location';
|
||||
|
|
@ -45,6 +48,8 @@ const FileResults = ({
|
|||
paddingTop,
|
||||
publicLinkEnabled,
|
||||
searchValue,
|
||||
isChannelFiles,
|
||||
isFilterEnabled,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
|
@ -53,16 +58,16 @@ const FileResults = ({
|
|||
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 containerStyle = useMemo(() => ([paddingTop, {top: fileInfos.length ? 8 : 0, flexGrow: 1}]), [fileInfos, paddingTop]);
|
||||
const numOptions = getNumberFileMenuOptions(canDownloadFiles, publicLinkEnabled);
|
||||
|
||||
const {images: imageAttachments, nonImages: nonImageAttachments} = useImageAttachments(fileInfos, publicLinkEnabled);
|
||||
const filesForGallery = imageAttachments.concat(nonImageAttachments);
|
||||
const filesForGallery = useMemo(() => imageAttachments.concat(nonImageAttachments), [imageAttachments, nonImageAttachments]);
|
||||
|
||||
const channelNames = useMemo(() => getChannelNamesWithID(fileChannels), [fileChannels]);
|
||||
const orderedFileInfos = useMemo(() => getOrderedFileInfos(filesForGallery), []);
|
||||
const fileInfosIndexes = useMemo(() => getFileInfosIndexes(orderedFileInfos), []);
|
||||
const orderedGalleryItems = useMemo(() => getOrderedGalleryItems(orderedFileInfos), []);
|
||||
const orderedFileInfos = useMemo(() => getOrderedFileInfos(filesForGallery), [filesForGallery]);
|
||||
const fileInfosIndexes = useMemo(() => getFileInfosIndexes(orderedFileInfos), [orderedFileInfos]);
|
||||
const orderedGalleryItems = useMemo(() => getOrderedGalleryItems(orderedFileInfos), [orderedFileInfos]);
|
||||
|
||||
const onPreviewPress = useCallback(preventDoubleTap((idx: number) => {
|
||||
openGalleryAtIndex(galleryIdentifier, idx, orderedGalleryItems);
|
||||
|
|
@ -88,10 +93,15 @@ const FileResults = ({
|
|||
}, [insets, isTablet, numOptions, theme]);
|
||||
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<FileInfo>) => {
|
||||
let channelName: string | undefined;
|
||||
if (!isChannelFiles && item.channel_id) {
|
||||
channelName = channelNames[item.channel_id];
|
||||
}
|
||||
|
||||
return (
|
||||
<FileResult
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
channelName={channelNames[item.channel_id!]}
|
||||
channelName={channelName}
|
||||
fileInfo={item}
|
||||
index={fileInfosIndexes[item.id!] || 0}
|
||||
key={`${item.id}-${item.name}`}
|
||||
|
|
@ -109,16 +119,24 @@ const FileResults = ({
|
|||
channelNames,
|
||||
fileInfosIndexes,
|
||||
onPreviewPress,
|
||||
setAction,
|
||||
onOptionsPress,
|
||||
numOptions,
|
||||
publicLinkEnabled,
|
||||
]);
|
||||
|
||||
const noResults = useMemo(() => (
|
||||
<NoResultsWithTerm
|
||||
term={searchValue}
|
||||
type={TabTypes.FILES}
|
||||
/>
|
||||
), [searchValue]);
|
||||
const noResults = useMemo(() => {
|
||||
if (!searchValue && isChannelFiles) {
|
||||
return (
|
||||
<NoResults isFilterEnabled={isFilterEnabled}/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<NoResultsWithTerm
|
||||
term={searchValue}
|
||||
type={TabTypes.FILES}
|
||||
/>
|
||||
);
|
||||
}, [searchValue]);
|
||||
|
||||
return (
|
||||
<>
|
||||
81
app/components/files_search/no_results.tsx
Normal file
81
app/components/files_search/no_results.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import SearchFilesIllustration from '@components/no_results_with_term/search_files_illustration';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const TEST_ID = 'channel_files';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: 32,
|
||||
height: '100%',
|
||||
alignItems: 'center' as const,
|
||||
justifyContent: 'center' as const,
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
textAlign: 'center',
|
||||
...typography('Heading', 400, 'SemiBold'),
|
||||
},
|
||||
paragraph: {
|
||||
marginTop: 8,
|
||||
textAlign: 'center',
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
...typography('Body', 200),
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
isFilterEnabled?: boolean;
|
||||
}
|
||||
function NoResults({isFilterEnabled}: Props) {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<SearchFilesIllustration/>
|
||||
{!isFilterEnabled && (
|
||||
<>
|
||||
<FormattedText
|
||||
defaultMessage='No files yet'
|
||||
id='channel_files.empty.title'
|
||||
style={styles.title}
|
||||
testID={`${TEST_ID}.empty.title`}
|
||||
/>
|
||||
<FormattedText
|
||||
defaultMessage={'Files posted in this channel will show here.'}
|
||||
id='channel_files.empty.paragraph'
|
||||
style={styles.paragraph}
|
||||
testID={`${TEST_ID}.empty.paragraph`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{isFilterEnabled && (
|
||||
<>
|
||||
<FormattedText
|
||||
defaultMessage='No files Found'
|
||||
id='channel_files.noFiles.title'
|
||||
style={styles.title}
|
||||
testID={`${TEST_ID}.empty.title`}
|
||||
/>
|
||||
<FormattedText
|
||||
defaultMessage={'This channel doesn\'t contain any files with the applied filters'}
|
||||
id='channel_files.noFiles.paragraph'
|
||||
style={styles.paragraph}
|
||||
testID={`${TEST_ID}.empty.paragraph`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default NoResults;
|
||||
|
|
@ -9,6 +9,7 @@ export const BROWSE_CHANNELS = 'BrowseChannels';
|
|||
export const CALL = 'Call';
|
||||
export const CHANNEL = 'Channel';
|
||||
export const CHANNEL_ADD_MEMBERS = 'ChannelAddMembers';
|
||||
export const CHANNEL_FILES = 'ChannelFiles';
|
||||
export const CHANNEL_INFO = 'ChannelInfo';
|
||||
export const CHANNEL_NOTIFICATION_PREFERENCES = 'ChannelNotificationPreferences';
|
||||
export const CODE = 'Code';
|
||||
|
|
@ -78,6 +79,7 @@ export default {
|
|||
CALL,
|
||||
CHANNEL,
|
||||
CHANNEL_ADD_MEMBERS,
|
||||
CHANNEL_FILES,
|
||||
CHANNEL_INFO,
|
||||
CHANNEL_NOTIFICATION_PREFERENCES,
|
||||
CODE,
|
||||
|
|
|
|||
172
app/screens/channel_files/channel_files.tsx
Normal file
172
app/screens/channel_files/channel_files.tsx
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Platform, StyleSheet, View} from 'react-native';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {searchFiles} from '@actions/remote/search';
|
||||
import FileResults from '@components/files_search/file_results';
|
||||
import Loading from '@components/loading';
|
||||
import Search from '@components/search';
|
||||
import {General} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {FileFilter, FileFilters, filterFileExtensions} from '@utils/file';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||
|
||||
import Header from './header';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
const TEST_ID = 'channel_files';
|
||||
|
||||
type Props = {
|
||||
channel: ChannelModel;
|
||||
canDownloadFiles: boolean;
|
||||
publicLinkEnabled: boolean;
|
||||
}
|
||||
|
||||
const edges: Edge[] = ['bottom', 'left', 'right'];
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
empty: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
list: {
|
||||
paddingVertical: 8,
|
||||
},
|
||||
loading: {
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
searchBar: {
|
||||
marginLeft: 20,
|
||||
marginRight: Platform.select({ios: 12, default: 20}),
|
||||
marginTop: 20,
|
||||
},
|
||||
noPaddingTop: {paddingTop: 0},
|
||||
});
|
||||
|
||||
const getSearchParams = (channelId: string, searchTerm?: string, filterValue?: FileFilter) => {
|
||||
const term = `channel:${channelId}`;
|
||||
const fileExtensions = filterFileExtensions(filterValue);
|
||||
const extensionTerms = fileExtensions ? ' ' + fileExtensions : '';
|
||||
const searchTerms = searchTerm ? ' ' + searchTerm : '';
|
||||
return {
|
||||
terms: term + searchTerms + extensionTerms,
|
||||
is_or_search: true,
|
||||
};
|
||||
};
|
||||
|
||||
const emptyFileResults: FileInfo[] = [];
|
||||
|
||||
function ChannelFiles({
|
||||
channel,
|
||||
canDownloadFiles,
|
||||
publicLinkEnabled,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const {formatMessage} = useIntl();
|
||||
const searchTimeoutId = useRef<NodeJS.Timeout | null>(null);
|
||||
const [filter, setFilter] = useState<FileFilter>(FileFilters.ALL);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [term, setTerm] = useState('');
|
||||
const lastSearchRequest = useRef<number>();
|
||||
|
||||
const [fileInfos, setFileInfos] = useState<FileInfo[]>(emptyFileResults);
|
||||
|
||||
const handleSearch = useCallback(async (searchTerm: string, ftr: FileFilter) => {
|
||||
const t = Date.now();
|
||||
lastSearchRequest.current = t;
|
||||
const searchParams = getSearchParams(channel.id, searchTerm, ftr);
|
||||
const {files} = await searchFiles(serverUrl, channel.teamId, searchParams);
|
||||
if (lastSearchRequest.current !== t) {
|
||||
return;
|
||||
}
|
||||
setFileInfos(files?.length ? files : emptyFileResults);
|
||||
setLoading(false);
|
||||
}, [serverUrl, channel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchTimeoutId.current) {
|
||||
clearTimeout(searchTimeoutId.current);
|
||||
}
|
||||
|
||||
searchTimeoutId.current = setTimeout(() => {
|
||||
handleSearch(term, filter);
|
||||
}, General.SEARCH_TIMEOUT_MILLISECONDS);
|
||||
}, [filter, term, handleSearch]);
|
||||
|
||||
const handleFilterChange = useCallback(async (filterValue: FileFilter) => {
|
||||
setLoading(true);
|
||||
setFilter(filterValue);
|
||||
}, []);
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
setTerm('');
|
||||
}, []);
|
||||
|
||||
const onTextChange = useCallback((searchTerm: string) => {
|
||||
if (term !== searchTerm) {
|
||||
setLoading(true);
|
||||
setTerm(searchTerm);
|
||||
}
|
||||
}, [term]);
|
||||
|
||||
const fileChannels = useMemo(() => [channel], [channel]);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
edges={edges}
|
||||
style={styles.flex}
|
||||
testID={`${TEST_ID}.screen`}
|
||||
>
|
||||
<View style={styles.searchBar}>
|
||||
<Search
|
||||
testID={`${TEST_ID}.search_bar`}
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelButtonTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
onChangeText={onTextChange}
|
||||
onCancel={clearSearch}
|
||||
autoCapitalize='none'
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
value={term}
|
||||
/>
|
||||
</View>
|
||||
<Header
|
||||
onFilterChanged={handleFilterChange}
|
||||
selectedFilter={filter}
|
||||
/>
|
||||
{loading &&
|
||||
<Loading
|
||||
color={theme.buttonBg}
|
||||
size='large'
|
||||
containerStyle={styles.loading}
|
||||
/>
|
||||
}
|
||||
{!loading &&
|
||||
<FileResults
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
fileChannels={fileChannels}
|
||||
fileInfos={fileInfos}
|
||||
paddingTop={styles.noPaddingTop}
|
||||
publicLinkEnabled={publicLinkEnabled}
|
||||
searchValue={term}
|
||||
isChannelFiles={true}
|
||||
isFilterEnabled={filter !== FileFilters.ALL}
|
||||
/>
|
||||
}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChannelFiles;
|
||||
132
app/screens/channel_files/header.tsx
Normal file
132
app/screens/channel_files/header.tsx
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import Badge from '@components/badge';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import Filter, {DIVIDERS_HEIGHT, FILTER_ITEM_HEIGHT, FilterData, NUMBER_FILTER_ITEMS} from '@components/files/file_filter';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {t} from '@i18n';
|
||||
import {TITLE_SEPARATOR_MARGIN, TITLE_SEPARATOR_MARGIN_TABLET, TITLE_HEIGHT} from '@screens/bottom_sheet/content';
|
||||
import {bottomSheet} from '@screens/navigation';
|
||||
import {FileFilter, FileFilters} from '@utils/file';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const TEST_ID = 'channel_files';
|
||||
|
||||
type Props = {
|
||||
onFilterChanged: (filter: FileFilter) => void;
|
||||
selectedFilter: FileFilter;
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 20,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 300, 'SemiBold'),
|
||||
},
|
||||
badge: {
|
||||
backgroundColor: theme.buttonBg,
|
||||
borderColor: theme.centerChannelBg,
|
||||
marginTop: 2,
|
||||
},
|
||||
iconsContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const Header = ({
|
||||
onFilterChanged,
|
||||
selectedFilter,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleFromTheme(theme);
|
||||
const intl = useIntl();
|
||||
const {bottom} = useSafeAreaInsets();
|
||||
const isTablet = useIsTablet();
|
||||
const hasFilters = selectedFilter !== FileFilters.ALL;
|
||||
const messageObject = hasFilters ? {
|
||||
id: FilterData[selectedFilter].id,
|
||||
defaultMessage: FilterData[selectedFilter].defaultMessage,
|
||||
} : {
|
||||
id: t('screen.channel_files.header.recent_files'),
|
||||
defaultMessage: 'Recent Files',
|
||||
};
|
||||
|
||||
const messagesText = intl.formatMessage(messageObject);
|
||||
const title = intl.formatMessage({id: 'screen.channel_files.results.filter.title', defaultMessage: 'Filter by file type'});
|
||||
|
||||
const snapPoints = useMemo(() => {
|
||||
return [
|
||||
1,
|
||||
bottomSheetSnapPoint(
|
||||
NUMBER_FILTER_ITEMS,
|
||||
FILTER_ITEM_HEIGHT,
|
||||
bottom,
|
||||
) + TITLE_HEIGHT + DIVIDERS_HEIGHT + (isTablet ? TITLE_SEPARATOR_MARGIN_TABLET : TITLE_SEPARATOR_MARGIN),
|
||||
];
|
||||
}, []);
|
||||
|
||||
const handleFilterPress = useCallback(() => {
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<Filter
|
||||
initialFilter={selectedFilter}
|
||||
setFilter={onFilterChanged}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
bottomSheet({
|
||||
closeButtonId: 'close-search-filters',
|
||||
renderContent,
|
||||
snapPoints,
|
||||
theme,
|
||||
title,
|
||||
});
|
||||
}, [onFilterChanged, selectedFilter]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text
|
||||
style={styles.title}
|
||||
testID={`${TEST_ID}.title`}
|
||||
>
|
||||
{messagesText}
|
||||
</Text>
|
||||
<View>
|
||||
<CompassIcon
|
||||
name={'filter-variant'}
|
||||
size={24}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.56)}
|
||||
onPress={handleFilterPress}
|
||||
/>
|
||||
<Badge
|
||||
style={styles.badge}
|
||||
visible={hasFilters}
|
||||
testID={`${TEST_ID}.filters.badge`}
|
||||
value={-1}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
||||
27
app/screens/channel_files/index.ts
Normal file
27
app/screens/channel_files/index.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// 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 {observeChannel} from '@queries/servers/channel';
|
||||
import {observeCanDownloadFiles, observeConfigBooleanValue} from '@queries/servers/system';
|
||||
|
||||
import ChannelFiles from './channel_files';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhance = withObservables(['channelId'], ({channelId, database}: Props) => {
|
||||
const channel = observeChannel(database, channelId);
|
||||
return {
|
||||
channel,
|
||||
canDownloadFiles: observeCanDownloadFiles(database),
|
||||
publicLinkEnabled: observeConfigBooleanValue(database, 'EnablePublicLink'),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhance(ChannelFiles));
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
import OptionItem from '@components/option_item';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {goToScreen} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
count: number;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
const ChannelFiles = ({channelId, count, displayName}: Props) => {
|
||||
const theme = useTheme();
|
||||
const {formatMessage} = useIntl();
|
||||
const title = formatMessage({id: 'channel_info.channel_files', defaultMessage: 'Files'});
|
||||
|
||||
const goToChannelFiles = preventDoubleTap(() => {
|
||||
const options = {
|
||||
topBar: {
|
||||
title: {
|
||||
text: title,
|
||||
},
|
||||
subtitle: {
|
||||
color: changeOpacity(theme.sidebarHeaderTextColor, 0.72),
|
||||
text: displayName,
|
||||
},
|
||||
},
|
||||
};
|
||||
goToScreen(Screens.CHANNEL_FILES, title, {channelId}, options);
|
||||
});
|
||||
|
||||
return (
|
||||
<OptionItem
|
||||
action={goToChannelFiles}
|
||||
label={title}
|
||||
icon='file-text-outline'
|
||||
type={Platform.select({ios: 'arrow', default: 'default'})}
|
||||
info={count.toString()}
|
||||
testID='channel_info.options.channel_files.option'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelFiles;
|
||||
33
app/screens/channel_info/options/channel_files/index.ts
Normal file
33
app/screens/channel_info/options/channel_files/index.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// 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 {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeChannel, observeChannelInfo} from '@queries/servers/channel';
|
||||
|
||||
import ChannelFiles from './channel_files';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
|
||||
const channel = observeChannel(database, channelId);
|
||||
const info = observeChannelInfo(database, channelId);
|
||||
const count = info.pipe(
|
||||
switchMap((i) => of$(i?.filesCount || 0)),
|
||||
);
|
||||
const displayName = channel.pipe(switchMap((c) => of$(c?.displayName)));
|
||||
|
||||
return {
|
||||
count,
|
||||
displayName,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(ChannelFiles));
|
||||
|
|
@ -8,6 +8,7 @@ import {General} from '@constants';
|
|||
import {isTypeDMorGM} from '@utils/channel';
|
||||
|
||||
import AddMembers from './add_members';
|
||||
import ChannelFiles from './channel_files';
|
||||
import EditChannel from './edit_channel';
|
||||
import IgnoreMentions from './ignore_mentions';
|
||||
import Members from './members';
|
||||
|
|
@ -36,6 +37,7 @@ const Options = ({
|
|||
}
|
||||
<NotificationPreference channelId={channelId}/>
|
||||
<PinnedMessages channelId={channelId}/>
|
||||
<ChannelFiles channelId={channelId}/>
|
||||
{type !== General.DM_CHANNEL &&
|
||||
<Members channelId={channelId}/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
|||
|
||||
import Badge from '@components/badge';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import Filter, {DIVIDERS_HEIGHT, FILTER_ITEM_HEIGHT, NUMBER_FILTER_ITEMS} from '@components/files/file_filter';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {TITLE_SEPARATOR_MARGIN, TITLE_SEPARATOR_MARGIN_TABLET, TITLE_HEIGHT} from '@screens/bottom_sheet/content';
|
||||
|
|
@ -17,7 +18,6 @@ import {bottomSheetSnapPoint} from '@utils/helpers';
|
|||
import {TabTypes, TabType} from '@utils/search';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import Filter, {DIVIDERS_HEIGHT, FILTER_ITEM_HEIGHT, NUMBER_FILTER_ITEMS} from './filter';
|
||||
import SelectButton from './header_button';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
|
@ -45,7 +45,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
badge: {
|
||||
backgroundColor: theme.buttonBg,
|
||||
borderColor: theme.centerChannelBg,
|
||||
marginTop: 8,
|
||||
marginTop: 2,
|
||||
},
|
||||
buttonsContainer: {
|
||||
marginBottom: 12,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const PostResults = ({
|
|||
searchValue,
|
||||
}: Props) => {
|
||||
const orderedPosts = useMemo(() => selectOrderedPosts(posts, 0, false, '', '', false, isTimezoneEnabled, currentTimezone, false).reverse(), [posts]);
|
||||
const containerStyle = useMemo(() => ({top: posts.length ? 4 : 8}), [posts]);
|
||||
const containerStyle = useMemo(() => ({top: posts.length ? 4 : 8, flexGrow: 1}), [posts]);
|
||||
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<PostListItem | PostListOtherItem>) => {
|
||||
switch (item.type) {
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import React, {useMemo} from 'react';
|
|||
import {StyleSheet, useWindowDimensions, View} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import FileResults from '@components/files_search/file_results';
|
||||
import Loading from '@components/loading';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {TabTypes, TabType} from '@utils/search';
|
||||
|
||||
import FileResults from './file_results';
|
||||
import PostResults from './post_results';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
|
@ -84,7 +84,7 @@ const Results = ({
|
|||
}, [selectedTab, width, !loading]);
|
||||
|
||||
const paddingTop = useMemo(() => (
|
||||
{paddingTop: scrollPaddingTop, flexGrow: 1}
|
||||
{paddingTop: scrollPaddingTop}
|
||||
), [scrollPaddingTop]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -82,6 +82,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
case Screens.CHANNEL_NOTIFICATION_PREFERENCES:
|
||||
screen = withServerDatabase(require('@screens/channel_notification_preferences').default);
|
||||
break;
|
||||
case Screens.CHANNEL_FILES:
|
||||
screen = withServerDatabase(require('@screens/channel_files').default);
|
||||
break;
|
||||
case Screens.CHANNEL_INFO:
|
||||
screen = withServerDatabase(require('@screens/channel_info').default);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -96,6 +96,10 @@
|
|||
"camera_type.video.option": "Record Video",
|
||||
"center_panel.archived.closeChannel": "Close Channel",
|
||||
"channel_add_members.add_members.button": "Add Members",
|
||||
"channel_files.empty.paragraph": "Files posted in this channel will show here.",
|
||||
"channel_files.empty.title": "No files yet",
|
||||
"channel_files.noFiles.paragraph": "This channel doesn't contain any files with the applied filters",
|
||||
"channel_files.noFiles.title": "No files Found",
|
||||
"channel_header.directchannel.you": "{displayName} (you)",
|
||||
"channel_header.info": "View info",
|
||||
"channel_header.member_count": "{count, plural, one {# member} other {# members}}",
|
||||
|
|
@ -108,6 +112,7 @@
|
|||
"channel_info.archive_description.cannot_view_archived": "This will archive the channel from the team and remove it from the user interface. Archived channels can be unarchived if needed again.\n\nAre you sure you wish to archive the {term} {name}?",
|
||||
"channel_info.archive_failed": "An error occurred trying to archive the channel {displayName}",
|
||||
"channel_info.archive_title": "Archive {term}",
|
||||
"channel_info.channel_files": "Files",
|
||||
"channel_info.close": "Close",
|
||||
"channel_info.close_dm": "Close direct message",
|
||||
"channel_info.close_dm_channel": "Are you sure you want to close this direct message? This will remove it from your home screen, but you can always open it again.",
|
||||
|
|
@ -822,6 +827,8 @@
|
|||
"rate.title": "Enjoying Mattermost?",
|
||||
"saved_messages.empty.paragraph": "To save something for later, long-press on a message and choose Save from the menu. Saved messages are only visible to you.",
|
||||
"saved_messages.empty.title": "No saved messages yet",
|
||||
"screen.channel_files.header.recent_files": "Recent Files",
|
||||
"screen.channel_files.results.filter.title": "Filter by file type",
|
||||
"screen.mentions.subtitle": "Messages you've been mentioned in",
|
||||
"screen.mentions.title": "Recent Mentions",
|
||||
"screen.saved_messages.subtitle": "All messages you've saved for follow up",
|
||||
|
|
|
|||
Loading…
Reference in a new issue