diff --git a/app/actions/remote/search.ts b/app/actions/remote/search.ts index c94faf4081..d3e40d8997 100644 --- a/app/actions/remote/search.ts +++ b/app/actions/remote/search.ts @@ -25,7 +25,7 @@ export async function fetchRecentMentions(serverUrl: string): Promise key).join(' ').trim() + ' '; - const results = await searchPosts(serverUrl, {terms, is_or_search: true}); + const results = await searchPosts(serverUrl, '', {terms, is_or_search: true}); if (results.error) { throw results.error; } @@ -46,13 +46,13 @@ export async function fetchRecentMentions(serverUrl: string): Promise => { +export const searchPosts = async (serverUrl: string, teamId: string, params: PostSearchParams): Promise => { try { const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const client = NetworkManager.getClient(serverUrl); let postsArray: Post[] = []; - const data = await client.searchPosts('', params.terms, params.is_or_search); + const data = await client.searchPosts(teamId, params.terms, params.is_or_search); const posts = data.posts || {}; const order = data.order || []; diff --git a/app/actions/remote/team.ts b/app/actions/remote/team.ts index e8d918b966..534f1be136 100644 --- a/app/actions/remote/team.ts +++ b/app/actions/remote/team.ts @@ -10,7 +10,7 @@ import DatabaseManager from '@database/manager'; import NetworkManager from '@managers/network_manager'; import {prepareCategories, prepareCategoryChannels} from '@queries/servers/categories'; import {prepareMyChannelsForTeam, getDefaultChannelForTeam} from '@queries/servers/channel'; -import {prepareCommonSystemValues, getCurrentTeamId} from '@queries/servers/system'; +import {prepareCommonSystemValues, getCurrentTeamId, getCurrentUserId} from '@queries/servers/system'; import {addTeamToTeamHistory, prepareDeleteTeam, prepareMyTeams, getNthLastChannelFromTeam, queryTeamsById, syncTeamTable} from '@queries/servers/team'; import EphemeralStore from '@store/ephemeral_store'; import {isTablet} from '@utils/helpers'; @@ -29,6 +29,22 @@ export type MyTeamsRequest = { error?: unknown; } +export async function addCurrentUserToTeam(serverUrl: string, teamId: string, fetchOnly = false) { + let database; + try { + database = DatabaseManager.getServerDatabaseAndOperator(serverUrl).database; + } catch (error) { + return {error}; + } + + const currentUserId = await getCurrentUserId(database); + + if (!currentUserId) { + return {error: 'no current user'}; + } + return addUserToTeam(serverUrl, teamId, currentUserId, fetchOnly); +} + export async function addUserToTeam(serverUrl: string, teamId: string, userId: string, fetchOnly = false) { let client; try { diff --git a/app/components/markdown/at_mention/at_mention.tsx b/app/components/markdown/at_mention/at_mention.tsx index c6aa97b8bd..3011d5f6ff 100644 --- a/app/components/markdown/at_mention/at_mention.tsx +++ b/app/components/markdown/at_mention/at_mention.tsx @@ -10,10 +10,10 @@ import {GestureResponderEvent, Keyboard, StyleProp, StyleSheet, Text, TextStyle, import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {fetchUserOrGroupsByMentionsInBatch} from '@actions/remote/user'; -import {useServerUrl} from '@app/context/server'; import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item'; import {Screens} from '@constants'; import {MM_TABLES} from '@constants/database'; +import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import GroupModel from '@database/models/server/group'; import UserModel from '@database/models/server/user'; diff --git a/app/components/no_results_with_term/index.tsx b/app/components/no_results_with_term/index.tsx index b5c8ae9cc0..b5ee725e6f 100644 --- a/app/components/no_results_with_term/index.tsx +++ b/app/components/no_results_with_term/index.tsx @@ -23,6 +23,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { return { container: { flexGrow: 1, + height: '100%', alignItems: 'center' as const, justifyContent: 'center' as const, }, diff --git a/app/components/team_sidebar/add_team/add_team_slide_up.tsx b/app/components/team_sidebar/add_team/add_team_slide_up.tsx index a37eb425ce..48ece80b3a 100644 --- a/app/components/team_sidebar/add_team/add_team_slide_up.tsx +++ b/app/components/team_sidebar/add_team/add_team_slide_up.tsx @@ -3,35 +3,69 @@ import React, {useCallback} from 'react'; import {useIntl} from 'react-intl'; +import {View} from 'react-native'; -import {handleTeamChange} from '@actions/remote/team'; +import {addCurrentUserToTeam, handleTeamChange} from '@actions/remote/team'; +import FormattedText from '@components/formatted_text'; +import Empty from '@components/illustrations/no_team'; import {useServerUrl} from '@context/server'; +import {useTheme} from '@context/theme'; import BottomSheetContent from '@screens/bottom_sheet/content'; import {dismissBottomSheet} from '@screens/navigation'; +import {makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; import TeamList from './team_list'; import type TeamModel from '@typings/database/models/servers/team'; +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ + empty: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + title: { + color: theme.centerChannelColor, + lineHeight: 28, + marginTop: 16, + ...typography('Heading', 400, 'Regular'), + }, + description: { + color: theme.centerChannelColor, + marginTop: 8, + maxWidth: 334, + ...typography('Body', 200, 'Regular'), + }, +})); + type Props = { otherTeams: TeamModel[]; + title: string; showTitle?: boolean; } -export default function AddTeamSlideUp({otherTeams, showTitle = true}: Props) { +export default function AddTeamSlideUp({otherTeams, title, showTitle = true}: Props) { const intl = useIntl(); const serverUrl = useServerUrl(); + const theme = useTheme(); + const styles = getStyleSheet(theme); const onPressCreate = useCallback(() => { //TODO Create team screen https://mattermost.atlassian.net/browse/MM-43622 dismissBottomSheet(); }, []); - const onTeamAdded = useCallback(async (teamId: string) => { - await dismissBottomSheet(); - handleTeamChange(serverUrl, teamId); + const onPress = useCallback(async (teamId: string) => { + const {error} = await addCurrentUserToTeam(serverUrl, teamId); + if (!error) { + await dismissBottomSheet(); + handleTeamChange(serverUrl, teamId); + } }, [serverUrl]); + const hasOtherTeams = otherTeams.length; + return ( - + {hasOtherTeams && + + } + {!hasOtherTeams && + + + + + + } ); } diff --git a/app/components/team_sidebar/add_team/index.tsx b/app/components/team_sidebar/add_team/index.tsx index df313772fd..7da48b61bd 100644 --- a/app/components/team_sidebar/add_team/index.tsx +++ b/app/components/team_sidebar/add_team/index.tsx @@ -9,7 +9,7 @@ import CompassIcon from '@components/compass_icon'; import TouchableWithFeedback from '@components/touchable_with_feedback'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; -import {bottomSheet} from '@screens/navigation'; +import {bottomSheetWithTeamList} from '@screens/navigation'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; @@ -21,12 +21,6 @@ type Props = { otherTeams: TeamModel[]; } -const ITEM_HEIGHT = 72; -const HEADER_HEIGHT = 66; -const CONTAINER_HEIGHT = 392; - -//const CREATE_HEIGHT = 97; - const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { return { container: { @@ -55,31 +49,27 @@ export default function AddTeam({otherTeams}: Props) { const dimensions = useWindowDimensions(); const intl = useIntl(); const isTablet = useIsTablet(); - const maxHeight = Math.round((dimensions.height * 0.9)); const onPress = useCallback(preventDoubleTap(() => { + const title = intl.formatMessage({id: 'mobile.add_team.join_team', defaultMessage: 'Join Another Team'}); const renderContent = () => { return ( ); }; - let height = CONTAINER_HEIGHT; - if (otherTeams.length) { - height = Math.min(maxHeight, HEADER_HEIGHT + ((otherTeams.length + 1) * ITEM_HEIGHT)); - } - - bottomSheet({ - closeButtonId: 'close-join-team', + bottomSheetWithTeamList({ + dimensions, renderContent, - snapPoints: [height, 10], theme, - title: intl.formatMessage({id: 'mobile.add_team.join_team', defaultMessage: 'Join Another Team'}), + title, + teams: otherTeams, }); - }), [otherTeams, isTablet, theme]); + }), [otherTeams, intl, isTablet, dimensions, theme]); return ( diff --git a/app/components/team_sidebar/add_team/team_list.tsx b/app/components/team_sidebar/add_team/team_list.tsx index a164c1ff7e..28da68a3c3 100644 --- a/app/components/team_sidebar/add_team/team_list.tsx +++ b/app/components/team_sidebar/add_team/team_list.tsx @@ -2,14 +2,9 @@ // See LICENSE.txt for license information. import React, {useCallback} from 'react'; -import {ListRenderItemInfo, View} from 'react-native'; +import {ListRenderItemInfo, StyleSheet, 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 FormattedText from '@components/formatted_text'; -import Empty from '@components/illustrations/no_team'; -import {useTheme} from '@context/theme'; -import {makeStyleSheetFromTheme} from '@utils/theme'; - import TeamListItem from './team_list_item'; import type TeamModel from '@typings/database/models/servers/team'; @@ -19,85 +14,44 @@ type Props = { textColor?: string; iconTextColor?: string; iconBackgroundColor?: string; - onTeamAdded: (id: string) => void; + onPress: (id: string) => void; testID?: string; + selectedTeamId?: string; } -const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ +const styles = StyleSheet.create({ container: { flexShrink: 1, }, contentContainer: { marginBottom: 4, }, - empty: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - }, - title: { - fontFamily: 'Metropolis', - fontSize: 20, - color: theme.centerChannelColor, - lineHeight: 28, - marginTop: 16, - }, - description: { - fontFamily: 'Open Sans', - fontSize: 16, - color: theme.centerChannelColor, - lineHeight: 24, - marginTop: 8, - maxWidth: 334, - }, -})); +}); const keyExtractor = (item: TeamModel) => item.id; -export default function TeamList({teams, textColor, iconTextColor, iconBackgroundColor, onTeamAdded, testID}: Props) { - const theme = useTheme(); - const styles = getStyleSheet(theme); - +export default function TeamList({teams, textColor, iconTextColor, iconBackgroundColor, onPress, testID, selectedTeamId}: Props) { const renderTeam = useCallback(({item: t}: ListRenderItemInfo) => { return ( ); - }, [textColor, iconTextColor, iconBackgroundColor, onTeamAdded]); - - if (teams.length) { - return ( - - - - ); - } + }, [textColor, iconTextColor, iconBackgroundColor, onPress, selectedTeamId]); return ( - - - - + ); diff --git a/app/components/team_sidebar/add_team/team_list_item/team_list_item.tsx b/app/components/team_sidebar/add_team/team_list_item/team_list_item.tsx index 15f4c7b9b1..0182bf0d33 100644 --- a/app/components/team_sidebar/add_team/team_list_item/team_list_item.tsx +++ b/app/components/team_sidebar/add_team/team_list_item/team_list_item.tsx @@ -4,10 +4,9 @@ import React, {useCallback} from 'react'; import {Text, View} from 'react-native'; -import {addUserToTeam} from '@actions/remote/team'; +import CompassIcon from '@components/compass_icon'; import TeamIcon from '@components/team_sidebar/team_list/team_item/team_icon'; import TouchableWithFeedback from '@components/touchable_with_feedback'; -import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -16,18 +15,22 @@ import type TeamModel from '@typings/database/models/servers/team'; type Props = { team: TeamModel | Team; - currentUserId: string; textColor?: string; iconTextColor?: string; iconBackgroundColor?: string; - onTeamAdded: (teamId: string) => void; + selectedTeamId?: string; + onPress: (teamId: string) => void; } +const CONTAINER_HEIGHT = 40; +const CONTAINER_VERTICAL_MARGIN = 8; +export const ITEM_HEIGHT = CONTAINER_HEIGHT + (CONTAINER_VERTICAL_MARGIN * 2); + const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { return { container: { - height: 64, - marginBottom: 2, + height: CONTAINER_HEIGHT, + marginVertical: CONTAINER_VERTICAL_MARGIN, }, touchable: { display: 'flex', @@ -46,28 +49,29 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { width: 40, height: 40, }, + compassContainer: { + flex: 1, + alignItems: 'flex-end', + }, }; }); -export default function TeamListItem({team, currentUserId, textColor, iconTextColor, iconBackgroundColor, onTeamAdded}: Props) { +export default function TeamListItem({team, textColor, iconTextColor, iconBackgroundColor, selectedTeamId, onPress}: Props) { const theme = useTheme(); const styles = getStyleSheet(theme); - const serverUrl = useServerUrl(); - const onPress = useCallback(async () => { - const {error} = await addUserToTeam(serverUrl, team.id, currentUserId); - if (!error) { - onTeamAdded(team.id); - } - }, [onTeamAdded]); const displayName = 'displayName' in team ? team.displayName : team.display_name; const lastTeamIconUpdateAt = 'lastTeamIconUpdatedAt' in team ? team.lastTeamIconUpdatedAt : team.last_team_icon_update; const teamListItemTestId = `team_sidebar.team_list.team_list_item.${team.id}`; + const handlePress = useCallback(() => { + onPress(team.id); + }, [team.id, onPress]); + return ( @@ -88,6 +92,15 @@ export default function TeamListItem({team, currentUserId, textColor, iconTextCo > {displayName} + {(team.id === selectedTeamId) && + + + + } ); diff --git a/app/components/team_sidebar/team_list/team_item/team_icon.tsx b/app/components/team_sidebar/team_list/team_item/team_icon.tsx index baadc5cda6..b8c0eadfe2 100644 --- a/app/components/team_sidebar/team_list/team_item/team_icon.tsx +++ b/app/components/team_sidebar/team_list/team_item/team_icon.tsx @@ -19,7 +19,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { alignItems: 'center', justifyContent: 'center', backgroundColor: theme.sidebarBg, - borderRadius: 10, + borderRadius: 8, }, containerSelected: { width: '100%', @@ -32,10 +32,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { text: { color: theme.sidebarText, textTransform: 'capitalize', - ...typography('Heading', 400, 'SemiBold'), }, image: { - borderRadius: 6, + borderRadius: 8, position: 'absolute', top: 0, bottom: 0, @@ -55,6 +54,7 @@ type Props = { displayName: string; selected: boolean; backgroundColor?: string; + smallText?: boolean; textColor?: string; testID?: string; } @@ -64,6 +64,7 @@ export default function TeamIcon({ lastIconUpdate, displayName, selected, + smallText = false, textColor, backgroundColor, testID, @@ -100,11 +101,20 @@ export default function TeamIcon({ return backgroundColor ? [styles.container, {backgroundColor}] : [styles.container, nameOnly && styles.nameOnly]; }, [styles, backgroundColor, selected, nameOnly]); + const textTypography = typography('Heading', smallText ? 200 : 400, 'SemiBold'); + textTypography.fontFamily = 'Metropolis-SemiBold'; + let teamIconContent; if (nameOnly) { + const textStyle = [ + styles.text, + textTypography, + textColor && {color: textColor}, + ]; + teamIconContent = ( {displayName.substring(0, 2)} diff --git a/app/screens/home/search/modifiers/index.tsx b/app/screens/home/search/modifiers/index.tsx index d9897c4835..dd2ce0cd1c 100644 --- a/app/screens/home/search/modifiers/index.tsx +++ b/app/screens/home/search/modifiers/index.tsx @@ -3,59 +3,37 @@ import React, {useCallback, useMemo, useState} from 'react'; import {IntlShape, useIntl} from 'react-intl'; +import {View} from 'react-native'; import Animated, {useSharedValue, useAnimatedStyle, withTiming} from 'react-native-reanimated'; import FormattedText from '@components/formatted_text'; import {useTheme} from '@context/theme'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; +import TeamPickerIcon from '../team_picker_icon'; + import Modifier, {ModifierItem} from './modifier'; import ShowMoreButton from './show_more'; -const SECTION_HEIGHT = 20; -const RECENT_SEPARATOR_HEIGHT = 3; const MODIFIER_LABEL_HEIGHT = 48; +const TEAM_PICKER_ICON_SIZE = 32; const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { return { - flex: { - flex: 1, - }, - sectionWrapper: { - marginBottom: 12, - height: 48, - backgroundColor: theme.centerChannelBg, - }, - sectionContainer: { - justifyContent: 'center', - paddingLeft: 20, - height: SECTION_HEIGHT, + titleContainer: { + alignItems: 'center', + flexDirection: 'row', + marginTop: 20, + marginRight: 18, }, title: { - marginTop: 20, - paddingVertical: 12, - paddingHorizontal: 20, + flex: 1, + alignItems: 'center', + paddingLeft: 18, color: theme.centerChannelColor, ...typography('Heading', 300, 'SemiBold'), }, - showMore: { - padding: 0, - color: theme.buttonBg, - ...typography('Body', 200, 'SemiBold'), - }, - separatorContainer: { - justifyContent: 'center', - flex: 1, - height: RECENT_SEPARATOR_HEIGHT, - }, - separator: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), - height: 1, - }, - sectionList: { - flex: 1, - }, }; }); @@ -98,8 +76,10 @@ const getModifiersSectionsData = (intl: IntlShape): ModifierItem[] => { type Props = { setSearchValue: (value: string) => void; searchValue?: string; + setTeamId: (id: string) => void; + teamId: string; } -const SearchModifiers = ({searchValue, setSearchValue}: Props) => { +const Modifiers = ({searchValue, setSearchValue, setTeamId, teamId}: Props) => { const theme = useTheme(); const intl = useIntl(); @@ -135,11 +115,18 @@ const SearchModifiers = ({searchValue, setSearchValue}: Props) => { return ( <> - + + + + {data.map((item) => renderModifier(item))} @@ -151,5 +138,5 @@ const SearchModifiers = ({searchValue, setSearchValue}: Props) => { ); }; -export default SearchModifiers; +export default Modifiers; diff --git a/app/screens/home/search/recent_searches/index.tsx b/app/screens/home/search/recent_searches/index.tsx index 4417069a24..608e4a57fb 100644 --- a/app/screens/home/search/recent_searches/index.tsx +++ b/app/screens/home/search/recent_searches/index.tsx @@ -4,8 +4,10 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; import compose from 'lodash/fp/compose'; +import {of as of$} from 'rxjs'; +import {switchMap, distinctUntilChanged} from 'rxjs/operators'; -import {queryTeamSearchHistoryByTeamId} from '@queries/servers/team'; +import {observeTeam, queryTeamSearchHistoryByTeamId} from '@queries/servers/team'; import RecentSearches from './recent_searches'; @@ -18,6 +20,10 @@ type EnhanceProps = WithDatabaseArgs & { const enhance = withObservables(['teamId'], ({database, teamId}: EnhanceProps) => { return { recentSearches: queryTeamSearchHistoryByTeamId(database, teamId).observe(), + teamName: observeTeam(database, teamId).pipe( + switchMap((t) => of$(t?.displayName || '')), + distinctUntilChanged(), + ), }; }); diff --git a/app/screens/home/search/recent_searches/recent_searches.tsx b/app/screens/home/search/recent_searches/recent_searches.tsx index d3ce6b4009..7c61c0b25f 100644 --- a/app/screens/home/search/recent_searches/recent_searches.tsx +++ b/app/screens/home/search/recent_searches/recent_searches.tsx @@ -3,10 +3,9 @@ import React, {useCallback} from 'react'; import {useIntl} from 'react-intl'; -import {FlatList, View} from 'react-native'; +import {FlatList, Text, View} from 'react-native'; import Animated from 'react-native-reanimated'; -import FormattedText from '@components/formatted_text'; import {useTheme} from '@context/theme'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -37,13 +36,21 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { type Props = { setRecentValue: (value: string) => void; recentSearches: TeamSearchHistoryModel[]; + teamName: string; } -const RecentSearches = ({setRecentValue, recentSearches}: Props) => { +const RecentSearches = ({setRecentValue, recentSearches, teamName}: Props) => { const theme = useTheme(); const {formatMessage} = useIntl(); const styles = getStyleFromTheme(theme); + const title = formatMessage({ + id: 'smobile.search.recent_title', + defaultMessage: 'Recent searches in {teamName}', + }, { + teamName, + }); + const renderRecentItem = useCallback(({item}) => { return ( { const header = ( <> - + numberOfLines={2} + > + {title} + ); diff --git a/app/screens/home/search/results/filter.tsx b/app/screens/home/search/results/filter.tsx index 4f0d58e5de..06d6b295e6 100644 --- a/app/screens/home/search/results/filter.tsx +++ b/app/screens/home/search/results/filter.tsx @@ -75,16 +75,15 @@ export const DIVIDERS_HEIGHT = data.length - 1; type FilterProps = { initialFilter: FileFilter; setFilter: (filter: FileFilter) => void; + title: string; } -const Filter = ({initialFilter, setFilter}: FilterProps) => { +const Filter = ({initialFilter, setFilter, title}: FilterProps) => { const intl = useIntl(); const theme = useTheme(); const style = getStyleSheet(theme); const isTablet = useIsTablet(); - const buttonTitle = intl.formatMessage({id: 'screen.search.results.filter.title', defaultMessage: 'Filter by file type'}); - const handleOnPress = useCallback((fileType: FileFilter) => { if (fileType !== initialFilter) { setFilter(fileType); @@ -110,7 +109,7 @@ const Filter = ({initialFilter, setFilter}: FilterProps) => { showButton={false} showTitle={!isTablet} testID='search.filters' - title={buttonTitle} + title={title} titleSeparator={true} > diff --git a/app/screens/home/search/results/header.tsx b/app/screens/home/search/results/header.tsx index 711e6ee320..49142e0e5d 100644 --- a/app/screens/home/search/results/header.tsx +++ b/app/screens/home/search/results/header.tsx @@ -5,7 +5,6 @@ import {useIntl} from 'react-intl'; import {View} from 'react-native'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; -import {bottomSheetSnapPoint} from '@app/utils/helpers'; import Badge from '@components/badge'; import CompassIcon from '@components/compass_icon'; import {useTheme} from '@context/theme'; @@ -13,14 +12,15 @@ import {useIsTablet} from '@hooks/device'; import {SEPARATOR_MARGIN, 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 {TabTypes, TabType} from '@utils/search'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import TeamPickerIcon from '../team_picker_icon'; + import Filter, {DIVIDERS_HEIGHT, FILTER_ITEM_HEIGHT, NUMBER_FILTER_ITEMS} from './filter'; import SelectButton from './header_button'; -const HEADER_HEIGHT = 64; - type Props = { onTabSelect: (tab: TabType) => void; onFilterChanged: (filter: FileFilter) => void; @@ -28,34 +28,34 @@ type Props = { selectedFilter: FileFilter; numberMessages: number; numberFiles: number; + setTeamId: (id: string) => void; + teamId: string; } const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { return { - flex: { - flex: 1, - }, container: { + marginTop: 10, backgroundColor: theme.centerChannelBg, - marginHorizontal: 12, + borderBottomWidth: 1, + borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1), + }, + buttonsContainer: { + marginBottom: 12, + paddingHorizontal: 12, flexDirection: 'row', - paddingVertical: 12, - flexGrow: 0, - height: HEADER_HEIGHT, - alignItems: 'center', }, filter: { - marginRight: 12, + alignItems: 'center', + flexDirection: 'row', marginLeft: 'auto', }, - divider: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.08), - height: 1, - }, }; }); const Header = ({ + teamId, + setTeamId, onTabSelect, onFilterChanged, numberMessages, @@ -71,6 +71,7 @@ const Header = ({ const messagesText = intl.formatMessage({id: 'screen.search.header.messages', defaultMessage: 'Messages'}); const filesText = intl.formatMessage({id: 'screen.search.header.files', defaultMessage: 'Files'}); + const title = intl.formatMessage({id: 'screen.search.results.filter.title', defaultMessage: 'Filter by file type'}); const showFilterIcon = selectedTab === TabTypes.FILES; const hasFilters = selectedFilter !== FileFilters.ALL; @@ -99,6 +100,7 @@ const Header = ({ ); }; @@ -107,13 +109,13 @@ const Header = ({ renderContent, snapPoints, theme, - title: intl.formatMessage({id: 'mobile.add_team.join_team', defaultMessage: 'Join Another Team'}), + title, }); }, [selectedFilter]); return ( - <> - + + } + - - - + ); }; diff --git a/app/screens/home/search/results/header_button.tsx b/app/screens/home/search/results/header_button.tsx index c2e742a7c7..3a0d7704be 100644 --- a/app/screens/home/search/results/header_button.tsx +++ b/app/screens/home/search/results/header_button.tsx @@ -11,13 +11,9 @@ import {typography} from '@utils/typography'; const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { return { - flex: { - flex: 1, - }, button: { alignItems: 'center', borderRadius: 4, - height: 40, }, text: { paddingHorizontal: 16, diff --git a/app/screens/home/search/search.tsx b/app/screens/home/search/search.tsx index 9d2e863e61..92414b74ec 100644 --- a/app/screens/home/search/search.tsx +++ b/app/screens/home/search/search.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {useIsFocused, useNavigation} from '@react-navigation/native'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {useIntl} from 'react-intl'; import {FlatList, StyleSheet} from 'react-native'; import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated'; @@ -58,17 +58,20 @@ const getSearchParams = (terms: string, filterValue?: FileFilter) => { }; }; +const searchScreenIndex = 1; + const SearchScreen = ({teamId}: Props) => { const nav = useNavigation(); const isFocused = useIsFocused(); const intl = useIntl(); const theme = useTheme(); - const searchScreenIndex = 1; + const stateIndex = nav.getState().index; const serverUrl = useServerUrl(); const searchTerm = (nav.getState().routes[stateIndex].params as any)?.searchTerm; const [searchValue, setSearchValue] = useState(searchTerm); + const [searchTeamId, setSearchTeamId] = useState(teamId); const [selectedTab, setSelectedTab] = useState(TabTypes.MESSAGES); const [filter, setFilter] = useState(FileFilters.ALL); const [showResults, setShowResults] = useState(false); @@ -80,18 +83,12 @@ const SearchScreen = ({teamId}: Props) => { const [fileInfos, setFileInfos] = useState(emptyFileResults); const [fileChannelIds, setFileChannelIds] = useState([]); - const handleSearch = useRef<(term: string) => void>(); - const onSnap = (offset: number) => { scrollRef.current?.scrollToOffset({offset, animated: true}); }; const {scrollPaddingTop, scrollRef, scrollValue, onScroll, headerHeight, hideHeader} = useCollapsibleHeader(true, onSnap); - const onSubmit = useCallback(() => { - handleSearch.current?.(searchValue); - }, [searchValue]); - const handleClearSearch = useCallback(() => { setSearchValue(''); setLastSearchedValue(''); @@ -101,48 +98,55 @@ const SearchScreen = ({teamId}: Props) => { const handleCancelSearch = useCallback(() => { handleClearSearch(); setShowResults(false); - }, [handleClearSearch, showResults]); + }, [handleClearSearch]); - useEffect(() => { - handleSearch.current = async (term: string) => { - const searchParams = getSearchParams(term); - if (!searchParams.terms) { - handleClearSearch(); - return; - } - setLoading(true); - setFilter(FileFilters.ALL); - setLastSearchedValue(term); - addSearchToTeamSearchHistory(serverUrl, teamId, term); - const [postResults, {files, channels}] = await Promise.all([ - searchPosts(serverUrl, searchParams), - searchFiles(serverUrl, teamId, searchParams), - ]); + const handleSearch = useCallback(async (newSearchTeamId: string, term: string) => { + const searchParams = getSearchParams(term); + if (!searchParams.terms) { + handleClearSearch(); + return; + } + setLoading(true); + setFilter(FileFilters.ALL); + setLastSearchedValue(term); + addSearchToTeamSearchHistory(serverUrl, newSearchTeamId, term); + const [postResults, {files, channels}] = await Promise.all([ + searchPosts(serverUrl, newSearchTeamId, searchParams), + searchFiles(serverUrl, newSearchTeamId, searchParams), + ]); - setFileInfos(files?.length ? files : emptyFileResults); - setPostIds(postResults?.order?.length ? postResults.order : emptyPostResults); - setFileChannelIds(channels?.length ? channels : emptyChannelIds); + setFileInfos(files?.length ? files : emptyFileResults); + setPostIds(postResults?.order?.length ? postResults.order : emptyPostResults); + setFileChannelIds(channels?.length ? channels : emptyChannelIds); - setShowResults(true); - setLoading(false); - }; - }, [teamId]); + setShowResults(true); + setLoading(false); + }, [handleClearSearch]); + + const onSubmit = useCallback(() => { + handleSearch(searchTeamId, searchValue); + }, [handleSearch, searchTeamId, searchValue]); const handleRecentSearch = useCallback((text: string) => { setSearchValue(text); - handleSearch.current?.(text); - }, []); + handleSearch(searchTeamId, text); + }, [handleSearch, searchTeamId]); const handleFilterChange = useCallback(async (filterValue: FileFilter) => { setLoading(true); setFilter(filterValue); const searchParams = getSearchParams(lastSearchedValue, filterValue); - const {files, channels} = await searchFiles(serverUrl, teamId, searchParams); + const {files, channels} = await searchFiles(serverUrl, searchTeamId, searchParams); setFileInfos(files?.length ? files : emptyFileResults); setFileChannelIds(channels?.length ? channels : emptyChannelIds); setLoading(false); - }, [getSearchParams, lastSearchedValue, searchFiles]); + }, [lastSearchedValue, searchTeamId]); + + const handleResultsTeamChange = useCallback((newTeamId: string) => { + setSearchTeamId(newTeamId); + handleSearch(newTeamId, lastSearchedValue); + }, [lastSearchedValue]); const loadingComponent = useMemo(() => ( { - ), [searchValue, teamId, handleRecentSearch]); + ), [searchValue, searchTeamId, handleRecentSearch]); const resultsComponent = useMemo(() => ( { scrollPaddingTop={scrollPaddingTop} fileChannelIds={fileChannelIds} /> - ), [selectedTab, lastSearchedValue, postIds, fileInfos, scrollPaddingTop]); + ), [selectedTab, lastSearchedValue, postIds, fileInfos, scrollPaddingTop, fileChannelIds]); const renderItem = useCallback(() => { if (loading) { @@ -204,7 +210,6 @@ const SearchScreen = ({teamId}: Props) => { return { opacity: withTiming(0, {duration: 150}), transform: [{translateX: withTiming(stateIndex < searchScreenIndex ? 25 : -25, {duration: 150})}], - }; }, [isFocused, stateIndex]); @@ -219,6 +224,8 @@ const SearchScreen = ({teamId}: Props) => { if (lastSearchedValue && !loading) { header = (
{ + const teams = queryJoinedTeams(database).observe(); + return { + teams, + }; +}); + +export default withDatabase(enhanced(TeamPickerIcon)); diff --git a/app/screens/home/search/team_picker_icon/search_team_slideup.tsx b/app/screens/home/search/team_picker_icon/search_team_slideup.tsx new file mode 100644 index 0000000000..16801b00f5 --- /dev/null +++ b/app/screens/home/search/team_picker_icon/search_team_slideup.tsx @@ -0,0 +1,44 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback} from 'react'; + +import TeamList from '@components/team_sidebar/add_team/team_list'; +import {useIsTablet} from '@hooks/device'; +import BottomSheetContent from '@screens/bottom_sheet/content'; +import {dismissBottomSheet} from '@screens/navigation'; + +import type TeamModel from '@typings/database/models/servers/team'; + +type Props = { + teams: TeamModel[]; + teamId: string; + setTeamId: (teamId: string) => void; + title: string; +} + +export default function SelectTeamSlideUp({teams, title, setTeamId, teamId}: Props) { + const isTablet = useIsTablet(); + const showTitle = !isTablet && Boolean(teams.length); + + const onPress = useCallback((newTeamId: string) => { + setTeamId(newTeamId); + dismissBottomSheet(); + }, [setTeamId]); + + return ( + + + + ); +} diff --git a/app/screens/home/search/team_picker_icon/team_picker_icon.tsx b/app/screens/home/search/team_picker_icon/team_picker_icon.tsx new file mode 100644 index 0000000000..90f6bd8bba --- /dev/null +++ b/app/screens/home/search/team_picker_icon/team_picker_icon.tsx @@ -0,0 +1,114 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback} from 'react'; +import {useIntl} from 'react-intl'; +import {View, useWindowDimensions} from 'react-native'; + +import CompassIcon from '@components/compass_icon'; +import TeamIcon from '@components/team_sidebar/team_list/team_item/team_icon'; +import TouchableWithFeedback from '@components/touchable_with_feedback'; +import {useTheme} from '@context/theme'; +import {bottomSheetWithTeamList} from '@screens/navigation'; +import {preventDoubleTap} from '@utils/tap'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +import SelectTeamSlideUp from './search_team_slideup'; + +import type TeamModel from '@typings/database/models/servers/team'; + +const MENU_DOWN_ICON_SIZE = 24; +const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { + return { + teamContainer: { + paddingLeft: 12, + flexDirection: 'row', + alignItems: 'center', + }, + border: { + marginLeft: 12, + borderLeftWidth: 1, + borderLeftColor: changeOpacity(theme.centerChannelColor, 0.16), + }, + teamIcon: { + flexDirection: 'row', + }, + compass: { + alignItems: 'center', + marginLeft: 0, + }, + }; +}); + +type Props = { + size?: number; + divider?: boolean; + teams: TeamModel[]; + setTeamId: (id: string) => void; + teamId: string; +} +const TeamPickerIcon = ({size = 24, divider = false, setTeamId, teams, teamId}: Props) => { + const intl = useIntl(); + const theme = useTheme(); + const dimensions = useWindowDimensions(); + const styles = getStyleFromTheme(theme); + + const selectedTeam = teams.find((t) => t.id === teamId); + + const title = intl.formatMessage({id: 'mobile.search.team.select', defaultMessage: 'Select a team to search'}); + + const handleTeamChange = useCallback(preventDoubleTap(() => { + const renderContent = () => { + return ( + + ); + }; + + bottomSheetWithTeamList({ + dimensions, + renderContent, + theme, + title, + teams, + }); + }), [theme, setTeamId, teamId, teams]); + + return ( + <> + {selectedTeam && + + + + + + + + + } + + ); +}; +export default TeamPickerIcon; diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts index 9786514f25..d89a2869ad 100644 --- a/app/screens/navigation.ts +++ b/app/screens/navigation.ts @@ -4,21 +4,25 @@ /* eslint-disable max-lines */ import merge from 'deepmerge'; -import {Appearance, DeviceEventEmitter, NativeModules, StatusBar, Platform, Alert} from 'react-native'; +import {Appearance, ScaledSize, DeviceEventEmitter, NativeModules, StatusBar, Platform, Alert} from 'react-native'; import {ImageResource, Navigation, Options, OptionsModalPresentationStyle, OptionsTopBarButton} from 'react-native-navigation'; import tinyColor from 'tinycolor2'; import CompassIcon from '@components/compass_icon'; +import {ITEM_HEIGHT} from '@components/team_sidebar/add_team/team_list_item/team_list_item'; import {Device, Events, Screens} from '@constants'; import NavigationConstants from '@constants/navigation'; import {NOT_READY} from '@constants/screens'; import {getDefaultThemeByAppearance} from '@context/theme'; +import {TITLE_HEIGHT} from '@screens/bottom_sheet/content'; import EphemeralStore from '@store/ephemeral_store'; import NavigationStore from '@store/navigation_store'; import {LaunchProps, LaunchType} from '@typings/launch'; +import {bottomSheetSnapPoint} from '@utils/helpers'; import {appearanceControlledScreens, mergeNavigationOptions} from '@utils/navigation'; import {changeOpacity, setNavigatorStyles} from '@utils/theme'; +import type TeamModel from '@typings/database/models/servers/team'; import type {NavButtons} from '@typings/screens/navigation'; const {MattermostManaged} = NativeModules; @@ -663,6 +667,34 @@ export async function bottomSheet({title, renderContent, snapPoints, initialSnap } } +type BottomSheetWithTeamListArgs = { + teams: TeamModel[]; + dimensions: ScaledSize; + renderContent: () => JSX.Element; + theme: Theme; + title: string; +} + +export async function bottomSheetWithTeamList({title, teams, dimensions, renderContent, theme}: BottomSheetWithTeamListArgs) { + const NO_TEAMS_HEIGHT = 392; + const maxHeight = Math.round((dimensions.height * 0.9)); + + let height = NO_TEAMS_HEIGHT; + if (teams.length) { + const itemsHeight = bottomSheetSnapPoint(teams.length, ITEM_HEIGHT, 0); + const heightWithHeader = TITLE_HEIGHT + itemsHeight; + height = Math.min(maxHeight, heightWithHeader); + } + + bottomSheet({ + closeButtonId: 'close-team_list', + renderContent, + snapPoints: [height, 10], + theme, + title, + }); +} + export async function dismissBottomSheet(alternativeScreen = Screens.BOTTOM_SHEET) { DeviceEventEmitter.emit(Events.CLOSE_BOTTOM_SHEET); await NavigationStore.waitUntilScreensIsRemoved(alternativeScreen); diff --git a/app/screens/select_team/team_list.tsx b/app/screens/select_team/team_list.tsx index 6ad2f4b369..ef567af82a 100644 --- a/app/screens/select_team/team_list.tsx +++ b/app/screens/select_team/team_list.tsx @@ -75,7 +75,7 @@ function TeamList({ textColor={theme.sidebarText} iconBackgroundColor={changeOpacity(theme.sidebarText, 0.16)} iconTextColor={theme.sidebarText} - onTeamAdded={onTeamAdded} + onPress={onTeamAdded} /> ); diff --git a/app/screens/user_profile/title/index.tsx b/app/screens/user_profile/title/index.tsx index 185299581b..89fdf2526e 100644 --- a/app/screens/user_profile/title/index.tsx +++ b/app/screens/user_profile/title/index.tsx @@ -5,13 +5,13 @@ import {useIntl} from 'react-intl'; import {Text, TouchableOpacity, View} from 'react-native'; import Animated from 'react-native-reanimated'; -import {useServerUrl} from '@app/context/server'; -import {useGalleryItem} from '@app/hooks/gallery'; -import {openGalleryAtIndex} from '@app/utils/gallery'; import {GalleryInit} from '@context/gallery'; +import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; +import {useGalleryItem} from '@hooks/gallery'; import NetworkManager from '@managers/network_manager'; +import {openGalleryAtIndex} from '@utils/gallery'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; import {displayUsername} from '@utils/user'; diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 07f2c1b619..ea94cb1814 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -519,8 +519,10 @@ "mobile.search.modifier.in": "a specific channel", "mobile.search.modifier.on": "a specific date", "mobile.search.modifier.phrases": "messages with phrases", + "mobile.search.recent_title": "Recent searches in {teamName}", "mobile.search.show_less": "Show less", "mobile.search.show_more": "Show more", + "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.", "mobile.server_link.error.title": "Link Error",