[NEW] Delete Server (#1975)

* [NEW] Delete server

Co-authored-by: Bruno Dantas <oliveiradantas96@gmail.com>
Co-authored-by: Calebe Rios <calebersmendes@gmail.com>

* [FIX] Revert removed function

Co-authored-by: Bruno Dantas <oliveiradantas96@gmail.com>
Co-authored-by: Calebe Rios <calebersmendes@gmail.com>

* pods

* i18n

* Revert "pods"

This reverts commit 2854a1650538159aeeafe90fdb2118d12b76a82f.

Co-authored-by: Bruno Dantas <oliveiradantas96@gmail.com>
Co-authored-by: Calebe Rios <calebersmendes@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Djorkaeff Alexandre 2020-05-04 17:20:45 -03:00 committed by GitHub
parent f272038ca1
commit ee5b7592b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 279 additions and 171 deletions

View File

@ -560,6 +560,7 @@ export default {
You_will_be_logged_out_of_this_application: 'You will be logged out of this application.', You_will_be_logged_out_of_this_application: 'You will be logged out of this application.',
Clear: 'Clear', Clear: 'Clear',
This_will_clear_all_your_offline_data: 'This will clear all your offline data.', This_will_clear_all_your_offline_data: 'This will clear all your offline data.',
This_will_remove_all_data_from_this_server: 'This will remove all data from this server.',
Mark_unread: 'Mark Unread', Mark_unread: 'Mark Unread',
Wait_activation_warning: 'Before you can login, your account must be manually activated by an administrator.' Wait_activation_warning: 'Before you can login, your account must be manually activated by an administrator.'
}; };

View File

@ -499,6 +499,7 @@ export default {
You_will_be_logged_out_of_this_application: 'Você sairá deste aplicativo.', You_will_be_logged_out_of_this_application: 'Você sairá deste aplicativo.',
Clear: 'Limpar', Clear: 'Limpar',
This_will_clear_all_your_offline_data: 'Isto limpará todos os seus dados offline.', This_will_clear_all_your_offline_data: 'Isto limpará todos os seus dados offline.',
This_will_remove_all_data_from_this_server: 'Isto removerá todos os dados desse servidor.',
Mark_unread: 'Marcar como não Lida', Mark_unread: 'Marcar como não Lida',
Wait_activation_warning: 'Antes que você possa fazer o login, sua conta deve ser manualmente ativada por um administrador.' Wait_activation_warning: 'Antes que você possa fazer o login, sua conta deve ser manualmente ativada por um administrador.'
}; };

View File

