Restore out of channel and some cleanup (#8050)

* Restore out of channel and some cleanup

* Fix i18n
This commit is contained in:
Daniel Espino García 2024-07-09 11:31:31 +02:00 committed by GitHub
parent 10135091d1
commit f3de4445c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 282 additions and 229 deletions

View file

@ -3,9 +3,8 @@
import {debounce} from 'lodash';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {Platform, SectionList, type SectionListData, type SectionListRenderItemInfo, type StyleProp, type ViewStyle} from 'react-native';
import {Platform, SectionList, type SectionListRenderItemInfo, type StyleProp, type ViewStyle} from 'react-native';
import {searchGroupsByName, searchGroupsByNameInChannel, searchGroupsByNameInTeam} from '@actions/local/group';
import {searchUsers} from '@actions/remote/user';
import GroupMentionItem from '@components/autocomplete/at_mention_group/at_mention_group';
import AtMentionItem from '@components/autocomplete/at_mention_item';
@ -13,184 +12,14 @@ import AutocompleteSectionHeader from '@components/autocomplete/autocomplete_sec
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 DatabaseManager from '@database/manager';
import {t} from '@i18n';
import {queryAllUsers, getUsersFromDMSorted} from '@queries/servers/user';
import {hasTrailingSpaces} from '@utils/helpers';
import {SECTION_KEY_GROUPS, SECTION_KEY_SPECIAL, emptyGroupList, emptySectionList, emptyUserlList} from './constants';
import {checkSpecialMentions, filterResults, getAllUsers, getMatchTermForAtMention, keyExtractor, makeSections, searchGroups, sortReceivedUsers} from './utils';
import type {SpecialMention, UserMentionSections} from './types';
import type GroupModel from '@typings/database/models/servers/group';
import type UserModel from '@typings/database/models/servers/user';
const SECTION_KEY_TEAM_MEMBERS = 'teamMembers';
const SECTION_KEY_IN_CHANNEL = 'inChannel';
const SECTION_KEY_SPECIAL = 'special';
const SECTION_KEY_GROUPS = 'groups';
type SpecialMention = {
completeHandle: string;
id: string;
defaultMessage: string;
}
type UserMentionSections = Array<SectionListData<UserProfile|UserModel|GroupModel|SpecialMention>>
const getMatchTermForAtMention = (() => {
let lastMatchTerm: string | null = null;
let lastValue: string;
let lastIsSearch: boolean;
return (value: string, isSearch: boolean) => {
if (value !== lastValue || isSearch !== lastIsSearch) {
const regex = isSearch ? AT_MENTION_SEARCH_REGEX : AT_MENTION_REGEX;
let term = value.toLowerCase();
if (term.startsWith('from: @') || term.startsWith('from:@')) {
term = term.replace('@', '');
}
const match = term.match(regex);
lastValue = value;
lastIsSearch = isSearch;
if (match) {
lastMatchTerm = (isSearch ? match[1] : match[2]).toLowerCase();
} else {
lastMatchTerm = null;
}
}
return lastMatchTerm;
};
})();
const getSpecialMentions: () => SpecialMention[] = () => {
return [{
completeHandle: 'all',
id: t('suggestion.mention.all'),
defaultMessage: 'Notifies everyone in this channel',
}, {
completeHandle: 'channel',
id: t('suggestion.mention.channel'),
defaultMessage: 'Notifies everyone in this channel',
}, {
completeHandle: 'here',
id: t('suggestion.mention.here'),
defaultMessage: 'Notifies everyone online in this channel',
}];
};
const checkSpecialMentions = (term: string) => {
return getSpecialMentions().filter((m) => m.completeHandle.startsWith(term)).length > 0;
};
const keyExtractor = (item: UserProfile) => {
return item.id;
};
const filterResults = (users: Array<UserModel | UserProfile>, term: string) => {
return users.filter((u) => {
const firstName = ('firstName' in u ? u.firstName : u.first_name).toLowerCase();
const lastName = ('lastName' in u ? u.lastName : u.last_name).toLowerCase();
const fullName = `${firstName} ${lastName}`;
return u.username.toLowerCase().includes(term) ||
u.nickname.toLowerCase().includes(term) ||
fullName.includes(term) ||
u.email.toLowerCase().includes(term);
});
};
const makeSections = (teamMembers: Array<UserProfile | UserModel>, users: Array<UserProfile | UserModel>, groups: GroupModel[], showSpecialMentions: boolean, isLocal = false, isSearch = false) => {
const newSections: UserMentionSections = [];
if (isSearch) {
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 (users.length) {
newSections.push({
id: t('suggestion.mention.users'),
defaultMessage: 'Users',
data: users,
key: SECTION_KEY_IN_CHANNEL,
});
}
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,
});
}
}
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;
@ -207,19 +36,6 @@ type Props = {
listStyle: StyleProp<ViewStyle>;
}
const emptyUserlList: Array<UserModel | UserProfile> = [];
const emptySectionList: UserMentionSections = [];
const emptyGroupList: GroupModel[] = [];
const getAllUsers = async (serverUrl: string) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return [];
}
return queryAllUsers(database).fetch();
};
const AtMention = ({
channelId,
teamId,
@ -238,7 +54,8 @@ const AtMention = ({
const serverUrl = useServerUrl();
const [sections, setSections] = useState<UserMentionSections>(emptySectionList);
const [users, setUsers] = useState<Array<UserProfile | UserModel>>(emptyUserlList);
const [usersInChannel, setUsersInChannel] = useState<Array<UserProfile | UserModel>>(emptyUserlList);
const [usersOutOfChannel, setUsersOutOfChannel] = useState<Array<UserProfile | UserModel>>(emptyUserlList);
const [groups, setGroups] = useState<GroupModel[]>(emptyGroupList);
const [loading, setLoading] = useState(false);
const [noResultsTerm, setNoResultsTerm] = useState<string|null>(null);
@ -278,49 +95,23 @@ const AtMention = ({
const filteredUsers = filterResults(fallbackUsers, term);
setFilteredLocalUsers(filteredUsers.length ? filteredUsers : emptyUserlList);
} else if (receivedUsers) {
await sortRecievedUsers(sUrl, term, receivedUsers?.users, receivedUsers?.out_of_channel);
const [sortedMembers, sortedOutOfChannel] = await sortReceivedUsers(sUrl, term, receivedUsers?.users, receivedUsers?.out_of_channel);
setUsersInChannel(sortedMembers.length ? sortedMembers : emptyUserlList);
setUsersOutOfChannel(sortedOutOfChannel.length ? sortedOutOfChannel : emptyUserlList);
}
setLoading(false);
}, 200), []);
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);
// This only get us the users we have on the database.
// We need to append those users from which we don't have
// information at the end of the list.
const outSortedMembers = await getUsersFromDMSorted(database, outChannelMemberIds);
const idSet = new Set(outSortedMembers.map((v) => v.id));
const outRest = outOfChannel.filter((v) => !idSet.has(v.id));
sortedMembers.push(...outSortedMembers, ...outRest);
}
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 teamMembers = useMemo(
() => [...usersInChannel, ...usersOutOfChannel],
[usersInChannel, usersOutOfChannel],
);
const matchTerm = getMatchTermForAtMention(value.substring(0, localCursorPosition), isSearch);
const resetState = () => {
setUsers(emptyUserlList);
setUsersInChannel(emptyUserlList);
setUsersOutOfChannel(emptyUserlList);
setGroups(emptyGroupList);
setFilteredLocalUsers(emptyUserlList);
setSections(emptySectionList);
@ -438,12 +229,12 @@ const AtMention = ({
return;
}
const showSpecialMentions = useChannelMentions && matchTerm != null && checkSpecialMentions(matchTerm);
const buildMemberSection = isSearch || (!channelId && users.length > 0);
const buildMemberSection = isSearch || (!channelId && teamMembers.length > 0);
let newSections;
if (useLocal) {
newSections = makeSections(filteredLocalUsers, [], groups, showSpecialMentions, true, buildMemberSection);
newSections = makeSections(filteredLocalUsers, [], [], groups, showSpecialMentions, true, buildMemberSection);
} else {
newSections = makeSections(users, users, groups, showSpecialMentions, buildMemberSection);
newSections = makeSections(teamMembers, usersInChannel, usersOutOfChannel, groups, showSpecialMentions, buildMemberSection);
}
const nSections = newSections.length;
@ -456,7 +247,7 @@ const AtMention = ({
}
setSections(nSections ? newSections : emptySectionList);
onShowingChange(Boolean(nSections));
}, [!useLocal && users, users, groups, loading, channelId, useLocal && filteredLocalUsers]);
}, [!useLocal && usersInChannel, !useLocal && usersOutOfChannel, 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

@ -0,0 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {UserMentionSections} from './types';
import type GroupModel from '@typings/database/models/servers/group';
import type UserModel from '@typings/database/models/servers/user';
export const SECTION_KEY_TEAM_MEMBERS = 'teamMembers';
export const SECTION_KEY_IN_CHANNEL = 'inChannel';
export const SECTION_KEY_OUT_OF_CHANNEL = 'outChannel';
export const SECTION_KEY_SPECIAL = 'special';
export const SECTION_KEY_GROUPS = 'groups';
export const emptyUserlList: Array<UserModel | UserProfile> = [];
export const emptySectionList: UserMentionSections = [];
export const emptyGroupList: GroupModel[] = [];

View file

@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {type SectionListData} from 'react-native';
import type GroupModel from '@typings/database/models/servers/group';
import type UserModel from '@typings/database/models/servers/user';
export type SpecialMention = {
completeHandle: string;
id: string;
defaultMessage: string;
}
export type UserMentionSections = Array<SectionListData<UserProfile|UserModel|GroupModel|SpecialMention>>;

View file

@ -0,0 +1,230 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {searchGroupsByName, searchGroupsByNameInChannel, searchGroupsByNameInTeam} from '@actions/local/group';
import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from '@constants/autocomplete';
import DatabaseManager from '@database/manager';
import {t} from '@i18n';
import {getUsersFromDMSorted, queryAllUsers} from '@queries/servers/user';
import {hasTrailingSpaces} from '@utils/helpers';
import {SECTION_KEY_GROUPS, SECTION_KEY_IN_CHANNEL, SECTION_KEY_OUT_OF_CHANNEL, SECTION_KEY_SPECIAL, SECTION_KEY_TEAM_MEMBERS, emptyGroupList} from './constants';
import type {SpecialMention, UserMentionSections} from './types';
import type GroupModel from '@typings/database/models/servers/group';
import type UserModel from '@typings/database/models/servers/user';
export const getMatchTermForAtMention = (() => {
let lastMatchTerm: string | null = null;
let lastValue: string;
let lastIsSearch: boolean;
return (value: string, isSearch: boolean) => {
if (value !== lastValue || isSearch !== lastIsSearch) {
const regex = isSearch ? AT_MENTION_SEARCH_REGEX : AT_MENTION_REGEX;
let term = value.toLowerCase();
if (term.startsWith('from: @') || term.startsWith('from:@')) {
term = term.replace('@', '');
}
const match = term.match(regex);
lastValue = value;
lastIsSearch = isSearch;
if (match) {
lastMatchTerm = (isSearch ? match[1] : match[2]).toLowerCase();
} else {
lastMatchTerm = null;
}
}
return lastMatchTerm;
};
})();
const getSpecialMentions: () => SpecialMention[] = () => {
return [{
completeHandle: 'all',
id: t('suggestion.mention.all'),
defaultMessage: 'Notifies everyone in this channel',
}, {
completeHandle: 'channel',
id: t('suggestion.mention.channel'),
defaultMessage: 'Notifies everyone in this channel',
}, {
completeHandle: 'here',
id: t('suggestion.mention.here'),
defaultMessage: 'Notifies everyone online in this channel',
}];
};
export const checkSpecialMentions = (term: string) => {
return getSpecialMentions().filter((m) => m.completeHandle.startsWith(term)).length > 0;
};
export const keyExtractor = (item: UserProfile) => {
return item.id;
};
const filterAndSlice = (term: string, sortedMembers: Array<UserModel | UserProfile>) => {
if (hasTrailingSpaces(term)) {
const filteredReceivedUsers = filterResults(sortedMembers, term);
return filteredReceivedUsers.slice(0, 20);
}
return sortedMembers.slice(0, 20);
};
export async function sortReceivedUsers(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 sortedMembersOutOfChannel: Array<UserProfile | UserModel> = [];
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);
// This only get us the users we have on the database.
// We need to append those users from which we don't have
// information at the end of the list.
const outSortedMembers = await getUsersFromDMSorted(database, outChannelMemberIds);
const idSet = new Set(outSortedMembers.map((v) => v.id));
const outRest = outOfChannel.filter((v) => !idSet.has(v.id));
sortedMembersOutOfChannel.push(...outSortedMembers, ...outRest);
}
const filteredMembers = filterAndSlice(term, sortedMembers);
const filteredOutOfChannel = filterAndSlice(term, sortedMembersOutOfChannel);
return [filteredMembers, filteredOutOfChannel];
}
export const filterResults = (users: Array<UserModel | UserProfile>, term: string) => {
return users.filter((u) => {
const firstName = ('firstName' in u ? u.firstName : u.first_name).toLowerCase();
const lastName = ('lastName' in u ? u.lastName : u.last_name).toLowerCase();
const fullName = `${firstName} ${lastName}`;
return u.username.toLowerCase().includes(term) ||
u.nickname.toLowerCase().includes(term) ||
fullName.includes(term) ||
u.email.toLowerCase().includes(term);
});
};
export const makeSections = (teamMembers: Array<UserProfile | UserModel>, usersInChannel: Array<UserProfile | UserModel>, usersOutOfChannel: Array<UserProfile | UserModel>, groups: GroupModel[], showSpecialMentions: boolean, isLocal = false, isSearch = false) => {
const newSections: UserMentionSections = [];
if (isSearch) {
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({
id: t('suggestion.mention.users'),
defaultMessage: 'Users',
data: usersInChannel,
key: SECTION_KEY_IN_CHANNEL,
});
}
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,
});
}
if (usersOutOfChannel.length) {
newSections.push({
id: t('suggestion.mention.nonmembers'),
defaultMessage: 'Not in Channel',
data: usersOutOfChannel,
key: SECTION_KEY_OUT_OF_CHANNEL,
});
}
}
return newSections;
};
export 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;
}
};
export const getAllUsers = async (serverUrl: string) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return [];
}
return queryAllUsers(database).fetch();
};

View file

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