diff --git a/app/containers/MessageBox/index.tsx b/app/containers/MessageBox/index.tsx index d90a77dba..306c95825 100644 --- a/app/containers/MessageBox/index.tsx +++ b/app/containers/MessageBox/index.tsx @@ -658,7 +658,8 @@ class MessageBox extends Component { }; getUsers = debounce(async (keyword: any) => { - let res = await search({ text: keyword, filterRooms: false, filterUsers: true }); + const { rid } = this.props; + let res = await search({ text: keyword, filterRooms: false, filterUsers: true, rid }); res = [...this.getFixedMentions(keyword), ...res]; this.setState({ mentions: res, mentionLoading: false }); }, 300); diff --git a/app/lib/constants/defaultSettings.ts b/app/lib/constants/defaultSettings.ts index 3a6017b44..f26acc755 100644 --- a/app/lib/constants/defaultSettings.ts +++ b/app/lib/constants/defaultSettings.ts @@ -226,5 +226,8 @@ export const defaultSettings = { }, Accounts_AllowDeleteOwnAccount: { type: 'valueAsBoolean' + }, + Number_of_users_autocomplete_suggestions: { + type: 'valueAsNumber' } } as const; diff --git a/app/lib/methods/search.ts b/app/lib/methods/search.ts index 538d10440..5a3c0433a 100644 --- a/app/lib/methods/search.ts +++ b/app/lib/methods/search.ts @@ -2,13 +2,16 @@ import { Q } from '@nozbe/watermelondb'; import { sanitizeLikeString } from '../database/utils'; import database from '../database/index'; +import { store as reduxStore } from '../store/auxStore'; import { spotlight } from '../services/restApi'; -import { ISearch, ISearchLocal, SubscriptionType } from '../../definitions'; +import { ISearch, ISearchLocal, IUserMessage, SubscriptionType } from '../../definitions'; import { isGroupChat } from './helpers'; +export type TSearch = ISearchLocal | IUserMessage | ISearch; + let debounce: null | ((reason: string) => void) = null; -export const localSearch = async ({ text = '', filterUsers = true, filterRooms = true }): Promise => { +export const localSearchSubscription = async ({ text = '', filterUsers = true, filterRooms = true }): Promise => { const searchText = text.trim(); const db = database.active; const likeString = sanitizeLikeString(searchText); @@ -41,29 +44,60 @@ export const localSearch = async ({ text = '', filterUsers = true, filterRooms = return search; }; -export const search = async ({ text = '', filterUsers = true, filterRooms = true }): Promise<(ISearch | ISearchLocal)[]> => { +export const localSearchUsersMessageByRid = async ({ text = '', rid = '' }): Promise => { + const userId = reduxStore.getState().login.user.id; + const numberOfSuggestions = reduxStore.getState().settings.Number_of_users_autocomplete_suggestions as number; + const searchText = text.trim(); + const db = database.active; + const likeString = sanitizeLikeString(searchText); + const messages = await db + .get('messages') + .query( + Q.and(Q.where('rid', rid), Q.where('u', Q.notLike(`%${userId}%`)), Q.where('t', null)), + Q.experimentalSortBy('ts', Q.desc), + Q.experimentalTake(50) + ) + .fetch(); + + const regExp = new RegExp(`${likeString}`, 'i'); + const users = messages.map(message => message.u); + + const usersFromLocal = users + .filter((item1, index) => users.findIndex(item2 => item2._id === item1._id) === index) // Remove duplicated data from response + .filter(user => user?.name?.match(regExp) || user?.username?.match(regExp)) + .slice(0, text ? 2 : numberOfSuggestions); + + return usersFromLocal; +}; + +export const search = async ({ text = '', filterUsers = true, filterRooms = true, rid = '' }): Promise => { const searchText = text.trim(); if (debounce) { debounce('cancel'); } - const localSearchData = await localSearch({ text, filterUsers, filterRooms }); - const usernames = localSearchData.map(sub => sub.name); + let localSearchData = []; + if (rid) { + localSearchData = await localSearchUsersMessageByRid({ text, rid }); + } else { + localSearchData = await localSearchSubscription({ text, filterUsers, filterRooms }); + } + const usernames = localSearchData.map(sub => sub.name as string); - const data = localSearchData as (ISearch | ISearchLocal)[]; + const data: TSearch[] = localSearchData; try { - if (localSearchData.length < 7) { + if (searchText && localSearchData.length < 7) { const { users, rooms } = (await Promise.race([ - spotlight(searchText, usernames, { users: filterUsers, rooms: filterRooms }), + spotlight(searchText, usernames, { users: filterUsers, rooms: filterRooms }, rid), new Promise((resolve, reject) => (debounce = reject)) ])) as { users: ISearch[]; rooms: ISearch[] }; if (filterUsers) { users .filter((item1, index) => users.findIndex(item2 => item2._id === item1._id) === index) // Remove duplicated data from response - .filter(user => !data.some(sub => user.username === sub.name)) // Make sure to remove users already on local database + .filter(user => !data.some(sub => 'username' in sub && user.username === sub.username)) // Make sure to remove users already on local database .forEach(user => { data.push({ ...user, @@ -77,7 +111,7 @@ export const search = async ({ text = '', filterUsers = true, filterRooms = true if (filterRooms) { rooms.forEach(room => { // Check if it exists on local database - const index = data.findIndex(item => item.rid === room._id); + const index = data.findIndex(item => 'rid' in item && item.rid === room._id); if (index === -1) { data.push({ ...room, diff --git a/app/lib/services/restApi.ts b/app/lib/services/restApi.ts index 54f169ebf..5fa3b1555 100644 --- a/app/lib/services/restApi.ts +++ b/app/lib/services/restApi.ts @@ -89,9 +89,16 @@ export const forgotPassword = (email: string) => export const sendConfirmationEmail = (email: string): Promise<{ message: string; success: boolean }> => sdk.methodCallWrapper('sendConfirmationEmail', email); -export const spotlight = (search: string, usernames: string[], type: { users: boolean; rooms: boolean }): Promise => +export const spotlight = ( + search: string, + usernames: string[], + type: { users: boolean; rooms: boolean }, + rid?: string +): Promise => // RC 0.51.0 - sdk.methodCallWrapper('spotlight', search, usernames, type); + rid + ? sdk.methodCallWrapper('spotlight', search, usernames, type, rid) + : sdk.methodCallWrapper('spotlight', search, usernames, type); export const createDirectMessage = (username: string) => // RC 0.59.0 diff --git a/app/views/CreateDiscussionView/SelectChannel.tsx b/app/views/CreateDiscussionView/SelectChannel.tsx index 7a8d799d7..2a4c01332 100644 --- a/app/views/CreateDiscussionView/SelectChannel.tsx +++ b/app/views/CreateDiscussionView/SelectChannel.tsx @@ -7,7 +7,7 @@ import I18n from '../../i18n'; import { getAvatarURL } from '../../lib/methods/helpers/getAvatarUrl'; import { ICreateDiscussionViewSelectChannel } from './interfaces'; import styles from './styles'; -import { localSearch } from '../../lib/methods'; +import { localSearchSubscription } from '../../lib/methods'; import { getRoomAvatar, getRoomTitle } from '../../lib/methods/helpers'; import { useTheme } from '../../theme'; @@ -25,7 +25,7 @@ const SelectChannel = ({ const getChannels = async (keyword = '') => { try { - const res = (await localSearch({ text: keyword, filterUsers: false })) as ISearchLocal[]; + const res = (await localSearchSubscription({ text: keyword, filterUsers: false })) as ISearchLocal[]; setChannels(res); return res.map(channel => ({ value: channel, diff --git a/app/views/SelectedUsersView/index.tsx b/app/views/SelectedUsersView/index.tsx index 3a0696462..ac4693d99 100644 --- a/app/views/SelectedUsersView/index.tsx +++ b/app/views/SelectedUsersView/index.tsx @@ -13,7 +13,6 @@ import * as List from '../../containers/List'; import { sendLoadingEvent } from '../../containers/Loading'; import SafeAreaView from '../../containers/SafeAreaView'; import StatusBar from '../../containers/StatusBar'; -import { ISearch, ISearchLocal } from '../../definitions'; import I18n from '../../i18n'; import database from '../../lib/database'; import UserItem from '../../containers/UserItem'; @@ -23,7 +22,7 @@ import { ChatsStackParamList } from '../../stacks/types'; import { useTheme } from '../../theme'; import { showErrorAlert } from '../../lib/methods/helpers/info'; import log, { events, logEvent } from '../../lib/methods/helpers/log'; -import { search as searchMethod } from '../../lib/methods'; +import { search as searchMethod, TSearch } from '../../lib/methods'; import { isGroupChat as isGroupChatMethod } from '../../lib/methods/helpers'; import { useAppSelector } from '../../lib/hooks'; import Header from './Header'; @@ -31,11 +30,9 @@ import Header from './Header'; type TRoute = RouteProp; type TNavigation = StackNavigationProp; -type TSearchItem = ISearch | ISearchLocal; - const SelectedUsersView = () => { const [chats, setChats] = useState([]); - const [search, setSearch] = useState([]); + const [search, setSearch] = useState([]); const { maxUsers, showButton, title, buttonText, nextAction } = useRoute().params; const navigation = useNavigation();