11import mergeWith from 'lodash.mergewith' ;
22import type {
3+ Middleware ,
34 SearchSourceOptions ,
45 SearchSourceType ,
6+ TextComposerMiddlewareExecutorState ,
57 TextComposerMiddlewareOptions ,
6- TextComposerMiddlewareParams ,
78 TextComposerSuggestion ,
89} from 'stream-chat' ;
910import {
@@ -18,6 +19,9 @@ import type {
1819 EmojiSearchIndexResult ,
1920} from '../../../components/MessageInput' ;
2021
22+ export type EmojiSuggestion < T extends EmojiSearchIndexResult = EmojiSearchIndexResult > =
23+ TextComposerSuggestion < T > ;
24+
2125class EmojiSearchSource <
2226 T extends TextComposerSuggestion < EmojiSearchIndexResult > ,
2327> extends BaseSearchSource < T > {
@@ -67,6 +71,12 @@ class EmojiSearchSource<
6771
6872const DEFAULT_OPTIONS : TextComposerMiddlewareOptions = { minChars : 1 , trigger : ':' } ;
6973
74+ export type EmojiMiddleware < T extends EmojiSearchIndexResult = EmojiSearchIndexResult > =
75+ Middleware <
76+ TextComposerMiddlewareExecutorState < EmojiSuggestion < T > > ,
77+ 'onChange' | 'onSuggestionItemSelect'
78+ > ;
79+
7080/**
7181 * TextComposer middleware for mentions
7282 * Usage:
@@ -84,102 +94,90 @@ const DEFAULT_OPTIONS: TextComposerMiddlewareOptions = { minChars: 1, trigger: '
8494 * }} options
8595 * @returns
8696 */
87- export const createTextComposerEmojiMiddleware = <
88- T extends EmojiSearchIndexResult = EmojiSearchIndexResult ,
89- > (
97+ export const createTextComposerEmojiMiddleware = (
9098 emojiSearchIndex : EmojiSearchIndex ,
9199 options ?: Partial < TextComposerMiddlewareOptions > ,
92- ) => {
100+ ) : EmojiMiddleware => {
93101 const finalOptions = mergeWith ( DEFAULT_OPTIONS , options ?? { } ) ;
94102 const emojiSearchSource = new EmojiSearchSource ( emojiSearchIndex ) ;
95103 emojiSearchSource . activate ( ) ;
96104
97105 return {
98106 id : 'stream-io/emoji-middleware' ,
99- onChange : async ( { input, nextHandler } : TextComposerMiddlewareParams < T > ) => {
100- const { state } = input ;
101- if ( ! state . selection ) return nextHandler ( input ) ;
102-
103- const triggerWithToken = getTriggerCharWithToken ( {
104- acceptTrailingSpaces : false ,
105- text : state . text . slice ( 0 , state . selection . end ) ,
106- trigger : finalOptions . trigger ,
107- } ) ;
108-
109- const triggerWasRemoved =
110- ! triggerWithToken || triggerWithToken . length < finalOptions . minChars ;
111-
112- if ( triggerWasRemoved ) {
113- const hasSuggestionsForTrigger =
114- input . state . suggestions ?. trigger === finalOptions . trigger ;
115- const newInput = { ...input } ;
116- if ( hasSuggestionsForTrigger && newInput . state . suggestions ) {
117- delete newInput . state . suggestions ;
107+ // eslint-disable-next-line sort-keys
108+ handlers : {
109+ onChange : async ( { complete, forward, next, state } ) => {
110+ if ( ! state . selection ) return forward ( ) ;
111+
112+ const triggerWithToken = getTriggerCharWithToken ( {
113+ acceptTrailingSpaces : false ,
114+ text : state . text . slice ( 0 , state . selection . end ) ,
115+ trigger : finalOptions . trigger ,
116+ } ) ;
117+
118+ const triggerWasRemoved =
119+ ! triggerWithToken || triggerWithToken . length < finalOptions . minChars ;
120+
121+ if ( triggerWasRemoved ) {
122+ const hasSuggestionsForTrigger =
123+ state . suggestions ?. trigger === finalOptions . trigger ;
124+ const newState = { ...state } ;
125+ if ( hasSuggestionsForTrigger && newState . suggestions ) {
126+ delete newState . suggestions ;
127+ }
128+ return next ( newState ) ;
118129 }
119- return nextHandler ( newInput ) ;
120- }
121130
122- const newSearchTriggerred =
123- triggerWithToken && triggerWithToken === finalOptions . trigger ;
131+ const newSearchTriggerred =
132+ triggerWithToken && triggerWithToken === finalOptions . trigger ;
124133
125- if ( newSearchTriggerred ) {
126- emojiSearchSource . resetStateAndActivate ( ) ;
127- }
134+ if ( newSearchTriggerred ) {
135+ emojiSearchSource . resetStateAndActivate ( ) ;
136+ }
128137
129- const textWithReplacedWord = await replaceWordWithEntity ( {
130- caretPosition : state . selection . end ,
131- getEntityString : async ( word : string ) => {
132- const { items } = await emojiSearchSource . query ( word ) ;
138+ const textWithReplacedWord = await replaceWordWithEntity ( {
139+ caretPosition : state . selection . end ,
140+ getEntityString : async ( word : string ) => {
141+ const { items } = await emojiSearchSource . query ( word ) ;
133142
134- const emoji = items
135- . filter ( Boolean )
136- . slice ( 0 , 10 )
137- . find ( ( { emoticons } ) => ! ! emoticons ?. includes ( word ) ) ;
143+ const emoji = items
144+ . filter ( Boolean )
145+ . slice ( 0 , 10 )
146+ . find ( ( { emoticons } ) => ! ! emoticons ?. includes ( word ) ) ;
138147
139- if ( ! emoji ) return null ;
148+ if ( ! emoji ) return null ;
140149
141- const [ firstSkin ] = emoji . skins ?? [ ] ;
150+ const [ firstSkin ] = emoji . skins ?? [ ] ;
142151
143- return emoji . native ?? firstSkin . native ;
144- } ,
145- text : state . text ,
146- } ) ;
152+ return emoji . native ?? firstSkin . native ;
153+ } ,
154+ text : state . text ,
155+ } ) ;
147156
148- if ( textWithReplacedWord !== state . text ) {
149- return {
150- state : {
157+ if ( textWithReplacedWord !== state . text ) {
158+ return complete ( {
151159 ...state ,
152160 suggestions : undefined , // to prevent the TextComposerMiddlewareExecutor to run the first page query
153161 text : textWithReplacedWord ,
154- } ,
155- stop : true , // Stop other middleware from processing '@' character
156- } ;
157- }
162+ } ) ;
163+ }
158164
159- return {
160- state : {
165+ return complete ( {
161166 ...state ,
162167 suggestions : {
163168 query : triggerWithToken . slice ( 1 ) ,
164169 searchSource : emojiSearchSource ,
165170 trigger : finalOptions . trigger ,
166171 } ,
167- } ,
168- stop : true , // Stop other middleware from processing '@' character
169- } ;
170- } ,
171- onSuggestionItemSelect : ( {
172- input,
173- nextHandler,
174- selectedSuggestion,
175- } : TextComposerMiddlewareParams < T > ) => {
176- const { state } = input ;
177- if ( ! selectedSuggestion || state . suggestions ?. trigger !== finalOptions . trigger )
178- return nextHandler ( input ) ;
179-
180- emojiSearchSource . resetStateAndActivate ( ) ;
181- return Promise . resolve ( {
182- state : {
172+ } ) ;
173+ } ,
174+ onSuggestionItemSelect : ( { complete, forward, state } ) => {
175+ const { selectedSuggestion } = state . change ?? { } ;
176+ if ( ! selectedSuggestion || state . suggestions ?. trigger !== finalOptions . trigger )
177+ return forward ( ) ;
178+
179+ emojiSearchSource . resetStateAndActivate ( ) ;
180+ return complete ( {
183181 ...state ,
184182 ...insertItemWithTrigger ( {
185183 insertText : `${ 'native' in selectedSuggestion ? selectedSuggestion . native : '' } ` ,
@@ -188,8 +186,8 @@ export const createTextComposerEmojiMiddleware = <
188186 trigger : finalOptions . trigger ,
189187 } ) ,
190188 suggestions : undefined , // Clear suggestions after selection
191- } ,
192- } ) ;
189+ } ) ;
190+ } ,
193191 } ,
194192 } ;
195193} ;
0 commit comments