Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions app/containers/MessageComposer/MessageComposer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { colors } from '../../lib/constants/colors';
import { type IRoomContext, RoomContext } from '../../views/RoomView/context';
import * as EmojiKeyboardHook from './hooks/useEmojiKeyboard';
import { initStore } from '../../lib/store/auxStore';
import { search } from '../../lib/methods/search';
import { searchRemote } from '../../lib/methods/search';
import database from '../../lib/database';
import { useMessageComposerApi } from './context';
import { sendFileMessage } from '../../lib/methods/sendFileMessage';
Expand All @@ -25,7 +25,8 @@ jest.useFakeTimers();

// Ensure search returns at least one item so autocomplete renders
jest.mock('../../lib/methods/search', () => ({
search: jest.fn(() => [{ _id: 'u1', username: 'john', name: 'John' }])
searchLocal: jest.fn(() => []),
searchRemote: jest.fn(() => [{ _id: 'u1', username: 'john', name: 'John' }])
}));

jest.mock('../../lib/services/restApi', () => ({
Expand Down Expand Up @@ -458,7 +459,7 @@ describe('MessageComposer', () => {

test('select @ user inserts mention and sends, autocomplete hides', async () => {
const onSendMessage = jest.fn();
(search as unknown as jest.Mock).mockImplementationOnce(() => [{ _id: 'u1', username: 'john', name: 'John' }]);
(searchRemote as unknown as jest.Mock).mockImplementationOnce(() => [{ _id: 'u1', username: 'john', name: 'John' }]);
render(<Render context={{ onSendMessage }} />);

await fireEvent(screen.getByTestId('message-composer-input'), 'focus');
Expand Down Expand Up @@ -543,7 +544,7 @@ describe('MessageComposer', () => {

test('select # room inserts channel and sends, autocomplete hides', async () => {
const onSendMessage = jest.fn();
(search as unknown as jest.Mock).mockImplementationOnce(() => [{ rid: 'r1', name: 'general', t: 'c' }]);
(searchRemote as unknown as jest.Mock).mockImplementationOnce(() => [{ rid: 'r1', name: 'general', t: 'c' }]);
render(<Render context={{ onSendMessage }} />);

await fireEvent(screen.getByTestId('message-composer-input'), 'focus');
Expand Down
95 changes: 62 additions & 33 deletions app/containers/MessageComposer/hooks/useAutocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
type TAutocompleteItem,
type TAutocompleteType
} from '../interfaces';
import { search } from '../../../lib/methods/search';
import { searchLocal, searchRemote, type TSearch } from '../../../lib/methods/search';
import { sanitizeLikeString } from '../../../lib/database/utils';
import database from '../../../lib/database';
import { emojis } from '../../../lib/constants/emojis';
Expand Down Expand Up @@ -56,6 +56,46 @@ export const useAutocomplete = ({
const [mentionAll, mentionHere] = usePermissions(['mention-all', 'mention-here']);

useEffect(() => {
// Guards against an older (slower) search overwriting the results of a newer one.
// The cleanup runs on every re-run (any type change), so a stale request can never call setItems.
let ignore = false;

const parseUserRoom = (res: TSearch[]): IAutocompleteUserRoom[] => {
const parsedRes: IAutocompleteUserRoom[] = res
// TODO: need to refactor search to have a more predictable return type
.map((item: any) => ({
id: type === '@' ? item._id : item.rid,
title: item.fname || item.name || item.username,
subtitle: item.username || item.name,
outside: item.outside,
t: item.t ?? 'd',
status: item.status,
teamMain: item.teamMain,
type
})) as IAutocompleteUserRoom[];
if (type === '@') {
if (mentionAll && 'all'.includes(text.toLocaleLowerCase())) {
parsedRes.push({
id: 'all',
title: 'all',
subtitle: I18n.t('Notify_all_in_this_room'),
type,
t: 'd'
});
}
if (mentionHere && 'here'.includes(text.toLocaleLowerCase())) {
parsedRes.push({
id: 'here',
title: 'here',
subtitle: I18n.t('Notify_active_in_this_room'),
type,
t: 'd'
});
}
}
return parsedRes;
};

const getAutocomplete = async () => {
try {
if (!rid || !type) {
Expand All @@ -76,39 +116,21 @@ export const useAutocomplete = ({
setItems(items);

if (type === '@' || type === '#') {
const res = await search({ text, filterRooms: type === '#', filterUsers: type === '@', rid });
const parsedRes: IAutocompleteUserRoom[] = res
// TODO: need to refactor search to have a more predictable return type
.map((item: any) => ({
id: type === '@' ? item._id : item.rid,
title: item.fname || item.name || item.username,
subtitle: item.username || item.name,
outside: item.outside,
t: item.t ?? 'd',
status: item.status,
teamMain: item.teamMain,
type
})) as IAutocompleteUserRoom[];
if (type === '@') {
if (mentionAll && 'all'.includes(text.toLocaleLowerCase())) {
parsedRes.push({
id: 'all',
title: 'all',
subtitle: I18n.t('Notify_all_in_this_room'),
type,
t: 'd'
});
}
if (mentionHere && 'here'.includes(text.toLocaleLowerCase())) {
parsedRes.push({
id: 'here',
title: 'here',
subtitle: I18n.t('Notify_active_in_this_room'),
type,
t: 'd'
});
}
const searchParams = { text, filterRooms: type === '#', filterUsers: type === '@', rid };

// Paint local results immediately while the backend request is still in flight
const localData = await searchLocal(searchParams);
if (ignore) return;
const parsedLocal = parseUserRoom(localData);
setItems(parsedLocal);
if (parsedLocal.length > 0) {
updateAutocompleteVisible(true);
accessibilityFocusOnInput();
}

const res = await searchRemote({ ...searchParams, localData });
if (ignore) return;
const parsedRes = parseUserRoom(res);
setItems(parsedRes);
Comment thread
OtavioStasiak marked this conversation as resolved.
if (parsedRes.length > 0) {
updateAutocompleteVisible(true);
Expand All @@ -117,6 +139,7 @@ export const useAutocomplete = ({
}
if (type === ':') {
const customEmojis = await getCustomEmojis(text);
if (ignore) return;
const filteredStandardEmojis = emojis.filter(emoji => emoji.indexOf(text) !== -1).slice(0, MENTIONS_COUNT_TO_DISPLAY);
let mergedEmojis: IAutocompleteEmoji[] = customEmojis.map(emoji => ({
id: emoji.name,
Expand Down Expand Up @@ -148,6 +171,7 @@ export const useAutocomplete = ({
subtitle: command.description,
type
}));
if (ignore) return;
setItems(commands);

if (commands.length > 0) {
Expand All @@ -162,6 +186,7 @@ export const useAutocomplete = ({
return;
}
const response = await getCommandPreview(text, rid, commandParams);
if (ignore) return;
if (response.success) {
const previewItems = (response.preview?.items || []).map(item => ({
id: item.id,
Expand All @@ -179,6 +204,7 @@ export const useAutocomplete = ({
}
if (type === '!') {
const res = await getListCannedResponse({ text });
if (ignore) return;
if (res.success) {
if (res.cannedResponses.length === 0) {
setItems([
Expand Down Expand Up @@ -212,6 +238,9 @@ export const useAutocomplete = ({
}
};
getAutocomplete();
return () => {
ignore = true;
};
}, [text, type, rid, commandParams]);
return items;
};
Loading
Loading