[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:
Ashish Dhama 2023-04-15 04:03:11 +05:30 committed by GitHub
parent 265c4fe8a6
commit 100a760502
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 570 additions and 32 deletions

View file

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

View file

@ -23,7 +23,7 @@ const styles = StyleSheet.create({
type Props = {
canDownloadFiles: boolean;
channelName: string | undefined;
channelName?: string;
fileInfo: FileInfo;
index: number;
numOptions: number;

View file

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

View 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;

View file

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

View 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;

View 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;

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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",