index.tsx 10.2 KB
Newer Older
1
import React from 'react';
2
import { FlatList, Text, View } from 'react-native';
3
import { connect } from 'react-redux';
4
import { dequal } from 'dequal';
5
import { StackNavigationProp } from '@react-navigation/stack';
6
import { CompositeNavigationProp, RouteProp } from '@react-navigation/core';
7

8
import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types';
9
import Message from '../../containers/message';
Diego Mello's avatar
Diego Mello committed
10
import ActivityIndicator from '../../containers/ActivityIndicator';
11
12
13
14
import I18n from '../../i18n';
import RocketChat from '../../lib/rocketchat';
import StatusBar from '../../containers/StatusBar';
import getFileUrlFromMessage from '../../lib/methods/helpers/getFileUrlFromMessage';
Diego Mello's avatar
Diego Mello committed
15
16
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
17
import { getUserSelector } from '../../selectors/login';
18
import { withActionSheet } from '../../containers/ActionSheet';
19
import SafeAreaView from '../../containers/SafeAreaView';
Diego Mello's avatar
Diego Mello committed
20
import getThreadName from '../../lib/methods/getThreadName';
21
import styles from './styles';
22
23
import { ChatsStackParamList } from '../../stacks/types';
import { IRoom, RoomType } from '../../definitions/IRoom';
24

25
26
27
28
29
interface IMessagesViewProps {
	user: {
		id: string;
	};
	baseUrl: string;
30
31
32
33
34
	navigation: CompositeNavigationProp<
		StackNavigationProp<ChatsStackParamList, 'MessagesView'>,
		StackNavigationProp<MasterDetailInsideStackParamList>
	>;
	route: RouteProp<ChatsStackParamList, 'MessagesView'>;
35
36
37
38
39
40
41
	customEmojis: { [key: string]: string };
	theme: string;
	showActionSheet: Function;
	useRealName: boolean;
	isMasterDetail: boolean;
}

42
43
44
45
46
47
48
49
interface IRoomInfoParam {
	room: IRoom;
	member: any;
	rid: string;
	t: RoomType;
	joined: boolean;
}

50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
interface IMessagesViewState {
	loading: boolean;
	messages: [];
	fileLoading: boolean;
	total: number;
}

interface IMessageItem {
	u?: string;
	user?: string;
	editedAt?: Date;
	attachments?: any;
	_id: string;
	tmid?: string;
	ts?: Date;
	uploadedAt?: Date;
	name?: string;
	description?: string;
	msg?: string;
	starred: string;
	pinned: boolean;
}

interface IParams {
74
75
	rid: string;
	t: RoomType;
76
	tmid?: string;
77
	message?: string;
78
	name?: string;
79
80
81
82
83
84
	fname?: string;
	prid?: string;
	room: IRoom;
	jumpToMessageId?: string;
	jumpToThreadId?: string;
	roomUserId?: string;
85
86
87
}

class MessagesView extends React.Component<IMessagesViewProps, any> {
88
89
	private rid: string;
	private t: RoomType;
90
91
92
93
	private content: any;
	private room: any;

	constructor(props: IMessagesViewProps) {
94
		super(props);
95
96
		this.state = {
			loading: false,
97
			messages: [],
98
			fileLoading: true
99
		};
100
		this.setHeader();
101
102
103
		this.rid = props.route.params?.rid;
		this.t = props.route.params?.t;
		this.content = this.defineMessagesViewContent(props.route.params?.name);
104
105
106
107
108
109
	}

	componentDidMount() {
		this.load();
	}

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

