3
3
import invariant from 'invariant' ;
4
4
import _sum from 'lodash/fp/sum' ;
5
5
import * as React from 'react' ;
6
- import { View , FlatList , Platform , TextInput } from 'react-native' ;
6
+ import {
7
+ View ,
8
+ FlatList ,
9
+ Platform ,
10
+ TextInput ,
11
+ TouchableWithoutFeedback ,
12
+ } from 'react-native' ;
7
13
import { FloatingAction } from 'react-native-floating-action' ;
8
14
import IonIcon from 'react-native-vector-icons/Ionicons' ;
9
15
import { createSelector } from 'reselect' ;
@@ -84,7 +90,9 @@ type Props = {|
84
90
// async functions that hit server APIs
85
91
+ searchUsers : ( usernamePrefix : string ) => Promise < UserSearchResult > ,
86
92
| } ;
93
+ type SearchStatus = 'inactive' | 'activating' | 'active' ;
87
94
type State = { |
95
+ + searchStatus : SearchStatus ,
88
96
+ searchText : string ,
89
97
+ threadsSearchResults : Set < string > ,
90
98
+ usersSearchResults : $ReadOnlyArray < GlobalAccountUserInfo > ,
@@ -93,6 +101,7 @@ type State = {|
93
101
type PropsAndState = { | ...Props , ...State | } ;
94
102
class ChatThreadList extends React . PureComponent < Props , State > {
95
103
state : State = {
104
+ searchStatus : 'inactive' ,
96
105
searchText : '' ,
97
106
threadsSearchResults : new Set ( ) ,
98
107
usersSearchResults : [ ] ,
@@ -126,6 +135,18 @@ class ChatThreadList extends React.PureComponent<Props, State> {
126
135
tabNavigation . removeListener ( 'tabPress' , this . onTabPress ) ;
127
136
}
128
137
138
+ componentDidUpdate ( prevProps : Props , prevState : State ) {
139
+ const { flatList } = this ;
140
+ if ( ! flatList ) {
141
+ return ;
142
+ }
143
+ const { searchStatus } = this . state ;
144
+ const prevSearchStatus = prevState . searchStatus ;
145
+ if ( searchStatus === 'activating' && prevSearchStatus === 'inactive' ) {
146
+ flatList . scrollToOffset ( { offset : 0 , animated : true } ) ;
147
+ }
148
+ }
149
+
129
150
onTabPress = ( ) = > {
130
151
if ( ! this . props . navigation . isFocused ( ) ) {
131
152
return ;
@@ -137,17 +158,53 @@ class ChatThreadList extends React.PureComponent<Props, State> {
137
158
}
138
159
} ;
139
160
140
- renderItem = ( row : { item : Item } ) = > {
141
- const item = row . item ;
142
- if ( item . type === 'search' ) {
143
- return (
161
+ onSearchFocus = ( ) => {
162
+ if ( this . state . searchStatus !== 'inactive' ) {
163
+ return ;
164
+ }
165
+ if ( this . scrollPos === 0 ) {
166
+ this . setState ( { searchStatus : 'active' } ) ;
167
+ } else {
168
+ this . setState ( { searchStatus : 'activating' } ) ;
169
+ }
170
+ } ;
171
+
172
+ onSearchBlur = ( ) = > {
173
+ if ( this . state . searchStatus !== 'active' ) {
174
+ return ;
175
+ }
176
+ const { flatList } = this ;
177
+ flatList && flatList . scrollToOffset ( { offset : 0 , animated : false } ) ;
178
+ this . setState ( { searchStatus : 'inactive' } ) ;
179
+ } ;
180
+
181
+ renderSearch ( additionalProps ?: $Shape < React . ElementConfig < typeof Search >> ) {
182
+ return (
183
+ < View style = { this . props . styles . searchContainer } >
144
184
< Search
145
185
searchText = { this . state . searchText }
146
186
onChangeText = { this . onChangeSearchText }
147
187
containerStyle = { this . props . styles . search }
188
+ onBlur = { this . onSearchBlur }
148
189
placeholder = "Search threads"
149
190
ref = { this . searchInputRef }
191
+ { ...additionalProps }
150
192
/>
193
+ </ View >
194
+ ) ;
195
+ }
196
+
197
+ searchInputRef = ( searchInput : ?React . ElementRef < typeof TextInput > ) => {
198
+ this. searchInput = searchInput ;
199
+ } ;
200
+
201
+ renderItem = ( row : { item : Item } ) => {
202
+ const item = row . item ;
203
+ if ( item . type === 'search' ) {
204
+ return (
205
+ < TouchableWithoutFeedback onPress = { this . onSearchFocus } >
206
+ { this . renderSearch ( { active : false } ) }
207
+ </ TouchableWithoutFeedback >
151
208
) ;
152
209
}
153
210
if ( item . type === 'empty' ) {
@@ -165,10 +222,6 @@ class ChatThreadList extends React.PureComponent<Props, State> {
165
222
) ;
166
223
} ;
167
224
168
- searchInputRef = ( searchInput : ?React . ElementRef < typeof TextInput > ) => {
169
- this . searchInput = searchInput ;
170
- } ;
171
-
172
225
static keyExtractor ( item : Item ) {
173
226
if ( item . type === 'chatThreadItem' ) {
174
227
return item . threadInfo . id ;
@@ -212,12 +265,14 @@ class ChatThreadList extends React.PureComponent<Props, State> {
212
265
213
266
listDataSelector = createSelector (
214
267
( propsAndState : PropsAndState ) => propsAndState . chatListData ,
268
+ ( propsAndState : PropsAndState ) => propsAndState . searchStatus ,
215
269
( propsAndState : PropsAndState ) => propsAndState . searchText ,
216
270
( propsAndState : PropsAndState ) => propsAndState . threadsSearchResults ,
217
271
( propsAndState : PropsAndState ) => propsAndState . emptyItem ,
218
272
( propsAndState : PropsAndState ) => propsAndState . usersSearchResults ,
219
273
(
220
274
reduxChatListData : $ReadOnlyArray < ChatThreadItem > ,
275
+ searchStatus : SearchStatus ,
221
276
searchText : string ,
222
277
threadsSearchResults : Set < string > ,
223
278
emptyItem ?: React . ComponentType < { || } > ,
@@ -264,7 +319,11 @@ class ChatThreadList extends React.PureComponent<Props, State> {
264
319
chatItems . push ( { type : 'empty' , emptyItem } ) ;
265
320
}
266
321
267
- return [{ type : 'search' , searchText } , ...chatItems];
322
+ if ( searchStatus === 'inactive' || searchStatus === 'activating' ) {
323
+ chatItems . unshift ( { type : 'search' , searchText } ) ;
324
+ }
325
+
326
+ return chatItems ;
268
327
} ,
269
328
) ;
270
329
@@ -273,7 +332,7 @@ class ChatThreadList extends React.PureComponent<Props, State> {
273
332
}
274
333
275
334
render ( ) {
276
- let floatingAction = null ;
335
+ let floatingAction ;
277
336
if ( Platform . OS === 'android' ) {
278
337
floatingAction = (
279
338
< FloatingAction
@@ -284,10 +343,18 @@ class ChatThreadList extends React.PureComponent<Props, State> {
284
343
/>
285
344
) ;
286
345
}
346
+ let fixedSearch ;
347
+ const { searchStatus } = this . state ;
348
+ if ( searchStatus === 'active' ) {
349
+ fixedSearch = this . renderSearch ( { autoFocus : true } ) ;
350
+ }
351
+ const scrollEnabled =
352
+ searchStatus === 'inactive' || searchStatus === 'active' ;
287
353
// this.props.viewerID is in extraData since it's used by MessagePreview
288
354
// within ChatThreadListItem
289
355
return (
290
356
< View style = { this . props . styles . container } >
357
+ { fixedSearch }
291
358
< FlatList
292
359
data = { this . listData }
293
360
renderItem = { this . renderItem }
@@ -301,6 +368,8 @@ class ChatThreadList extends React.PureComponent<Props, State> {
301
368
onScroll = { this . onScroll }
302
369
style = { this . props . styles . flatList }
303
370
indicatorStyle = { this . props . indicatorStyle }
371
+ scrollEnabled = { scrollEnabled }
372
+ removeClippedSubviews = { true }
304
373
ref = { this . flatListRef }
305
374
/>
306
375
{ floatingAction }
@@ -313,7 +382,14 @@ class ChatThreadList extends React.PureComponent<Props, State> {
313
382
} ;
314
383
315
384
onScroll = ( event : { + nativeEvent : { + contentOffset : { + y : number } } } ) => {
385
+ const oldScrollPos = this . scrollPos ;
316
386
this . scrollPos = event . nativeEvent . contentOffset . y ;
387
+ if ( this . scrollPos !== 0 || oldScrollPos === 0 ) {
388
+ return ;
389
+ }
390
+ if ( this . state . searchStatus === 'activating' ) {
391
+ this . setState ( { searchStatus : 'active' } ) ;
392
+ }
317
393
} ;
318
394
319
395
async searchUsers ( usernamePrefix : string ) {
@@ -389,6 +465,9 @@ const unboundStyles = {
389
465
container : {
390
466
flex : 1 ,
391
467
} ,
468
+ searchContainer : {
469
+ backgroundColor : 'listBackground' ,
470
+ } ,
392
471
search : {
393
472
marginBottom : 8 ,
394
473
marginHorizontal : 12 ,
0 commit comments