From 2323f4aa31c6bd1edeaed7ffb0445b4fce3c7df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Tue, 17 May 2022 17:05:56 +0200 Subject: [PATCH] Use local users for At Mention Autocomplete (#6269) * Use local users for At Mention Autocomplete * Only load local users if we need them --- .../autocomplete/at_mention/at_mention.tsx | 97 ++++++++++++++++--- .../autocomplete/at_mention/index.ts | 1 + 2 files changed, 85 insertions(+), 13 deletions(-) diff --git a/app/components/autocomplete/at_mention/at_mention.tsx b/app/components/autocomplete/at_mention/at_mention.tsx index 6f4f47f680..f11ee9c04f 100644 --- a/app/components/autocomplete/at_mention/at_mention.tsx +++ b/app/components/autocomplete/at_mention/at_mention.tsx @@ -14,9 +14,13 @@ import SpecialMentionItem from '@components/autocomplete/special_mention_item'; import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from '@constants/autocomplete'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; +import DatabaseManager from '@database/manager'; import {t} from '@i18n'; +import {queryAllUsers} from '@queries/servers/user'; import {makeStyleSheetFromTheme} from '@utils/theme'; +import type UserModel from '@typings/database/models/servers/user'; + const SECTION_KEY_TEAM_MEMBERS = 'teamMembers'; const SECTION_KEY_IN_CHANNEL = 'inChannel'; const SECTION_KEY_OUT_OF_CHANNEL = 'outChannel'; @@ -29,7 +33,7 @@ type SpecialMention = { defaultMessage: string; } -type UserMentionSections = Array> +type UserMentionSections = Array> const getMatchTermForAtMention = (() => { let lastMatchTerm: string | null = null; @@ -80,16 +84,54 @@ const keyExtractor = (item: UserProfile) => { return item.id; }; -const makeSections = (teamMembers: UserProfile[], usersInChannel: UserProfile[], usersOutOfChannel: UserProfile[], groups: Group[], showSpecialMentions: boolean, isSearch = false) => { +const filterLocalResults = (users: UserModel[], term: string) => { + return users.filter((u) => + u.username.toLowerCase().startsWith(term) || + u.nickname.toLowerCase().startsWith(term) || + u.firstName.toLowerCase().startsWith(term) || + u.lastName.toLowerCase().startsWith(term), + ); +}; + +const makeSections = (teamMembers: Array, usersInChannel: Array, usersOutOfChannel: Array, groups: Group[], showSpecialMentions: boolean, isLocal = false, isSearch = false) => { const newSections: UserMentionSections = []; if (isSearch) { - newSections.push({ - id: t('mobile.suggestion.members'), - defaultMessage: 'Members', - data: teamMembers, - key: SECTION_KEY_TEAM_MEMBERS, - }); + if (teamMembers.length) { + newSections.push({ + id: t('mobile.suggestion.members'), + defaultMessage: 'Members', + data: teamMembers, + key: SECTION_KEY_TEAM_MEMBERS, + }); + } + } else if (isLocal) { + if (teamMembers.length) { + newSections.push({ + id: t('mobile.suggestion.members'), + defaultMessage: 'Members', + data: teamMembers, + key: SECTION_KEY_TEAM_MEMBERS, + }); + } + + if (groups.length) { + newSections.push({ + id: t('suggestion.mention.groups'), + defaultMessage: 'Group Mentions', + data: groups, + key: SECTION_KEY_GROUPS, + }); + } + + if (showSpecialMentions) { + newSections.push({ + id: t('suggestion.mention.special'), + defaultMessage: 'Special Mentions', + data: getSpecialMentions(), + key: SECTION_KEY_SPECIAL, + }); + } } else { if (usersInChannel.length) { newSections.push({ @@ -153,9 +195,19 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { }); const emptyProfileList: UserProfile[] = []; +const emptyModelList: UserModel[] = []; const empytSectionList: UserMentionSections = []; const emptyGroupList: Group[] = []; +const getAllUsers = async (serverUrl: string) => { + const database = DatabaseManager.serverDatabases[serverUrl]?.database; + if (!database) { + return []; + } + + return queryAllUsers(database).fetch(); +}; + const AtMention = ({ channelId, cursorPosition, @@ -179,11 +231,24 @@ const AtMention = ({ 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 [localUsers, setLocalUsers] = useState(); + const [filteredLocalUsers, setFilteredLocalUsers] = useState(emptyModelList); const runSearch = useMemo(() => debounce(async (sUrl: string, term: string, cId?: string) => { setLoading(true); - const {users: receivedUsers} = await searchUsers(sUrl, term, cId); - if (receivedUsers) { + const {users: receivedUsers, error} = await searchUsers(sUrl, term, cId); + + setUseLocal(Boolean(error)); + if (error) { + let fallbackUsers = localUsers; + if (!fallbackUsers) { + fallbackUsers = await getAllUsers(sUrl); + setLocalUsers(fallbackUsers); + } + const filteredUsers = filterLocalResults(fallbackUsers, term); + setFilteredLocalUsers(filteredUsers.length ? filteredUsers : emptyModelList); + } else if (receivedUsers) { setUsersInChannel(receivedUsers.users.length ? receivedUsers.users : emptyProfileList); setUsersOutOfChannel(receivedUsers.out_of_channel?.length ? receivedUsers.out_of_channel : emptyProfileList); } @@ -200,6 +265,7 @@ const AtMention = ({ const resetState = () => { setUsersInChannel(emptyProfileList); setUsersOutOfChannel(emptyProfileList); + setFilteredLocalUsers(emptyModelList); setSections(empytSectionList); runSearch.cancel(); }; @@ -249,7 +315,7 @@ const AtMention = ({ ); }, [completeMention]); - const renderAtMentions = useCallback((item: UserProfile) => { + const renderAtMentions = useCallback((item: UserProfile | UserModel) => { return ( { const showSpecialMentions = useChannelMentions && matchTerm != null && checkSpecialMentions(matchTerm); const buildMemberSection = isSearch || (!channelId && teamMembers.length > 0); - const newSections = makeSections(teamMembers, usersInChannel, usersOutOfChannel, groups, showSpecialMentions, buildMemberSection); + let newSections; + if (useLocal) { + newSections = makeSections(filteredLocalUsers, [], [], groups, showSpecialMentions, true, buildMemberSection); + } else { + newSections = makeSections(teamMembers, usersInChannel, usersOutOfChannel, groups, showSpecialMentions, buildMemberSection); + } const nSections = newSections.length; if (!loading && !nSections && noResultsTerm == null) { @@ -324,7 +395,7 @@ const AtMention = ({ } setSections(nSections ? newSections : empytSectionList); onShowingChange(Boolean(nSections)); - }, [usersInChannel, usersOutOfChannel, teamMembers, groups, loading, channelId]); + }, [!useLocal && usersInChannel, !useLocal && usersOutOfChannel, teamMembers, groups, loading, channelId, useLocal && filteredLocalUsers]); if (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 diff --git a/app/components/autocomplete/at_mention/index.ts b/app/components/autocomplete/at_mention/index.ts index d5c9c677b9..e350012216 100644 --- a/app/components/autocomplete/at_mention/index.ts +++ b/app/components/autocomplete/at_mention/index.ts @@ -26,6 +26,7 @@ const enhanced = withObservables([], ({database, channelId}: WithDatabaseArgs & let useChannelMentions: Observable; let useGroupMentions: Observable; + if (channelId) { const currentChannel = observeChannel(database, channelId); useChannelMentions = combineLatest([currentUser, currentChannel]).pipe(switchMap(([u, c]) => (u && c ? observePermissionForChannel(c, u, Permissions.USE_CHANNEL_MENTIONS, false) : of$(false))));