@ -33,6 +33,36 @@ if (__DEV__ && isIOS) {
console.log(appGroupPath); console.log(appGroupPath);
} }
export const getDatabase = (database = '') => {
const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//g, '.');
const dbName = `${ appGroupPath }${ path }.db`;
const adapter = new SQLiteAdapter({
dbName,
schema: appSchema,
migrations
});
return new Database({
adapter,
modelClasses: [
Subscription,
Room,
Message,
Thread,
ThreadMessage,
CustomEmoji,
FrequentlyUsedEmoji,
Upload,
Setting,
Role,
Permission,
SlashCommand
],
actionsEnabled: true
});
};
class DB { class DB {
databases = { databases = {
serversDB: new Database({ serversDB: new Database({
@ -86,34 +116,8 @@ class DB {
}); });
} }
setActiveDB(database = '') { setActiveDB(database) {
const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//g, '.'); this.databases.activeDB = getDatabase(database);
const dbName = `${ appGroupPath }${ path }.db`;
const adapter = new SQLiteAdapter({
dbName,
schema: appSchema,
migrations
});
this.databases.activeDB = new Database({
adapter,
modelClasses: [
Subscription,
Room,
Message,
Thread,
ThreadMessage,
CustomEmoji,
FrequentlyUsedEmoji,
Upload,
Setting,
Role,
Permission,
SlashCommand
],
actionsEnabled: true
});
} }
} }

127
app/lib/methods/logout.js Normal file
View File

@ -0,0 +1,127 @@
import RNUserDefaults from 'rn-user-defaults';
import * as FileSystem from 'expo-file-system';
import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
import { SERVERS, SERVER_URL } from '../../constants/userDefaults';
import { getDeviceToken } from '../../notifications/push';
import { extractHostname } from '../../utils/server';
import { BASIC_AUTH_KEY } from '../../utils/fetch';
import database, { getDatabase } from '../database';
import RocketChat from '../rocketchat';
import { useSsl } from '../../utils/url';
async function removeServerKeys({ server, userId }) {
await RNUserDefaults.clear(`${ RocketChat.TOKEN_KEY }-${ server }`);
await RNUserDefaults.clear(`${ RocketChat.TOKEN_KEY }-${ userId }`);
await RNUserDefaults.clear(`${ BASIC_AUTH_KEY }-${ server }`);
}
async function removeSharedCredentials({ server }) {
try {
const servers = await RNUserDefaults.objectForKey(SERVERS);
await RNUserDefaults.setObjectForKey(SERVERS, servers && servers.filter(srv => srv[SERVER_URL] !== server));
// clear certificate for server - SSL Pinning
const certificate = await RNUserDefaults.objectForKey(extractHostname(server));
if (certificate && certificate.path) {
await RNUserDefaults.clear(extractHostname(server));
await FileSystem.deleteAsync(certificate.path);
}
} catch (e) {
console.log('removeSharedCredentials', e);
}
}
async function removeServerData({ server }) {
try {
const batch = [];
const serversDB = database.servers;
const userId = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`);
const usersCollection = serversDB.collections.get('users');
if (userId) {
const userRecord = await usersCollection.find(userId);
batch.push(userRecord.prepareDestroyPermanently());
}
const serverCollection = serversDB.collections.get('servers');
const serverRecord = await serverCollection.find(server);
batch.push(serverRecord.prepareDestroyPermanently());
await serversDB.action(() => serversDB.batch(...batch));
await removeSharedCredentials({ server });
await removeServerKeys({ server });
} catch (e) {
console.log('removeServerData', e);
}
}
async function removeCurrentServer() {
await RNUserDefaults.clear('currentServer');
await RNUserDefaults.clear(RocketChat.TOKEN_KEY);
}
async function removeServerDatabase({ server }) {
try {
const db = getDatabase(server);
await db.action(() => db.unsafeResetDatabase());
} catch (e) {
console.log(e);
}
}
export async function removeServer({ server }) {
try {
const userId = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`);
if (userId) {
const resume = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ userId }`);
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
await sdk.login({ resume });
const token = getDeviceToken();
if (token) {
await sdk.del('push.token', { token });
}
await sdk.logout();
}
await removeServerData({ server });
await removeServerDatabase({ server });
} catch (e) {
console.log('removePush', e);
}
}
export default async function logout({ server }) {
if (this.roomsSub) {
this.roomsSub.stop();
this.roomsSub = null;
}
if (this.activeUsersSubTimeout) {
clearTimeout(this.activeUsersSubTimeout);
this.activeUsersSubTimeout = false;
}
try {
await this.removePushToken();
} catch (e) {
console.log('removePushToken', e);
}
try {
// RC 0.60.0
await this.sdk.logout();
} catch (e) {
console.log('logout', e);
}
if (this.sdk) {
this.sdk = null;
}
await removeServerData({ server });
await removeCurrentServer();
await removeServerDatabase({ server });
}

View File

@ -3,7 +3,6 @@ import semver from 'semver';
import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk'; import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
import RNUserDefaults from 'rn-user-defaults'; import RNUserDefaults from 'rn-user-defaults';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import * as FileSystem from 'expo-file-system';
import reduxStore from './createStore'; import reduxStore from './createStore';
import defaultSettings from '../constants/settings'; import defaultSettings from '../constants/settings';
@ -11,8 +10,7 @@ import messagesStatus from '../constants/messagesStatus';
import database from './database'; import database from './database';
import log from '../utils/log'; import log from '../utils/log';
import { isIOS, getBundleId } from '../utils/deviceInfo'; import { isIOS, getBundleId } from '../utils/deviceInfo';
import { extractHostname } from '../utils/server'; import fetch from '../utils/fetch';
import fetch, { BASIC_AUTH_KEY } from '../utils/fetch';
import { setUser, setLoginServices, loginRequest } from '../actions/login'; import { setUser, setLoginServices, loginRequest } from '../actions/login';
import { disconnect, connectSuccess, connectRequest } from '../actions/connect'; import { disconnect, connectSuccess, connectRequest } from '../actions/connect';
@ -43,12 +41,13 @@ import sendMessage, { sendMessageCall } from './methods/sendMessage';
import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage'; import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage';
import callJitsi from './methods/callJitsi'; import callJitsi from './methods/callJitsi';
import logout, { removeServer } from './methods/logout';
import { getDeviceToken } from '../notifications/push'; import { getDeviceToken } from '../notifications/push';
import { SERVERS, SERVER_URL } from '../constants/userDefaults';
import { setActiveUsers } from '../actions/activeUsers'; import { setActiveUsers } from '../actions/activeUsers';
import I18n from '../i18n'; import I18n from '../i18n';
import { twoFactor } from '../utils/twoFactor'; import { twoFactor } from '../utils/twoFactor';
import { useSsl } from '../utils/url';
const TOKEN_KEY = 'reactnativemeteor_usertoken'; const TOKEN_KEY = 'reactnativemeteor_usertoken';
const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY'; const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
@ -86,10 +85,7 @@ const RocketChat = {
} }
}, },
async getWebsocketInfo({ server }) { async getWebsocketInfo({ server }) {
// Use useSsl: false only if server url starts with http:// const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
const useSsl = !/http:\/\//.test(server);
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
try { try {
await sdk.connect(); await sdk.connect();
@ -200,10 +196,7 @@ const RocketChat = {
this.code = null; this.code = null;
} }
// Use useSsl: false only if server url starts with http:// this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
const useSsl = !/http:\/\//.test(server);
this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
this.getSettings(); this.getSettings();
const sdkConnect = () => this.sdk.connect() const sdkConnect = () => this.sdk.connect()
@ -270,10 +263,7 @@ const RocketChat = {
this.shareSDK = null; this.shareSDK = null;
} }
// Use useSsl: false only if server url starts with http:// this.shareSDK = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
const useSsl = !/http:\/\//.test(server);
this.shareSDK = new RocketchatClient({ host: server, protocol: 'ddp', useSsl });
// set Server // set Server
const serversDB = database.servers; const serversDB = database.servers;
@ -408,73 +398,8 @@ const RocketChat = {
throw e; throw e;
} }
}, },
async logout({ server }) { logout,
if (this.roomsSub) { removeServer,
this.roomsSub.stop();
this.roomsSub = null;
}
if (this.activeUsersSubTimeout) {
clearTimeout(this.activeUsersSubTimeout);
this.activeUsersSubTimeout = false;
}
try {
await this.removePushToken();
} catch (error) {
console.log('logout -> removePushToken -> catch -> error', error);
}
try {
// RC 0.60.0
await this.sdk.logout();
} catch (error) {
console.log('logout -> api logout -> catch -> error', error);
}
this.sdk = null;
try {
const servers = await RNUserDefaults.objectForKey(SERVERS);
await RNUserDefaults.setObjectForKey(SERVERS, servers && servers.filter(srv => srv[SERVER_URL] !== server));
// clear certificate for server - SSL Pinning
const certificate = await RNUserDefaults.objectForKey(extractHostname(server));
if (certificate && certificate.path) {
await RNUserDefaults.clear(extractHostname(server));
await FileSystem.deleteAsync(certificate.path);
}
} catch (error) {
console.log('logout_rn_user_defaults', error);
}
const userId = await RNUserDefaults.get(`${ TOKEN_KEY }-${ server }`);
try {
const serversDB = database.servers;
await serversDB.action(async() => {
const usersCollection = serversDB.collections.get('users');
const userRecord = await usersCollection.find(userId);
const serverCollection = serversDB.collections.get('servers');
const serverRecord = await serverCollection.find(server);
await serversDB.batch(
userRecord.prepareDestroyPermanently(),
serverRecord.prepareDestroyPermanently()
);
});
} catch (error) {
// Do nothing
}
await RNUserDefaults.clear('currentServer');
await RNUserDefaults.clear(TOKEN_KEY);
await RNUserDefaults.clear(`${ TOKEN_KEY }-${ server }`);
await RNUserDefaults.clear(`${ BASIC_AUTH_KEY }-${ server }`);
try {
const db = database.active;
await db.action(() => db.unsafeResetDatabase());
} catch (error) {
console.log(error);
}
},
async clearCache({ server }) { async clearCache({ server }) {
try { try {
const serversDB = database.servers; const serversDB = database.servers;

View File

@ -1,13 +1,13 @@
import React from 'react'; import React from 'react';
import { Text, View, StyleSheet } from 'react-native'; import { Text, View, StyleSheet } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { LongPressGestureHandler, State } from 'react-native-gesture-handler';
import Avatar from '../containers/Avatar'; import Avatar from '../containers/Avatar';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import Touch from '../utils/touch'; import Touch from '../utils/touch';
import LongPress from '../utils/longPress';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
@ -41,38 +41,25 @@ const styles = StyleSheet.create({
const UserItem = ({ const UserItem = ({
name, username, onPress, testID, onLongPress, style, icon, baseUrl, user, theme name, username, onPress, testID, onLongPress, style, icon, baseUrl, user, theme
}) => { }) => (
const longPress = ({ nativeEvent }) => { <LongPress onLongPress={onLongPress}>
if (nativeEvent.state === State.ACTIVE) { <Touch
if (onLongPress) { onPress={onPress}
onLongPress(); style={{ backgroundColor: themes[theme].backgroundColor }}
} testID={testID}
} theme={theme}
};
return (
<LongPressGestureHandler
onHandlerStateChange={longPress}
minDurationMs={800}
> >
<Touch <View style={[styles.container, styles.button, style]}>
onPress={onPress} <Avatar text={username} size={30} type='d' style={styles.avatar} baseUrl={baseUrl} userId={user.id} token={user.token} />
style={{ backgroundColor: themes[theme].backgroundColor }} <View style={styles.textContainer}>
testID={testID} <Text style={[styles.name, { color: themes[theme].titleText }]} numberOfLines={1}>{name}</Text>
theme={theme} <Text style={[styles.username, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>@{username}</Text>
>
<View style={[styles.container, styles.button, style]}>
<Avatar text={username} size={30} type='d' style={styles.avatar} baseUrl={baseUrl} userId={user.id} token={user.token} />
<View style={styles.textContainer}>
<Text style={[styles.name, { color: themes[theme].titleText }]} numberOfLines={1}>{name}</Text>
<Text style={[styles.username, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>@{username}</Text>
</View>
{icon ? <CustomIcon name={icon} size={22} style={[styles.icon, { color: themes[theme].actionTintColor }]} /> : null}
</View> </View>
</Touch> {icon ? <CustomIcon name={icon} size={22} style={[styles.icon, { color: themes[theme].actionTintColor }]} /> : null}
</LongPressGestureHandler> </View>
); </Touch>
}; </LongPress>
);
UserItem.propTypes = { UserItem.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,

44
app/utils/longPress.js Normal file
View File

@ -0,0 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import { State, LongPressGestureHandler } from 'react-native-gesture-handler';
class LongPress extends React.Component {
setNativeProps(props) {
this.ref.setNativeProps(props);
}
getRef = (ref) => {
this.ref = ref;
};
longPress = ({ nativeEvent }) => {
const { onLongPress } = this.props;
if (nativeEvent.state === State.ACTIVE) {
if (onLongPress) {
onLongPress();
}
}
};
render() {
const { children, ...props } = this.props;
return (
<LongPressGestureHandler
onHandlerStateChange={this.longPress}
minDurationMs={800}
ref={this.getRef}
{...props}
>
{children}
</LongPressGestureHandler>
);
}
}
LongPress.propTypes = {
children: PropTypes.node,
onLongPress: PropTypes.func
};
export default LongPress;

View File

@ -7,3 +7,6 @@ export const isValidURL = (url) => {
+ '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator + '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator
return !!pattern.test(url); return !!pattern.test(url);
}; };
// Use useSsl: false only if server url starts with http://
export const useSsl = url => !/http:\/\//.test(url);

View File

@ -22,6 +22,8 @@ import { withTheme } from '../../theme';
import { KEY_COMMAND, handleCommandSelectServer } from '../../commands'; import { KEY_COMMAND, handleCommandSelectServer } from '../../commands';
import { isTablet } from '../../utils/deviceInfo'; import { isTablet } from '../../utils/deviceInfo';
import { withSplit } from '../../split'; import { withSplit } from '../../split';
import LongPress from '../../utils/longPress';
import { showConfirmationAlert } from '../../utils/info';
const ROW_HEIGHT = 68; const ROW_HEIGHT = 68;
const ANIMATION_DURATION = 200; const ANIMATION_DURATION = 200;
@ -132,7 +134,6 @@ class ServerDropdown extends Component {
const { const {
server: currentServer, selectServerRequest, navigation, split server: currentServer, selectServerRequest, navigation, split
} = this.props; } = this.props;
this.close(); this.close();
if (currentServer !== server) { if (currentServer !== server) {
const userId = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`); const userId = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`);
@ -152,6 +153,19 @@ class ServerDropdown extends Component {
} }
} }
remove = server => showConfirmationAlert({
message: I18n.t('This_will_remove_all_data_from_this_server'),
callToAction: I18n.t('Delete'),
onPress: async() => {
this.close();
try {
await RocketChat.removeServer({ server });
} catch {
// do nothing
}
}
});
handleCommands = ({ event }) => { handleCommands = ({ event }) => {
const { servers } = this.state; const { servers } = this.state;
const { navigation } = this.props; const { navigation } = this.props;
@ -173,35 +187,37 @@ class ServerDropdown extends Component {
const { server, theme } = this.props; const { server, theme } = this.props;
return ( return (
<Touch <LongPress onLongPress={() => (item.id === server || this.remove(item.id))}>
onPress={() => this.select(item.id)} <Touch
testID={`rooms-list-header-server-${ item.id }`} onPress={() => this.select(item.id)}
theme={theme} testID={`rooms-list-header-server-${ item.id }`}
> theme={theme}
<View style={styles.serverItemContainer}> >
{item.iconURL <View style={styles.serverItemContainer}>
? ( {item.iconURL
<Image ? (
source={{ uri: item.iconURL }} <Image
defaultSource={{ uri: 'logo' }} source={{ uri: item.iconURL }}
style={styles.serverIcon} defaultSource={{ uri: 'logo' }}
onError={() => console.warn('error loading serverIcon')} style={styles.serverIcon}
/> onError={() => console.warn('error loading serverIcon')}
) />
: ( )
<Image : (
source={{ uri: 'logo' }} <Image
style={styles.serverIcon} source={{ uri: 'logo' }}
/> style={styles.serverIcon}
) />
} )
<View style={styles.serverTextContainer}> }
<Text style={[styles.serverName, { color: themes[theme].titleText }]}>{item.name || item.id}</Text> <View style={styles.serverTextContainer}>
<Text style={[styles.serverUrl, { color: themes[theme].auxiliaryText }]}>{item.id}</Text> <Text style={[styles.serverName, { color: themes[theme].titleText }]}>{item.name || item.id}</Text>
<Text style={[styles.serverUrl, { color: themes[theme].auxiliaryText }]}>{item.id}</Text>
</View>
{item.id === server ? <Check theme={theme} /> : null}
</View> </View>
{item.id === server ? <Check theme={theme} /> : null} </Touch>
</View> </LongPress>
</Touch>
); );
} }