77 type TAutocompleteItem ,
88 type TAutocompleteType
99} from '../interfaces' ;
10- import { search } from '../../../lib/methods/search' ;
10+ import { searchLocal , searchRemote , type TSearch } from '../../../lib/methods/search' ;
1111import { sanitizeLikeString } from '../../../lib/database/utils' ;
1212import database from '../../../lib/database' ;
1313import { emojis } from '../../../lib/constants/emojis' ;
@@ -56,6 +56,46 @@ export const useAutocomplete = ({
5656 const [ mentionAll , mentionHere ] = usePermissions ( [ 'mention-all' , 'mention-here' ] ) ;
5757
5858 useEffect ( ( ) => {
59+ // Guards against an older (slower) search overwriting the results of a newer one.
60+ // The cleanup runs on every re-run (any type change), so a stale request can never call setItems.
61+ let ignore = false ;
62+
63+ const parseUserRoom = ( res : TSearch [ ] ) : IAutocompleteUserRoom [ ] => {
64+ const parsedRes : IAutocompleteUserRoom [ ] = res
65+ // TODO: need to refactor search to have a more predictable return type
66+ . map ( ( item : any ) => ( {
67+ id : type === '@' ? item . _id : item . rid ,
68+ title : item . fname || item . name || item . username ,
69+ subtitle : item . username || item . name ,
70+ outside : item . outside ,
71+ t : item . t ?? 'd' ,
72+ status : item . status ,
73+ teamMain : item . teamMain ,
74+ type
75+ } ) ) as IAutocompleteUserRoom [ ] ;
76+ if ( type === '@' ) {
77+ if ( mentionAll && 'all' . includes ( text . toLocaleLowerCase ( ) ) ) {
78+ parsedRes . push ( {
79+ id : 'all' ,
80+ title : 'all' ,
81+ subtitle : I18n . t ( 'Notify_all_in_this_room' ) ,
82+ type,
83+ t : 'd'
84+ } ) ;
85+ }
86+ if ( mentionHere && 'here' . includes ( text . toLocaleLowerCase ( ) ) ) {
87+ parsedRes . push ( {
88+ id : 'here' ,
89+ title : 'here' ,
90+ subtitle : I18n . t ( 'Notify_active_in_this_room' ) ,
91+ type,
92+ t : 'd'
93+ } ) ;
94+ }
95+ }
96+ return parsedRes ;
97+ } ;
98+
5999 const getAutocomplete = async ( ) => {
60100 try {
61101 if ( ! rid || ! type ) {
@@ -76,39 +116,23 @@ export const useAutocomplete = ({
76116 setItems ( items ) ;
77117
78118 if ( type === '@' || type === '#' ) {
79- const res = await search ( { text, filterRooms : type === '#' , filterUsers : type === '@' , rid } ) ;
80- const parsedRes : IAutocompleteUserRoom [ ] = res
81- // TODO: need to refactor search to have a more predictable return type
82- . map ( ( item : any ) => ( {
83- id : type === '@' ? item . _id : item . rid ,
84- title : item . fname || item . name || item . username ,
85- subtitle : item . username || item . name ,
86- outside : item . outside ,
87- t : item . t ?? 'd' ,
88- status : item . status ,
89- teamMain : item . teamMain ,
90- type
91- } ) ) as IAutocompleteUserRoom [ ] ;
92- if ( type === '@' ) {
93- if ( mentionAll && 'all' . includes ( text . toLocaleLowerCase ( ) ) ) {
94- parsedRes . push ( {
95- id : 'all' ,
96- title : 'all' ,
97- subtitle : I18n . t ( 'Notify_all_in_this_room' ) ,
98- type,
99- t : 'd'
100- } ) ;
101- }
102- if ( mentionHere && 'here' . includes ( text . toLocaleLowerCase ( ) ) ) {
103- parsedRes . push ( {
104- id : 'here' ,
105- title : 'here' ,
106- subtitle : I18n . t ( 'Notify_active_in_this_room' ) ,
107- type,
108- t : 'd'
109- } ) ;
110- }
119+ const searchParams = { text, filterRooms : type === '#' , filterUsers : type === '@' , rid } ;
120+
121+ // Paint local results immediately, keeping a loading row at the bottom while the
122+ // backend request is still in flight
123+ const localData = await searchLocal ( searchParams ) ;
124+ if ( ignore ) return ;
125+ const parsedLocal = parseUserRoom ( localData ) ;
126+ const loadingItem : TAutocompleteItem = { id : 'loading' , type : 'loading' } ;
127+ setItems ( [ ...parsedLocal , loadingItem ] ) ;
128+ if ( parsedLocal . length > 0 ) {
129+ updateAutocompleteVisible ( true ) ;
130+ accessibilityFocusOnInput ( ) ;
111131 }
132+
133+ const res = await searchRemote ( { ...searchParams , localData } ) ;
134+ if ( ignore ) return ;
135+ const parsedRes = parseUserRoom ( res ) ;
112136 setItems ( parsedRes ) ;
113137 if ( parsedRes . length > 0 ) {
114138 updateAutocompleteVisible ( true ) ;
@@ -117,6 +141,7 @@ export const useAutocomplete = ({
117141 }
118142 if ( type === ':' ) {
119143 const customEmojis = await getCustomEmojis ( text ) ;
144+ if ( ignore ) return ;
120145 const filteredStandardEmojis = emojis . filter ( emoji => emoji . indexOf ( text ) !== - 1 ) . slice ( 0 , MENTIONS_COUNT_TO_DISPLAY ) ;
121146 let mergedEmojis : IAutocompleteEmoji [ ] = customEmojis . map ( emoji => ( {
122147 id : emoji . name ,
@@ -148,6 +173,7 @@ export const useAutocomplete = ({
148173 subtitle : command . description ,
149174 type
150175 } ) ) ;
176+ if ( ignore ) return ;
151177 setItems ( commands ) ;
152178
153179 if ( commands . length > 0 ) {
@@ -162,6 +188,7 @@ export const useAutocomplete = ({
162188 return ;
163189 }
164190 const response = await getCommandPreview ( text , rid , commandParams ) ;
191+ if ( ignore ) return ;
165192 if ( response . success ) {
166193 const previewItems = ( response . preview ?. items || [ ] ) . map ( item => ( {
167194 id : item . id ,
@@ -179,6 +206,7 @@ export const useAutocomplete = ({
179206 }
180207 if ( type === '!' ) {
181208 const res = await getListCannedResponse ( { text } ) ;
209+ if ( ignore ) return ;
182210 if ( res . success ) {
183211 if ( res . cannedResponses . length === 0 ) {
184212 setItems ( [
@@ -212,6 +240,9 @@ export const useAutocomplete = ({
212240 }
213241 } ;
214242 getAutocomplete ( ) ;
243+ return ( ) => {
244+ ignore = true ;
245+ } ;
215246 } , [ text , type , rid , commandParams ] ) ;
216247 return items ;
217248} ;
0 commit comments