Skip to content

Commit 76924da

Browse files
committed
[native] Fix ChatThreadList search to top when active
Summary: This diff solves this [Notion issue](https://www.notion.so/commapp/ThreadList-scrolls-to-top-when-item-pressed-c60cd69ababd4b8bba0b957ff8b85de5), and is a better UX anyways. Test Plan: Tested it on iOS and Android and made sure it's smooth Reviewers: palys-swm Subscribers: KatPo, Adrian, atul Differential Revision: https://phabricator.ashoat.com/D1083
1 parent 9ada621 commit 76924da

File tree

1 file changed

+90
-11
lines changed

1 file changed

+90
-11
lines changed

native/chat/chat-thread-list.react.js

+90-11
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
import invariant from 'invariant';
44
import _sum from 'lodash/fp/sum';
55
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';
713
import { FloatingAction } from 'react-native-floating-action';
814
import IonIcon from 'react-native-vector-icons/Ionicons';
915
import { createSelector } from 'reselect';
@@ -84,7 +90,9 @@ type Props = {|
8490
// async functions that hit server APIs
8591
+searchUsers: (usernamePrefix: string) => Promise<UserSearchResult>,
8692
|};
93+
type SearchStatus = 'inactive' | 'activating' | 'active';
8794
type State = {|
95+
+searchStatus: SearchStatus,
8896
+searchText: string,
8997
+threadsSearchResults: Set<string>,
9098
+usersSearchResults: $ReadOnlyArray<GlobalAccountUserInfo>,
@@ -93,6 +101,7 @@ type State = {|
93101
type PropsAndState = {| ...Props, ...State |};
94102
class ChatThreadList extends React.PureComponent<Props, State> {
95103
state: State = {
104+
searchStatus: 'inactive',
96105
searchText: '',
97106
threadsSearchResults: new Set(),
98107
usersSearchResults: [],
@@ -126,6 +135,18 @@ class ChatThreadList extends React.PureComponent<Props, State> {
126135
tabNavigation.removeListener('tabPress', this.onTabPress);
127136
}
128137

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+
129150
onTabPress = () => {
130151
if (!this.props.navigation.isFocused()) {
131152
return;
@@ -137,17 +158,53 @@ class ChatThreadList extends React.PureComponent<Props, State> {
137158
}
138159
};
139160

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}>
144184
<Search
145185
searchText={this.state.searchText}
146186
onChangeText={this.onChangeSearchText}
147187
containerStyle={this.props.styles.search}
188+
onBlur={this.onSearchBlur}
148189
placeholder="Search threads"
149190
ref={this.searchInputRef}
191+
{...additionalProps}
150192
/>
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>
151208
);
152209
}
153210
if (item.type === 'empty') {
@@ -165,10 +222,6 @@ class ChatThreadList extends React.PureComponent<Props, State> {
165222
);
166223
};
167224

168-
searchInputRef = (searchInput: ?React.ElementRef<typeof TextInput>) => {
169-
this.searchInput = searchInput;
170-
};
171-
172225
static keyExtractor(item: Item) {
173226
if (item.type === 'chatThreadItem') {
174227
return item.threadInfo.id;
@@ -212,12 +265,14 @@ class ChatThreadList extends React.PureComponent<Props, State> {
212265

213266
listDataSelector = createSelector(
214267
(propsAndState: PropsAndState) => propsAndState.chatListData,
268+
(propsAndState: PropsAndState) => propsAndState.searchStatus,
215269
(propsAndState: PropsAndState) => propsAndState.searchText,
216270
(propsAndState: PropsAndState) => propsAndState.threadsSearchResults,
217271
(propsAndState: PropsAndState) => propsAndState.emptyItem,
218272
(propsAndState: PropsAndState) => propsAndState.usersSearchResults,
219273
(
220274
reduxChatListData: $ReadOnlyArray<ChatThreadItem>,
275+
searchStatus: SearchStatus,
221276
searchText: string,
222277
threadsSearchResults: Set<string>,
223278
emptyItem?: React.ComponentType<{||}>,
@@ -264,7 +319,11 @@ class ChatThreadList extends React.PureComponent<Props, State> {
264319
chatItems.push({ type: 'empty', emptyItem });
265320
}
266321

267-
return [{ type: 'search', searchText }, ...chatItems];
322+
if (searchStatus === 'inactive' || searchStatus === 'activating') {
323+
chatItems.unshift({ type: 'search', searchText });
324+
}
325+
326+
return chatItems;
268327
},
269328
);
270329

@@ -273,7 +332,7 @@ class ChatThreadList extends React.PureComponent<Props, State> {
273332
}
274333

275334
render() {
276-
let floatingAction = null;
335+
let floatingAction;
277336
if (Platform.OS === 'android') {
278337
floatingAction = (
279338
<FloatingAction
@@ -284,10 +343,18 @@ class ChatThreadList extends React.PureComponent<Props, State> {
284343
/>
285344
);
286345
}
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';
287353
// this.props.viewerID is in extraData since it's used by MessagePreview
288354
// within ChatThreadListItem
289355
return (
290356
<View style={this.props.styles.container}>
357+
{fixedSearch}
291358
<FlatList
292359
data={this.listData}
293360
renderItem={this.renderItem}
@@ -301,6 +368,8 @@ class ChatThreadList extends React.PureComponent<Props, State> {
301368
onScroll={this.onScroll}
302369
style={this.props.styles.flatList}
303370
indicatorStyle={this.props.indicatorStyle}
371+
scrollEnabled={scrollEnabled}
372+
removeClippedSubviews={true}
304373
ref={this.flatListRef}
305374
/>
306375
{floatingAction}
@@ -313,7 +382,14 @@ class ChatThreadList extends React.PureComponent<Props, State> {
313382
};
314383

315384
onScroll = (event: { +nativeEvent: { +contentOffset: { +y: number } } }) => {
385+
const oldScrollPos = this.scrollPos;
316386
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+
}
317393
};
318394

319395
async searchUsers(usernamePrefix: string) {
@@ -389,6 +465,9 @@ const unboundStyles = {
389465
container: {
390466
flex: 1,
391467
},
468+
searchContainer: {
469+
backgroundColor: 'listBackground',
470+
},
392471
search: {
393472
marginBottom: 8,
394473
marginHorizontal: 12,

0 commit comments

Comments
 (0)