index.tsx 12.5 KB
Newer Older
Rodrigo Nascimento's avatar
Rodrigo Nascimento committed
1
import React from 'react';
2
import { Text, Keyboard, StyleSheet, View, BackHandler, Image } from 'react-native';
Guilherme Gazzo's avatar
Guilherme Gazzo committed
3
import { connect } from 'react-redux';
Diego Mello's avatar
Diego Mello committed
4
import { Base64 } from 'js-base64';
5
import parse from 'url-parse';
6
7
import { Q } from '@nozbe/watermelondb';
import { TouchableOpacity } from 'react-native-gesture-handler';
8
import Orientation from 'react-native-orientation-locker';
9
10
11
import { StackNavigationProp } from '@react-navigation/stack';
import { Dispatch } from 'redux';
import Model from '@nozbe/watermelondb/Model';
12

13
14
import UserPreferences from '../../lib/userPreferences';
import EventEmitter from '../../utils/events';
15
import { selectServerRequest, serverRequest, serverFinishAdd as serverFinishAddAction } from '../../actions/server';
16
17
18
19
20
21
22
import { inviteLinksClear as inviteLinksClearAction } from '../../actions/inviteLinks';
import sharedStyles from '../Styles';
import Button from '../../containers/Button';
import OrSeparator from '../../containers/OrSeparator';
import FormContainer, { FormContainerInner } from '../../containers/FormContainer';
import I18n from '../../i18n';
import { themes } from '../../constants/colors';
23
import { events, logEvent } from '../../utils/log';
24
import { withTheme } from '../../theme';
25
import { BASIC_AUTH_KEY, setBasicAuth } from '../../utils/fetch';
26
import * as HeaderButton from '../../containers/HeaderButton';
27
28
import { showConfirmationAlert } from '../../utils/info';
import database from '../../lib/database';
29
import { sanitizeLikeString } from '../../lib/database/utils';
30
31
import SSLPinning from '../../utils/sslPinning';
import RocketChat from '../../lib/rocketchat';
32
33
34
import { isTablet } from '../../utils/deviceInfo';
import { verticalScale, moderateScale } from '../../utils/scaling';
import { withDimensions } from '../../dimensions';
35
import ServerInput from './ServerInput';
36
import { OutsideParamList } from '../../stacks/types';
Diego Mello's avatar
Diego Mello committed
37
38

const styles = StyleSheet.create({
39
40
41
42
	onboardingImage: {
		alignSelf: 'center',
		resizeMode: 'contain'
	},
Diego Mello's avatar
Diego Mello committed
43
	title: {
44
		...sharedStyles.textBold,
45
46
47
48
49
50
		letterSpacing: 0,
		alignSelf: 'center'
	},
	subtitle: {
		...sharedStyles.textRegular,
		alignSelf: 'center'
Diego Mello's avatar
Diego Mello committed
51
	},
52
53
	certificatePicker: {
		alignItems: 'center',
Diego Mello's avatar
Diego Mello committed
54
		justifyContent: 'flex-end'
55
56
	},
	chooseCertificateTitle: {
Diego Mello's avatar
Diego Mello committed
57
		...sharedStyles.textRegular
58
59
	},
	chooseCertificate: {
Diego Mello's avatar
Diego Mello committed
60
		...sharedStyles.textSemibold
Diego Mello's avatar
Diego Mello committed
61
62
63
	},
	description: {
		...sharedStyles.textRegular,
64
		textAlign: 'center'
Diego Mello's avatar
Diego Mello committed
65
66
67
	},
	connectButton: {
		marginBottom: 0
Diego Mello's avatar
Diego Mello committed
68
69
70
	}
});

71
72
73
74
export interface IServer extends Model {
	url: string;
	username: string;
}
75

76
interface INewServerView {
77
	navigation: StackNavigationProp<OutsideParamList, 'NewServerView'>;
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
	theme: string;
	connecting: boolean;
	connectServer(server: string, username?: string, fromServerHistory?: boolean): void;
	selectServer(server: string): void;
	previousServer: string;
	inviteLinksClear(): void;
	serverFinishAdd(): void;
	width: number;
	height: number;
}

interface IState {
	text: string;
	connectingOpen: boolean;
	certificate: any;
	serversHistory: IServer[];
}
Rodrigo Nascimento's avatar
Rodrigo Nascimento committed
95

96
97
98
99
100
interface ISubmitParams {
	fromServerHistory?: boolean;
	username?: string;
}