	setHeader = () => {
		const { route, navigation } = this.props;
		navigation.setOptions({
			title: I18n.t(route.params?.name)
		});
133
	};
134

135
	navToRoomInfo = (navParam: IRoomInfoParam) => {
136
137
138
139
140
		const { navigation, user } = this.props;
		if (navParam.rid === user.id) {
			return;
		}
		navigation.navigate('RoomInfoView', navParam);
141
	};
142

143
	jumpToMessage = async ({ item }: { item: IMessageItem }) => {
Diego Mello's avatar
Diego Mello committed
144
		const { navigation, isMasterDetail } = this.props;
145
		let params: IParams = {
Diego Mello's avatar
Diego Mello committed
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
			rid: this.rid,
			jumpToMessageId: item._id,
			t: this.t,
			room: this.room
		};
		if (item.tmid) {
			if (isMasterDetail) {
				navigation.navigate('DrawerNavigator');
			} else {
				navigation.pop(2);
			}
			params = {
				...params,
				tmid: item.tmid,
				name: await getThreadName(this.rid, item.tmid, item._id),
161
				t: RoomType.THREAD
Diego Mello's avatar
Diego Mello committed
162
163
164
165
166
			};
			navigation.push('RoomView', params);
		} else {
			navigation.navigate('RoomView', params);
		}
167
	};
Diego Mello's avatar
Diego Mello committed
168

169
	defineMessagesViewContent = (name: string) => {
170
		const { user, baseUrl, theme, useRealName } = this.props;
171
		const renderItemCommonProps = (item: IMessageItem) => ({
172
			item,
173
174
175
176
			baseUrl,
			user,
			author: item.u || item.user,
			timeFormat: 'MMM Do YYYY, h:mm:ss a',
177
178
			isEdited: !!item.editedAt,
			isHeader: true,
Diego Mello's avatar
Diego Mello committed
179
			isThreadRoom: true,
180
			attachments: item.attachments || [],
181
			useRealName,
182
			showAttachment: this.showAttachment,
183
			getCustomEmoji: this.getCustomEmoji,
Diego Mello's avatar
Diego Mello committed
184
185
			navToRoomInfo: this.navToRoomInfo,
			onPress: () => this.jumpToMessage({ item })
186
187
		});

188
		return {
189
190
191
			// Files Messages Screen
			Files: {
				name: I18n.t('Files'),
192
				fetchFunc: async () => {
193
					const { messages } = this.state;
194
195
196
197
198
					const result = await RocketChat.getFiles(this.rid, this.t, messages.length);
					return { ...result, messages: result.files };
				},
				noDataMsg: I18n.t('No_files'),
				testID: 'room-files-view',
199
				renderItem: (item: IMessageItem) => (
200
201
202
203
204
					<Message
						{...renderItemCommonProps(item)}
						item={{
							...item,
							u: item.user,
205
							ts: item.ts || item.uploadedAt,
206
207
208
209
210
211
212
							attachments: [
								{
									title: item.name,
									description: item.description,
									...getFileUrlFromMessage(item)
								}
							]
213
214
215
216
						}}
						theme={theme}
					/>
				)
217
218
219
220
			},
			// Mentions Messages Screen
			Mentions: {
				name: I18n.t('Mentions'),
221
222
				fetchFunc: () => {
					const { messages } = this.state;
223
					return RocketChat.getMessages(this.rid, this.t, { 'mentions._id': { $in: [user.id] } }, messages.length);
224
				},
225
226
				noDataMsg: I18n.t('No_mentioned_messages'),
				testID: 'mentioned-messages-view',
227
				renderItem: (item: IMessageItem) => <Message {...renderItemCommonProps(item)} msg={item.msg} theme={theme} />
228
229
230
231
			},
			// Starred Messages Screen
			Starred: {
				name: I18n.t('Starred'),
232
233
				fetchFunc: () => {
					const { messages } = this.state;
234
					return RocketChat.getMessages(this.rid, this.t, { 'starred._id': { $in: [user.id] } }, messages.length);
235
				},
236
237
				noDataMsg: I18n.t('No_starred_messages'),
				testID: 'starred-messages-view',
238
				renderItem: (item: IMessageItem) => (
239
					<Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} theme={theme} />
240
				),
241
				action: (message: IMessageItem) => ({
242
243
244
245
					title: I18n.t('Unstar'),
					icon: message.starred ? 'star-filled' : 'star',
					onPress: this.handleActionPress
				}),
246
				handleActionPress: (message: IMessageItem) => RocketChat.toggleStarMessage(message._id, message.starred)
247
248
249
250
			},
			// Pinned Messages Screen
			Pinned: {
				name: I18n.t('Pinned'),
251
252
253
254
				fetchFunc: () => {
					const { messages } = this.state;
					return RocketChat.getMessages(this.rid, this.t, { pinned: true }, messages.length);
				},
255
256
				noDataMsg: I18n.t('No_pinned_messages'),
				testID: 'pinned-messages-view',
257
				renderItem: (item: IMessageItem) => (
258
					<Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} theme={theme} />
259
				),
260
				action: () => ({ title: I18n.t('Unpin'), icon: 'pin', onPress: this.handleActionPress }),
261
				handleActionPress: (message: IMessageItem) => RocketChat.togglePinMessage(message._id, message.pinned)
262
			}
263
			// @ts-ignore
264
265
		}[name];
	};
