Use always first the local channels (#6594)

* Use always first the local channels

* Fix several race condition related issues

* Remove clean after cursor change

* Address feedback

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Daniel Espino <danielespino@MacBook-Pro-de-Daniel.local>
This commit is contained in:
Daniel Espino García 2022-10-24 10:55:31 +02:00 committed by GitHub
parent c82c634523
commit bb051b83b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 274 additions and 133 deletions

View file

@ -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};

View file

@ -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};

View file

@ -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<UserProfile | UserModel>, 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<UserModel[]>();
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;

View file

@ -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))),
};
});

View file

@ -61,6 +61,7 @@ type Props = {
availableSpace: SharedValue<number>;
inPost?: boolean;
growDown?: boolean;
teamId?: string;
containerStyle?: StyleProp<ViewStyle>;
}
@ -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}
/>
<ChannelMention
cursorPosition={cursorPosition}
@ -161,6 +164,8 @@ const Autocomplete = ({
value={value || ''}
nestedScrollEnabled={nestedScrollEnabled}
isSearch={isSearch}
channelId={channelId}
teamId={teamId}
/>
{!isSearch &&
<EmojiSuggestion

View file

@ -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 {searchChannels} from '@actions/remote/channel';
@ -11,11 +11,8 @@ import ChannelMentionItem from '@components/autocomplete/channel_mention_item';
import {General} from '@constants';
import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from '@constants/autocomplete';
import {useServerUrl} from '@context/server';
import DatabaseManager from '@database/manager';
import useDidUpdate from '@hooks/did_update';
import {t} from '@i18n';
import {queryAllChannelsForTeam} from '@queries/servers/channel';
import {getCurrentTeamId} from '@queries/servers/system';
import {hasTrailingSpaces} from '@utils/helpers';
import type ChannelModel from '@typings/database/models/servers/channel';
@ -25,32 +22,6 @@ const keyExtractor = (item: Channel) => {
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<Channel | ChannelModel>, members: MyChannelModel[]) => {
const memberIds = new Set(members.map((m) => m.id));
return channels.reduce<Array<Array<Channel | ChannelModel>>>(([pubC, priC, dms], c) => {
@ -83,7 +54,7 @@ const reduceChannelsForAutocomplete = (channels: Array<Channel | ChannelModel>,
}, [[], []]);
};
const makeSections = (channels: Array<Channel | ChannelModel>, myMembers: MyChannelModel[], isSearch = false) => {
const makeSections = (channels: Array<Channel | ChannelModel>, 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<Channel | ChannelModel>, 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<ViewStyle>;
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<SectionListData<Channel>> = [];
const emptyChannels: Array<Channel | ChannelModel> = [];
@ -188,47 +152,45 @@ const ChannelMention = ({
value,
nestedScrollEnabled,
listStyle,
matchTerm,
localChannels,
teamId,
}: Props) => {
const serverUrl = useServerUrl();
const [sections, setSections] = useState<Array<SectionListData<(Channel | ChannelModel)>>>(emptySections);
const [channels, setChannels] = useState<Array<ChannelModel | Channel>>(emptyChannels);
const [remoteChannels, setRemoteChannels] = useState<Array<ChannelModel | Channel>>(emptyChannels);
const [loading, setLoading] = useState(false);
const [noResultsTerm, setNoResultsTerm] = useState<string|null>(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<ChannelModel[]>();
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<Channel | ChannelModel> = 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<Channel | ChannelModel> = 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<Channel | ChannelModel>) => {
@ -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;

View file

@ -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))));

View file

@ -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);
};

View file

@ -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;

View file

@ -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<ChannelModel>(CHANNEL).query(...clauses);
};

View file

@ -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);
}

View file

@ -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 (
<FreezeScreen freeze={!isFocused}>