diff --git a/app/components/team_list/index.test.tsx b/app/components/team_list/index.test.tsx
new file mode 100644
index 00000000000..7a510c1fc48
--- /dev/null
+++ b/app/components/team_list/index.test.tsx
@@ -0,0 +1,61 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+
+import {fireEvent, renderWithEverything} from '@test/intl-test-helper';
+import TestHelper from '@test/test_helper';
+
+import TeamList from './index';
+
+import type Database from '@nozbe/watermelondb/Database';
+import type TeamModel from '@typings/database/models/servers/team';
+
+describe('TeamList', () => {
+ let database: Database;
+ beforeAll(async () => {
+ const server = await TestHelper.setupServerDatabase();
+ database = server.database;
+ });
+ const teams = [
+ {id: 'team1', displayName: 'Team 1'} as TeamModel,
+ {id: 'team2', displayName: 'Team 2'} as TeamModel,
+ ];
+
+ it('should call onPress when a team is pressed', () => {
+ const onPress = jest.fn();
+ const {getByText} = renderWithEverything(
+ ,
+ {database},
+ );
+ fireEvent.press(getByText('Team 1'));
+ expect(onPress).toHaveBeenCalledWith('team1');
+ });
+
+ it('should render loading component when loading is true', () => {
+ const {getByTestId} = renderWithEverything(
+ ,
+ {database},
+ );
+ expect(getByTestId('team_list.loading')).toBeTruthy();
+ });
+
+ it('should render separator after the first item when separatorAfterFirstItem is true', () => {
+ const {getByTestId} = renderWithEverything(
+ ,
+ {database},
+ );
+ expect(getByTestId('team_list.separator')).toBeTruthy();
+ });
+});
diff --git a/app/components/team_list/index.tsx b/app/components/team_list/index.tsx
index b1e5e412468..f107812e13f 100644
--- a/app/components/team_list/index.tsx
+++ b/app/components/team_list/index.tsx
@@ -3,10 +3,12 @@
import {BottomSheetFlatList} from '@gorhom/bottom-sheet';
import React, {useCallback, useMemo} from 'react';
-import {type ListRenderItemInfo, StyleSheet, View} from 'react-native';
+import {type ListRenderItemInfo, View} from 'react-native';
import {FlatList} from 'react-native-gesture-handler'; // Keep the FlatList from gesture handler so it works well with bottom sheet
import Loading from '@components/loading';
+import {useTheme} from '@context/theme';
+import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import TeamListItem from './team_list_item';
@@ -23,15 +25,23 @@ type Props = {
testID?: string;
textColor?: string;
type?: BottomSheetList;
+ hideIcon?: boolean;
+ separatorAfterFirstItem?: boolean;
}
-const styles = StyleSheet.create({
- container: {
- flexGrow: 1,
- },
- contentContainer: {
- marginBottom: 4,
- },
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ container: {
+ flexGrow: 1,
+ },
+ contentContainer: {
+ marginBottom: 4,
+ },
+ separator: {
+ height: 1,
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
+ },
+ };
});
const keyExtractor = (item: TeamModel) => item.id;
@@ -47,11 +57,15 @@ export default function TeamList({
testID,
textColor,
type = 'FlatList',
+ hideIcon = false,
+ separatorAfterFirstItem = false,
}: Props) {
const List = useMemo(() => (type === 'FlatList' ? FlatList : BottomSheetFlatList), [type]);
+ const theme = useTheme();
+ const styles = getStyleSheet(theme);
- const renderTeam = useCallback(({item: t}: ListRenderItemInfo) => {
- return (
+ const renderTeam = useCallback(({item: t, index: i}: ListRenderItemInfo) => {
+ let teamListItem = (
);
- }, [textColor, iconTextColor, iconBackgroundColor, onPress, selectedTeamId]);
+ if (separatorAfterFirstItem && i === 0) {
+ teamListItem = (<>
+ {teamListItem}
+
+ >);
+ }
+ return teamListItem;
+ }, [textColor, iconTextColor, iconBackgroundColor, onPress, selectedTeamId, hideIcon, separatorAfterFirstItem, styles.separator]);
let footer;
if (loading) {
- footer = ();
+ footer = ();
}
return (
diff --git a/app/components/team_list/team_list_item/team_list_item.test.tsx b/app/components/team_list/team_list_item/team_list_item.test.tsx
new file mode 100644
index 00000000000..21524344b31
--- /dev/null
+++ b/app/components/team_list/team_list_item/team_list_item.test.tsx
@@ -0,0 +1,57 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+
+import {renderWithIntlAndTheme, fireEvent} from '@test/intl-test-helper';
+
+import TeamListItem from './team_list_item';
+
+import type TeamModel from '@typings/database/models/servers/team';
+
+describe('TeamListItem', () => {
+ const team = {
+ id: 'team_id',
+ displayName: 'Team Display Name',
+ lastTeamIconUpdatedAt: 0,
+ } as TeamModel;
+ const iconTestId = `team_sidebar.team_list.team_list_item.${team.id}.team_icon`;
+
+ it('should call onPress when pressed', () => {
+ const onPressMock = jest.fn();
+ const {getByText} = renderWithIntlAndTheme(
+ ,
+ );
+
+ fireEvent.press(getByText('Team Display Name'));
+
+ expect(onPressMock).toHaveBeenCalledWith('team_id');
+ });
+
+ it('should render TeamIcon when hideIcon is false', () => {
+ const {getByTestId} = renderWithIntlAndTheme(
+ ,
+ );
+
+ expect(getByTestId(iconTestId)).toBeTruthy();
+ });
+
+ it('should not render TeamIcon when hideIcon is true', () => {
+ const {queryByTestId} = renderWithIntlAndTheme(
+ ,
+ );
+
+ expect(queryByTestId(iconTestId)).toBeNull();
+ });
+});
diff --git a/app/components/team_list/team_list_item/team_list_item.tsx b/app/components/team_list/team_list_item/team_list_item.tsx
index a903e25beb1..eb012c743b2 100644
--- a/app/components/team_list/team_list_item/team_list_item.tsx
+++ b/app/components/team_list/team_list_item/team_list_item.tsx
@@ -20,6 +20,7 @@ type Props = {
iconBackgroundColor?: string;
selectedTeamId?: string;
onPress: (teamId: string) => void;
+ hideIcon?: boolean;
}
export const ITEM_HEIGHT = 56;
@@ -50,7 +51,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
};
});
-export default function TeamListItem({team, textColor, iconTextColor, iconBackgroundColor, selectedTeamId, onPress}: Props) {
+export default function TeamListItem({team, textColor, iconTextColor, iconBackgroundColor, selectedTeamId, onPress, hideIcon}: Props) {
const theme = useTheme();
const styles = getStyleSheet(theme);
@@ -68,17 +69,19 @@ export default function TeamListItem({team, textColor, iconTextColor, iconBackgr
type='opacity'
style={styles.touchable}
>
-
-
-
+ {!hideIcon &&
+
+
+
+ }
void;
title: string;
+ crossTeamSearchEnabled: boolean;
}
-export default function BottomSheetTeamList({teams, title, setTeamId, teamId}: Props) {
+export default function BottomSheetTeamList({teams, title, setTeamId, teamId, crossTeamSearchEnabled}: Props) {
+ const intl = useIntl();
const isTablet = useIsTablet();
const showTitle = !isTablet && Boolean(teams.length);
@@ -26,6 +30,14 @@ export default function BottomSheetTeamList({teams, title, setTeamId, teamId}: P
dismissBottomSheet();
}, [setTeamId]);
+ const teamList = useMemo(() => {
+ const list = [...teams];
+ if (crossTeamSearchEnabled) {
+ list.unshift({id: ALL_TEAMS_ID, displayName: intl.formatMessage({id: 'mobile.search.team.all_teams', defaultMessage: 'All teams'})} as TeamModel);
+ }
+ return list;
+ }, [teams, crossTeamSearchEnabled, intl]);
+
return (
);
diff --git a/app/screens/home/search/index.tsx b/app/screens/home/search/index.tsx
index 7240fd2b7b0..5ed7666785c 100644
--- a/app/screens/home/search/index.tsx
+++ b/app/screens/home/search/index.tsx
@@ -4,7 +4,7 @@
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import compose from 'lodash/fp/compose';
-import {observeCurrentTeamId} from '@queries/servers/system';
+import {observeConfigBooleanValue, observeCurrentTeamId} from '@queries/servers/system';
import {queryJoinedTeams} from '@queries/servers/team';
import SearchScreen from './search';
@@ -16,6 +16,7 @@ const enhance = withObservables([], ({database}: WithDatabaseArgs) => {
return {
teamId: currentTeamId,
teams: queryJoinedTeams(database).observe(),
+ crossTeamSearchEnabled: observeConfigBooleanValue(database, 'FeatureFlagExperimentalCrossTeamSearch'),
};
});
diff --git a/app/screens/home/search/initial/index.tsx b/app/screens/home/search/initial/index.tsx
index 2782170eb71..10fe9d14730 100644
--- a/app/screens/home/search/initial/index.tsx
+++ b/app/screens/home/search/initial/index.tsx
@@ -5,6 +5,7 @@ import compose from 'lodash/fp/compose';
import {of as of$} from 'rxjs';
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
+import {observeConfigBooleanValue} from '@queries/servers/system';
import {observeTeam, queryTeamSearchHistoryByTeamId} from '@queries/servers/team';
import Initial from './initial';
@@ -22,6 +23,7 @@ const enhance = withObservables(['teamId'], ({database, teamId}: EnhanceProps) =
switchMap((t) => of$(t?.displayName || '')),
distinctUntilChanged(),
),
+ crossTeamSearchEnabled: observeConfigBooleanValue(database, 'FeatureFlagExperimentalCrossTeamSearch'),
};
});
diff --git a/app/screens/home/search/initial/initial.tsx b/app/screens/home/search/initial/initial.tsx
index 3a70ccf02db..6053bd18eee 100644
--- a/app/screens/home/search/initial/initial.tsx
+++ b/app/screens/home/search/initial/initial.tsx
@@ -3,6 +3,8 @@
import React, {type Dispatch, type RefObject, type SetStateAction} from 'react';
+import {ALL_TEAMS_ID} from '@constants/team';
+
import Modifiers from './modifiers';
import RecentSearches from './recent_searches';
@@ -22,9 +24,11 @@ type Props = {
teamId: string;
teamName: string;
teams: TeamModel[];
+ crossTeamSearchEnabled: boolean;
}
-const Initial = ({recentSearches, scrollEnabled, searchValue, setRecentValue, searchRef, teamId, teamName, teams, setTeamId, setSearchValue}: Props) => {
+const Initial = ({recentSearches, scrollEnabled, searchValue, setRecentValue, searchRef, teamId, teamName, teams, setTeamId, setSearchValue, crossTeamSearchEnabled}: Props) => {
+ const showRecentSearches = Boolean(recentSearches.length) && teamId !== ALL_TEAMS_ID;
return (
<>
- {Boolean(recentSearches.length) &&
+ {showRecentSearches &&
jest.fn(() => null));
+jest.mock('./show_more', () => jest.fn(() => null));
+jest.mock('@react-native-camera-roll/camera-roll', () => jest.fn());
+
+describe('Modifiers', () => {
+ const scrollEnabled = useSharedValue(true);
+ const searchRef = React.createRef();
+ const setSearchValue = jest.fn();
+ const setTeamId = jest.fn();
+ const teams = [{id: 'team1', displayName: 'Team 1'}, {id: 'team2', displayName: 'Team 2'}] as TeamModel[];
+
+ const renderComponent = (teamId = ALL_TEAMS_ID) => {
+ return renderWithIntlAndTheme(
+ ,
+ );
+ };
+
+ it('should render correctly', () => {
+ const {getByTestId} = renderComponent();
+ expect(getByTestId('search.modifier.header')).toBeTruthy();
+ });
+
+ it('should render TeamPicker when there are multiple teams', () => {
+ renderComponent();
+ expect(TeamPicker).toHaveBeenCalled();
+ });
+
+ it('should render the From: and In: modifiers when a team is selected', () => {
+ const {getByTestId} = renderComponent('team1');
+ expect(getByTestId('search.modifier.from')).toBeTruthy();
+ expect(getByTestId('search.modifier.in')).toBeTruthy();
+ });
+
+ it('should not render the From: and In: modifiers when all teams are selected', () => {
+ const {queryByTestId} = renderComponent(ALL_TEAMS_ID);
+ expect(queryByTestId('search.modifier.from')).toBeNull();
+ expect(queryByTestId('search.modifier.in')).toBeNull();
+ });
+});
diff --git a/app/screens/home/search/initial/modifiers/index.tsx b/app/screens/home/search/initial/modifiers/index.tsx
index f2028d9a571..a08cfe4955d 100644
--- a/app/screens/home/search/initial/modifiers/index.tsx
+++ b/app/screens/home/search/initial/modifiers/index.tsx
@@ -4,11 +4,12 @@
import React, {type Dispatch, type RefObject, type SetStateAction, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {type IntlShape, useIntl} from 'react-intl';
import {View} from 'react-native';
-import Animated, {useSharedValue, useAnimatedStyle, withTiming} from 'react-native-reanimated';
+import Animated, {type SharedValue, useSharedValue, useAnimatedStyle, withTiming} from 'react-native-reanimated';
import FormattedText from '@components/formatted_text';
+import {ALL_TEAMS_ID} from '@constants/team';
import {useTheme} from '@context/theme';
-import TeamPickerIcon from '@screens/home/search/team_picker_icon';
+import TeamPicker from '@screens/home/search/team_picker';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@@ -19,31 +20,37 @@ import type {SearchRef} from '@components/search';
import type TeamModel from '@typings/database/models/servers/team';
const MODIFIER_LABEL_HEIGHT = 48;
-const TEAM_PICKER_ICON_SIZE = 32;
const NUM_ITEMS_BEFORE_EXPAND = 4;
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
- titleContainer: {
+ header: {
alignItems: 'center',
flexDirection: 'row',
marginTop: 20,
- marginRight: 18,
+ marginHorizontal: 18,
},
- title: {
+ titleContainer: {
flex: 1,
+ },
+ title: {
alignItems: 'center',
- paddingLeft: 18,
color: theme.centerChannelColor,
...typography('Heading', 300, 'SemiBold'),
},
+ teamPickerContainer: {
+ flex: 1,
+ alignItems: 'flex-end',
+ },
};
});
-const getModifiersSectionsData = (intl: IntlShape): ModifierItem[] => {
+const getModifiersSectionsData = (intl: IntlShape, teamId: string): ModifierItem[] => {
const formatMessage = intl.formatMessage;
- const sectionsData = [
- {
+
+ const sectionsData = [];
+ if (teamId !== ALL_TEAMS_ID) {
+ sectionsData.push({
term: 'From:',
testID: 'search.modifier.from',
description: formatMessage({id: 'mobile.search.modifier.from', defaultMessage: ' a specific user'}),
@@ -51,52 +58,40 @@ const getModifiersSectionsData = (intl: IntlShape): ModifierItem[] => {
term: 'In:',
testID: 'search.modifier.in',
description: formatMessage({id: 'mobile.search.modifier.in', defaultMessage: ' a specific channel'}),
- },
+ });
+ }
+
+ sectionsData.push({
+ term: '-',
+ testID: 'search.modifier.exclude',
+ description: formatMessage({id: 'mobile.search.modifier.exclude', defaultMessage: ' exclude search terms'}),
+ }, {
+ term: '""',
+ testID: 'search.modifier.phrases',
+ description: formatMessage({id: 'mobile.search.modifier.phrases', defaultMessage: ' messages with phrases'}),
+ cursorPosition: -1,
+ });
- // {
- // term: 'On:',
- // testID: 'search.modifier.on',
- // description: formatMessage({id: 'mobile.search.modifier.on', defaultMessage: ' a specific date'}),
- // },
- // {
- // term: 'After:',
- // testID: 'search.modifier.after',
- // description: formatMessage({id: 'mobile.search.modifier.after', defaultMessage: ' after a date'}),
- // }, {
- // term: 'Before:',
- // testID: 'search.modifier.before',
- // description: formatMessage({id: 'mobile.search.modifier.before', defaultMessage: ' before a date'}),
- // },
- {
- term: '-',
- testID: 'search.modifier.exclude',
- description: formatMessage({id: 'mobile.search.modifier.exclude', defaultMessage: ' exclude search terms'}),
- }, {
- term: '""',
- testID: 'search.modifier.phrases',
- description: formatMessage({id: 'mobile.search.modifier.phrases', defaultMessage: ' messages with phrases'}),
- cursorPosition: -1,
- },
- ];
return sectionsData;
};
type Props = {
- scrollEnabled: Animated.SharedValue;
+ scrollEnabled: SharedValue;
searchRef: RefObject;
setSearchValue: Dispatch>;
searchValue?: string;
setTeamId: (id: string) => void;
teamId: string;
teams: TeamModel[];
+ crossTeamSearchEnabled: boolean;
}
-const Modifiers = ({scrollEnabled, searchValue, setSearchValue, searchRef, setTeamId, teamId, teams}: Props) => {
+const Modifiers = ({scrollEnabled, searchValue, setSearchValue, searchRef, setTeamId, teamId, teams, crossTeamSearchEnabled}: Props) => {
const theme = useTheme();
const intl = useIntl();
const [showMore, setShowMore] = useState(false);
const height = useSharedValue(NUM_ITEMS_BEFORE_EXPAND * MODIFIER_LABEL_HEIGHT);
- const data = useMemo(() => getModifiersSectionsData(intl), [intl]);
+ const data = useMemo(() => getModifiersSectionsData(intl, teamId), [intl, teamId]);
const timeoutRef = useRef();
const styles = getStyleFromTheme(theme);
@@ -143,20 +138,25 @@ const Modifiers = ({scrollEnabled, searchValue, setSearchValue, searchRef, setTe
return (
<>
-
-
+
+
+
+
+
{teams.length > 1 &&
-
+
+
+
}
diff --git a/app/screens/home/search/results/header.test.tsx b/app/screens/home/search/results/header.test.tsx
new file mode 100644
index 00000000000..ec1a44da3a5
--- /dev/null
+++ b/app/screens/home/search/results/header.test.tsx
@@ -0,0 +1,114 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+
+import {renderWithIntlAndTheme, fireEvent} from '@test/intl-test-helper';
+import {FileFilters} from '@utils/file';
+import {TabTypes} from '@utils/search';
+
+import Header from './header';
+
+import type TeamModel from '@typings/database/models/servers/team';
+
+// Some subcomponents require react-native-camera-roll, which is not available in the test environment
+jest.mock('@react-native-camera-roll/camera-roll', () => ({}));
+
+describe('Header', () => {
+ const onTabSelect = jest.fn();
+ const onFilterChanged = jest.fn();
+ const setTeamId = jest.fn();
+
+ const teams = [
+ {id: 'team1', displayName: 'Team 1'},
+ {id: 'team2', displayName: 'Team 2'},
+ ] as TeamModel[];
+
+ it('should render correctly', () => {
+ const {getByText} = renderWithIntlAndTheme(
+ ,
+ );
+
+ expect(getByText('Messages')).toBeTruthy();
+ expect(getByText('Files')).toBeTruthy();
+ });
+
+ it('should call onTabSelect with MESSAGES when Messages button is pressed', () => {
+ const {getByText} = renderWithIntlAndTheme(
+ ,
+ );
+
+ fireEvent.press(getByText('Messages'));
+ expect(onTabSelect).toHaveBeenCalledWith(TabTypes.MESSAGES);
+ });
+
+ it('should call onTabSelect with FILES when Files button is pressed', () => {
+ const {getByText} = renderWithIntlAndTheme(
+ ,
+ );
+
+ fireEvent.press(getByText('Files'));
+ expect(onTabSelect).toHaveBeenCalledWith(TabTypes.FILES);
+ });
+
+ it('should render TeamPicker when there are multiple teams', () => {
+ const {getByText} = renderWithIntlAndTheme(
+ ,
+ );
+
+ expect(getByText('Team 1')).toBeTruthy();
+ });
+
+ it('should render the file type filter when the Files tab is selected', () => {
+ const {getByTestId} = renderWithIntlAndTheme(
+ ,
+ );
+
+ expect(getByTestId('search.filters.file_type_icon')).toBeTruthy();
+ });
+});
diff --git a/app/screens/home/search/results/header.tsx b/app/screens/home/search/results/header.tsx
index e989ecd38a5..76fe8ba6bca 100644
--- a/app/screens/home/search/results/header.tsx
+++ b/app/screens/home/search/results/header.tsx
@@ -10,7 +10,7 @@ import Filter, {DIVIDERS_HEIGHT, FILTER_ITEM_HEIGHT, NUMBER_FILTER_ITEMS} from '
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';
-import TeamPickerIcon from '@screens/home/search/team_picker_icon';
+import TeamPicker from '@screens/home/search/team_picker';
import {bottomSheet} from '@screens/navigation';
import {type FileFilter, FileFilters} from '@utils/file';
import {bottomSheetSnapPoint} from '@utils/helpers';
@@ -29,13 +29,13 @@ type Props = {
setTeamId: (id: string) => void;
teamId: string;
teams: TeamModel[];
+ crossTeamSearchEnabled: boolean;
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
return {
container: {
marginTop: 10,
- backgroundColor: theme.centerChannelBg,
borderBottomWidth: 1,
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
},
@@ -44,15 +44,26 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
borderColor: theme.centerChannelBg,
marginTop: 2,
},
- buttonsContainer: {
+ header: {
marginBottom: 12,
paddingHorizontal: 12,
flexDirection: 'row',
+ justifyContent: 'space-between',
},
- iconsContainer: {
- alignItems: 'center',
+ buttonContainer: {
+ flexDirection: 'row',
+ },
+ teamPickerContainer: {
+ flex: 1,
flexDirection: 'row',
- marginLeft: 'auto',
+ justifyContent: 'flex-end',
+ },
+ filterContainer: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ maxWidth: 32,
},
};
});
@@ -65,6 +76,7 @@ const Header = ({
selectedTab,
selectedFilter,
teams,
+ crossTeamSearchEnabled,
}: Props) => {
const theme = useTheme();
const styles = getStyleFromTheme(theme);
@@ -113,50 +125,50 @@ const Header = ({
theme,
title,
});
- }, [onFilterChanged, selectedFilter]);
-
- const filterStyle = useMemo(() => ({marginRight: teams.length > 1 ? 0 : 8}), [teams.length > 1]);
+ }, [onFilterChanged, selectedFilter, snapPoints, title, theme]);
return (
-
-
-
-
- {showFilterIcon && (
-
-
-
-
- )}
+
+
+
+
+
+ {showFilterIcon && (
+
+
+
+
+ )}
+
{teams.length > 1 && (
-
)}
diff --git a/app/screens/home/search/search.tsx b/app/screens/home/search/search.tsx
index 45386ab6bfc..b5288d532ff 100644
--- a/app/screens/home/search/search.tsx
+++ b/app/screens/home/search/search.tsx
@@ -18,6 +18,7 @@ import Loading from '@components/loading';
import NavigationHeader from '@components/navigation_header';
import RoundedHeaderContext from '@components/rounded_header_context';
import {Screens} from '@constants';
+import {ALL_TEAMS_ID} from '@constants/team';
import {BOTTOM_TAB_HEIGHT} from '@constants/view';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
@@ -50,6 +51,7 @@ const AutocompletePaddingTop = 4;
type Props = {
teamId: string;
teams: TeamModel[];
+ crossTeamSearchEnabled: boolean;
}
const styles = StyleSheet.create({
@@ -77,7 +79,7 @@ const getSearchParams = (terms: string, filterValue?: FileFilter) => {
const searchScreenIndex = 1;
-const SearchScreen = ({teamId, teams}: Props) => {
+const SearchScreen = ({teamId, teams, crossTeamSearchEnabled}: Props) => {
const nav = useNavigation();
const isFocused = useIsFocused();
const intl = useIntl();
@@ -188,7 +190,10 @@ const SearchScreen = ({teamId, teams}: Props) => {
hideHeader(true);
handleLoading(true);
setLastSearchedValue(term);
- addSearchToTeamSearchHistory(serverUrl, newSearchTeamId, term);
+
+ if (newSearchTeamId !== ALL_TEAMS_ID) {
+ addSearchToTeamSearchHistory(serverUrl, newSearchTeamId, term);
+ }
const [postResults, {files, channels}] = await Promise.all([
searchPosts(serverUrl, newSearchTeamId, searchParams),
searchFiles(serverUrl, newSearchTeamId, searchParams),
@@ -384,6 +389,7 @@ const SearchScreen = ({teamId, teams}: Props) => {
selectedTab={selectedTab}
selectedFilter={filter}
teams={teams}
+ crossTeamSearchEnabled={crossTeamSearchEnabled}
/>
}
diff --git a/app/screens/home/search/team_picker.test.tsx b/app/screens/home/search/team_picker.test.tsx
new file mode 100644
index 00000000000..e6726f11d03
--- /dev/null
+++ b/app/screens/home/search/team_picker.test.tsx
@@ -0,0 +1,63 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+
+import {ALL_TEAMS_ID} from '@constants/team';
+import {bottomSheet} from '@screens/navigation';
+import {fireEvent, renderWithIntlAndTheme} from '@test/intl-test-helper';
+
+import TeamPicker from './team_picker';
+
+import type {TeamModel} from '@database/models/server';
+
+jest.mock('@screens/navigation', () => ({
+ bottomSheet: jest.fn(),
+}));
+
+// Some subcomponents require react-native-camera-roll, which is not available in the test environment
+jest.mock('@react-native-camera-roll/camera-roll', () => ({}));
+
+describe('TeamPicker', () => {
+ const teams = [
+ {id: 'team1', displayName: 'Team 1'} as TeamModel,
+ {id: 'team2', displayName: 'Team 2'} as TeamModel,
+ ];
+
+ it('should render the selected team name', () => {
+ const {getByText} = renderWithIntlAndTheme(
+ ,
+ );
+ expect(getByText('Team 1')).toBeTruthy();
+ });
+
+ it('should render "All teams" when teamId is ALL_TEAMS_ID', () => {
+ const {getByText} = renderWithIntlAndTheme(
+ ,
+ );
+ expect(getByText('All teams')).toBeTruthy();
+ });
+
+ it('should call bottomSheet when the team picker is pressed', () => {
+ const {getByTestId} = renderWithIntlAndTheme(
+ ,
+ );
+ fireEvent.press(getByTestId('team_picker.button'));
+ expect(bottomSheet).toHaveBeenCalled();
+ });
+});
diff --git a/app/screens/home/search/team_picker_icon.tsx b/app/screens/home/search/team_picker.tsx
similarity index 64%
rename from app/screens/home/search/team_picker_icon.tsx
rename to app/screens/home/search/team_picker.tsx
index 954e9043693..4ee98af481d 100644
--- a/app/screens/home/search/team_picker_icon.tsx
+++ b/app/screens/home/search/team_picker.tsx
@@ -3,12 +3,12 @@
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
-import {View} from 'react-native';
+import {Text, View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import {ITEM_HEIGHT} from '@components/slide_up_panel_item';
-import TeamIcon from '@components/team_sidebar/team_list/team_item/team_icon';
import TouchableWithFeedback from '@components/touchable_with_feedback';
+import {ALL_TEAMS_ID} from '@constants/team';
import {useTheme} from '@context/theme';
import {TITLE_HEIGHT} from '@screens/bottom_sheet';
import {bottomSheet} from '@screens/navigation';
@@ -25,39 +25,38 @@ const NO_TEAMS_HEIGHT = 392;
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
- teamContainer: {
- paddingLeft: 8,
+ teamPicker: {
flexDirection: 'row',
alignItems: 'center',
+ justifyContent: 'flex-end',
+ width: '100%',
},
- border: {
- marginLeft: 12,
- borderLeftWidth: 1,
- borderLeftColor: changeOpacity(theme.centerChannelColor, 0.16),
+ container: {
+ flex: 1,
},
- teamIcon: {
- flexDirection: 'row',
- },
- compass: {
- alignItems: 'center',
- marginLeft: 0,
+ teamName: {
+ color: theme.centerChannelColor,
+ fontSize: 12,
+ textAlign: 'right',
},
};
});
type Props = {
- size?: number;
- divider?: boolean;
teams: TeamModel[];
setTeamId: (id: string) => void;
teamId: string;
+ crossTeamSearchEnabled: boolean;
}
-const TeamPickerIcon = ({size = 24, divider = false, setTeamId, teams, teamId}: Props) => {
+const TeamPicker = ({setTeamId, teams, teamId, crossTeamSearchEnabled}: Props) => {
const intl = useIntl();
const theme = useTheme();
const styles = getStyleFromTheme(theme);
- const selectedTeam = teams.find((t) => t.id === teamId);
+ let selectedTeam = teams.find((t) => t.id === teamId);
+ if (teamId === ALL_TEAMS_ID) {
+ selectedTeam = {id: ALL_TEAMS_ID, displayName: intl.formatMessage({id: 'mobile.search.team.all_teams', defaultMessage: 'All teams'})} as TeamModel;
+ }
const title = intl.formatMessage({id: 'mobile.search.team.select', defaultMessage: 'Select a team to search'});
@@ -69,6 +68,7 @@ const TeamPickerIcon = ({size = 24, divider = false, setTeamId, teams, teamId}:
teams={teams}
teamId={teamId}
title={title}
+ crossTeamSearchEnabled={crossTeamSearchEnabled}
/>
);
};
@@ -98,23 +98,18 @@ const TeamPickerIcon = ({size = 24, divider = false, setTeamId, teams, teamId}:
onPress={handleTeamChange}
type='opacity'
testID='team_picker.button'
+ style={styles.teamPicker}
>
-
-
-
-
+
+ {selectedTeam.displayName}
+
+
@@ -124,4 +119,4 @@ const TeamPickerIcon = ({size = 24, divider = false, setTeamId, teams, teamId}:
>
);
};
-export default TeamPickerIcon;
+export default TeamPicker;
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 76f2e399ed9..0ec805a5af1 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -774,6 +774,7 @@
"mobile.search.results": "{count} search {count, plural, one {result} other {results}}",
"mobile.search.show_less": "Show less",
"mobile.search.show_more": "Show more",
+ "mobile.search.team.all_teams": "All teams",
"mobile.search.team.select": "Select a team to search",
"mobile.server_identifier.exists": "You are already connected to this server.",
"mobile.server_link.error.text": "The link could not be found on this server.",
diff --git a/types/api/config.d.ts b/types/api/config.d.ts
index 6221eeac8a5..e7b05c3e0be 100644
--- a/types/api/config.d.ts
+++ b/types/api/config.d.ts
@@ -121,6 +121,7 @@ interface ClientConfig {
FeatureFlagCollapsedThreads?: string;
FeatureFlagPostPriority?: string;
FeatureFlagChannelBookmarks?: string;
+ FeatureFlagExperimentalCrossTeamSearch?: string;
ForgotPasswordLink?: string;
GfycatApiKey: string;
GfycatApiSecret: string;