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:
parent
c82c634523
commit
bb051b83b9
12 changed files with 274 additions and 133 deletions
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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))),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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))));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue