index.tsx 9.1 KB
Newer Older
Guilherme Gazzo's avatar
Guilherme Gazzo committed
1
import React from 'react';
2
import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
3
import { CompositeNavigationProp, RouteProp } from '@react-navigation/core';
4
import { FlatList, Text, View } from 'react-native';
5
import { Q } from '@nozbe/watermelondb';
Guilherme Gazzo's avatar
Guilherme Gazzo committed
6
import { connect } from 'react-redux';
7
import { dequal } from 'dequal';
Guilherme Gazzo's avatar
Guilherme Gazzo committed
8

9
10
import { IRoom, RoomType } from '../../definitions/IRoom';
import { IAttachment } from '../../definitions/IAttachment';
Guilherme Gazzo's avatar
Guilherme Gazzo committed
11
import RCTextInput from '../../containers/TextInput';
Diego Mello's avatar
Diego Mello committed
12
import ActivityIndicator from '../../containers/ActivityIndicator';
13
import Markdown from '../../containers/markdown';
Guilherme Gazzo's avatar
Guilherme Gazzo committed
14
15
import debounce from '../../utils/debounce';
import RocketChat from '../../lib/rocketchat';
16
import Message from '../../containers/message';
Guilherme Gazzo's avatar
Guilherme Gazzo committed
17
import scrollPersistTaps from '../../utils/scrollPersistTaps';
18
import { IMessage } from '../../containers/message/interfaces';
Diego Mello's avatar
Diego Mello committed
19
import I18n from '../../i18n';
20
import StatusBar from '../../containers/StatusBar';
21
import log from '../../utils/log';
Diego Mello's avatar
Diego Mello committed
22
23
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
24
import { getUserSelector } from '../../selectors/login';
25
import SafeAreaView from '../../containers/SafeAreaView';
26
import * as HeaderButton from '../../containers/HeaderButton';
27
import database from '../../lib/database';
28
import { sanitizeLikeString } from '../../lib/database/utils';
Diego Mello's avatar
Diego Mello committed
29
30
import getThreadName from '../../lib/methods/getThreadName';
import getRoomInfo from '../../lib/methods/getRoomInfo';
31
32
import { isIOS } from '../../utils/deviceInfo';
import { compareServerVersion, methods } from '../../lib/utils';
33
import styles from './styles';
34
import { InsideStackParamList, ChatsStackParamList } from '../../stacks/types';
Guilherme Gazzo's avatar
Guilherme Gazzo committed
35

36
const QUERY_SIZE = 50;
37
38
39
40
41
42

interface ISearchMessagesViewState {
	loading: boolean;
	messages: IMessage[];
	searchText: string;
}
43
44
45
46
47
48
49
50
51

interface IRoomInfoParam {
	room: IRoom;
	member: any;
	rid: string;
	t: RoomType;
	joined: boolean;
}

52
interface INavigationOption {
53
54
55
56
57
	navigation: CompositeNavigationProp<
		StackNavigationProp<ChatsStackParamList, 'SearchMessagesView'>,
		StackNavigationProp<InsideStackParamList>
	>;
	route: RouteProp<ChatsStackParamList, 'SearchMessagesView'>;
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
}

interface ISearchMessagesViewProps extends INavigationOption {
	user: { id: string };
	baseUrl: string;
	serverVersion: string;
	customEmojis: {
		[key: string]: {
			name: string;
			extension: string;
		};
	};
	theme: string;
	useRealName: boolean;
}
class SearchMessagesView extends React.Component<ISearchMessagesViewProps, ISearchMessagesViewState> {
	private offset: number;

	private rid: string;

	private t: string | undefined;

	private encrypted: boolean | undefined;

	private room: { rid: any; name: any; fname: any; t: any } | null | undefined;

	static navigationOptions = ({ navigation, route }: INavigationOption) => {
		const options: StackNavigationOptions = {
86
87
88
89
			title: I18n.t('Search')
		};
		const showCloseModal = route.params?.showCloseModal;
		if (showCloseModal) {
90
			options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
91
92
		}
		return options;
93
	};
Diego Mello's avatar
Diego Mello committed
94

95
	constructor(props: ISearchMessagesViewProps) {
96
		super(props);
Guilherme Gazzo's avatar
Guilherme Gazzo committed
97
		this.state = {
Diego Mello's avatar
Diego Mello committed
98
			loading: false,
Guilherme Gazzo's avatar
Guilherme Gazzo committed
99
			messages: [],
Diego Mello's avatar
Diego Mello committed
100
			searchText: ''
Guilherme Gazzo's avatar
Guilherme Gazzo committed
101
		};
102
		this.offset = 0;
103
		this.rid = props.route.params.rid;
Diego Mello's avatar
Diego Mello committed
104
		this.t = props.route.params?.t;
105
		this.encrypted = props.route.params?.encrypted;
Guilherme Gazzo's avatar
Guilherme Gazzo committed
106
107
	}

Diego Mello's avatar
Diego Mello committed
108
109
110
111
	async componentDidMount() {
		this.room = await getRoomInfo(this.rid);
	}

112
	shouldComponentUpdate(nextProps: ISearchMessagesViewProps, nextState: ISearchMessagesViewState) {
113
		const { loading, searchText, messages } = this.state;
Diego Mello's avatar
Diego Mello committed
114
115
116
117
		const { theme } = this.props;
		if (nextProps.theme !== theme) {
			return true;
		}
118
119
120
121
122
123
		if (nextState.loading !== loading) {
			return true;
		}
		if (nextState.searchText !== searchText) {
			return true;
		}
124
		if (!dequal(nextState.messages, messages)) {
125
126
127
128
129
			return true;
		}
		return false;
	}

Guilherme Gazzo's avatar
Guilherme Gazzo committed
130
	componentWillUnmount() {
131
		this.searchDebounced?.stop?.();
132
133
134
	}

