Prioritize @ autocomplete results based on recency and thread activity (#7506)

* changed autocomplete results on recency

* fixed issues

* fixed imports, moved get fucn to queries folder

* fixed en.json file

* fixed lint promblems

---------

Co-authored-by: Ilia Polozov <ilya.polozov@onza.me>
This commit is contained in:
Ilya Polozov 2024-05-06 10:45:44 +02:00 committed by GitHub
parent 8799f2bb55
commit 105073ce76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 57 additions and 39 deletions

View file

@ -15,7 +15,7 @@ import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from '@constants/autocomplete
import {useServerUrl} from '@context/server';
import DatabaseManager from '@database/manager';
import {t} from '@i18n';
import {queryAllUsers} from '@queries/servers/user';
import {queryAllUsers, getUsersFromDMSorted} from '@queries/servers/user';
import {hasTrailingSpaces} from '@utils/helpers';
import type GroupModel from '@typings/database/models/servers/group';
@ -23,7 +23,6 @@ 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';
const SECTION_KEY_SPECIAL = 'special';
const SECTION_KEY_GROUPS = 'groups';
@ -96,7 +95,7 @@ const filterResults = (users: Array<UserModel | UserProfile>, term: string) => {
});
};
const makeSections = (teamMembers: Array<UserProfile | UserModel>, usersInChannel: Array<UserProfile | UserModel>, usersOutOfChannel: Array<UserProfile | UserModel>, groups: GroupModel[], showSpecialMentions: boolean, isLocal = false, isSearch = false) => {
const makeSections = (teamMembers: Array<UserProfile | UserModel>, users: Array<UserProfile | UserModel>, groups: GroupModel[], showSpecialMentions: boolean, isLocal = false, isSearch = false) => {
const newSections: UserMentionSections = [];
if (isSearch) {
@ -136,11 +135,11 @@ const makeSections = (teamMembers: Array<UserProfile | UserModel>, usersInChanne
});
}
} else {
if (usersInChannel.length) {
if (users.length) {
newSections.push({
id: t('suggestion.mention.members'),
defaultMessage: 'Channel Members',
data: usersInChannel,
id: t('suggestion.mention.users'),
defaultMessage: 'Users',
data: users,
key: SECTION_KEY_IN_CHANNEL,
});
}
@ -162,15 +161,6 @@ const makeSections = (teamMembers: Array<UserProfile | UserModel>, usersInChanne
key: SECTION_KEY_SPECIAL,
});
}
if (usersOutOfChannel.length) {
newSections.push({
id: t('suggestion.mention.nonmembers'),
defaultMessage: 'Not in Channel',
data: usersOutOfChannel,
key: SECTION_KEY_OUT_OF_CHANNEL,
});
}
}
return newSections;
};
@ -248,8 +238,7 @@ const AtMention = ({
const serverUrl = useServerUrl();
const [sections, setSections] = useState<UserMentionSections>(emptySectionList);
const [usersInChannel, setUsersInChannel] = useState<Array<UserProfile | UserModel>>(emptyUserlList);
const [usersOutOfChannel, setUsersOutOfChannel] = useState<Array<UserProfile | UserModel>>(emptyUserlList);
const [users, setUsers] = useState<Array<UserProfile | UserModel>>(emptyUserlList);
const [groups, setGroups] = useState<GroupModel[]>(emptyGroupList);
const [loading, setLoading] = useState(false);
const [noResultsTerm, setNoResultsTerm] = useState<string|null>(null);
@ -289,30 +278,43 @@ const AtMention = ({
const filteredUsers = filterResults(fallbackUsers, term);
setFilteredLocalUsers(filteredUsers.length ? filteredUsers : emptyUserlList);
} else if (receivedUsers) {
if (hasTrailingSpaces(term)) {
const filteredReceivedUsers = filterResults(receivedUsers.users, term);
const filteredReceivedOutOfChannelUsers = filterResults(receivedUsers.out_of_channel || [], term);
setUsersInChannel(filteredReceivedUsers.length ? filteredReceivedUsers : emptyUserlList);
setUsersOutOfChannel(filteredReceivedOutOfChannelUsers.length ? filteredReceivedOutOfChannelUsers : emptyUserlList);
} else {
setUsersInChannel(receivedUsers.users.length ? receivedUsers.users : emptyUserlList);
setUsersOutOfChannel(receivedUsers.out_of_channel?.length ? receivedUsers.out_of_channel : emptyUserlList);
}
sortRecievedUsers(sUrl, term, receivedUsers?.users, receivedUsers?.out_of_channel);
}
setLoading(false);
}, 200), []);
const teamMembers = useMemo(
() => [...usersInChannel, ...usersOutOfChannel],
[usersInChannel, usersOutOfChannel],
);
async function sortRecievedUsers(sUrl: string, term: string, receivedUsers: UserProfile[], outOfChannel: UserProfile[] | undefined) {
const database = DatabaseManager.serverDatabases[sUrl]?.database;
if (!database) {
return;
}
const memberIds = receivedUsers.map((e) => e.id);
const sortedMembers: Array<UserProfile | UserModel> = await getUsersFromDMSorted(database, memberIds);
const sortedMembersId = new Set<string>(sortedMembers.map((e) => e.id));
const membersNoDm = receivedUsers.filter((u) => !sortedMembersId.has(u.id));
sortedMembers.push(...membersNoDm);
if (outOfChannel?.length) {
const outChannelMemberIds = outOfChannel.map((e) => e.id);
const outSortedMembers = await getUsersFromDMSorted(database, outChannelMemberIds);
sortedMembers.push(...outSortedMembers);
}
if (hasTrailingSpaces(term)) {
const filteredReceivedUsers = filterResults(sortedMembers, term);
const slicedArray = filteredReceivedUsers.slice(0, 20);
setUsers(slicedArray.length ? slicedArray : emptyUserlList);
} else {
const slicedArray = sortedMembers.slice(0, 20);
setUsers(slicedArray.length ? slicedArray : emptyUserlList);
}
}
const matchTerm = getMatchTermForAtMention(value.substring(0, localCursorPosition), isSearch);
const resetState = () => {
setUsersInChannel(emptyUserlList);
setUsersOutOfChannel(emptyUserlList);
setUsers(emptyUserlList);
setGroups(emptyGroupList);
setFilteredLocalUsers(emptyUserlList);
setSections(emptySectionList);
@ -430,12 +432,12 @@ const AtMention = ({
return;
}
const showSpecialMentions = useChannelMentions && matchTerm != null && checkSpecialMentions(matchTerm);
const buildMemberSection = isSearch || (!channelId && teamMembers.length > 0);
const buildMemberSection = isSearch || (!channelId && users.length > 0);
let newSections;
if (useLocal) {
newSections = makeSections(filteredLocalUsers, [], [], groups, showSpecialMentions, true, buildMemberSection);
newSections = makeSections(filteredLocalUsers, [], groups, showSpecialMentions, true, buildMemberSection);
} else {
newSections = makeSections(teamMembers, usersInChannel, usersOutOfChannel, groups, showSpecialMentions, buildMemberSection);
newSections = makeSections(users, users, groups, showSpecialMentions, buildMemberSection);
}
const nSections = newSections.length;
@ -448,7 +450,7 @@ const AtMention = ({
}
setSections(nSections ? newSections : emptySectionList);
onShowingChange(Boolean(nSections));
}, [!useLocal && usersInChannel, !useLocal && usersOutOfChannel, teamMembers, groups, loading, channelId, useLocal && filteredLocalUsers]);
}, [!useLocal && users, users, 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

@ -5,6 +5,7 @@ import {Database, Q} from '@nozbe/watermelondb';
import {combineLatest, of as of$} from 'rxjs';
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import {General} from '@constants';
import {MM_TABLES} from '@constants/database';
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
import {sanitizeLikeString} from '@helpers/database';
@ -17,7 +18,7 @@ import type ChannelMembershipModel from '@typings/database/models/servers/channe
import type TeamMembershipModel from '@typings/database/models/servers/team_membership';
import type UserModel from '@typings/database/models/servers/user';
const {SERVER: {CHANNEL_MEMBERSHIP, USER, TEAM_MEMBERSHIP}} = MM_TABLES;
const {SERVER: {CHANNEL_MEMBERSHIP, USER, TEAM_MEMBERSHIP, CHANNEL, MY_CHANNEL}} = MM_TABLES;
export const getUserById = async (database: Database, userId: string) => {
try {
const userRecord = (await database.get<UserModel>(USER).find(userId));
@ -135,3 +136,17 @@ export const observeDeactivatedUsers = (database: Database) => {
}),
);
};
export const getUsersFromDMSorted = async (database: Database, memberIds: string[]) => {
try {
return database.get<UserModel>(USER).query(
Q.unsafeSqlQuery(`SELECT DISTINCT u.* FROM User u
INNER JOIN ${CHANNEL_MEMBERSHIP} cm ON cm.user_id=u.id
INNER JOIN ${CHANNEL} c ON c.id=cm.id AND c.type='${General.DM_CHANNEL}'
INNER JOIN ${MY_CHANNEL} my
WHERE cm.user_id IN (${`'${memberIds.join("','")}'`})
ORDER BY my.last_viewed_at DESC`)).fetch();
} catch (error) {
return [];
}
};

View file

@ -1056,6 +1056,7 @@
"suggestion.mention.here": "Notifies everyone online in this channel",
"suggestion.mention.members": "Channel Members",
"suggestion.mention.morechannels": "Other Channels",
"suggestion.mention.users": "Users",
"suggestion.mention.nonmembers": "Not in Channel",
"suggestion.mention.special": "Special Mentions",
"suggestion.search.direct": "Direct Messages",