From 33d9e6257eac39e548c9dc4fdf313c09fa20cdfc Mon Sep 17 00:00:00 2001 From: Jason Frerich Date: Fri, 15 Jul 2022 07:05:58 -0500 Subject: [PATCH] [Gekidou MM-44943] Add team picker to the search screen and results screens (#6455) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * initial check in * add search value to memoized dependencies in modifier component * ignore the back press * UI adjustments from PR feedback * initial commit * recent search are getting rendered from WDB * search terms from the search bar are getting added * can delete recent searches from WDB from recent searches Options * will now add new ters to the table and recreate existing terms with new timestamp * push for scrollview * use flatlist instead of scrolview * s/deleteRecentTeamSearchById/removeSearchFromTeamSearchHistory/ * s/addRecentTeamSearch/addSearchToTeamSearchHistory/ * Fix search to use a flatlist and remove douplicate reference * fix eslint * Fix android autoscroll search field to the top * limit the number of saved searches to 20 for a team. return the results a team Search History sorted by createdAt * set display to term for now * clean up * clean up * initial commit * - From the search screen, you cna now set the team for the search - Recent searches are saved for the specified team * fix styling fo icon in modifiers * - move team picker to its own component and call from modifiers - will use for results header also * - show team image if available - pass optional size to TeamPickerIcon - add TeamPickerIcon to Resuls Header - styling fixes * add team name to recent searches title * parameter renaming * fix lint * fix callback bug that was calling itself * when changing a team while showing search results: - update the search results with new selected team and current search term. - save the recent search the new selected team TeamSearchHistory for that team * move to bottom for reduction of PR review lines and comparison to changes in theh component logic * - add dependencies - rename function * fix PR feedback * - created helper function for bottom sheet with TeamList BottomSheetContent - share the bottomScreen calls from AddTeam and TeamPickerIcon * Add title back to renderContent of bottomSheet call. This is needed for tablets * remove unnecessary check of selectTeamId. it will be undefined and fail the equality check * - now all team_icons are all radius of 8. Includes the following icons: - team picker icon in in search screen - each team icon in the search screen bottom sheet team list - team picker icon in in search results screen - each team icon in the home screen team side bar list - each team icon in the join new team bottom sheet team list (from home screen) * use padding in the width of the header so the margins are extended full width, and dateline separator of post list does not creep into the header region * add smallText prop to team_icon. - allows using 200 instead of default 400 value. - 200 is used for the TeamPickerIcon used in the searcha nd results headers * - add dependency back to handle renderItem and allow selecting files or messages view - when handling the search, save the term to the correct team * adjust styling so the rounded edges appear for the header. Use the header container height to set the height of the header to 40 and then set top and bottom margins for the rounded top edge and the bottom margin to the divider * use typography * add title dependency * update dependencies * use bottomSheetSnapPoint to get the height of the total items * rename variable * Always use Metropolis-SemiBold for the team icon fallback * use a fragment because there is not styling * move title inside onPress function and can remove the title as a dependency * just use strings for testID * calculate the observable inside the return object * - remove const and use string - correct the name of the testID * - use more specific dependency value team.id - add onPress dependency * move to a constant * move to a constant * replace with logical AND * add all logic for the style to textStyle * remove the separate divider view and just use the outside container with bottomBorder with and color * extend the image vertically in the results * Fix size of bottom sheet * Refactor unneeded change * Minor tweaks Co-authored-by: Elias Nahum Co-authored-by: Daniel Espino GarcĂ­a --- app/actions/remote/search.ts | 6 +- app/actions/remote/team.ts | 18 ++- .../markdown/at_mention/at_mention.tsx | 2 +- app/components/no_results_with_term/index.tsx | 1 + .../add_team/add_team_slide_up.tsx | 77 ++++++++++-- .../team_sidebar/add_team/index.tsx | 26 ++-- .../team_sidebar/add_team/team_list.tsx | 78 +++--------- .../team_list_item/team_list_item.tsx | 43 ++++--- .../team_list/team_item/team_icon.tsx | 18 ++- app/screens/home/search/modifiers/index.tsx | 71 +++++------ .../home/search/recent_searches/index.tsx | 8 +- .../recent_searches/recent_searches.tsx | 22 ++-- app/screens/home/search/results/filter.tsx | 7 +- app/screens/home/search/results/header.tsx | 50 ++++---- .../home/search/results/header_button.tsx | 4 - app/screens/home/search/search.tsx | 85 +++++++------ .../home/search/team_picker_icon/index.ts | 20 +++ .../team_picker_icon/search_team_slideup.tsx | 44 +++++++ .../team_picker_icon/team_picker_icon.tsx | 114 ++++++++++++++++++ app/screens/navigation.ts | 34 +++++- app/screens/select_team/team_list.tsx | 2 +- app/screens/user_profile/title/index.tsx | 6 +- assets/base/i18n/en.json | 2 + 23 files changed, 498 insertions(+), 240 deletions(-) create mode 100644 app/screens/home/search/team_picker_icon/index.ts create mode 100644 app/screens/home/search/team_picker_icon/search_team_slideup.tsx create mode 100644 app/screens/home/search/team_picker_icon/team_picker_icon.tsx 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",