	// Handle encrypted rooms search messages
135
	searchMessages = async (searchText: string) => {
136
137
138
		if (!searchText) {
			return [];
		}
139
140
141
		// If it's a encrypted, room we'll search only on the local stored messages
		if (this.encrypted) {
			const db = database.active;
142
			const messagesCollection = db.get('messages');
143
			const likeString = sanitizeLikeString(searchText);
144
145
146
147
148
			return messagesCollection
				.query(
					// Messages of this room
					Q.where('rid', this.rid),
					// Message content is like the search text
149
					Q.where('msg', Q.like(`%${likeString}%`))
150
151
152
153
				)
				.fetch();
		}
		// If it's not a encrypted room, search messages on the server
154
		const result = await RocketChat.searchMessages(this.rid, searchText, QUERY_SIZE, this.offset);
155
156
157
		if (result.success) {
			return result.messages;
		}
158
	};
Guilherme Gazzo's avatar
Guilherme Gazzo committed
159

160
	getMessages = async (searchText: string, debounced?: boolean) => {
Guilherme Gazzo's avatar
Guilherme Gazzo committed
161
		try {
162
			const messages = await this.searchMessages(searchText);
163
164
			this.setState(prevState => ({
				messages: debounced ? messages : [...prevState.messages, ...messages],
165
				loading: false
166
			}));
167
		} catch (e) {
Diego Mello's avatar
Diego Mello committed
168
			this.setState({ loading: false });
169
			log(e);
Guilherme Gazzo's avatar
Guilherme Gazzo committed
170
		}
171
172
	};

173
	search = (searchText: string) => {
174
175
176
177
178
		this.offset = 0;
		this.setState({ searchText, loading: true, messages: [] });
		this.searchDebounced(searchText);
	};

179
	searchDebounced = debounce(async (searchText: string) => {
180
		await this.getMessages(searchText, true);
181
	}, 1000);
Guilherme Gazzo's avatar
Guilherme Gazzo committed
182

183
	getCustomEmoji = (name: string) => {
184
185
186
187
188
189
		const { customEmojis } = this.props;
		const emoji = customEmojis[name];
		if (emoji) {
			return emoji;
		}
		return null;
190
	};
191

192
	showAttachment = (attachment: IAttachment) => {
Diego Mello's avatar
Diego Mello committed
193
194
		const { navigation } = this.props;
		navigation.navigate('AttachmentView', { attachment });
195
	};
Diego Mello's avatar
Diego Mello committed
196

197
	navToRoomInfo = (navParam: IRoomInfoParam) => {
198
199
200
201
202
		const { navigation, user } = this.props;
		if (navParam.rid === user.id) {
			return;
		}
		navigation.navigate('RoomInfoView', navParam);
203
	};
204

205
	jumpToMessage = async ({ item }: { item: IMessage }) => {
Diego Mello's avatar
Diego Mello committed
206
		const { navigation } = this.props;
207
		let params: any = {
Diego Mello's avatar
Diego Mello committed
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
			rid: this.rid,
			jumpToMessageId: item._id,
			t: this.t,
			room: this.room
		};
		if (item.tmid) {
			navigation.pop();
			params = {
				...params,
				tmid: item.tmid,
				name: await getThreadName(this.rid, item.tmid, item._id),
				t: 'thread'
			};
			navigation.push('RoomView', params);
		} else {
			navigation.navigate('RoomView', params);
		}
225
	};
Diego Mello's avatar
Diego Mello committed
226

227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
	onEndReached = async () => {
		const { serverVersion } = this.props;
		const { searchText, messages, loading } = this.state;
		if (
			messages.length < this.offset ||
			this.encrypted ||
			loading ||
			compareServerVersion(serverVersion, '3.17.0', methods.lowerThan)
		) {
			return;
		}
		this.setState({ loading: true });
		this.offset += QUERY_SIZE;

		await this.getMessages(searchText);
	};

Diego Mello's avatar
Diego Mello committed
244
245
246
247
248
249
250
	renderEmpty = () => {
		const { theme } = this.props;
		return (
			<View style={[styles.listEmptyContainer, { backgroundColor: themes[theme].backgroundColor }]}>
				<Text style={[styles.noDataFound, { color: themes[theme].titleText }]}>{I18n.t('No_results_found')}</Text>
			</View>
		);
251
	};
Guilherme Gazzo's avatar
Guilherme Gazzo committed
252

253
	renderItem = ({ item }: { item: IMessage }) => {
254
		const { user, baseUrl, theme, useRealName } = this.props;
Diego Mello's avatar
Diego Mello committed
255
256
		return (
			<Message
257
				item={item}
Diego Mello's avatar
Diego Mello committed
258
				baseUrl={baseUrl}
Diego Mello's avatar
Diego Mello committed
259
				user={user}
Diego Mello's avatar
Diego Mello committed
260
				timeFormat='MMM Do YYYY, h:mm:ss a'
261
				isHeader
Diego Mello's avatar
Diego Mello committed
262
263
				isThreadRoom
				showAttachment={this.showAttachment}
264
				getCustomEmoji={this.getCustomEmoji}
265
				navToRoomInfo={this.navToRoomInfo}
266
				useRealName={useRealName}
Diego Mello's avatar
Diego Mello committed
267
				theme={theme}
Diego Mello's avatar
Diego Mello committed
268
269
				onPress={() => this.jumpToMessage({ item })}
				jumpToMessage={() => this.jumpToMessage({ item })}
Diego Mello's avatar
Diego Mello committed
270
271
			/>
		);
272
	};
Diego Mello's avatar
Diego Mello committed
273
274
275

