Use local users for At Mention Autocomplete (#6269)

* Use local users for At Mention Autocomplete

* Only load local users if we need them
This commit is contained in:
Daniel Espino García
2022-05-17 17:05:56 +02:00
committed by GitHub
parent 1706b673ba
commit 2323f4aa31
2 changed files with 85 additions and 13 deletions

View File

@@ -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<SectionListData<UserProfile|Group|SpecialMention>>
type UserMentionSections = Array<SectionListData<UserProfile|UserModel|Group|SpecialMention>>
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<UserProfile | UserModel>, usersInChannel: Array<UserProfile | UserModel>, usersOutOfChannel: Array<UserProfile | UserModel>, 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<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 [localUsers, setLocalUsers] = useState<UserModel[]>();
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 (
<AtMentionItem
testID={`autocomplete.at_mention.item.${item}`}
@@ -316,7 +382,12 @@ const AtMention = ({
useEffect(() => {
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

View File

@@ -26,6 +26,7 @@ const enhanced = withObservables([], ({database, channelId}: WithDatabaseArgs &
let useChannelMentions: Observable<boolean>;
let useGroupMentions: Observable<boolean>;
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))));