101
102
class NewServerView extends React.Component<INewServerView, IState> {
	constructor(props: INewServerView) {
103
		super(props);
104
105
106
		if (!isTablet) {
			Orientation.lockToPortrait();
		}
107
		this.setHeader();
108

109
		this.state = {
Diego Mello's avatar
Diego Mello committed
110
111
			text: '',
			connectingOpen: false,
112
113
			certificate: null,
			serversHistory: []
114
		};
Diego Mello's avatar
Diego Mello committed
115
		EventEmitter.addEventListener('NewServer', this.handleNewServerEvent);
116
		BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
Diego Mello's avatar
Diego Mello committed
117
118
	}

119
120
121
122
	componentDidMount() {
		this.queryServerHistory();
	}

Diego Mello's avatar
Diego Mello committed
123
124
	componentWillUnmount() {
		EventEmitter.removeListener('NewServer', this.handleNewServerEvent);
125
		BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
126
127
128
129
		const { previousServer, serverFinishAdd } = this.props;
		if (previousServer) {
			serverFinishAdd();
		}
130
131
	}

132
	setHeader = () => {
133
134
135
136
		const { previousServer, navigation } = this.props;
		if (previousServer) {
			return navigation.setOptions({
				headerTitle: I18n.t('Workspaces'),
137
				headerLeft: () => <HeaderButton.CloseModal navigation={navigation} onPress={this.close} testID='new-server-view-close' />
138
139
			});
		}
140
141
142
143

		return navigation.setOptions({
			headerShown: false
		});
144
	};
145

146
	handleBackPress = () => {
147
148
		const { navigation, previousServer } = this.props;
		if (navigation.isFocused() && previousServer) {
149
150
151
152
			this.close();
			return true;
		}
		return false;
153
	};
Diego Mello's avatar
Diego Mello committed
154

155
	onChangeText = (text: string) => {
156
		this.setState({ text });
157
		this.queryServerHistory(text);
158
	};
159

160
	queryServerHistory = async (text?: string) => {
161
162
		const db = database.servers;
		try {
163
			const serversHistoryCollection = db.get('servers_history');
164
			let whereClause = [Q.where('username', Q.notEq(null)), Q.experimentalSortBy('updated_at', Q.desc), Q.experimentalTake(3)];
165
			if (text) {
166
				const likeString = sanitizeLikeString(text);
167
				whereClause = [...whereClause, Q.where('url', Q.like(`%${likeString}%`))];
168
			}
169
			const serversHistory = (await serversHistoryCollection.query(...whereClause).fetch()) as IServer[];
170
171
172
173
			this.setState({ serversHistory });
		} catch {
			// Do nothing
		}
174
	};
175

Diego Mello's avatar
Diego Mello committed
176
	close = () => {
177
178
		const { selectServer, previousServer, inviteLinksClear } = this.props;
		inviteLinksClear();
179
		selectServer(previousServer);
180
	};
Diego Mello's avatar
Diego Mello committed
181

182
	handleNewServerEvent = (event: { server: string }) => {
Diego Mello's avatar
Diego Mello committed
183
		let { server } = event;
184
185
186
		if (!server) {
			return;
		}
Diego Mello's avatar
Diego Mello committed
187
188
189
190
		const { connectServer } = this.props;
		this.setState({ text: server });
		server = this.completeUrl(server);
		connectServer(server);
191
	};
Diego Mello's avatar
Diego Mello committed
192

193
	onPressServerHistory = (serverHistory: IServer) => {
194
		this.setState({ text: serverHistory.url }, () => this.submit({ fromServerHistory: true, username: serverHistory?.username }));
195
	};
196

197
	submit = async ({ fromServerHistory = false, username }: ISubmitParams = {}) => {
198
		logEvent(events.NS_CONNECT_TO_WORKSPACE);
199
		const { text, certificate } = this.state;
Diego Mello's avatar
Diego Mello committed
200
		const { connectServer } = this.props;
201

Diego Mello's avatar
Diego Mello committed
202
203
		this.setState({ connectingOpen: false });

Diego Mello's avatar
Diego Mello committed
204
		if (text) {
205
			Keyboard.dismiss();
206
			const server = this.completeUrl(text);
207
208

			// Save info - SSL Pinning
209
			await UserPreferences.setStringAsync(`${RocketChat.CERTIFICATE_KEY}-${server}`, certificate);
210
211

			// Save info - HTTP Basic Authentication
212
			await this.basicAuth(server, text);
213

214
			if (fromServerHistory) {
215
				connectServer(server, username, true);
216
			} else {
217
				connectServer(server);
218
			}
219
		}
220
	};
221

Diego Mello's avatar
Diego Mello committed
222
	connectOpen = () => {
223
		logEvent(events.NS_JOIN_OPEN_WORKSPACE);
Diego Mello's avatar
Diego Mello committed
224
225
226
		this.setState({ connectingOpen: true });
		const { connectServer } = this.props;
		connectServer('https://open.rocket.chat');
227
	};
Diego Mello's avatar
Diego Mello committed
228

229
	basicAuth = async (server: string, text: string) => {
230
231
232
		try {
			const parsedUrl = parse(text, true);
			if (parsedUrl.auth.length) {
Diego Mello's avatar
Diego Mello committed
233
				const credentials = Base64.encode(parsedUrl.auth);
234
				await UserPreferences.setStringAsync(`${BASIC_AUTH_KEY}-${server}`, credentials);
235
236
237
238
				setBasicAuth(credentials);
			}
		} catch {
			// do nothing
239
		}
240
	};
241

242
	chooseCertificate = async () => {
243
		try {
244
			const certificate = await SSLPinning?.pickCertificate();
245
246
247
			this.setState({ certificate });
		} catch {
			// Do nothing
248
		}
249
	};
250

251
	completeUrl = (url: string) => {
252
253
		const parsedUrl = parse(url, true);
		if (parsedUrl.auth.length) {
254
			url = parsedUrl.origin;
255
256
		}

257
		url = url && url.replace(/\s/g, '');
Guilherme Gazzo's avatar
Guilherme Gazzo committed
258

259
260
		if (/^(\w|[0-9-_]){3,}$/.test(url) && /^(htt(ps?)?)|(loca((l)?|(lh)?|(lho)?|(lhos)?|(lhost:?\d*)?)$)/.test(url) === false) {
			url = `${url}.rocket.chat`;
261
262
		}

Filipe Brito's avatar
Filipe Brito committed
263
		if (/^(https?:\/\/)?(((\w|[0-9-_])+(\.(\w|[0-9-_])+)+)|localhost)(:\d+)?$/.test(url)) {
264
			if (/^localhost(:\d+)?/.test(url)) {
265
				url = `http://${url}`;
266
			} else if (/^https?:\/\//.test(url) === false) {
267
				url = `https://${url}`;
Rodrigo Nascimento's avatar
Rodrigo Nascimento committed
268
			}
269
		}
Rodrigo Nascimento's avatar
Rodrigo Nascimento committed
270

271
		return url.replace(/\/+$/, '').replace(/\\/g, '/');
272
	};
273

274
	uriToPath = (uri: string) => uri.replace('file://', '');
275

276
	handleRemove = () => {
277
278
		// TODO: Remove ts-ignore when migrate the showConfirmationAlert
		// @ts-ignore
279
280
		showConfirmationAlert({
			message: I18n.t('You_will_unset_a_certificate_for_this_server'),
281
			confirmationText: I18n.t('Remove'),
282
			onPress: this.setState({ certificate: null }) // We not need delete file from DocumentPicker because it is a temp file
283
		});
284
	};
285

286
	deleteServerHistory = async (item: IServer) => {
287
288
		const db = database.servers;
		try {
289
			await db.write(async () => {
290
291
				await item.destroyPermanently();
			});
292
293
294
			this.setState((prevstate: IState) => ({
				serversHistory: prevstate.serversHistory.filter((server: IServer) => server.id !== item.id)
			}));
295
296
297
		} catch {
			// Nothing
		}
298
	};
299

300
301
	renderCertificatePicker = () => {
		const { certificate } = this.state;
302
		const { theme, width, height, previousServer } = this.props;
303
		return (
304
305
306
307
			<View
				style={[
					styles.certificatePicker,
					{
308
						marginBottom: verticalScale({ size: previousServer && !isTablet ? 10 : 30, height })
309
310
311
312
313
					}
				]}>
				<Text
					style={[
						styles.chooseCertificateTitle,
314
						{ color: themes[theme].auxiliaryText, fontSize: moderateScale({ size: 13, width }) }
315
					]}>
Diego Mello's avatar
Diego Mello committed
316
317
318
					{certificate ? I18n.t('Your_certificate') : I18n.t('Do_you_have_a_certificate')}
				</Text>
				<TouchableOpacity
319
					onPress={certificate ? this.handleRemove : this.chooseCertificate}
320
					testID='new-server-choose-certificate'>
321
322
					<Text
						style={[styles.chooseCertificate, { color: themes[theme].tintColor, fontSize: moderateScale({ size: 13, width }) }]}>
323
						{certificate ?? I18n.t('Apply_Your_Certificate')}
Diego Mello's avatar
Diego Mello committed
324
					</Text>
325
326
327
				</TouchableOpacity>
			</View>
		);
328
	};
329

Rodrigo Nascimento's avatar
Rodrigo Nascimento committed
330
	render() {
331
		const { connecting, theme, previousServer, width, height } = this.props;
332
		const { text, connectingOpen, serversHistory } = this.state;
333
334
		const marginTop = previousServer ? 0 : 35;

Rodrigo Nascimento's avatar
Rodrigo Nascimento committed
335
		return (
336
			<FormContainer theme={theme} testID='new-server-view' keyboardShouldPersistTaps='never'>
Diego Mello's avatar
Diego Mello committed
337
				<FormContainerInner>
338
339
340
341
					<Image
						style={[
							styles.onboardingImage,
							{
342
343
344
345
								marginBottom: verticalScale({ size: 10, height }),
								marginTop: isTablet ? 0 : verticalScale({ size: marginTop, height }),
								width: verticalScale({ size: 100, height }),
								height: verticalScale({ size: 100, height })
346
347
348
349
350
351
352
353
354
355
							}
						]}
						source={require('../../static/images/logo.png')}
						fadeDuration={0}
					/>
					<Text
						style={[
							styles.title,
							{
								color: themes[theme].titleText,
356
357
								fontSize: moderateScale({ size: 22, width }),
								marginBottom: verticalScale({ size: 8, height })
358
359
360
361
362
363
364
365
366
							}
						]}>
						Rocket.Chat
					</Text>
					<Text
						style={[
							styles.subtitle,
							{
								color: themes[theme].controlText,
367
368
								fontSize: moderateScale({ size: 16, width }),
								marginBottom: verticalScale({ size: 30, height })
369
370
371
372
							}
						]}>
						{I18n.t('Onboarding_subtitle')}
					</Text>
373
374
					<ServerInput
						text={text}
Diego Mello's avatar
Diego Mello committed
375
						theme={theme}
376
377
378
379
380
						serversHistory={serversHistory}
						onChangeText={this.onChangeText}
						onSubmit={this.submit}
						onDelete={this.deleteServerHistory}
						onPressServerHistory={this.onPressServerHistory}
Diego Mello's avatar
Diego Mello committed
381
382
383
384
385
386
387
					/>
					<Button
						title={I18n.t('Connect')}
						type='primary'
						onPress={this.submit}
						disabled={!text || connecting}
						loading={!connectingOpen && connecting}
388
						style={[styles.connectButton, { marginTop: verticalScale({ size: 16, height }) }]}
Diego Mello's avatar
Diego Mello committed
389
						theme={theme}
390
						testID='new-server-view-button'
Diego Mello's avatar
Diego Mello committed
391
					/>
Djorkaeff Alexandre's avatar
Djorkaeff Alexandre committed
392
					<OrSeparator theme={theme} />
393
394
395
396
397
					<Text
						style={[
							styles.description,
							{
								color: themes[theme].auxiliaryText,
398
399
								fontSize: moderateScale({ size: 14, width }),
								marginBottom: verticalScale({ size: 16, height })
400
401
							}
						]}>
402
403
						{I18n.t('Onboarding_join_open_description')}
					</Text>
Diego Mello's avatar
Diego Mello committed
404
405
406
407
408
409
410
411
					<Button
						title={I18n.t('Join_our_open_workspace')}
						type='secondary'
						backgroundColor={themes[theme].chatComponentBackground}
						onPress={this.connectOpen}
						disabled={connecting}
						loading={connectingOpen && connecting}
						theme={theme}
412
						testID='new-server-view-open'
Diego Mello's avatar
Diego Mello committed
413
414
					/>
				</FormContainerInner>
415
				{this.renderCertificatePicker()}
Diego Mello's avatar
Diego Mello committed
416
			</FormContainer>
Rodrigo Nascimento's avatar
Rodrigo Nascimento committed
417
418
419
		);
	}
}
420

421
const mapStateToProps = (state: any) => ({
422
423
	connecting: state.server.connecting,
	previousServer: state.server.previousServer
424
425
});

426
427
428
429
const mapDispatchToProps = (dispatch: Dispatch) => ({
	connectServer: (server: string, username: string & null, fromServerHistory?: boolean) =>
		dispatch(serverRequest(server, username, fromServerHistory)),
	selectServer: (server: string) => dispatch(selectServerRequest(server)),
430
431
	inviteLinksClear: () => dispatch(inviteLinksClearAction()),
	serverFinishAdd: () => dispatch(serverFinishAddAction())
432
433
});

434
export default connect(mapStateToProps, mapDispatchToProps)(withDimensions(withTheme(NewServerView)));