	renderList = () => {
		const { messages, loading, searchText } = this.state;
Diego Mello's avatar
Diego Mello committed
276
		const { theme } = this.props;
Diego Mello's avatar
Diego Mello committed
277
278
279
280
281
282
283
284
285

		if (!loading && messages.length === 0 && searchText.length) {
			return this.renderEmpty();
		}

		return (
			<FlatList
				data={messages}
				renderItem={this.renderItem}
Diego Mello's avatar
Diego Mello committed
286
				style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]}
Diego Mello's avatar
Diego Mello committed
287
				keyExtractor={item => item._id}
288
				onEndReached={this.onEndReached}
Diego Mello's avatar
Diego Mello committed
289
				ListFooterComponent={loading ? <ActivityIndicator theme={theme} /> : null}
290
291
				onEndReachedThreshold={0.5}
				removeClippedSubviews={isIOS}
Diego Mello's avatar
Diego Mello committed
292
				{...scrollPersistTaps}
Diego Mello's avatar
Diego Mello committed
293
294
			/>
		);
295
	};
Guilherme Gazzo's avatar
Guilherme Gazzo committed
296
297

	render() {
Diego Mello's avatar
Diego Mello committed
298
		const { theme } = this.props;
Guilherme Gazzo's avatar
Guilherme Gazzo committed
299
		return (
300
301
			<SafeAreaView style={{ backgroundColor: themes[theme].backgroundColor }} testID='search-messages-view'>
				<StatusBar />
Guilherme Gazzo's avatar
Guilherme Gazzo committed
302
303
				<View style={styles.searchContainer}>
					<RCTextInput
304
						autoFocus
Diego Mello's avatar
Diego Mello committed
305
						label={I18n.t('Search')}
Diego Mello's avatar
Diego Mello committed
306
						onChangeText={this.search}
Diego Mello's avatar
Diego Mello committed
307
						placeholder={I18n.t('Search_Messages')}
Guilherme Gazzo's avatar
Guilherme Gazzo committed
308
						testID='search-message-view-input'
Diego Mello's avatar
Diego Mello committed
309
						theme={theme}
Guilherme Gazzo's avatar
Guilherme Gazzo committed
310
					/>
311
					{/* @ts-ignore */}
Diego Mello's avatar
Diego Mello committed
312
313
					<Markdown msg={I18n.t('You_can_search_using_RegExp_eg')} username='' baseUrl='' theme={theme} />
					<View style={[styles.divider, { backgroundColor: themes[theme].separatorColor }]} />
Guilherme Gazzo's avatar
Guilherme Gazzo committed
314
				</View>
Diego Mello's avatar
Diego Mello committed
315
				{this.renderList()}
316
			</SafeAreaView>
Guilherme Gazzo's avatar
Guilherme Gazzo committed
317
318
319
		);
	}
}
320

321
const mapStateToProps = (state: any) => ({
322
	serverVersion: state.server.version,
323
324
	baseUrl: state.server.server,
	user: getUserSelector(state),
325
	useRealName: state.settings.UI_Use_Real_Name,
326
	customEmojis: state.customEmojis
327
328
});

Diego Mello's avatar
Diego Mello committed
329
export default connect(mapStateToProps)(withTheme(SearchMessagesView));