diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index 3d2f91d40..c76127a68 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -1056,7 +1056,7 @@ export async function switchToLastChannel(serverUrl: string, teamId?: string) { } } -export async function searchChannels(serverUrl: string, term: string, isSearch = false) { +export async function searchChannels(serverUrl: string, term: string, teamId: string, isSearch = false) { const database = DatabaseManager.serverDatabases[serverUrl]?.database; if (!database) { return {error: `${serverUrl} database not found`}; @@ -1070,9 +1070,8 @@ export async function searchChannels(serverUrl: string, term: string, isSearch = } try { - const currentTeamId = await getCurrentTeamId(database); const autoCompleteFunc = isSearch ? client.autocompleteChannelsForSearch : client.autocompleteChannels; - const channels = await autoCompleteFunc(currentTeamId, term); + const channels = await autoCompleteFunc(teamId, term); return {channels}; } catch (error) { return {error}; diff --git a/app/actions/remote/user.ts b/app/actions/remote/user.ts index 72965badf..e04a795e8 100644 --- a/app/actions/remote/user.ts +++ b/app/actions/remote/user.ts @@ -15,7 +15,7 @@ import {debounce} from '@helpers/api/general'; import NetworkManager from '@managers/network_manager'; import {getMembersCountByChannelsId, queryChannelsByTypes} from '@queries/servers/channel'; import {queryGroupsByNames} from '@queries/servers/group'; -import {getConfig, getCurrentTeamId, getCurrentUserId} from '@queries/servers/system'; +import {getConfig, getCurrentUserId} from '@queries/servers/system'; import {getCurrentUser, prepareUsers, queryAllUsers, queryUsersById, queryUsersByIdsOrUsernames, queryUsersByUsername} from '@queries/servers/user'; import {logError} from '@utils/log'; import {getDeviceTimezone, isTimezoneEnabled} from '@utils/timezone'; @@ -818,7 +818,7 @@ export const uploadUserProfileImage = async (serverUrl: string, localPath: strin return {error: undefined}; }; -export const searchUsers = async (serverUrl: string, term: string, channelId?: string) => { +export const searchUsers = async (serverUrl: string, term: string, teamId: string, channelId?: string) => { const database = DatabaseManager.serverDatabases[serverUrl]?.database; if (!database) { return {error: `${serverUrl} database not found`}; @@ -832,8 +832,7 @@ export const searchUsers = async (serverUrl: string, term: string, channelId?: s } try { - const currentTeamId = await getCurrentTeamId(database); - const users = await client.autocompleteUsers(term, currentTeamId, channelId); + const users = await client.autocompleteUsers(term, teamId, channelId); return {users}; } catch (error) { return {error}; diff --git a/app/components/autocomplete/at_mention/at_mention.tsx b/app/components/autocomplete/at_mention/at_mention.tsx index 7956a504f..65179bed8 100644 --- a/app/components/autocomplete/at_mention/at_mention.tsx +++ b/app/components/autocomplete/at_mention/at_mention.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {debounce} from 'lodash'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {Platform, SectionList, SectionListData, SectionListRenderItemInfo, StyleProp, ViewStyle} from 'react-native'; import {searchGroupsByName, searchGroupsByNameInChannel, searchGroupsByNameInTeam} from '@actions/local/group'; @@ -175,9 +175,35 @@ const makeSections = (teamMembers: Array, usersInChanne return newSections; }; +const searchGroups = async (serverUrl: string, matchTerm: string, useGroupMentions: boolean, isChannelConstrained: boolean, isTeamConstrained: boolean, channelId?: string, teamId?: string) => { + try { + if (useGroupMentions && matchTerm && matchTerm !== '') { + let g = emptyGroupList; + + if (isChannelConstrained) { + // If the channel is constrained, we only show groups for that channel + if (channelId) { + g = await searchGroupsByNameInChannel(serverUrl, matchTerm, channelId); + } + } else if (isTeamConstrained) { + // If there is no channel constraint, but a team constraint - only show groups for team + g = await searchGroupsByNameInTeam(serverUrl, matchTerm, teamId!); + } else { + // No constraints? Search all groups + g = await searchGroupsByName(serverUrl, matchTerm || ''); + } + + return g.length ? g : emptyGroupList; + } + return emptyGroupList; + } catch (error) { + return emptyGroupList; + } +}; + type Props = { channelId?: string; - teamId?: string; + teamId: string; cursorPosition: number; isSearch: boolean; updateValue: (v: string) => void; @@ -232,9 +258,22 @@ const AtMention = ({ const [localUsers, setLocalUsers] = useState(); const [filteredLocalUsers, setFilteredLocalUsers] = useState(emptyUserlList); - const runSearch = useMemo(() => debounce(async (sUrl: string, term: string, cId?: string) => { - setLoading(true); - const {users: receivedUsers, error} = await searchUsers(sUrl, term, cId); + const latestSearchAt = useRef(0); + + const runSearch = useMemo(() => debounce(async (sUrl: string, term: string, groupMentions: boolean, channelConstrained: boolean, teamConstrained: boolean, tId: string, cId?: string) => { + const searchAt = Date.now(); + latestSearchAt.current = searchAt; + + const [{users: receivedUsers, error}, groupsResult] = await Promise.all([ + searchUsers(sUrl, term, tId, cId), + searchGroups(sUrl, term, groupMentions, channelConstrained, teamConstrained, cId, tId), + ]); + + if (latestSearchAt.current > searchAt) { + return; + } + + setGroups(groupsResult); setUseLocal(Boolean(error)); if (error) { @@ -243,6 +282,10 @@ const AtMention = ({ fallbackUsers = await getAllUsers(sUrl); setLocalUsers(fallbackUsers); } + if (latestSearchAt.current > searchAt) { + return; + } + const filteredUsers = filterResults(fallbackUsers, term); setFilteredLocalUsers(filteredUsers.length ? filteredUsers : emptyUserlList); } else if (receivedUsers) { @@ -270,8 +313,12 @@ const AtMention = ({ const resetState = () => { setUsersInChannel(emptyUserlList); setUsersOutOfChannel(emptyUserlList); + setGroups(emptyGroupList); setFilteredLocalUsers(emptyUserlList); setSections(emptySectionList); + setNoResultsTerm(null); + latestSearchAt.current = Date.now(); + setLoading(false); runSearch.cancel(); }; @@ -285,7 +332,7 @@ const AtMention = ({ completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `); } - const newCursorPosition = completedDraft.length - 1; + const newCursorPosition = completedDraft.length; if (value.length > cursorPosition) { completedDraft += value.substring(cursorPosition); @@ -297,6 +344,7 @@ const AtMention = ({ onShowingChange(false); setNoResultsTerm(mention); setSections(emptySectionList); + latestSearchAt.current = Date.now(); }, [value, localCursorPosition, isSearch]); const renderSpecialMentions = useCallback((item: SpecialMention) => { @@ -361,39 +409,6 @@ const AtMention = ({ } }, [cursorPosition]); - useEffect(() => { - if (useGroupMentions && matchTerm && matchTerm !== '') { - // If the channel is constrained, we only show groups for that channel - if (isChannelConstrained && channelId) { - searchGroupsByNameInChannel(serverUrl, matchTerm, channelId).then((g) => { - setGroups(g.length ? g : emptyGroupList); - }).catch(() => { - setGroups(emptyGroupList); - }); - } - - // If there is no channel constraint, but a team constraint - only show groups for team - if (isTeamConstrained && !isChannelConstrained) { - searchGroupsByNameInTeam(serverUrl, matchTerm, teamId!).then((g) => { - setGroups(g.length ? g : emptyGroupList); - }).catch(() => { - setGroups(emptyGroupList); - }); - } - - // No constraints? Search all groups - if (!isTeamConstrained && !isChannelConstrained) { - searchGroupsByName(serverUrl, matchTerm || '').then((g) => { - setGroups(Array.isArray(g) ? g : emptyGroupList); - }).catch(() => { - setGroups(emptyGroupList); - }); - } - } else { - setGroups(emptyGroupList); - } - }, [matchTerm, useGroupMentions]); - useEffect(() => { if (matchTerm === null) { resetState(); @@ -406,10 +421,14 @@ const AtMention = ({ } setNoResultsTerm(null); - runSearch(serverUrl, matchTerm, channelId); - }, [matchTerm]); + setLoading(true); + runSearch(serverUrl, matchTerm, useGroupMentions, isChannelConstrained, isTeamConstrained, teamId, channelId); + }, [matchTerm, teamId, useGroupMentions, isChannelConstrained, isTeamConstrained]); useEffect(() => { + if (noResultsTerm && !loading) { + return; + } const showSpecialMentions = useChannelMentions && matchTerm != null && checkSpecialMentions(matchTerm); const buildMemberSection = isSearch || (!channelId && teamMembers.length > 0); let newSections; diff --git a/app/components/autocomplete/at_mention/index.ts b/app/components/autocomplete/at_mention/index.ts index 03fd09d5b..b34b40e68 100644 --- a/app/components/autocomplete/at_mention/index.ts +++ b/app/components/autocomplete/at_mention/index.ts @@ -18,8 +18,11 @@ import AtMention from './at_mention'; import type {WithDatabaseArgs} from '@typings/database/database'; import type TeamModel from '@typings/database/models/servers/team'; -type OwnProps = {channelId?: string} -const enhanced = withObservables([], ({database, channelId}: WithDatabaseArgs & OwnProps) => { +type OwnProps = { + channelId?: string; + teamId?: string; +} +const enhanced = withObservables(['teamId'], ({database, channelId, teamId}: WithDatabaseArgs & OwnProps) => { const currentUser = observeCurrentUser(database); const hasLicense = observeLicense(database).pipe( @@ -51,20 +54,19 @@ const enhanced = withObservables([], ({database, channelId}: WithDatabaseArgs & useGroupMentions = of$(false); isChannelConstrained = of$(false); isTeamConstrained = of$(false); - team = observeCurrentTeam(database); + team = teamId ? observeTeam(database, teamId) : observeCurrentTeam(database); } isTeamConstrained = team.pipe( switchMap((t) => of$(Boolean(t?.isGroupConstrained))), ); - const teamId = team.pipe(switchMap((t) => of$(t?.id))); return { isChannelConstrained, isTeamConstrained, useChannelMentions, useGroupMentions, - teamId, + teamId: team.pipe(switchMap((t) => of$(t?.id))), }; }); diff --git a/app/components/autocomplete/autocomplete.tsx b/app/components/autocomplete/autocomplete.tsx index 4cd5a962c..f92a45bfa 100644 --- a/app/components/autocomplete/autocomplete.tsx +++ b/app/components/autocomplete/autocomplete.tsx @@ -61,6 +61,7 @@ type Props = { availableSpace: SharedValue; inPost?: boolean; growDown?: boolean; + teamId?: string; containerStyle?: StyleProp; } @@ -81,6 +82,7 @@ const Autocomplete = ({ inPost = false, growDown = false, containerStyle, + teamId, }: Props) => { const theme = useTheme(); const isTablet = useIsTablet(); @@ -152,6 +154,7 @@ const Autocomplete = ({ nestedScrollEnabled={nestedScrollEnabled} isSearch={isSearch} channelId={channelId} + teamId={teamId} /> {!isSearch && { return item.id; }; -const getMatchTermForChannelMention = (() => { - let lastMatchTerm: string | null = null; - let lastValue: string; - let lastIsSearch: boolean; - return (value: string, isSearch: boolean) => { - if (value !== lastValue || isSearch !== lastIsSearch) { - const regex = isSearch ? CHANNEL_MENTION_SEARCH_REGEX : CHANNEL_MENTION_REGEX; - const match = value.match(regex); - lastValue = value; - lastIsSearch = isSearch; - if (match) { - if (isSearch) { - lastMatchTerm = match[1].toLowerCase(); - } else if (match.index && match.index > 0 && value[match.index - 1] === '~') { - lastMatchTerm = null; - } else { - lastMatchTerm = match[2].toLowerCase(); - } - } else { - lastMatchTerm = null; - } - } - return lastMatchTerm; - }; -})(); - const reduceChannelsForSearch = (channels: Array, members: MyChannelModel[]) => { const memberIds = new Set(members.map((m) => m.id)); return channels.reduce>>(([pubC, priC, dms], c) => { @@ -83,7 +54,7 @@ const reduceChannelsForAutocomplete = (channels: Array, }, [[], []]); }; -const makeSections = (channels: Array, myMembers: MyChannelModel[], isSearch = false) => { +const makeSections = (channels: Array, myMembers: MyChannelModel[], loading: boolean, isSearch = false) => { const newSections = []; if (isSearch) { const [publicChannels, privateChannels, directAndGroupMessages] = reduceChannelsForSearch(channels, myMembers); @@ -128,7 +99,7 @@ const makeSections = (channels: Array, myMembers: MyChan }); } - if (otherChannels.length) { + if (otherChannels.length || (!myChannels.length && loading)) { newSections.push({ id: t('suggestion.mention.morechannels'), defaultMessage: 'Other Channels', @@ -164,18 +135,11 @@ type Props = { value: string; nestedScrollEnabled: boolean; listStyle: StyleProp; + matchTerm: string; + localChannels: ChannelModel[]; + teamId: string; } -const getAllChannels = async (serverUrl: string) => { - const database = DatabaseManager.serverDatabases[serverUrl]?.database; - if (!database) { - return []; - } - - const teamId = await getCurrentTeamId(database); - return queryAllChannelsForTeam(database, teamId).fetch(); -}; - const emptySections: Array> = []; const emptyChannels: Array = []; @@ -188,47 +152,45 @@ const ChannelMention = ({ value, nestedScrollEnabled, listStyle, + matchTerm, + localChannels, + teamId, }: Props) => { const serverUrl = useServerUrl(); const [sections, setSections] = useState>>(emptySections); - const [channels, setChannels] = useState>(emptyChannels); + const [remoteChannels, setRemoteChannels] = useState>(emptyChannels); const [loading, setLoading] = useState(false); const [noResultsTerm, setNoResultsTerm] = useState(null); const [localCursorPosition, setLocalCursorPosition] = useState(cursorPosition); // To avoid errors due to delay between value changes and cursor position changes. - const [useLocal, setUseLocal] = useState(true); - const [localChannels, setlocalChannels] = useState(); - const [filteredLocalChannels, setFilteredLocalChannels] = useState(emptyChannels); - const runSearch = useMemo(() => debounce(async (sUrl: string, term: string) => { - setLoading(true); - const {channels: receivedChannels, error} = await searchChannels(sUrl, term, isSearch); - setUseLocal(Boolean(error)); + const latestSearchAt = useRef(0); - if (error) { - let fallbackChannels = localChannels; - if (!fallbackChannels) { - fallbackChannels = await getAllChannels(sUrl); - setlocalChannels(fallbackChannels); - } - const filteredChannels = filterResults(fallbackChannels, term); - setFilteredLocalChannels(filteredChannels.length ? filteredChannels : emptyChannels); - } else if (receivedChannels) { - let channelsToStore: Array = receivedChannels; - if (hasTrailingSpaces(term)) { - channelsToStore = filterResults(receivedChannels, term); - } - setChannels(channelsToStore.length ? channelsToStore : emptyChannels); + const runSearch = useMemo(() => debounce(async (sUrl: string, term: string, tId: string) => { + const searchAt = Date.now(); + latestSearchAt.current = searchAt; + + const {channels: receivedChannels} = await searchChannels(sUrl, term, tId, isSearch); + + if (latestSearchAt.current > searchAt) { + return; } + let channelsToStore: Array = receivedChannels || []; + if (hasTrailingSpaces(term)) { + channelsToStore = filterResults(receivedChannels || [], term); + } + setRemoteChannels(channelsToStore.length ? channelsToStore : emptyChannels); + setLoading(false); }, 200), []); - const matchTerm = getMatchTermForChannelMention(value.substring(0, localCursorPosition), isSearch); const resetState = () => { - setFilteredLocalChannels(emptyChannels); - setChannels(emptyChannels); + latestSearchAt.current = Date.now(); + setRemoteChannels(emptyChannels); setSections(emptySections); + setNoResultsTerm(null); runSearch.cancel(); + setLoading(false); }; const completeMention = useCallback((mention: string) => { @@ -264,8 +226,11 @@ const ChannelMention = ({ } onShowingChange(false); + setLoading(false); setNoResultsTerm(mention); setSections(emptySections); + setRemoteChannels(emptyChannels); + latestSearchAt.current = Date.now(); }, [value, localCursorPosition, isSearch]); const renderItem = useCallback(({item}: SectionListRenderItemInfo) => { @@ -306,21 +271,41 @@ const ChannelMention = ({ } setNoResultsTerm(null); - runSearch(serverUrl, matchTerm); - }, [matchTerm]); + setLoading(true); + runSearch(serverUrl, matchTerm, teamId); + }, [matchTerm, teamId]); + + const channels = useMemo(() => { + const ids = new Set(localChannels.map((c) => c.id)); + return [...localChannels, ...remoteChannels.filter((c) => !ids.has(c.id))].sort((a, b) => { + const aDisplay = 'display_name' in a ? a.display_name : a.displayName; + const bDisplay = 'display_name' in b ? b.display_name : b.displayName; + const displayResult = aDisplay.localeCompare(bDisplay); + if (displayResult === 0) { + return a.name.localeCompare(b.name); + } + return displayResult; + }); + }, [localChannels, remoteChannels]); useDidUpdate(() => { - const newSections = makeSections(useLocal ? filteredLocalChannels : channels, myMembers, isSearch); + if (noResultsTerm && !loading) { + return; + } + const newSections = makeSections(channels, myMembers, loading, isSearch); const nSections = newSections.length; if (!loading && !nSections && noResultsTerm == null) { setNoResultsTerm(matchTerm); } + if (nSections) { + setNoResultsTerm(null); + } setSections(newSections.length ? newSections : emptySections); onShowingChange(Boolean(nSections)); }, [channels, myMembers, loading]); - if (sections.length === 0 || noResultsTerm != null) { + if (!loading && (sections.length === 0 || noResultsTerm != null)) { // If we are not in an active state or the mention has been completed return null so nothing is rendered // other components are not blocked. return null; diff --git a/app/components/autocomplete/channel_mention/index.ts b/app/components/autocomplete/channel_mention/index.ts index 08e83bc99..17eb57b25 100644 --- a/app/components/autocomplete/channel_mention/index.ts +++ b/app/components/autocomplete/channel_mention/index.ts @@ -3,17 +3,90 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; +import {of as of$} from 'rxjs'; +import {switchMap} from 'rxjs/operators'; -import {queryAllMyChannel} from '@queries/servers/channel'; +import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from '@constants/autocomplete'; +import {observeChannel, queryAllMyChannel, queryChannelsForAutocomplete} from '@queries/servers/channel'; +import {observeCurrentTeamId} from '@queries/servers/system'; import ChannelMention from './channel_mention'; import type {WithDatabaseArgs} from '@typings/database/database'; +import type ChannelModel from '@typings/database/models/servers/channel'; -const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { +const getMatchTermForChannelMention = (() => { + let lastMatchTerm: string | null = null; + let lastValue: string; + let lastIsSearch: boolean; + return (value: string, isSearch: boolean) => { + if (value !== lastValue || isSearch !== lastIsSearch) { + const regex = isSearch ? CHANNEL_MENTION_SEARCH_REGEX : CHANNEL_MENTION_REGEX; + const match = value.match(regex); + lastValue = value; + lastIsSearch = isSearch; + if (match) { + if (isSearch) { + lastMatchTerm = match[1].toLowerCase(); + } else if (match.index && match.index > 0 && value[match.index - 1] === '~') { + lastMatchTerm = null; + } else { + lastMatchTerm = match[2].toLowerCase(); + } + } else { + lastMatchTerm = null; + } + } + return lastMatchTerm; + }; +})(); + +type WithTeamIdProps = { + teamId?: string; + channelId?: string; +} & WithDatabaseArgs; + +type OwnProps = { + value: string; + isSearch: boolean; + cursorPosition: number; + teamId: string; +} & WithDatabaseArgs; + +const emptyChannelList: ChannelModel[] = []; + +const withMembers = withObservables([], ({database}: WithDatabaseArgs) => { return { myMembers: queryAllMyChannel(database).observe(), }; }); -export default withDatabase(enhanced(ChannelMention)); +const withTeamId = withObservables(['teamId', 'channelId'], ({teamId, channelId, database}: WithTeamIdProps) => { + let currentTeamId; + if (teamId) { + currentTeamId = of$(teamId); + } else if (channelId) { + currentTeamId = observeChannel(database, channelId).pipe(switchMap((c) => { + return c?.teamId ? of$(c.teamId) : observeCurrentTeamId(database); + })); + } else { + currentTeamId = observeCurrentTeamId(database); + } + + return { + teamId: currentTeamId, + }; +}); + +const enhanced = withObservables(['value', 'isSearch', 'teamId', 'cursorPosition'], ({value, isSearch, teamId, cursorPosition, database}: OwnProps) => { + const matchTerm = getMatchTermForChannelMention(value.substring(0, cursorPosition), isSearch); + + const localChannels = matchTerm === null ? of$(emptyChannelList) : queryChannelsForAutocomplete(database, matchTerm, isSearch, teamId).observe(); + + return { + matchTerm: of$(matchTerm), + localChannels, + }; +}); + +export default withDatabase(withMembers(withTeamId(enhanced(ChannelMention)))); diff --git a/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser.ts b/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser.ts index c44caa52e..5ba220d4d 100644 --- a/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser.ts +++ b/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser.ts @@ -1899,7 +1899,7 @@ export class AppCommandParser { if (input[0] === '@') { input = input.substring(1); } - const res = await searchUsers(this.serverUrl, input, this.channelID); + const res = await searchUsers(this.serverUrl, input, this.teamID, this.channelID); return getUserSuggestions(res.users); }; @@ -1908,7 +1908,7 @@ export class AppCommandParser { if (input[0] === '~') { input = input.substring(1); } - const res = await searchChannels(this.serverUrl, input); + const res = await searchChannels(this.serverUrl, input, this.teamID); return getChannelSuggestions(res.channels); }; diff --git a/app/components/autocomplete/slash_suggestion/app_command_parser/mentions.ts b/app/components/autocomplete/slash_suggestion/app_command_parser/mentions.ts index 436779d41..f50c67812 100644 --- a/app/components/autocomplete/slash_suggestion/app_command_parser/mentions.ts +++ b/app/components/autocomplete/slash_suggestion/app_command_parser/mentions.ts @@ -10,7 +10,7 @@ export async function inTextMentionSuggestions(serverUrl: string, pretext: strin const incompleteLessLastWord = separatedWords.slice(0, -1).join(' '); const lastWord = separatedWords[separatedWords.length - 1]; if (lastWord.startsWith('@')) { - const res = await searchUsers(serverUrl, lastWord.substring(1), channelID); + const res = await searchUsers(serverUrl, lastWord.substring(1), teamID, channelID); const users = await getUserSuggestions(res.users); users.forEach((u) => { let complete = incompleteLessLastWord ? incompleteLessLastWord + ' ' + u.Complete : u.Complete; @@ -23,7 +23,7 @@ export async function inTextMentionSuggestions(serverUrl: string, pretext: strin } if (lastWord.startsWith('~') && !lastWord.startsWith('~~')) { - const res = await searchChannels(serverUrl, lastWord.substring(1)); + const res = await searchChannels(serverUrl, lastWord.substring(1), teamID); const channels = await getChannelSuggestions(res.channels); channels.forEach((c) => { let complete = incompleteLessLastWord ? incompleteLessLastWord + ' ' + c.Complete : c.Complete; diff --git a/app/queries/servers/channel.ts b/app/queries/servers/channel.ts index 881716bcc..ab917c6ae 100644 --- a/app/queries/servers/channel.ts +++ b/app/queries/servers/channel.ts @@ -605,3 +605,61 @@ export const observeChannelsByLastPostAt = (database: Database, myChannels: MyCh ORDER BY CASE mc.last_post_at WHEN 0 THEN c.create_at ELSE mc.last_post_at END DESC`), ).observe(); }; + +export const queryChannelsForAutocomplete = (database: Database, matchTerm: string, isSearch: boolean, teamId: string) => { + const likeTerm = `%${Q.sanitizeLikeString(matchTerm)}%`; + const clauses: Q.Clause[] = []; + if (isSearch) { + clauses.push( + Q.experimentalJoinTables([CHANNEL_MEMBERSHIP]), + Q.experimentalNestedJoin(CHANNEL_MEMBERSHIP, USER), + ); + } + const orConditions: Q.Condition[] = [ + Q.where('display_name', Q.like(matchTerm)), + Q.where('name', Q.like(likeTerm)), + ]; + + if (isSearch) { + orConditions.push( + Q.and( + Q.where('type', Q.oneOf([General.DM_CHANNEL, General.GM_CHANNEL])), + Q.on(CHANNEL_MEMBERSHIP, Q.on(USER, + Q.or( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: condition type error + Q.unsafeSqlExpr(`first_name || ' ' || last_name LIKE '${likeTerm}'`), + Q.where('nickname', Q.like(likeTerm)), + Q.where('email', Q.like(likeTerm)), + Q.where('username', Q.like(likeTerm)), + ), + )), + ), + ); + } + + const teamsToSearch = [teamId]; + if (isSearch) { + teamsToSearch.push(''); + } + + const andConditions: Q.Condition[] = [ + Q.where('team_id', Q.oneOf(teamsToSearch)), + ]; + if (!isSearch) { + andConditions.push( + Q.where('type', Q.oneOf([General.OPEN_CHANNEL, General.PRIVATE_CHANNEL])), + Q.where('delete_at', 0), + ); + } + + clauses.push( + ...andConditions, + Q.or(...orConditions), + Q.sortBy('display_name', Q.asc), + Q.sortBy('name', Q.asc), + Q.take(25), + ); + + return database.get(CHANNEL).query(...clauses); +}; diff --git a/app/screens/browse_channels/search_handler.tsx b/app/screens/browse_channels/search_handler.tsx index 4c6d51d2e..f50eb95fb 100644 --- a/app/screens/browse_channels/search_handler.tsx +++ b/app/screens/browse_channels/search_handler.tsx @@ -218,7 +218,7 @@ export default function SearchHandler(props: Props) { clearTimeout(searchTimeout.current); } searchTimeout.current = setTimeout(async () => { - const results = await searchChannels(serverUrl, text); + const results = await searchChannels(serverUrl, text, currentTeamId); if (results.channels) { setSearchResults(results.channels); } diff --git a/app/screens/home/search/search.tsx b/app/screens/home/search/search.tsx index 137cb7e12..abc3d59db 100644 --- a/app/screens/home/search/search.tsx +++ b/app/screens/home/search/search.tsx @@ -263,8 +263,9 @@ const SearchScreen = ({teamId}: Props) => { position={autocompletePosition} growDown={true} containerStyle={styles.autocompleteContainer} + teamId={searchTeamId} /> - ), [cursorPosition, handleTextChange, searchValue, autocompleteMaxHeight, autocompletePosition]); + ), [cursorPosition, handleTextChange, searchValue, autocompleteMaxHeight, autocompletePosition, searchTeamId]); return (