11import mergeWith from 'lodash.mergewith' ;
2-
32import type {
3+ Middleware ,
44 SearchSourceOptions ,
55 SearchSourceType ,
6+ TextComposerMiddlewareExecutorState ,
67 TextComposerMiddlewareOptions ,
7- TextComposerMiddlewareParams ,
88 TextComposerSuggestion ,
99} from 'stream-chat' ;
1010import {
@@ -14,19 +14,11 @@ import {
1414 insertItemWithTrigger ,
1515 replaceWordWithEntity ,
1616} from 'stream-chat' ;
17- import { EmojiSearchIndex } from 'stream-chat-react-native' ;
18-
19- export type EmojiSearchIndexResult = {
20- id : string ;
21- name : string ;
22- skins : Array < { native : string } > ;
23- emoticons ?: Array < string > ;
24- native ?: string ;
25- } ;
17+ import { Emoji , EmojiSearchIndex } from 'stream-chat-react-native' ;
18+
19+ export type EmojiSuggestion < T extends Emoji = Emoji > = TextComposerSuggestion < T > ;
2620
27- class EmojiSearchSource <
28- T extends TextComposerSuggestion < EmojiSearchIndexResult > ,
29- > extends BaseSearchSource < T > {
21+ class EmojiSearchSource < T extends TextComposerSuggestion < Emoji > > extends BaseSearchSource < T > {
3022 readonly type : SearchSourceType = 'emoji' ;
3123 private emojiSearchIndex : EmojiSearchIndex ;
3224
@@ -41,6 +33,7 @@ class EmojiSearchSource<
4133 }
4234 const emojis = ( await this . emojiSearchIndex . search ( searchQuery ) ) ?? [ ] ;
4335
36+ // emojiIndex.search sometimes returns undefined values, so filter those out first
4437 return {
4538 items : emojis
4639 . filter ( Boolean )
@@ -72,6 +65,11 @@ class EmojiSearchSource<
7265
7366const DEFAULT_OPTIONS : TextComposerMiddlewareOptions = { minChars : 1 , trigger : ':' } ;
7467
68+ export type EmojiMiddleware < T extends Emoji = Emoji > = Middleware <
69+ TextComposerMiddlewareExecutorState < EmojiSuggestion < T > > ,
70+ 'onChange' | 'onSuggestionItemSelect'
71+ > ;
72+
7573/**
7674 * TextComposer middleware for mentions
7775 * Usage:
@@ -89,105 +87,91 @@ const DEFAULT_OPTIONS: TextComposerMiddlewareOptions = { minChars: 1, trigger: '
8987 * }} options
9088 * @returns
9189 */
92- export const createTextComposerEmojiMiddleware = <
93- T extends EmojiSearchIndexResult = EmojiSearchIndexResult ,
94- > (
90+ export const createTextComposerEmojiMiddleware = (
9591 emojiSearchIndex : EmojiSearchIndex ,
9692 options ?: Partial < TextComposerMiddlewareOptions > ,
97- ) => {
93+ ) : EmojiMiddleware => {
9894 const finalOptions = mergeWith ( DEFAULT_OPTIONS , options ?? { } ) ;
9995 const emojiSearchSource = new EmojiSearchSource ( emojiSearchIndex ) ;
10096 emojiSearchSource . activate ( ) ;
10197
10298 return {
103- id : 'stream-io/emoji-middleware' ,
104- onChange : async ( { input, nextHandler } : TextComposerMiddlewareParams < T > ) => {
105- const { state } = input ;
106- if ( ! state . selection ) {
107- return nextHandler ( input ) ;
108- }
109-
110- const triggerWithToken = getTriggerCharWithToken ( {
111- acceptTrailingSpaces : false ,
112- text : state . text . slice ( 0 , state . selection . end ) ,
113- trigger : finalOptions . trigger ,
114- } ) ;
115-
116- const triggerWasRemoved =
117- ! triggerWithToken || triggerWithToken . length < finalOptions . minChars ;
118-
119- if ( triggerWasRemoved ) {
120- const hasSuggestionsForTrigger = input . state . suggestions ?. trigger === finalOptions . trigger ;
121- const newInput = { ...input } ;
122- if ( hasSuggestionsForTrigger && newInput . state . suggestions ) {
123- delete newInput . state . suggestions ;
99+ handlers : {
100+ onChange : async ( { complete, forward, next, state } ) => {
101+ if ( ! state . selection ) {
102+ return forward ( ) ;
124103 }
125- return nextHandler ( newInput ) ;
126- }
127104
128- const newSearchTriggerred = triggerWithToken && triggerWithToken === finalOptions . trigger ;
105+ const triggerWithToken = getTriggerCharWithToken ( {
106+ acceptTrailingSpaces : false ,
107+ text : state . text . slice ( 0 , state . selection . end ) ,
108+ trigger : finalOptions . trigger ,
109+ } ) ;
129110
130- if ( newSearchTriggerred ) {
131- emojiSearchSource . resetStateAndActivate ( ) ;
132- }
111+ const triggerWasRemoved =
112+ ! triggerWithToken || triggerWithToken . length < finalOptions . minChars ;
133113
134- const textWithReplacedWord = await replaceWordWithEntity ( {
135- caretPosition : state . selection . end ,
136- getEntityString : async ( word : string ) => {
137- const { items } = await emojiSearchSource . query ( word ) ;
114+ if ( triggerWasRemoved ) {
115+ const hasSuggestionsForTrigger = state . suggestions ?. trigger === finalOptions . trigger ;
116+ const newState = { ...state } ;
117+ if ( hasSuggestionsForTrigger && newState . suggestions ) {
118+ delete newState . suggestions ;
119+ }
120+ return next ( newState ) ;
121+ }
138122
139- const emoji = items
140- . filter ( Boolean )
141- . slice ( 0 , 10 )
142- . find ( ( { emoticons } ) => ! ! emoticons ?. includes ( word ) ) ;
123+ const newSearchTriggerred = triggerWithToken && triggerWithToken === finalOptions . trigger ;
143124
144- if ( ! emoji ) {
145- return null ;
146- }
125+ if ( newSearchTriggerred ) {
126+ emojiSearchSource . resetStateAndActivate ( ) ;
127+ }
128+
129+ const textWithReplacedWord = await replaceWordWithEntity ( {
130+ caretPosition : state . selection . end ,
131+ getEntityString : async ( word : string ) => {
132+ const { items } = await emojiSearchSource . query ( word ) ;
147133
148- const [ firstSkin ] = emoji . skins ?? [ ] ;
134+ const emoji = items
135+ . filter ( Boolean )
136+ . slice ( 0 , 10 )
137+ . find ( ( { emoticons } ) => ! ! emoticons ?. includes ( word ) ) ;
149138
150- return emoji . native ?? firstSkin . native ;
151- } ,
152- text : state . text ,
153- } ) ;
139+ if ( ! emoji ) {
140+ return null ;
141+ }
154142
155- if ( textWithReplacedWord !== state . text ) {
156- return {
157- state : {
143+ const [ firstSkin ] = emoji . skins ?? [ ] ;
144+
145+ return emoji . native ?? firstSkin . native ;
146+ } ,
147+ text : state . text ,
148+ } ) ;
149+
150+ if ( textWithReplacedWord !== state . text ) {
151+ return complete ( {
158152 ...state ,
159153 suggestions : undefined , // to prevent the TextComposerMiddlewareExecutor to run the first page query
160154 text : textWithReplacedWord ,
161- } ,
162- stop : true , // Stop other middleware from processing '@' character
163- } ;
164- }
155+ } ) ;
156+ }
165157
166- return {
167- state : {
158+ return complete ( {
168159 ...state ,
169160 suggestions : {
170161 query : triggerWithToken . slice ( 1 ) ,
171162 searchSource : emojiSearchSource ,
172163 trigger : finalOptions . trigger ,
173164 } ,
174- } ,
175- stop : true , // Stop other middleware from processing '@' character
176- } ;
177- } ,
178- onSuggestionItemSelect : ( {
179- input,
180- nextHandler,
181- selectedSuggestion,
182- } : TextComposerMiddlewareParams < T > ) => {
183- const { state } = input ;
184- if ( ! selectedSuggestion || state . suggestions ?. trigger !== finalOptions . trigger ) {
185- return nextHandler ( input ) ;
186- }
187-
188- emojiSearchSource . resetStateAndActivate ( ) ;
189- return Promise . resolve ( {
190- state : {
165+ } ) ;
166+ } ,
167+ onSuggestionItemSelect : ( { complete, forward, state } ) => {
168+ const { selectedSuggestion } = state . change ?? { } ;
169+ if ( ! selectedSuggestion || state . suggestions ?. trigger !== finalOptions . trigger ) {
170+ return forward ( ) ;
171+ }
172+
173+ emojiSearchSource . resetStateAndActivate ( ) ;
174+ return complete ( {
191175 ...state ,
192176 ...insertItemWithTrigger ( {
193177 insertText : `${ 'native' in selectedSuggestion ? selectedSuggestion . native : '' } ` ,
@@ -196,8 +180,9 @@ export const createTextComposerEmojiMiddleware = <
196180 trigger : finalOptions . trigger ,
197181 } ) ,
198182 suggestions : undefined , // Clear suggestions after selection
199- } ,
200- } ) ;
183+ } ) ;
184+ } ,
201185 } ,
186+ id : 'stream-io/emoji-middleware' ,
202187 } ;
203188} ;
0 commit comments