From 315d19d37a8c6bf464c08ee7e2653f61ce378978 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 26 Jun 2019 11:30:49 -0300 Subject: [PATCH 01/58] Bump version to 1.16.0 (#1014) --- android/app/build.gradle | 2 +- ios/RocketChatRN/Info.plist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index e531214f12a..46d8789ddcc 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,7 +110,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode VERSIONCODE as Integer - versionName "1.15.1" + versionName "1.16.0" vectorDrawables.useSupportLibrary = true } diff --git a/ios/RocketChatRN/Info.plist b/ios/RocketChatRN/Info.plist index aafefb84cce..65cabbc5ae8 100644 --- a/ios/RocketChatRN/Info.plist +++ b/ios/RocketChatRN/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.15.1 + 1.16.0 CFBundleSignature ???? CFBundleURLTypes From 255ea84599240d9cf8914f215a64a2cf0bf99ca4 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Wed, 26 Jun 2019 16:50:03 -0300 Subject: [PATCH 02/58] [IMPROVEMENT] Share credentials with Rocket.Chat.iOS (#982) * :sparkles: Create user table * :sparkles: Introduce user table * :fire: Remove unused table * :heavy_plus_sign: Add userdefaults to storage data * :green_heart: Fix android build * :sparkles: Get credentials from iOS native client * :fire: Remove unused code * :rewind: Revert sign xcode * :bug: Fix first login-logout * :art: Use constants to UserDefaults Keys * :bug: Fix clear server-user-info on logout * :bug: Fix filter null value * :ambulance: Remove user object in logout * :sparkles: Fix get servers from native-client * :ambulance: Fix error on change server --- app/constants/userDefaults.js | 6 +++ app/lib/realm.js | 15 ++++++++ app/lib/rocketchat.js | 28 +++++++++++--- app/sagas/deepLinking.js | 6 +-- app/sagas/init.js | 44 +++++++++++++++++++--- app/sagas/login.js | 18 +++++++-- app/sagas/selectServer.js | 24 ++++++++---- app/views/RoomsListView/ServerDropdown.js | 7 ++-- app/views/RoomsListView/index.js | 8 ++-- app/views/SidebarView/index.js | 2 +- ios/RocketChatRN.xcodeproj/project.pbxproj | 30 +++++++++++++++ ios/RocketChatRN/RocketChatRN.entitlements | 4 ++ package.json | 1 + yarn.lock | 5 +++ 14 files changed, 166 insertions(+), 32 deletions(-) create mode 100644 app/constants/userDefaults.js diff --git a/app/constants/userDefaults.js b/app/constants/userDefaults.js new file mode 100644 index 00000000000..e21b7b852d0 --- /dev/null +++ b/app/constants/userDefaults.js @@ -0,0 +1,6 @@ +export const SERVERS = 'kServers'; +export const TOKEN = 'kAuthToken'; +export const USER_ID = 'kUserId'; +export const SERVER_URL = 'kAuthServerURL'; +export const SERVER_NAME = 'kServerName'; +export const SERVER_ICON = 'kServerIconURL'; diff --git a/app/lib/realm.js b/app/lib/realm.js index f296752b856..93ebf00dc69 100644 --- a/app/lib/realm.js +++ b/app/lib/realm.js @@ -4,6 +4,20 @@ import Realm from 'realm'; // Realm.clearTestState(); // AsyncStorage.clear(); +const userSchema = { + name: 'user', + primaryKey: 'id', + properties: { + id: 'string', + token: { type: 'string', optional: true }, + username: { type: 'string', optional: true }, + name: { type: 'string', optional: true }, + language: { type: 'string', optional: true }, + status: { type: 'string', optional: true }, + roles: { type: 'string[]', optional: true } + } +}; + const serversSchema = { name: 'servers', primaryKey: 'id', @@ -370,6 +384,7 @@ class DB { serversDB: new Realm({ path: 'default.realm', schema: [ + userSchema, serversSchema ], schemaVersion: 8, diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index a0118f5e79c..673c3358afb 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -1,6 +1,7 @@ import { AsyncStorage, InteractionManager } from 'react-native'; import semver from 'semver'; import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk'; +import RNUserDefaults from 'rn-user-defaults'; import reduxStore from './createStore'; import defaultSettings from '../constants/settings'; @@ -36,6 +37,7 @@ import sendMessage, { getMessage, sendMessageCall } from './methods/sendMessage' import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage'; import { getDeviceToken } from '../notifications/push'; +import { SERVERS, SERVER_URL } from '../constants/userDefaults'; const TOKEN_KEY = 'reactnativemeteor_usertoken'; const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY'; @@ -58,9 +60,9 @@ const RocketChat = { }, async getUserToken() { try { - return await AsyncStorage.getItem(TOKEN_KEY); + return await RNUserDefaults.get(TOKEN_KEY); } catch (error) { - console.warn(`AsyncStorage error: ${ error.message }`); + console.warn(`RNUserDefaults error: ${ error.message }`); } }, async getServerInfo(server) { @@ -321,10 +323,26 @@ const RocketChat = { } this.sdk = null; + try { + const servers = await RNUserDefaults.objectForKey(SERVERS); + await RNUserDefaults.setObjectForKey(SERVERS, servers && servers.filter(srv => srv[SERVER_URL] !== server)); + } catch (error) { + console.log('logout_rn_user_defaults', error); + } + + const { serversDB } = database.databases; + + const userId = await RNUserDefaults.get(`${ TOKEN_KEY }-${ server }`); + + serversDB.write(() => { + const user = serversDB.objectForPrimaryKey('user', userId); + serversDB.delete(user); + }); + Promise.all([ - AsyncStorage.removeItem('currentServer'), - AsyncStorage.removeItem(TOKEN_KEY), - AsyncStorage.removeItem(`${ TOKEN_KEY }-${ server }`) + RNUserDefaults.clear('currentServer'), + RNUserDefaults.clear(TOKEN_KEY), + RNUserDefaults.clear(`${ TOKEN_KEY }-${ server }`) ]).catch(error => console.log(error)); try { diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js index de7bed5948b..04eccfca946 100644 --- a/app/sagas/deepLinking.js +++ b/app/sagas/deepLinking.js @@ -1,8 +1,8 @@ -import { AsyncStorage } from 'react-native'; import { delay } from 'redux-saga'; import { takeLatest, take, select, put, all } from 'redux-saga/effects'; +import RNUserDefaults from 'rn-user-defaults'; import Navigation from '../lib/Navigation'; import * as types from '../actions/actionsTypes'; @@ -43,8 +43,8 @@ const handleOpen = function* handleOpen({ params }) { } const [server, user] = yield all([ - AsyncStorage.getItem('currentServer'), - AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ host }`) + RNUserDefaults.get('currentServer'), + RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ host }`) ]); // TODO: needs better test diff --git a/app/sagas/init.js b/app/sagas/init.js index 4d92b3fa237..0420d58de0f 100644 --- a/app/sagas/init.js +++ b/app/sagas/init.js @@ -1,6 +1,6 @@ -import { AsyncStorage } from 'react-native'; import { put, takeLatest, all } from 'redux-saga/effects'; import SplashScreen from 'react-native-splash-screen'; +import RNUserDefaults from 'rn-user-defaults'; import * as actions from '../actions'; import { selectServerRequest } from '../actions/server'; @@ -11,14 +11,46 @@ import RocketChat from '../lib/rocketchat'; import log from '../utils/log'; import Navigation from '../lib/Navigation'; import database from '../lib/realm'; +import { + SERVERS, SERVER_ICON, SERVER_NAME, SERVER_URL, TOKEN, USER_ID +} from '../constants/userDefaults'; const restore = function* restore() { try { - const { token, server } = yield all({ - token: AsyncStorage.getItem(RocketChat.TOKEN_KEY), - server: AsyncStorage.getItem('currentServer') + yield RNUserDefaults.setName('group.ios.chat.rocket'); + + let { token, server } = yield all({ + token: RNUserDefaults.get(RocketChat.TOKEN_KEY), + server: RNUserDefaults.get('currentServer') }); + // get native credentials + const { serversDB } = database.databases; + const servers = yield RNUserDefaults.objectForKey(SERVERS); + if (servers) { + serversDB.write(() => { + servers.forEach(async(serverItem) => { + const serverInfo = { + id: serverItem[SERVER_URL], + name: serverItem[SERVER_NAME], + iconURL: serverItem[SERVER_ICON] + }; + try { + serversDB.create('servers', serverInfo, true); + await RNUserDefaults.set(`${ RocketChat.TOKEN_KEY }-${ serverInfo.id }`, serverItem[USER_ID]); + } catch (e) { + log('err_create_servers', e); + } + }); + }); + } + + // if not have current + if (servers.length !== 0 && (!token || !server)) { + server = servers[0][SERVER_URL]; + token = servers[0][TOKEN]; + } + const sortPreferences = yield RocketChat.getSortPreferences(); yield put(setAllPreferences(sortPreferences)); @@ -27,8 +59,8 @@ const restore = function* restore() { if (!token || !server) { yield all([ - AsyncStorage.removeItem(RocketChat.TOKEN_KEY), - AsyncStorage.removeItem('currentServer') + RNUserDefaults.clear(RocketChat.TOKEN_KEY), + RNUserDefaults.clear('currentServer') ]); yield put(actions.appStart('outside')); } else if (server) { diff --git a/app/sagas/login.js b/app/sagas/login.js index 66b4183adb7..369f636ff83 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -1,7 +1,7 @@ -import { AsyncStorage } from 'react-native'; import { put, call, takeLatest, select, take, fork, cancel } from 'redux-saga/effects'; +import RNUserDefaults from 'rn-user-defaults'; import * as types from '../actions/actionsTypes'; import { appStart } from '../actions'; @@ -60,7 +60,7 @@ const fetchUserPresence = function* fetchUserPresence() { const handleLoginSuccess = function* handleLoginSuccess({ user }) { try { const adding = yield select(state => state.server.adding); - yield AsyncStorage.setItem(RocketChat.TOKEN_KEY, user.token); + yield RNUserDefaults.set(RocketChat.TOKEN_KEY, user.token); const server = yield select(getServer); yield put(roomsRequest()); @@ -72,7 +72,17 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) { yield fork(fetchUserPresence); I18n.locale = user.language; - yield AsyncStorage.setItem(`${ RocketChat.TOKEN_KEY }-${ server }`, JSON.stringify(user)); + + const { serversDB } = database.databases; + serversDB.write(() => { + try { + serversDB.create('user', user, true); + } catch (e) { + log('err_set_user_token', e); + } + }); + + yield RNUserDefaults.set(`${ RocketChat.TOKEN_KEY }-${ server }`, user.id); yield put(setUser(user)); EventEmitter.emit('connected'); @@ -105,7 +115,7 @@ const handleLogout = function* handleLogout() { // see if there's other logged in servers and selects first one if (servers.length > 0) { const newServer = servers[0].id; - const token = yield AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ newServer }`); + const token = yield RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ newServer }`); if (token) { return yield put(selectServerRequest(newServer)); } diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js index b6f1efec538..f6793f998db 100644 --- a/app/sagas/selectServer.js +++ b/app/sagas/selectServer.js @@ -1,7 +1,8 @@ import { put, take, takeLatest, fork, cancel, race } from 'redux-saga/effects'; -import { AsyncStorage, Alert } from 'react-native'; +import { Alert } from 'react-native'; +import RNUserDefaults from 'rn-user-defaults'; import Navigation from '../lib/Navigation'; import { SERVER } from '../actions/actionsTypes'; @@ -14,6 +15,7 @@ import RocketChat from '../lib/rocketchat'; import database from '../lib/realm'; import log from '../utils/log'; import I18n from '../i18n'; +import { SERVERS, TOKEN, SERVER_URL } from '../constants/userDefaults'; const getServerInfo = function* getServerInfo({ server, raiseError = true }) { try { @@ -38,13 +40,21 @@ const getServerInfo = function* getServerInfo({ server, raiseError = true }) { const handleSelectServer = function* handleSelectServer({ server, version, fetchVersion }) { try { - yield AsyncStorage.setItem('currentServer', server); - const userStringified = yield AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ server }`); + const { serversDB } = database.databases; - if (userStringified) { - const user = JSON.parse(userStringified); - yield RocketChat.connect({ server, user }); - yield put(setUser(user)); + yield RNUserDefaults.set('currentServer', server); + const userId = yield RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`); + const user = userId && serversDB.objectForPrimaryKey('user', userId); + + const servers = yield RNUserDefaults.objectForKey(SERVERS); + const userCredentials = servers && servers.find(srv => srv[SERVER_URL] === server); + const userLogin = userCredentials && { + token: userCredentials[TOKEN] + }; + + if (user || userLogin) { + yield RocketChat.connect({ server, user: user || userLogin }); + yield put(setUser(user || userLogin)); yield put(actions.appStart('inside')); } else { yield RocketChat.connect({ server }); diff --git a/app/views/RoomsListView/ServerDropdown.js b/app/views/RoomsListView/ServerDropdown.js index 4bc8db9d11b..60e161d08a7 100644 --- a/app/views/RoomsListView/ServerDropdown.js +++ b/app/views/RoomsListView/ServerDropdown.js @@ -1,11 +1,12 @@ import React, { Component } from 'react'; import { - View, Text, Animated, Easing, TouchableWithoutFeedback, TouchableOpacity, FlatList, Image, AsyncStorage + View, Text, Animated, Easing, TouchableWithoutFeedback, TouchableOpacity, FlatList, Image } from 'react-native'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import equal from 'deep-equal'; import { withNavigation } from 'react-navigation'; +import RNUserDefaults from 'rn-user-defaults'; import { toggleServerDropdown as toggleServerDropdownAction } from '../../actions/rooms'; import { selectServerRequest as selectServerRequestAction } from '../../actions/server'; @@ -124,8 +125,8 @@ class ServerDropdown extends Component { this.close(); if (currentServer !== server) { - const token = await AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ server }`); - if (!token) { + const userId = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`); + if (!userId) { appStart(); this.newServerTimeout = setTimeout(() => { EventEmitter.emit('NewServer', { server }); diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js index a8297a166f3..ee5aa0fcfbd 100644 --- a/app/views/RoomsListView/index.js +++ b/app/views/RoomsListView/index.js @@ -39,6 +39,7 @@ const keyExtractor = item => item.rid; @connect(state => ({ userId: state.login.user && state.login.user.id, + isAuthenticated: state.login.isAuthenticated, server: state.server.server, baseUrl: state.settings.baseUrl || state.server ? state.server.server : '', searchText: state.rooms.searchText, @@ -111,7 +112,8 @@ export default class RoomsListView extends React.Component { openSearchHeader: PropTypes.func, closeSearchHeader: PropTypes.func, appStart: PropTypes.func, - roomsRequest: PropTypes.func + roomsRequest: PropTypes.func, + isAuthenticated: PropTypes.bool } constructor(props) { @@ -187,7 +189,7 @@ export default class RoomsListView extends React.Component { componentDidUpdate(prevProps) { const { - sortBy, groupByType, showFavorites, showUnread, appState, roomsRequest + sortBy, groupByType, showFavorites, showUnread, appState, roomsRequest, isAuthenticated } = this.props; if (!( @@ -197,7 +199,7 @@ export default class RoomsListView extends React.Component { && (prevProps.showUnread === showUnread) )) { this.getSubscriptions(); - } else if (appState === 'foreground' && appState !== prevProps.appState) { + } else if (appState === 'foreground' && appState !== prevProps.appState && isAuthenticated) { roomsRequest(); } } diff --git a/app/views/SidebarView/index.js b/app/views/SidebarView/index.js index 49a0fa9e728..2249e8ad5db 100644 --- a/app/views/SidebarView/index.js +++ b/app/views/SidebarView/index.js @@ -152,7 +152,7 @@ export default class Sidebar extends Component { const permissionsFiltered = database.objects('permissions') .filter(permission => permissions.includes(permission._id)); return permissionsFiltered.reduce((result, permission) => ( - result || permission.roles.some(r => roles.includes(r))), + result || permission.roles.some(r => roles.indexOf(r) !== -1)), false); } return false; diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj index f879bc003fa..a9a733da8a5 100644 --- a/ios/RocketChatRN.xcodeproj/project.pbxproj +++ b/ios/RocketChatRN.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; + 1E02221122B2F76B00001862 /* libRNUserDefaults.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E02220D22B2F76400001862 /* libRNUserDefaults.a */; }; 24A2AEF2383D44B586D31C01 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 06BB44DD4855498082A744AD /* libz.tbd */; }; 38CEA0ED468E49CFABCD82FD /* libRNFirebase.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A36F9982B71E4662AA8DEB77 /* libRNFirebase.a */; }; 50046CB6BDA69B9232CF66D9 /* libPods-RocketChatRN.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C235DC7B31A4D1578EDEF219 /* libPods-RocketChatRN.a */; }; @@ -99,6 +100,13 @@ remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; remoteInfo = React; }; + 1E02220C22B2F76400001862 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1E0221D522B2F76300001862 /* RNUserDefaults.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RNUserDefaults; + }; 3DAD3E831DF850E9000B6D8A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; @@ -452,6 +460,7 @@ 1845C223DA364898A8400573 /* FastImage.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = FastImage.xcodeproj; path = "../node_modules/react-native-fast-image/ios/FastImage.xcodeproj"; sourceTree = ""; }; 1A34D902CC074FF1BCC7DB48 /* libimageCropPicker.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libimageCropPicker.a; sourceTree = ""; }; 1D3BB00B9ABF44EA9BD71318 /* libSafariViewManager.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libSafariViewManager.a; sourceTree = ""; }; + 1E0221D522B2F76300001862 /* RNUserDefaults.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNUserDefaults.xcodeproj; path = "../node_modules/rn-user-defaults/ios/RNUserDefaults.xcodeproj"; sourceTree = ""; }; 20CE3E407E0D4D9E8C9885F2 /* libRCTVideo.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRCTVideo.a; sourceTree = ""; }; 22A8B76C8EBA443BB97CE82D /* RNVectorIcons.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNVectorIcons.xcodeproj; path = "../node_modules/react-native-vector-icons/RNVectorIcons.xcodeproj"; sourceTree = ""; }; 22D3971EAF2E4660B4FAB3DD /* RNI18n.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNI18n.xcodeproj; path = "../node_modules/react-native-i18n/ios/RNI18n.xcodeproj"; sourceTree = ""; }; @@ -492,6 +501,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 1E02221122B2F76B00001862 /* libRNUserDefaults.a in Frameworks */, 7ACD4897222860DE00442C55 /* JavaScriptCore.framework in Frameworks */, 7A8DEB5A20ED0BEC00C5DCE4 /* libRNNotifications.a in Frameworks */, B8971BB2202A093B0000D245 /* libKeyboardTrackingView.a in Frameworks */, @@ -626,6 +636,14 @@ name = Products; sourceTree = ""; }; + 1E0221D622B2F76300001862 /* Products */ = { + isa = PBXGroup; + children = ( + 1E02220D22B2F76400001862 /* libRNUserDefaults.a */, + ); + name = Products; + sourceTree = ""; + }; 22CA7F59107E0C79C2506C7C /* Pods */ = { isa = PBXGroup; children = ( @@ -731,6 +749,7 @@ 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( + 1E0221D522B2F76300001862 /* RNUserDefaults.xcodeproj */, 7A8DEB1B20ED0BDE00C5DCE4 /* RNNotifications.xcodeproj */, B8971BAC202A091D0000D245 /* KeyboardTrackingView.xcodeproj */, 7A430E1620238C01008F55BC /* RCTCustomInputController.xcodeproj */, @@ -1016,6 +1035,10 @@ ProductGroup = 7A8DEB1C20ED0BDE00C5DCE4 /* Products */; ProjectRef = 7A8DEB1B20ED0BDE00C5DCE4 /* RNNotifications.xcodeproj */; }, + { + ProductGroup = 1E0221D622B2F76300001862 /* Products */; + ProjectRef = 1E0221D522B2F76300001862 /* RNUserDefaults.xcodeproj */; + }, { ProductGroup = B8E79A8A1F3CCC6C005B464F /* Products */; ProjectRef = 22A8B76C8EBA443BB97CE82D /* RNVectorIcons.xcodeproj */; @@ -1085,6 +1108,13 @@ remoteRef = 146834031AC3E56700842450 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 1E02220D22B2F76400001862 /* libRNUserDefaults.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRNUserDefaults.a; + remoteRef = 1E02220C22B2F76400001862 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 3DAD3E841DF850E9000B6D8A /* libRCTImage-tvOS.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/ios/RocketChatRN/RocketChatRN.entitlements b/ios/RocketChatRN/RocketChatRN.entitlements index 1484e4f68b9..dce5d2deca4 100644 --- a/ios/RocketChatRN/RocketChatRN.entitlements +++ b/ios/RocketChatRN/RocketChatRN.entitlements @@ -8,5 +8,9 @@ applinks:go.rocket.chat + com.apple.security.application-groups + + group.ios.chat.rocket + diff --git a/package.json b/package.json index befd083576a..43d49492e96 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "redux-saga": "^0.16.2", "remove-markdown": "^0.3.0", "rn-fetch-blob": "^0.10.15", + "rn-user-defaults": "^1.3.4", "semver": "6.0.0", "snyk": "^1.156.0", "strip-ansi": "5.2.0" diff --git a/yarn.lock b/yarn.lock index f418405c55f..f077b08aeec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12550,6 +12550,11 @@ rn-fetch-blob@^0.10.15: base-64 "0.1.0" glob "7.0.6" +rn-user-defaults@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/rn-user-defaults/-/rn-user-defaults-1.3.4.tgz#1fbdd1bf29d9f853918dca5219e45db54d19fe37" + integrity sha512-CnzZbq3Q1VQUr6wKl9Z48eKmOzu+6dMcl2wN0ty+sBnpUyIqFvv3CMzYzb26v1qK8z7q/PcMr75shFHcrJH9WA== + rsvp@^3.3.3: version "3.6.2" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" From 636518b6b335d8cc07911eee825e3a337fb13fb5 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Thu, 27 Jun 2019 09:35:20 -0300 Subject: [PATCH 03/58] [FIX] Don't run UserDefaults credentials on Android (#1015) * :bug: Fix native credentials (android) * Fix migration loop --- app/sagas/init.js | 55 +++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/app/sagas/init.js b/app/sagas/init.js index 0420d58de0f..5218de5f111 100644 --- a/app/sagas/init.js +++ b/app/sagas/init.js @@ -1,3 +1,4 @@ +import { AsyncStorage } from 'react-native'; import { put, takeLatest, all } from 'redux-saga/effects'; import SplashScreen from 'react-native-splash-screen'; import RNUserDefaults from 'rn-user-defaults'; @@ -14,10 +15,15 @@ import database from '../lib/realm'; import { SERVERS, SERVER_ICON, SERVER_NAME, SERVER_URL, TOKEN, USER_ID } from '../constants/userDefaults'; +import { isIOS } from '../utils/deviceInfo'; const restore = function* restore() { try { - yield RNUserDefaults.setName('group.ios.chat.rocket'); + let hasMigration; + if (isIOS) { + yield RNUserDefaults.setName('group.ios.chat.rocket'); + hasMigration = yield AsyncStorage.getItem('hasMigration'); + } let { token, server } = yield all({ token: RNUserDefaults.get(RocketChat.TOKEN_KEY), @@ -25,30 +31,33 @@ const restore = function* restore() { }); // get native credentials - const { serversDB } = database.databases; - const servers = yield RNUserDefaults.objectForKey(SERVERS); - if (servers) { - serversDB.write(() => { - servers.forEach(async(serverItem) => { - const serverInfo = { - id: serverItem[SERVER_URL], - name: serverItem[SERVER_NAME], - iconURL: serverItem[SERVER_ICON] - }; - try { - serversDB.create('servers', serverInfo, true); - await RNUserDefaults.set(`${ RocketChat.TOKEN_KEY }-${ serverInfo.id }`, serverItem[USER_ID]); - } catch (e) { - log('err_create_servers', e); - } + if (isIOS && !hasMigration) { + const { serversDB } = database.databases; + const servers = yield RNUserDefaults.objectForKey(SERVERS); + if (servers) { + serversDB.write(() => { + servers.forEach(async(serverItem) => { + const serverInfo = { + id: serverItem[SERVER_URL], + name: serverItem[SERVER_NAME], + iconURL: serverItem[SERVER_ICON] + }; + try { + serversDB.create('servers', serverInfo, true); + await RNUserDefaults.set(`${ RocketChat.TOKEN_KEY }-${ serverInfo.id }`, serverItem[USER_ID]); + } catch (e) { + log('err_create_servers', e); + } + }); }); - }); - } + yield AsyncStorage.setItem('hasMigration', true); + } - // if not have current - if (servers.length !== 0 && (!token || !server)) { - server = servers[0][SERVER_URL]; - token = servers[0][TOKEN]; + // if not have current + if (servers && servers.length !== 0 && (!token || !server)) { + server = servers[0][SERVER_URL]; + token = servers[0][TOKEN]; + } } const sortPreferences = yield RocketChat.getSortPreferences(); From fc264f0601b6a9650ab392cdcc91d14062fa0874 Mon Sep 17 00:00:00 2001 From: IlarionHalushka Date: Thu, 27 Jun 2019 19:16:05 +0300 Subject: [PATCH 04/58] [IMPROVEMENT] Hide frequently used emoji tab when empty (#792) --- app/containers/EmojiPicker/index.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/containers/EmojiPicker/index.js b/app/containers/EmojiPicker/index.js index 3fe6f9371a1..cfe57255987 100644 --- a/app/containers/EmojiPicker/index.js +++ b/app/containers/EmojiPicker/index.js @@ -141,7 +141,7 @@ export default class EmojiPicker extends Component { } render() { - const { show } = this.state; + const { show, frequentlyUsed } = this.state; const { tabEmojiStyle } = this.props; if (!show) { @@ -155,15 +155,17 @@ export default class EmojiPicker extends Component { > { categories.tabs.map((tab, i) => ( - - {this.renderCategory(tab.category, i)} - - )) + (i === 0 && frequentlyUsed.length === 0) ? null // when no frequentlyUsed don't show the tab + : ( + + {this.renderCategory(tab.category, i)} + + ))) } ); From 013c151c10702c44f497e81f63b3b1b182af609c Mon Sep 17 00:00:00 2001 From: IlarionHalushka Date: Thu, 27 Jun 2019 19:35:45 +0300 Subject: [PATCH 05/58] [IMPROVEMENT] Bigger emoji in emoji only messages (#793) * issue #725: bigger emoji in emoji only message * issue-725/add storybook for Message/Emoji * issue-725: update storybook/Message jest snapshot * comment storybook import * allow spaces and line breaks in emoji only message * merge develop * revert unnecessary spacing --- .../__snapshots__/Storyshots.test.js.snap | 871 +++++++++++++++++- app/containers/message/Markdown.js | 41 +- app/containers/message/styles.js | 9 + storybook/stories/Message.js | 12 + 4 files changed, 923 insertions(+), 10 deletions(-) diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 335bfb8b390..bbf417d5635 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -2780,7 +2780,7 @@ exports[`Storyshots Message list 1`] = ` "backgroundColor": "transparent", "color": "#2F343D", "fontFamily": "System", - "fontSize": 16, + "fontSize": 46, "fontWeight": "400", } } @@ -2812,7 +2812,7 @@ exports[`Storyshots Message list 1`] = ` ] } > - Custom Emojis + Single Emoji + + 👏 + + + + + + + + + + + Custom Emojis + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + @@ -3013,8 +3217,8 @@ exports[`Storyshots Message list 1`] = ` } style={ Object { - "height": 20, - "width": 20, + "height": 50, + "width": 50, } } /> @@ -3029,8 +3233,8 @@ exports[`Storyshots Message list 1`] = ` } style={ Object { - "height": 20, - "width": 20, + "height": 50, + "width": 50, } } /> @@ -3042,6 +3246,657 @@ exports[`Storyshots Message list 1`] = ` + + Single Custom Emojis + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + + + + + + + + + + + Normal Emoji + Custom Emojis + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + + 🤙 + + + + + + + + + + + + Four emoji + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + + 🤙 + + + + 🤙🤙 + + + + + + + + + text.replace( (match, url, title) => `[${ title }](${ url })` ); +const emojiRanges = [ + '\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]', // unicode emoji from https://www.regextester.com/106421 + ':.{1,40}:', // custom emoji + ' |\n' // allow spaces and line breaks +].join('|'); + +const removeAllEmoji = str => str.replace(new RegExp(emojiRanges, 'g'), ''); + +const isOnlyEmoji = str => !removeAllEmoji(str).length; + +const removeOneEmoji = str => str.replace(new RegExp(emojiRanges), ''); + +const emojiCount = (str) => { + let oldLength = 0; + let counter = 0; + + while (oldLength !== str.length) { + oldLength = str.length; + str = removeOneEmoji(str); + if (oldLength !== str.length) { + counter += 1; + } + } + + return counter; +}; + const Markdown = React.memo(({ msg, style, rules, baseUrl, username, isEdited, numberOfLines, mentions, channels, getCustomEmoji, useMarkdown = true }) => { @@ -30,6 +57,7 @@ const Markdown = React.memo(({ if (m) { m = emojify(m, { output: 'unicode' }); } + m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)/, '').trim(); if (numberOfLines > 0) { m = m.replace(/[\n]+/g, '\n').trim(); @@ -39,6 +67,8 @@ const Markdown = React.memo(({ return {m}; } + const isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3; + return ( ; + return ( + + ); } return :{content}:; } @@ -102,7 +139,7 @@ const Markdown = React.memo(({ }} style={{ paragraph: styles.paragraph, - text: styles.text, + text: isMessageContainsOnlyEmoji ? styles.textBig : styles.text, codeInline: styles.codeInline, codeBlock: styles.codeBlock, link: styles.link, diff --git a/app/containers/message/styles.js b/app/containers/message/styles.js index c08467ef094..6939b3abd3c 100644 --- a/app/containers/message/styles.js +++ b/app/containers/message/styles.js @@ -39,6 +39,11 @@ export default StyleSheet.create({ ...sharedStyles.textColorNormal, ...sharedStyles.textRegular }, + textBig: { + fontSize: 46, + ...sharedStyles.textColorNormal, + ...sharedStyles.textRegular + }, textInfo: { fontStyle: 'italic', fontSize: 16, @@ -49,6 +54,10 @@ export default StyleSheet.create({ width: 20, height: 20 }, + customEmojiBig: { + width: 50, + height: 50 + }, temp: { opacity: 0.3 }, marginTop: { marginTop: 6 diff --git a/storybook/stories/Message.js b/storybook/stories/Message.js index 87c8213846a..79511c74491 100644 --- a/storybook/stories/Message.js +++ b/storybook/stories/Message.js @@ -128,9 +128,21 @@ export default ( + + + + + + + + + + + + From d017348dfec431f33f533a8f65447a60650d8bd3 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Thu, 27 Jun 2019 14:49:44 -0300 Subject: [PATCH 06/58] [FIX] Empty message if contains only a link (#787) * Fix empty message if contains only a link * :bug: Fix empty space --- app/containers/message/Markdown.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/containers/message/Markdown.js b/app/containers/message/Markdown.js index 86fc7d63a25..2431d0bec93 100644 --- a/app/containers/message/Markdown.js +++ b/app/containers/message/Markdown.js @@ -57,8 +57,10 @@ const Markdown = React.memo(({ if (m) { m = emojify(m, { output: 'unicode' }); } - - m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)/, '').trim(); + const matched = m.match(/^\[([^\]]*)\]\(([^)]*)\)\1/); + if (matched && matched[0] !== msg) { + m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)/, '').trim(); + } if (numberOfLines > 0) { m = m.replace(/[\n]+/g, '\n').trim(); } From 89311180323e0474211d88f62db473e6b95e0cb5 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Fri, 28 Jun 2019 09:33:39 -0300 Subject: [PATCH 07/58] [IMPROVEMENT] Refactor empty space regex on quote (#1017) * :art: Improve regex to empty space on quote * :art: Improve on regex to empty space on quote --- app/containers/message/Markdown.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/containers/message/Markdown.js b/app/containers/message/Markdown.js index 2431d0bec93..403af4872fb 100644 --- a/app/containers/message/Markdown.js +++ b/app/containers/message/Markdown.js @@ -57,10 +57,7 @@ const Markdown = React.memo(({ if (m) { m = emojify(m, { output: 'unicode' }); } - const matched = m.match(/^\[([^\]]*)\]\(([^)]*)\)\1/); - if (matched && matched[0] !== msg) { - m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)/, '').trim(); - } + m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)\s/, '').trim(); if (numberOfLines > 0) { m = m.replace(/[\n]+/g, '\n').trim(); } From 3ca1bdf7d12673db82e54dd660b962631b0c09c2 Mon Sep 17 00:00:00 2001 From: pranavpandey1998official <44601530+pranavpandey1998official@users.noreply.github.com> Date: Fri, 28 Jun 2019 18:10:23 +0530 Subject: [PATCH 08/58] [NEW] Custom fields on signup (#1013) * added custom feilds on registration * added flag as leftIcon and removed lable * added try and catch * typo --- app/views/RegisterView.js | 118 ++++++++++++++++++++++++++++++++++---- 1 file changed, 106 insertions(+), 12 deletions(-) diff --git a/app/views/RegisterView.js b/app/views/RegisterView.js index 6c199a3704c..abb8f52b3b1 100644 --- a/app/views/RegisterView.js +++ b/app/views/RegisterView.js @@ -5,6 +5,8 @@ import { } from 'react-native'; import { connect } from 'react-redux'; import { SafeAreaView } from 'react-navigation'; +import RNPickerSelect from 'react-native-picker-select'; +import equal from 'deep-equal'; import TextInput from '../containers/TextInput'; import Button from '../containers/Button'; @@ -17,10 +19,13 @@ import { loginRequest as loginRequestAction } from '../actions/login'; import isValidEmail from '../utils/isValidEmail'; import { LegalButton } from '../containers/HeaderButton'; import StatusBar from '../containers/StatusBar'; +import log from '../utils/log'; const shouldUpdateState = ['name', 'email', 'password', 'username', 'saving']; -@connect(null, dispatch => ({ +@connect(state => ({ + Accounts_CustomFields: state.settings.Accounts_CustomFields +}), dispatch => ({ loginRequest: params => dispatch(loginRequestAction(params)) })) export default class RegisterView extends React.Component { @@ -35,15 +40,34 @@ export default class RegisterView extends React.Component { static propTypes = { navigation: PropTypes.object, loginRequest: PropTypes.func, - Site_Name: PropTypes.string + Site_Name: PropTypes.string, + Accounts_CustomFields: PropTypes.string } - state = { - name: '', - email: '', - password: '', - username: '', - saving: false + constructor(props) { + super(props); + const customFields = {}; + this.parsedCustomFields = {}; + if (props.Accounts_CustomFields) { + try { + this.parsedCustomFields = JSON.parse(props.Accounts_CustomFields); + } catch (e) { + log('err_parsing_account_custom_fields', e); + } + } + Object.keys(this.parsedCustomFields).forEach((key) => { + if (this.parsedCustomFields[key].defaultValue) { + customFields[key] = this.parsedCustomFields[key].defaultValue; + } + }); + this.state = { + name: '', + email: '', + password: '', + username: '', + saving: false, + customFields + }; } componentDidMount() { @@ -53,6 +77,10 @@ export default class RegisterView extends React.Component { } shouldComponentUpdate(nextProps, nextState) { + const { customFields } = this.state; + if (!equal(nextState.customFields, customFields)) { + return true; + } // eslint-disable-next-line react/destructuring-assignment return shouldUpdateState.some(key => nextState[key] !== this.state[key]); } @@ -77,9 +105,15 @@ export default class RegisterView extends React.Component { valid = () => { const { - name, email, password, username + name, email, password, username, customFields } = this.state; - return name.trim() && email.trim() && password.trim() && username.trim() && isValidEmail(email); + let requiredCheck = true; + Object.keys(this.parsedCustomFields).forEach((key) => { + if (this.parsedCustomFields[key].required) { + requiredCheck = requiredCheck && customFields[key] && Boolean(customFields[key].trim()); + } + }); + return name.trim() && email.trim() && password.trim() && username.trim() && isValidEmail(email) && requiredCheck; } submit = async() => { @@ -90,13 +124,13 @@ export default class RegisterView extends React.Component { Keyboard.dismiss(); const { - name, email, password, username + name, email, password, username, customFields } = this.state; const { loginRequest } = this.props; try { await RocketChat.register({ - name, email, pass: password, username + name, email, pass: password, username, ...customFields }); await loginRequest({ user: email, password }); } catch (e) { @@ -105,6 +139,64 @@ export default class RegisterView extends React.Component { this.setState({ saving: false }); } + renderCustomFields = () => { + const { customFields } = this.state; + const { Accounts_CustomFields } = this.props; + if (!Accounts_CustomFields) { + return null; + } + try { + return Object.keys(this.parsedCustomFields).map((key, index, array) => { + if (this.parsedCustomFields[key].type === 'select') { + const options = this.parsedCustomFields[key].options.map(option => ({ label: option, value: option })); + return ( + { + const newValue = {}; + newValue[key] = value; + this.setState({ customFields: { ...customFields, ...newValue } }); + }} + value={customFields[key]} + > + { this[key] = e; }} + placeholder={key} + value={customFields[key]} + iconLeft='flag' + testID='register-view-custom-picker' + /> + + ); + } + + return ( + { this[key] = e; }} + key={key} + placeholder={key} + value={customFields[key]} + iconLeft='flag' + onChangeText={(value) => { + const newValue = {}; + newValue[key] = value; + this.setState({ customFields: { ...customFields, ...newValue } }); + }} + onSubmitEditing={() => { + if (array.length - 1 > index) { + return this[array[index + 1]].focus(); + } + this.avatarUrl.focus(); + }} + /> + ); + }); + } catch (error) { + return null; + } + } + render() { const { saving } = this.state; return ( @@ -153,6 +245,8 @@ export default class RegisterView extends React.Component { containerStyle={sharedStyles.inputLastChild} /> + {this.renderCustomFields()} +