266

267
268
	load = async () => {
		const { messages, total, loading } = this.state;
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
		if (messages.length === total || loading) {
			return;
		}

		this.setState({ loading: true });

		try {
			const result = await this.content.fetchFunc();
			if (result.success) {
				this.setState({
					messages: [...messages, ...result.messages],
					total: result.total,
					loading: false
				});
			}
		} catch (error) {
			this.setState({ loading: false });
			console.warn('MessagesView -> catch -> error', error);
		}
288
	};
289

290
	getCustomEmoji = (name: string) => {
291
292
293
294
295
296
		const { customEmojis } = this.props;
		const emoji = customEmojis[name];
		if (emoji) {
			return emoji;
		}
		return null;
297
	};
298

299
	showAttachment = (attachment: any) => {
300
301
		const { navigation } = this.props;
		navigation.navigate('AttachmentView', { attachment });
302
	};
303

304
	onLongPress = (message: IMessageItem) => {
305
		this.setState({ message }, this.showActionSheet);
306
	};
307
308

	showActionSheet = () => {
309
310
311
		const { message } = this.state;
		const { showActionSheet } = this.props;
		showActionSheet({ options: [this.content.action(message)], hasCancel: true });
312
	};
313

314
	handleActionPress = async () => {
315
		const { message } = this.state;
316

317
318
319
		try {
			const result = await this.content.handleActionPress(message);
			if (result.success) {
320
321
				this.setState((prevState: IMessagesViewState) => ({
					messages: prevState.messages.filter((item: IMessageItem) => item._id !== message._id),
322
323
					total: prevState.total - 1
				}));
324
			}
325
326
		} catch {
			// Do nothing
327
		}
328
	};
329

330
	setFileLoading = (fileLoading: boolean) => {
331
		this.setState({ fileLoading });
332
	};
333

Diego Mello's avatar
Diego Mello committed
334
335
336
	renderEmpty = () => {
		const { theme } = this.props;
		return (
337
			<View style={[styles.listEmptyContainer, { backgroundColor: themes[theme].backgroundColor }]} testID={this.content.testID}>
Diego Mello's avatar
Diego Mello committed
338
339
340
				<Text style={[styles.noDataFound, { color: themes[theme].titleText }]}>{this.content.noDataMsg}</Text>
			</View>
		);
341
	};
342

343
	renderItem = ({ item }: { item: IMessageItem }) => this.content.renderItem(item);
344
345

	render() {
346
347
		const { messages, loading } = this.state;
		const { theme } = this.props;
348
349
350
351
352
353

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

		return (
354
			<SafeAreaView style={{ backgroundColor: themes[theme].backgroundColor }} testID={this.content.testID}>
355
				<StatusBar />
356
357
358
				<FlatList
					data={messages}
					renderItem={this.renderItem}
Diego Mello's avatar
Diego Mello committed
359
					style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]}
360
361
					keyExtractor={item => item._id}
					onEndReached={this.load}
Diego Mello's avatar
Diego Mello committed
362
					ListFooterComponent={loading ? <ActivityIndicator theme={theme} /> : null}
363
364
365
366
367
				/>
			</SafeAreaView>
		);
	}
}
368

369
const mapStateToProps = (state: any) => ({
370
371
	baseUrl: state.server.server,
	user: getUserSelector(state),
372
	customEmojis: state.customEmojis,
Diego Mello's avatar
Diego Mello committed
373
374
	useRealName: state.settings.UI_Use_Real_Name,
	isMasterDetail: state.app.isMasterDetail
375
376
});

377
export default connect(mapStateToProps)(withTheme(withActionSheet(MessagesView)));