forked from Ivasoft/mattermost-mobile
[Gekidou MM-44943] Add team picker to the search screen and results screens (#6455)
* 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 <nahumhbl@gmail.com>
Co-authored-by: Daniel Espino García <larkox@gmail.com>
This commit is contained in:
@@ -25,7 +25,7 @@ export async function fetchRecentMentions(serverUrl: string): Promise<PostSearch
|
||||
};
|
||||
}
|
||||
const terms = currentUser.userMentionKeys.map(({key}) => 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<PostSearch
|
||||
}
|
||||
}
|
||||
|
||||
export const searchPosts = async (serverUrl: string, params: PostSearchParams): Promise<PostSearchRequest> => {
|
||||
export const searchPosts = async (serverUrl: string, teamId: string, params: PostSearchParams): Promise<PostSearchRequest> => {
|
||||
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 || [];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -23,6 +23,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
alignItems: 'center' as const,
|
||||
justifyContent: 'center' as const,
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
<BottomSheetContent
|
||||
buttonIcon='plus'
|
||||
@@ -39,14 +73,33 @@ export default function AddTeamSlideUp({otherTeams, showTitle = true}: Props) {
|
||||
onPress={onPressCreate}
|
||||
showButton={false}
|
||||
showTitle={showTitle}
|
||||
testID='team_sidebar.add_team_slide_up'
|
||||
title={intl.formatMessage({id: 'mobile.add_team.join_team', defaultMessage: 'Join Another Team'})}
|
||||
testID={'team_sidebar.add_team_slide_up'}
|
||||
title={title}
|
||||
>
|
||||
<TeamList
|
||||
teams={otherTeams}
|
||||
onTeamAdded={onTeamAdded}
|
||||
testID='team_sidebar.add_team_slide_up.team_list'
|
||||
/>
|
||||
{hasOtherTeams &&
|
||||
<TeamList
|
||||
teams={otherTeams}
|
||||
onPress={onPress}
|
||||
testID='team_sidebar.add_team_slide_up.team_list'
|
||||
/>
|
||||
}
|
||||
{!hasOtherTeams &&
|
||||
<View style={styles.empty}>
|
||||
<Empty theme={theme}/>
|
||||
<FormattedText
|
||||
id='team_list.no_other_teams.title'
|
||||
defaultMessage='No additional teams to join'
|
||||
style={styles.title}
|
||||
testID={'team_sidebar.add_team_slide_up.no_other_teams.title'}
|
||||
/>
|
||||
<FormattedText
|
||||
id='team_list.no_other_teams.description'
|
||||
defaultMessage='To join another team, ask a Team Admin for an invitation, or create your own team.'
|
||||
style={styles.description}
|
||||
testID={'team_sidebar.add_team_slide_up.no_other_teams.description'}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
</BottomSheetContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<AddTeamSlideUp
|
||||
otherTeams={otherTeams}
|
||||
showTitle={!isTablet && Boolean(otherTeams.length)}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<View style={styles.container}>
|
||||
|
||||
@@ -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<Team|TeamModel>) => {
|
||||
return (
|
||||
<TeamListItem
|
||||
onPress={onPress}
|
||||
team={t}
|
||||
textColor={textColor}
|
||||
iconBackgroundColor={iconBackgroundColor}
|
||||
iconTextColor={iconTextColor}
|
||||
onTeamAdded={onTeamAdded}
|
||||
selectedTeamId={selectedTeamId}
|
||||
/>
|
||||
);
|
||||
}, [textColor, iconTextColor, iconBackgroundColor, onTeamAdded]);
|
||||
|
||||
if (teams.length) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<FlatList
|
||||
data={teams}
|
||||
renderItem={renderTeam}
|
||||
keyExtractor={keyExtractor}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
testID={`${testID}.flat_list`}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}, [textColor, iconTextColor, iconBackgroundColor, onPress, selectedTeamId]);
|
||||
|
||||
return (
|
||||
<View style={styles.empty}>
|
||||
<Empty theme={theme}/>
|
||||
<FormattedText
|
||||
id='team_list.no_other_teams.title'
|
||||
defaultMessage='No additional teams to join'
|
||||
style={styles.title}
|
||||
testID={`${testID}.no_other_teams.title`}
|
||||
/>
|
||||
<FormattedText
|
||||
id='team_list.no_other_teams.description'
|
||||
defaultMessage='To join another team, ask a Team Admin for an invitation, or create your own team.'
|
||||
style={styles.description}
|
||||
testID={`${testID}.no_other_teams.description`}
|
||||
<View style={styles.container}>
|
||||
<FlatList
|
||||
data={teams}
|
||||
renderItem={renderTeam}
|
||||
keyExtractor={keyExtractor}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
testID={`${testID}.flat_list`}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<View style={styles.container}>
|
||||
<TouchableWithFeedback
|
||||
onPress={onPress}
|
||||
onPress={handlePress}
|
||||
type='opacity'
|
||||
style={styles.touchable}
|
||||
>
|
||||
@@ -88,6 +92,15 @@ export default function TeamListItem({team, currentUserId, textColor, iconTextCo
|
||||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
{(team.id === selectedTeamId) &&
|
||||
<View style={styles.compassContainer}>
|
||||
<CompassIcon
|
||||
color={theme.buttonBg}
|
||||
name='check'
|
||||
size={24}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -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 = (
|
||||
<Text
|
||||
style={textColor ? [styles.text, {color: textColor}] : styles.text}
|
||||
style={textStyle}
|
||||
testID={`${testID}.display_name_abbreviation`}
|
||||
>
|
||||
{displayName.substring(0, 2)}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<FormattedText
|
||||
style={styles.title}
|
||||
id={'screen.search.modifier.header'}
|
||||
defaultMessage='Search options'
|
||||
/>
|
||||
<View style={styles.titleContainer}>
|
||||
<FormattedText
|
||||
style={styles.title}
|
||||
id={'screen.search.modifier.header'}
|
||||
defaultMessage='Search options'
|
||||
/>
|
||||
<TeamPickerIcon
|
||||
size={TEAM_PICKER_ICON_SIZE}
|
||||
setTeamId={setTeamId}
|
||||
teamId={teamId}
|
||||
/>
|
||||
</View>
|
||||
<Animated.View style={animatedStyle}>
|
||||
{data.map((item) => renderModifier(item))}
|
||||
</Animated.View>
|
||||
@@ -151,5 +138,5 @@ const SearchModifiers = ({searchValue, setSearchValue}: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchModifiers;
|
||||
export default Modifiers;
|
||||
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<RecentItem
|
||||
@@ -56,11 +63,12 @@ const RecentSearches = ({setRecentValue, recentSearches}: Props) => {
|
||||
const header = (
|
||||
<>
|
||||
<View style={styles.divider}/>
|
||||
<FormattedText
|
||||
<Text
|
||||
style={styles.title}
|
||||
id={'screen.search.recent.header'}
|
||||
defaultMessage={formatMessage({id: 'mobile.search.recent_title', defaultMessage: 'Recent searches'})}
|
||||
/>
|
||||
numberOfLines={2}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<View style={style.container}>
|
||||
|
||||
@@ -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 = ({
|
||||
<Filter
|
||||
initialFilter={selectedFilter}
|
||||
setFilter={onFilterChanged}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.buttonsContainer}>
|
||||
<SelectButton
|
||||
selected={selectedTab === TabTypes.MESSAGES}
|
||||
onPress={handleMessagesPress}
|
||||
@@ -144,11 +146,15 @@ const Header = ({
|
||||
/>
|
||||
</>
|
||||
}
|
||||
<TeamPickerIcon
|
||||
size={32}
|
||||
divider={true}
|
||||
setTeamId={setTeamId}
|
||||
teamId={teamId}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.divider}/>
|
||||
</>
|
||||
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string>(searchTerm);
|
||||
const [searchTeamId, setSearchTeamId] = useState<string>(teamId);
|
||||
const [selectedTab, setSelectedTab] = useState<TabType>(TabTypes.MESSAGES);
|
||||
const [filter, setFilter] = useState<FileFilter>(FileFilters.ALL);
|
||||
const [showResults, setShowResults] = useState(false);
|
||||
@@ -80,18 +83,12 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
const [fileInfos, setFileInfos] = useState<FileInfo[]>(emptyFileResults);
|
||||
const [fileChannelIds, setFileChannelIds] = useState<string[]>([]);
|
||||
|
||||
const handleSearch = useRef<(term: string) => void>();
|
||||
|
||||
const onSnap = (offset: number) => {
|
||||
scrollRef.current?.scrollToOffset({offset, animated: true});
|
||||
};
|
||||
|
||||
const {scrollPaddingTop, scrollRef, scrollValue, onScroll, headerHeight, hideHeader} = useCollapsibleHeader<FlatList>(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(() => (
|
||||
<Loading
|
||||
@@ -157,13 +161,15 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
<Modifiers
|
||||
setSearchValue={setSearchValue}
|
||||
searchValue={searchValue}
|
||||
teamId={searchTeamId}
|
||||
setTeamId={setSearchTeamId}
|
||||
/>
|
||||
<RecentSearches
|
||||
setRecentValue={handleRecentSearch}
|
||||
teamId={teamId}
|
||||
teamId={searchTeamId}
|
||||
/>
|
||||
</>
|
||||
), [searchValue, teamId, handleRecentSearch]);
|
||||
), [searchValue, searchTeamId, handleRecentSearch]);
|
||||
|
||||
const resultsComponent = useMemo(() => (
|
||||
<Results
|
||||
@@ -174,7 +180,7 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
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 = (
|
||||
<Header
|
||||
teamId={searchTeamId}
|
||||
setTeamId={handleResultsTeamChange}
|
||||
onTabSelect={setSelectedTab}
|
||||
onFilterChanged={handleFilterChange}
|
||||
numberMessages={postIds.length}
|
||||
|
||||
20
app/screens/home/search/team_picker_icon/index.ts
Normal file
20
app/screens/home/search/team_picker_icon/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
|
||||
import {queryJoinedTeams} from '@queries/servers/team';
|
||||
|
||||
import TeamPickerIcon from './team_picker_icon';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const teams = queryJoinedTeams(database).observe();
|
||||
return {
|
||||
teams,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(TeamPickerIcon));
|
||||
@@ -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 (
|
||||
<BottomSheetContent
|
||||
showButton={false}
|
||||
showTitle={showTitle}
|
||||
testID={'search.search_team_slide_up'}
|
||||
title={title}
|
||||
>
|
||||
<TeamList
|
||||
selectedTeamId={teamId}
|
||||
teams={teams}
|
||||
onPress={onPress}
|
||||
testID='search.search_team_slide_up.team_list'
|
||||
/>
|
||||
</BottomSheetContent>
|
||||
);
|
||||
}
|
||||
114
app/screens/home/search/team_picker_icon/team_picker_icon.tsx
Normal file
114
app/screens/home/search/team_picker_icon/team_picker_icon.tsx
Normal file
@@ -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 (
|
||||
<SelectTeamSlideUp
|
||||
setTeamId={setTeamId}
|
||||
teams={teams}
|
||||
teamId={teamId}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
bottomSheetWithTeamList({
|
||||
dimensions,
|
||||
renderContent,
|
||||
theme,
|
||||
title,
|
||||
teams,
|
||||
});
|
||||
}), [theme, setTeamId, teamId, teams]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedTeam &&
|
||||
<TouchableWithFeedback
|
||||
onPress={handleTeamChange}
|
||||
type='opacity'
|
||||
testID={selectedTeam.id}
|
||||
>
|
||||
<View style={[styles.teamContainer, divider && styles.border]}>
|
||||
<View style={[styles.teamIcon, {width: size, height: size}]}>
|
||||
<TeamIcon
|
||||
displayName={selectedTeam.displayName}
|
||||
id={selectedTeam.id}
|
||||
lastIconUpdate={selectedTeam.lastTeamIconUpdatedAt}
|
||||
textColor={theme.centerChannelColor}
|
||||
backgroundColor={changeOpacity(theme.centerChannelColor, 0.16)}
|
||||
selected={false}
|
||||
testID={`${selectedTeam}.team_icon`}
|
||||
smallText={true}
|
||||
/>
|
||||
</View>
|
||||
<CompassIcon
|
||||
color={changeOpacity(theme.centerChannelColor, 0.56)}
|
||||
style={styles.compass}
|
||||
name='menu-down'
|
||||
size={MENU_DOWN_ICON_SIZE}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default TeamPickerIcon;
|
||||
@@ -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);
|
||||
|
||||
@@ -75,7 +75,7 @@ function TeamList({
|
||||
textColor={theme.sidebarText}
|
||||
iconBackgroundColor={changeOpacity(theme.sidebarText, 0.16)}
|
||||
iconTextColor={theme.sidebarText}
|
||||
onTeamAdded={onTeamAdded}
|
||||
onPress={onTeamAdded}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user