diff --git a/.circleci/config.yml b/.circleci/config.yml index c2195c3b2..3e3a66b87 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -237,9 +237,13 @@ commands: if [[ $CIRCLE_JOB == "ios-build-official" ]]; then /usr/libexec/PlistBuddy -c "Set BugsnagAPIKey $BUGSNAG_KEY_OFFICIAL" ./RocketChatRN/Info.plist /usr/libexec/PlistBuddy -c "Set IS_OFFICIAL YES" ./RocketChatRN/Info.plist + /usr/libexec/PlistBuddy -c "Set IS_OFFICIAL YES" ./ShareRocketChatRN/Info.plist + /usr/libexec/PlistBuddy -c "Set IS_OFFICIAL YES" ./NotificationService/Info.plist else /usr/libexec/PlistBuddy -c "Set BugsnagAPIKey $BUGSNAG_KEY" ./RocketChatRN/Info.plist /usr/libexec/PlistBuddy -c "Set IS_OFFICIAL NO" ./RocketChatRN/Info.plist + /usr/libexec/PlistBuddy -c "Set IS_OFFICIAL NO" ./ShareRocketChatRN/Info.plist + /usr/libexec/PlistBuddy -c "Set IS_OFFICIAL NO" ./NotificationService/Info.plist fi if [[ $APP_STORE_CONNECT_API_KEY ]]; then @@ -416,9 +420,13 @@ workflows: - lint-testunit # iOS Experimental + - ios-hold-build-experimental: + type: approval + requires: + - lint-testunit - ios-build-experimental: requires: - - lint-testunit + - ios-hold-build-experimental - ios-hold-testflight-experimental: type: approval requires: @@ -444,9 +452,13 @@ workflows: - ios-hold-testflight-official # Android Experimental + - android-hold-build-experimental: + type: approval + requires: + - lint-testunit - android-build-experimental: requires: - - lint-testunit + - android-hold-build-experimental - android-hold-google-play-beta-experimental: type: approval requires: diff --git a/.eslintrc.js b/.eslintrc.js index 44e4eaf4c..add372a0b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,7 +6,7 @@ module.exports = { } } }, - "parser": "babel-eslint", + "parser": "@babel/eslint-parser", "extends": "airbnb", "parserOptions": { "sourceType": "module", @@ -21,7 +21,8 @@ module.exports = { "react", "jsx-a11y", "import", - "react-native" + "react-native", + "@babel" ], "env": { "browser": true, @@ -148,7 +149,8 @@ module.exports = { "react/jsx-curly-newline": [0], "react/state-in-constructor": [0], "no-async-promise-executor": [0], - "max-classes-per-file": [0] + "max-classes-per-file": [0], + "no-multiple-empty-lines": [0] }, "globals": { "__DEV__": true diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 17bfb4dd2..0eb72257c 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -44404,6 +44404,309 @@ exports[`Storyshots Message list message 1`] = ` + + Toggle e2e encryption + + + + + + + + + + + + + + + This room's encryption has been disabled by diego.mello + + + + + + + + + + + + + + + + + + + + This room's encryption has been enabled by diego.mello + + + + + + diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index 0d18c9445..1fd8679bc 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -70,3 +70,5 @@ export const SETTINGS = createRequestTypes('SETTINGS', ['CLEAR', 'ADD']); export const APP_STATE = createRequestTypes('APP_STATE', ['FOREGROUND', 'BACKGROUND']); export const ENTERPRISE_MODULES = createRequestTypes('ENTERPRISE_MODULES', ['CLEAR', 'SET']); export const ENCRYPTION = createRequestTypes('ENCRYPTION', ['INIT', 'STOP', 'DECODE_KEY', 'SET', 'SET_BANNER']); + +export const PERMISSIONS = createRequestTypes('PERMISSIONS', ['SET']); diff --git a/app/actions/permissions.js b/app/actions/permissions.js new file mode 100644 index 000000000..88179c34f --- /dev/null +++ b/app/actions/permissions.js @@ -0,0 +1,8 @@ +import * as types from './actionsTypes'; + +export function setPermissions(permissions) { + return { + type: types.PERMISSIONS.SET, + permissions + }; +} diff --git a/app/constants/colors.js b/app/constants/colors.js index b1fdcd7f3..f29f03d1d 100644 --- a/app/constants/colors.js +++ b/app/constants/colors.js @@ -63,6 +63,7 @@ export const themes = { passcodeDotFull: '#6C727A', previewBackground: '#1F2329', previewTintColor: '#ffffff', + backdropOpacity: 0.3, ...mentions }, dark: { @@ -109,6 +110,7 @@ export const themes = { passcodeDotFull: '#6C727A', previewBackground: '#030b1b', previewTintColor: '#ffffff', + backdropOpacity: 0.9, ...mentions }, black: { @@ -155,6 +157,7 @@ export const themes = { passcodeDotFull: '#6C727A', previewBackground: '#000000', previewTintColor: '#ffffff', + backdropOpacity: 0.9, ...mentions } }; diff --git a/app/constants/settings.js b/app/constants/settings.js index 359dff092..9f0df8865 100644 --- a/app/constants/settings.js +++ b/app/constants/settings.js @@ -101,6 +101,9 @@ export default { Jitsi_Enabled_TokenAuth: { type: 'valueAsBoolean' }, + Jitsi_URL_Room_Hash: { + type: 'valueAsBoolean' + }, Jitsi_URL_Room_Prefix: { type: 'valueAsString' }, diff --git a/app/containers/ActionSheet/ActionSheet.js b/app/containers/ActionSheet/ActionSheet.js index 81b3496ae..af98a75ce 100644 --- a/app/containers/ActionSheet/ActionSheet.js +++ b/app/containers/ActionSheet/ActionSheet.js @@ -147,7 +147,7 @@ const ActionSheet = React.memo(forwardRef(({ children, theme }, ref) => { const animatedPosition = React.useRef(new Value(0)); const opacity = interpolate(animatedPosition.current, { inputRange: [0, 1], - outputRange: [0, 0.7], + outputRange: [0, themes[theme].backdropOpacity], extrapolate: Extrapolate.CLAMP }); diff --git a/app/containers/Avatar/index.js b/app/containers/Avatar/index.js index 73314a17d..4d7c6d7a5 100644 --- a/app/containers/Avatar/index.js +++ b/app/containers/Avatar/index.js @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Q } from '@nozbe/watermelondb'; -import isEqual from 'react-fast-compare'; import database from '../../lib/database'; import { getUserSelector } from '../../selectors/login'; @@ -34,7 +33,8 @@ class AvatarContainer extends React.Component { } componentDidUpdate(prevProps) { - if (!isEqual(prevProps, this.props)) { + const { text, type } = this.props; + if (prevProps.text !== text || prevProps.type !== type) { this.init(); } } @@ -52,8 +52,8 @@ class AvatarContainer extends React.Component { init = async() => { const db = database.active; - const usersCollection = db.collections.get('users'); - const subsCollection = db.collections.get('subscriptions'); + const usersCollection = db.get('users'); + const subsCollection = db.get('subscriptions'); let record; try { diff --git a/app/containers/EmojiPicker/index.js b/app/containers/EmojiPicker/index.js index 65a973715..89446c067 100644 --- a/app/containers/EmojiPicker/index.js +++ b/app/containers/EmojiPicker/index.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import { View } from 'react-native'; import PropTypes from 'prop-types'; import ScrollableTabView from 'react-native-scrollable-tab-view'; -import equal from 'deep-equal'; +import { dequal } from 'dequal'; import { connect } from 'react-redux'; import orderBy from 'lodash/orderBy'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; @@ -67,7 +67,7 @@ class EmojiPicker extends Component { if (nextState.width !== width) { return true; } - if (!equal(nextState.frequentlyUsed, frequentlyUsed)) { + if (!dequal(nextState.frequentlyUsed, frequentlyUsed)) { return true; } return false; @@ -95,7 +95,7 @@ class EmojiPicker extends Component { // eslint-disable-next-line react/sort-comp _addFrequentlyUsed = protectedFunction(async(emoji) => { const db = database.active; - const freqEmojiCollection = db.collections.get('frequently_used_emojis'); + const freqEmojiCollection = db.get('frequently_used_emojis'); let freqEmojiRecord; try { freqEmojiRecord = await freqEmojiCollection.find(emoji.content); @@ -120,7 +120,7 @@ class EmojiPicker extends Component { updateFrequentlyUsed = async() => { const db = database.active; - const frequentlyUsedRecords = await db.collections.get('frequently_used_emojis').query().fetch(); + const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch(); let frequentlyUsed = orderBy(frequentlyUsedRecords, ['count'], ['desc']); frequentlyUsed = frequentlyUsed.map((item) => { if (item.isCustom) { diff --git a/app/containers/InAppNotification/index.js b/app/containers/InAppNotification/index.js index dd6573f33..7d22b127a 100644 --- a/app/containers/InAppNotification/index.js +++ b/app/containers/InAppNotification/index.js @@ -1,5 +1,8 @@ import React, { memo, useEffect } from 'react'; +import PropTypes from 'prop-types'; import { NotifierRoot, Notifier, Easing } from 'react-native-notifier'; +import { connect } from 'react-redux'; +import { dequal } from 'dequal'; import NotifierComponent from './NotifierComponent'; import EventEmitter from '../../utils/events'; @@ -8,13 +11,17 @@ import { getActiveRoute } from '../../utils/navigation'; export const INAPP_NOTIFICATION_EMITTER = 'NotificationInApp'; -const InAppNotification = memo(() => { +const InAppNotification = memo(({ rooms, appState }) => { const show = (notification) => { + if (appState !== 'foreground') { + return; + } + const { payload } = notification; const state = Navigation.navigationRef.current?.getRootState(); const route = getActiveRoute(state); if (payload.rid) { - if ((route?.name === 'RoomView' && route.params?.rid === payload.rid) || route?.name === 'JitsiMeetView') { + if (rooms.includes(payload.rid) || route?.name === 'JitsiMeetView') { return; } Notifier.showNotification({ @@ -28,13 +35,23 @@ const InAppNotification = memo(() => { }; useEffect(() => { - EventEmitter.addEventListener(INAPP_NOTIFICATION_EMITTER, show); + const listener = EventEmitter.addEventListener(INAPP_NOTIFICATION_EMITTER, show); return () => { - EventEmitter.removeListener(INAPP_NOTIFICATION_EMITTER); + EventEmitter.removeListener(INAPP_NOTIFICATION_EMITTER, listener); }; - }, []); + }, [rooms]); return ; +}, (prevProps, nextProps) => dequal(prevProps.rooms, nextProps.rooms)); + +const mapStateToProps = state => ({ + rooms: state.room.rooms, + appState: state.app.ready && state.app.foreground ? 'foreground' : 'background' }); -export default InAppNotification; +InAppNotification.propTypes = { + rooms: PropTypes.array, + appState: PropTypes.string +}; + +export default connect(mapStateToProps)(InAppNotification); diff --git a/app/containers/MessageActions/Header.js b/app/containers/MessageActions/Header.js index ab011e445..78aa9ae3d 100644 --- a/app/containers/MessageActions/Header.js +++ b/app/containers/MessageActions/Header.js @@ -96,7 +96,7 @@ const Header = React.memo(({ const setEmojis = async() => { try { const db = database.active; - const freqEmojiCollection = db.collections.get('frequently_used_emojis'); + const freqEmojiCollection = db.get('frequently_used_emojis'); let freqEmojis = await freqEmojiCollection.query().fetch(); const isLandscape = width > height; diff --git a/app/containers/MessageActions/index.js b/app/containers/MessageActions/index.js index 0ce32bb71..cf0ba6918 100644 --- a/app/containers/MessageActions/index.js +++ b/app/containers/MessageActions/index.js @@ -34,20 +34,24 @@ const MessageActions = React.memo(forwardRef(({ Message_AllowPinning, Message_AllowStarring, Message_Read_Receipt_Store_Users, - isMasterDetail + isMasterDetail, + editMessagePermission, + deleteMessagePermission, + forceDeleteMessagePermission, + pinMessagePermission }, ref) => { let permissions = {}; const { showActionSheet, hideActionSheet } = useActionSheet(); const getPermissions = async() => { try { - const permission = ['edit-message', 'delete-message', 'force-delete-message', 'pin-message']; + const permission = [editMessagePermission, deleteMessagePermission, forceDeleteMessagePermission, pinMessagePermission]; const result = await RocketChat.hasPermission(permission, room.rid); permissions = { - hasEditPermission: result[permission[0]], - hasDeletePermission: result[permission[1]], - hasForceDeletePermission: result[permission[2]], - hasPinPermission: result[permission[3]] + hasEditPermission: result[0], + hasDeletePermission: result[1], + hasForceDeletePermission: result[2], + hasPinPermission: result[3] }; } catch { // Do nothing @@ -141,7 +145,7 @@ const MessageActions = React.memo(forwardRef(({ const db = database.active; const result = await RocketChat.markAsUnread({ messageId }); if (result.success) { - const subCollection = db.collections.get('subscriptions'); + const subCollection = db.get('subscriptions'); const subRecord = await subCollection.find(rid); await db.action(async() => { try { @@ -171,7 +175,7 @@ const MessageActions = React.memo(forwardRef(({ const handleCopy = async(message) => { logEvent(events.ROOM_MSG_ACTION_COPY); - await Clipboard.setString(message.msg); + await Clipboard.setString(message?.attachments?.[0]?.description || message.msg); EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') }); }; @@ -440,7 +444,11 @@ MessageActions.propTypes = { Message_AllowPinning: PropTypes.bool, Message_AllowStarring: PropTypes.bool, Message_Read_Receipt_Store_Users: PropTypes.bool, - server: PropTypes.string + server: PropTypes.string, + editMessagePermission: PropTypes.array, + deleteMessagePermission: PropTypes.array, + forceDeleteMessagePermission: PropTypes.array, + pinMessagePermission: PropTypes.array }; const mapStateToProps = state => ({ @@ -452,7 +460,11 @@ const mapStateToProps = state => ({ Message_AllowPinning: state.settings.Message_AllowPinning, Message_AllowStarring: state.settings.Message_AllowStarring, Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users, - isMasterDetail: state.app.isMasterDetail + isMasterDetail: state.app.isMasterDetail, + editMessagePermission: state.permissions['edit-message'], + deleteMessagePermission: state.permissions['delete-message'], + forceDeleteMessagePermission: state.permissions['force-delete-message'], + pinMessagePermission: state.permissions['pin-message'] }); export default connect(mapStateToProps, null, null, { forwardRef: true })(MessageActions); diff --git a/app/containers/MessageBox/CommandsPreview/index.js b/app/containers/MessageBox/CommandsPreview/index.js index 1b6a27acd..1bb03ca8b 100644 --- a/app/containers/MessageBox/CommandsPreview/index.js +++ b/app/containers/MessageBox/CommandsPreview/index.js @@ -1,7 +1,7 @@ import React from 'react'; import { FlatList } from 'react-native'; import PropTypes from 'prop-types'; -import equal from 'deep-equal'; +import { dequal } from 'dequal'; import Item from './Item'; import styles from '../styles'; @@ -31,7 +31,7 @@ const CommandsPreview = React.memo(({ theme, commandPreview, showCommandPreview if (prevProps.showCommandPreview !== nextProps.showCommandPreview) { return false; } - if (!equal(prevProps.commandPreview, nextProps.commandPreview)) { + if (!dequal(prevProps.commandPreview, nextProps.commandPreview)) { return false; } return true; diff --git a/app/containers/MessageBox/Mentions/index.js b/app/containers/MessageBox/Mentions/index.js index 37c30c30b..fb845b2ad 100644 --- a/app/containers/MessageBox/Mentions/index.js +++ b/app/containers/MessageBox/Mentions/index.js @@ -1,7 +1,7 @@ import React from 'react'; import { FlatList, View } from 'react-native'; import PropTypes from 'prop-types'; -import equal from 'deep-equal'; +import { dequal } from 'dequal'; import styles from '../styles'; import MentionItem from './MentionItem'; @@ -30,7 +30,7 @@ const Mentions = React.memo(({ mentions, trackingType, theme }) => { if (prevProps.trackingType !== nextProps.trackingType) { return false; } - if (!equal(prevProps.mentions, nextProps.mentions)) { + if (!dequal(prevProps.mentions, nextProps.mentions)) { return false; } return true; diff --git a/app/containers/MessageBox/ReplyPreview.js b/app/containers/MessageBox/ReplyPreview.js index e1fcbcca7..eee81236d 100644 --- a/app/containers/MessageBox/ReplyPreview.js +++ b/app/containers/MessageBox/ReplyPreview.js @@ -3,7 +3,6 @@ import { View, Text, StyleSheet } from 'react-native'; import PropTypes from 'prop-types'; import moment from 'moment'; import { connect } from 'react-redux'; -import isEqual from 'lodash/isEqual'; import Markdown from '../markdown'; import { CustomIcon } from '../../lib/Icons'; @@ -43,7 +42,7 @@ const styles = StyleSheet.create({ }); const ReplyPreview = React.memo(({ - message, Message_TimeFormat, baseUrl, username, replying, getCustomEmoji, close, theme + message, Message_TimeFormat, baseUrl, username, replying, getCustomEmoji, close, theme, useRealName }) => { if (!replying) { return null; @@ -59,7 +58,7 @@ const ReplyPreview = React.memo(({ > - {message.u?.username} + {useRealName ? message.u?.name : message.u?.username} {time} ); -}, (prevProps, nextProps) => prevProps.replying === nextProps.replying && prevProps.theme === nextProps.theme && isEqual(prevProps.message, nextProps.message)); +}, (prevProps, nextProps) => prevProps.replying === nextProps.replying && prevProps.theme === nextProps.theme && prevProps.message.id === nextProps.message.id); ReplyPreview.propTypes = { replying: PropTypes.bool, @@ -85,12 +84,14 @@ ReplyPreview.propTypes = { baseUrl: PropTypes.string.isRequired, username: PropTypes.string.isRequired, getCustomEmoji: PropTypes.func, - theme: PropTypes.string + theme: PropTypes.string, + useRealName: PropTypes.bool }; const mapStateToProps = state => ({ Message_TimeFormat: state.settings.Message_TimeFormat, - baseUrl: state.server.server + baseUrl: state.server.server, + useRealName: state.settings.UI_Use_Real_Name }); export default connect(mapStateToProps)(ReplyPreview); diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js index 12fad3f15..9a075e31a 100644 --- a/app/containers/MessageBox/index.js +++ b/app/containers/MessageBox/index.js @@ -6,7 +6,7 @@ import { import { connect } from 'react-redux'; import { KeyboardAccessoryView } from 'react-native-ui-lib/keyboard'; import ImagePicker from 'react-native-image-crop-picker'; -import equal from 'deep-equal'; +import { dequal } from 'dequal'; import DocumentPicker from 'react-native-document-picker'; import { Q } from '@nozbe/watermelondb'; import { TouchableWithoutFeedback } from 'react-native-gesture-handler'; @@ -63,6 +63,7 @@ const imagePickerConfig = { const libraryPickerConfig = { multiple: true, + compressVideoPreset: 'Passthrough', mediaType: 'any' }; @@ -189,8 +190,8 @@ class MessageBox extends Component { } = this.props; let msg; try { - const threadsCollection = db.collections.get('threads'); - const subsCollection = db.collections.get('subscriptions'); + const threadsCollection = db.get('threads'); + const subsCollection = db.get('subscriptions'); try { this.room = await subsCollection.find(rid); } catch (error) { @@ -270,7 +271,7 @@ class MessageBox extends Component { } = this.state; const { - roomType, replying, editing, isFocused, message, theme, children + roomType, replying, editing, isFocused, message, theme } = this.props; if (nextProps.theme !== theme) { return true; @@ -299,16 +300,13 @@ class MessageBox extends Component { if (nextState.tshow !== tshow) { return true; } - if (!equal(nextState.mentions, mentions)) { + if (!dequal(nextState.mentions, mentions)) { return true; } - if (!equal(nextState.commandPreview, commandPreview)) { + if (!dequal(nextState.commandPreview, commandPreview)) { return true; } - if (!equal(nextProps.message, message)) { - return true; - } - if (!equal(nextProps.children, children)) { + if (!dequal(nextProps.message?.id, message?.id)) { return true; } return false; @@ -366,7 +364,7 @@ class MessageBox extends Component { const slashCommand = text.match(/^\/([a-z0-9._-]+) (.+)/im); if (slashCommand) { const [, name, params] = slashCommand; - const commandsCollection = db.collections.get('slash_commands'); + const commandsCollection = db.get('slash_commands'); try { const command = await commandsCollection.find(name); if (command.providesPreview) { @@ -507,7 +505,7 @@ class MessageBox extends Component { getEmojis = debounce(async(keyword) => { const db = database.active; if (keyword) { - const customEmojisCollection = db.collections.get('custom_emojis'); + const customEmojisCollection = db.get('custom_emojis'); const likeString = sanitizeLikeString(keyword); let customEmojis = await customEmojisCollection.query( Q.where('name', Q.like(`${ likeString }%`)) @@ -521,7 +519,7 @@ class MessageBox extends Component { getSlashCommands = debounce(async(keyword) => { const db = database.active; - const commandsCollection = db.collections.get('slash_commands'); + const commandsCollection = db.get('slash_commands'); const likeString = sanitizeLikeString(keyword); const commands = await commandsCollection.query( Q.where('id', Q.like(`${ likeString }%`)) @@ -753,7 +751,7 @@ class MessageBox extends Component { // Slash command if (message[0] === MENTIONS_TRACKING_TYPE_COMMANDS) { const db = database.active; - const commandsCollection = db.collections.get('slash_commands'); + const commandsCollection = db.get('slash_commands'); const command = message.replace(/ .*/, '').slice(1); const likeString = sanitizeLikeString(command); const slashCommand = await commandsCollection.query( @@ -941,7 +939,7 @@ class MessageBox extends Component { keyboardType='twitter' blurOnSubmit={false} placeholder={I18n.t('New_Message')} - placeholderTextColor={themes[theme].auxiliaryTintColor} + placeholderTextColor={themes[theme].auxiliaryText} onChangeText={this.onChangeText} onSelectionChange={this.onSelectionChange} underlineColorAndroid='transparent' diff --git a/app/containers/MessageErrorActions.js b/app/containers/MessageErrorActions.js index c9975b0e3..e84a99048 100644 --- a/app/containers/MessageErrorActions.js +++ b/app/containers/MessageErrorActions.js @@ -19,8 +19,8 @@ const MessageErrorActions = forwardRef(({ tmid }, ref) => { try { const db = database.active; const deleteBatch = []; - const msgCollection = db.collections.get('messages'); - const threadCollection = db.collections.get('threads'); + const msgCollection = db.get('messages'); + const threadCollection = db.get('threads'); // Delete the object (it can be Message or ThreadMessage instance) deleteBatch.push(message.prepareDestroyPermanently()); diff --git a/app/containers/Toast.js b/app/containers/Toast.js index eed7cfd7c..4b982fb49 100644 --- a/app/containers/Toast.js +++ b/app/containers/Toast.js @@ -28,7 +28,7 @@ class Toast extends React.Component { } componentDidMount() { - EventEmitter.addEventListener(LISTENER, this.showToast); + this.listener = EventEmitter.addEventListener(LISTENER, this.showToast); } shouldComponentUpdate(nextProps) { @@ -40,7 +40,7 @@ class Toast extends React.Component { } componentWillUnmount() { - EventEmitter.removeListener(LISTENER); + EventEmitter.removeListener(LISTENER, this.listener); } getToastRef = toast => this.toast = toast; diff --git a/app/containers/TwoFactor/index.js b/app/containers/TwoFactor/index.js index c6ad5467a..deb5c25db 100644 --- a/app/containers/TwoFactor/index.js +++ b/app/containers/TwoFactor/index.js @@ -58,9 +58,9 @@ const TwoFactor = React.memo(({ theme, isMasterDetail }) => { const showTwoFactor = args => setData(args); useEffect(() => { - EventEmitter.addEventListener(TWO_FACTOR, showTwoFactor); + const listener = EventEmitter.addEventListener(TWO_FACTOR, showTwoFactor); - return () => EventEmitter.removeListener(TWO_FACTOR); + return () => EventEmitter.removeListener(TWO_FACTOR, listener); }, []); const onCancel = () => { diff --git a/app/containers/UIKit/MultiSelect/index.js b/app/containers/UIKit/MultiSelect/index.js index b88330e4e..246a44ec0 100644 --- a/app/containers/UIKit/MultiSelect/index.js +++ b/app/containers/UIKit/MultiSelect/index.js @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { - View, Text, TouchableWithoutFeedback, Modal, KeyboardAvoidingView, Animated, Easing + View, Text, TouchableWithoutFeedback, Modal, KeyboardAvoidingView, Animated, Easing, StyleSheet } from 'react-native'; import PropTypes from 'prop-types'; import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; @@ -172,7 +172,7 @@ export const MultiSelect = React.memo(({ > - + {showContent ? renderContent() : null} diff --git a/app/containers/UIKit/MultiSelect/styles.js b/app/containers/UIKit/MultiSelect/styles.js index f261932c3..8bc2c2124 100644 --- a/app/containers/UIKit/MultiSelect/styles.js +++ b/app/containers/UIKit/MultiSelect/styles.js @@ -8,10 +8,6 @@ export default StyleSheet.create({ alignItems: 'center', justifyContent: 'flex-end' }, - backdrop: { - ...StyleSheet.absoluteFill, - opacity: 0.3 - }, modal: { height: 300, width: '100%', diff --git a/app/containers/message/Attachments.js b/app/containers/message/Attachments.js index 3d4ff48b4..0d068e9fd 100644 --- a/app/containers/message/Attachments.js +++ b/app/containers/message/Attachments.js @@ -1,5 +1,5 @@ import React from 'react'; -import isEqual from 'lodash/isEqual'; +import { dequal } from 'dequal'; import PropTypes from 'prop-types'; import Image from './Image'; @@ -28,7 +28,7 @@ const Attachments = React.memo(({ // eslint-disable-next-line react/no-array-index-key return ; }); -}, (prevProps, nextProps) => isEqual(prevProps.attachments, nextProps.attachments) && prevProps.theme === nextProps.theme); +}, (prevProps, nextProps) => dequal(prevProps.attachments, nextProps.attachments) && prevProps.theme === nextProps.theme); Attachments.propTypes = { attachments: PropTypes.array, diff --git a/app/containers/message/Audio.js b/app/containers/message/Audio.js index fad195890..e22dff408 100644 --- a/app/containers/message/Audio.js +++ b/app/containers/message/Audio.js @@ -6,7 +6,7 @@ import { import { Audio } from 'expo-av'; import Slider from '@react-native-community/slider'; import moment from 'moment'; -import equal from 'deep-equal'; +import { dequal } from 'dequal'; import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake'; import Touchable from './Touchable'; @@ -150,7 +150,7 @@ class MessageAudio extends React.Component { if (nextState.paused !== paused) { return true; } - if (!equal(nextProps.file, file)) { + if (!dequal(nextProps.file, file)) { return true; } if (nextState.loading !== loading) { diff --git a/app/containers/message/Content.js b/app/containers/message/Content.js index c963fed5c..2f29bf4ac 100644 --- a/app/containers/message/Content.js +++ b/app/containers/message/Content.js @@ -1,7 +1,7 @@ import React, { useContext } from 'react'; import { Text, View } from 'react-native'; import PropTypes from 'prop-types'; -import equal from 'deep-equal'; +import { dequal } from 'dequal'; import I18n from '../../i18n'; import styles from './styles'; @@ -108,10 +108,10 @@ const Content = React.memo((props) => { if (prevProps.isIgnored !== nextProps.isIgnored) { return false; } - if (!equal(prevProps.mentions, nextProps.mentions)) { + if (!dequal(prevProps.mentions, nextProps.mentions)) { return false; } - if (!equal(prevProps.channels, nextProps.channels)) { + if (!dequal(prevProps.channels, nextProps.channels)) { return false; } return true; diff --git a/app/containers/message/Image.js b/app/containers/message/Image.js index 1471b449e..6f466b3b3 100644 --- a/app/containers/message/Image.js +++ b/app/containers/message/Image.js @@ -2,7 +2,7 @@ import React, { useContext } from 'react'; import { View } from 'react-native'; import PropTypes from 'prop-types'; import FastImage from '@rocket.chat/react-native-fast-image'; -import equal from 'deep-equal'; +import { dequal } from 'dequal'; import { createImageProgress } from 'react-native-image-progress'; import * as Progress from 'react-native-progress'; @@ -66,7 +66,7 @@ const ImageContainer = React.memo(({ ); -}, (prevProps, nextProps) => equal(prevProps.file, nextProps.file) && prevProps.theme === nextProps.theme); +}, (prevProps, nextProps) => dequal(prevProps.file, nextProps.file) && prevProps.theme === nextProps.theme); ImageContainer.propTypes = { file: PropTypes.object, diff --git a/app/containers/message/Message.js b/app/containers/message/Message.js index bb59dbee0..104244740 100644 --- a/app/containers/message/Message.js +++ b/app/containers/message/Message.js @@ -119,7 +119,7 @@ const MessageTouchable = React.memo((props) => { @@ -132,6 +132,7 @@ MessageTouchable.displayName = 'MessageTouchable'; MessageTouchable.propTypes = { hasError: PropTypes.bool, isInfo: PropTypes.bool, + isThreadReply: PropTypes.bool, isTemp: PropTypes.bool, archived: PropTypes.bool }; diff --git a/app/containers/message/Reply.js b/app/containers/message/Reply.js index fba4c0fbd..88c748baa 100644 --- a/app/containers/message/Reply.js +++ b/app/containers/message/Reply.js @@ -2,7 +2,7 @@ import React, { useContext } from 'react'; import { View, Text, StyleSheet } from 'react-native'; import PropTypes from 'prop-types'; import moment from 'moment'; -import isEqual from 'deep-equal'; +import { dequal } from 'dequal'; import Touchable from './Touchable'; import Markdown from '../markdown'; @@ -125,7 +125,7 @@ const Fields = React.memo(({ attachment, theme }) => { ))} ); -}, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields) && prevProps.theme === nextProps.theme); +}, (prevProps, nextProps) => dequal(prevProps.attachment.fields, nextProps.attachment.fields) && prevProps.theme === nextProps.theme); const Reply = React.memo(({ attachment, timeFormat, index, getCustomEmoji, theme @@ -172,7 +172,6 @@ const Reply = React.memo(({ /> @@ -188,7 +187,7 @@ const Reply = React.memo(({ /> ); -}, (prevProps, nextProps) => isEqual(prevProps.attachment, nextProps.attachment) && prevProps.theme === nextProps.theme); +}, (prevProps, nextProps) => dequal(prevProps.attachment, nextProps.attachment) && prevProps.theme === nextProps.theme); Reply.propTypes = { attachment: PropTypes.object, diff --git a/app/containers/message/Urls.js b/app/containers/message/Urls.js index 946433d43..742b2f478 100644 --- a/app/containers/message/Urls.js +++ b/app/containers/message/Urls.js @@ -4,7 +4,7 @@ import { } from 'react-native'; import PropTypes from 'prop-types'; import FastImage from '@rocket.chat/react-native-fast-image'; -import isEqual from 'lodash/isEqual'; +import { dequal } from 'dequal'; import Touchable from './Touchable'; import openLink from '../../utils/openLink'; @@ -112,7 +112,7 @@ const Url = React.memo(({ url, index, theme }) => { ); -}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url) && oldProps.theme === newProps.theme); +}, (oldProps, newProps) => dequal(oldProps.url, newProps.url) && oldProps.theme === newProps.theme); const Urls = React.memo(({ urls, theme }) => { if (!urls || urls.length === 0) { @@ -122,7 +122,7 @@ const Urls = React.memo(({ urls, theme }) => { return urls.map((url, index) => ( )); -}, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls) && oldProps.theme === newProps.theme); +}, (oldProps, newProps) => dequal(oldProps.urls, newProps.urls) && oldProps.theme === newProps.theme); UrlImage.propTypes = { image: PropTypes.string diff --git a/app/containers/message/Video.js b/app/containers/message/Video.js index 2e9ef6c2a..fed0c67f2 100644 --- a/app/containers/message/Video.js +++ b/app/containers/message/Video.js @@ -1,7 +1,7 @@ import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import { StyleSheet } from 'react-native'; -import isEqual from 'deep-equal'; +import { dequal } from 'dequal'; import Touchable from './Touchable'; import Markdown from '../markdown'; @@ -57,7 +57,7 @@ const Video = React.memo(({ ); -}, (prevProps, nextProps) => isEqual(prevProps.file, nextProps.file) && prevProps.theme === nextProps.theme); +}, (prevProps, nextProps) => dequal(prevProps.file, nextProps.file) && prevProps.theme === nextProps.theme); Video.propTypes = { file: PropTypes.object, diff --git a/app/containers/message/utils.js b/app/containers/message/utils.js index 325d3d764..22f4d90ca 100644 --- a/app/containers/message/utils.js +++ b/app/containers/message/utils.js @@ -37,7 +37,9 @@ export const SYSTEM_MESSAGES = [ 'room_changed_privacy', 'room_changed_avatar', 'message_snippeted', - 'thread-created' + 'thread-created', + 'room_e2e_enabled', + 'room_e2e_disabled' ]; export const SYSTEM_MESSAGE_TYPES = { @@ -100,6 +102,10 @@ export const getInfoMessage = ({ return I18n.t('Room_changed_avatar', { userBy: username }); } else if (type === 'message_snippeted') { return I18n.t('Created_snippet'); + } else if (type === 'room_e2e_disabled') { + return I18n.t('This_room_encryption_has_been_disabled_by__username_', { username }); + } else if (type === 'room_e2e_enabled') { + return I18n.t('This_room_encryption_has_been_enabled_by__username_', { username }); } return ''; }; diff --git a/app/ee/omnichannel/views/QueueListView.js b/app/ee/omnichannel/views/QueueListView.js index 34a307699..a261d481e 100644 --- a/app/ee/omnichannel/views/QueueListView.js +++ b/app/ee/omnichannel/views/QueueListView.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FlatList } from 'react-native'; import { connect } from 'react-redux'; -import isEqual from 'react-fast-compare'; +import { dequal } from 'dequal'; import I18n from '../../../i18n'; import RoomItem, { ROW_HEIGHT } from '../../../presentation/RoomItem'; @@ -56,7 +56,7 @@ class QueueListView extends React.Component { shouldComponentUpdate(nextProps) { const { queued } = this.props; - if (!isEqual(nextProps.queued, queued)) { + if (!dequal(nextProps.queued, queued)) { return true; } diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index c35da810b..02adcb460 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -704,5 +704,7 @@ export default { Direct_message: 'Direct message', Message_Ignored: 'Message ignored. Tap to display it.', Enter_workspace_URL: 'Enter workspace URL', - Workspace_URL_Example: 'Ex. your-company.rocket.chat' + Workspace_URL_Example: 'Ex. your-company.rocket.chat', + This_room_encryption_has_been_enabled_by__username_: 'This room\'s encryption has been enabled by {{username}}', + This_room_encryption_has_been_disabled_by__username_: 'This room\'s encryption has been disabled by {{username}}' }; diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js index 3819a3b3b..e3dc8e106 100644 --- a/app/i18n/locales/pt-BR.js +++ b/app/i18n/locales/pt-BR.js @@ -651,5 +651,7 @@ export default { Direct_message: 'Mensagem direta', Message_Ignored: 'Mensagem ignorada. Toque para mostrar.', Enter_workspace_URL: 'Digite a URL da sua workspace', - Workspace_URL_Example: 'Ex. sua-empresa.rocket.chat' + Workspace_URL_Example: 'Ex. sua-empresa.rocket.chat', + This_room_encryption_has_been_enabled_by__username_: 'A criptografia para essa sala foi habilitada por {{username}}', + This_room_encryption_has_been_disabled_by__username_: 'A criptografia para essa sala foi desabilitada por {{username}}' }; diff --git a/app/i18n/locales/ru.js b/app/i18n/locales/ru.js index 4d37232bf..fcbcd5e47 100644 --- a/app/i18n/locales/ru.js +++ b/app/i18n/locales/ru.js @@ -678,5 +678,31 @@ export default { No_threads: 'Тредов нет', No_threads_following: 'Нет тредов, за которыми вы следите', No_threads_unread: 'Непрочитанных тредов нет', - Messagebox_Send_to_channel: 'Отправить в чат' + Messagebox_Send_to_channel: 'Отправить в чат', + Set_as_leader: 'Назначить лидером', + Set_as_moderator: 'Назначить модератором', + Set_as_owner: 'Назначить владельцем', + Remove_as_leader: 'Удалить из лидеров', + Remove_as_moderator: 'Удалить из модераторов', + Remove_as_owner: 'Удалить из владельцев', + Remove_from_room: 'Удалить из чата', + Ignore: 'Игнориновать', + Unignore: 'Прекратить игнорировать', + User_has_been_ignored: 'Пользователь теперь игнорируется', + User_has_been_unignored: 'Пользователь больше не игнорируется', + User_has_been_removed_from_s: 'Пользователь удален из {{s}}', + User__username__is_now_a_leader_of__room_name_: 'Пользователь {{username}} больше не лидер в чате {{room_name}}', + User__username__is_now_a_moderator_of__room_name_: 'Пользователь {{username}} больше не модератор в чате {{room_name}}', + User__username__is_now_a_owner_of__room_name_: 'Пользователь {{username}} больше не владелец в чате {{room_name}}', + User__username__removed_from__room_name__leaders: 'Пользователь {{username}} удален из {{room_name}} лидеров', + User__username__removed_from__room_name__moderators: 'Пользователь {{username}} удален из {{room_name}} модераторов', + User__username__removed_from__room_name__owners: 'Пользователь {{username}} удален из {{room_name}} владельцев', + The_user_will_be_removed_from_s: 'Пользователь будет удален из {{s}}', + Yes_remove_user: 'Да, удалить пользователя!', + Direct_message: 'Личное сообщение', + Message_Ignored: 'Сообщение игнорируется. Тапните по нему, чтобы отобразить его.', + Enter_workspace_URL: 'Введите URL вашего рабочего пространства', + Workspace_URL_Example: 'Например, your-company.rocket.chat', + This_room_encryption_has_been_enabled_by__username_: 'Шифрование для этого чата включено {{username}}', + This_room_encryption_has_been_disabled_by__username_: 'Шифрование для этого чата выключено {{username}}' }; diff --git a/app/index.js b/app/index.js index 8bf338dad..048323a5b 100644 --- a/app/index.js +++ b/app/index.js @@ -112,16 +112,25 @@ export default class Root extends React.Component { init = async() => { UserPreferences.getMapAsync(THEME_PREFERENCES_KEY).then(this.setTheme); - const [notification, deepLinking] = await Promise.all([initializePushNotifications(), Linking.getInitialURL()]); - const parsedDeepLinkingURL = parseDeepLinking(deepLinking); store.dispatch(appInitLocalSettings()); + + // Open app from push notification + const notification = await initializePushNotifications(); if (notification) { onNotification(notification); - } else if (parsedDeepLinkingURL) { - store.dispatch(deepLinkingOpen(parsedDeepLinkingURL)); - } else { - store.dispatch(appInit()); + return; } + + // Open app from deep linking + const deepLinking = await Linking.getInitialURL(); + const parsedDeepLinkingURL = parseDeepLinking(deepLinking); + if (parsedDeepLinkingURL) { + store.dispatch(deepLinkingOpen(parsedDeepLinkingURL)); + return; + } + + // Open app from app icon + store.dispatch(appInit()); } getMasterDetail = (width) => { diff --git a/app/lib/encryption/encryption.js b/app/lib/encryption/encryption.js index 972f2c588..e4a7f2c14 100644 --- a/app/lib/encryption/encryption.js +++ b/app/lib/encryption/encryption.js @@ -229,9 +229,9 @@ class Encryption { decryptPendingMessages = async(roomId) => { const db = database.active; - const messagesCollection = db.collections.get('messages'); - const threadsCollection = db.collections.get('threads'); - const threadMessagesCollection = db.collections.get('thread_messages'); + const messagesCollection = db.get('messages'); + const threadsCollection = db.get('threads'); + const threadMessagesCollection = db.get('thread_messages'); // e2e status is null or 'pending' and message type is 'e2e' const whereClause = [ @@ -286,7 +286,7 @@ class Encryption { // after initialize the encryption client decryptPendingSubscriptions = async() => { const db = database.active; - const subCollection = db.collections.get('subscriptions'); + const subCollection = db.get('subscriptions'); try { // Find all rooms that can have a lastMessage encrypted // If we select only encrypted rooms we can miss some room that changed their encrypted status @@ -347,7 +347,7 @@ class Encryption { const { rid } = subscription; const db = database.active; - const subCollection = db.collections.get('subscriptions'); + const subCollection = db.get('subscriptions'); let subRecord; try { @@ -400,7 +400,7 @@ class Encryption { encryptMessage = async(message) => { const { rid } = message; const db = database.active; - const subCollection = db.collections.get('subscriptions'); + const subCollection = db.get('subscriptions'); try { // Find the subscription diff --git a/app/lib/encryption/room.js b/app/lib/encryption/room.js index 835bd9ab5..0aa1b932e 100644 --- a/app/lib/encryption/room.js +++ b/app/lib/encryption/room.js @@ -49,7 +49,7 @@ export default class EncryptionRoom { } const db = database.active; - const subCollection = db.collections.get('subscriptions'); + const subCollection = db.get('subscriptions'); try { // Find the subscription const subscription = await subCollection.find(this.roomId); diff --git a/app/lib/methods/callJitsi.js b/app/lib/methods/callJitsi.js index 7410ed8bd..7275ef812 100644 --- a/app/lib/methods/callJitsi.js +++ b/app/lib/methods/callJitsi.js @@ -2,7 +2,7 @@ import reduxStore from '../createStore'; import Navigation from '../Navigation'; import { logEvent, events } from '../../utils/log'; -async function jitsiURL({ rid }) { +async function jitsiURL({ room }) { const { settings } = reduxStore.getState(); const { Jitsi_Enabled } = settings; @@ -11,31 +11,37 @@ async function jitsiURL({ rid }) { } const { - Jitsi_Domain, Jitsi_URL_Room_Prefix, Jitsi_SSL, Jitsi_Enabled_TokenAuth, uniqueID + Jitsi_Domain, Jitsi_URL_Room_Prefix, Jitsi_SSL, Jitsi_Enabled_TokenAuth, uniqueID, Jitsi_URL_Room_Hash } = settings; const domain = `${ Jitsi_Domain }/`; const prefix = Jitsi_URL_Room_Prefix; - const uniqueIdentifier = uniqueID || 'undefined'; const protocol = Jitsi_SSL ? 'https://' : 'http://'; let queryString = ''; if (Jitsi_Enabled_TokenAuth) { try { - const accessToken = await this.methodCallWrapper('jitsi:generateAccessToken', rid); + const accessToken = await this.methodCallWrapper('jitsi:generateAccessToken', room?.rid); queryString = `?jwt=${ accessToken }`; } catch { logEvent(events.RA_JITSI_F); } } - return `${ protocol }${ domain }${ prefix }${ uniqueIdentifier }${ rid }${ queryString }`; + let rname; + if (Jitsi_URL_Room_Hash) { + rname = uniqueID + room?.rid; + } else { + rname = encodeURIComponent(room.t === 'd' ? room?.usernames?.join?.(' x ') : room?.name); + } + + return `${ protocol }${ domain }${ prefix }${ rname }${ queryString }`; } -async function callJitsi(rid, onlyAudio = false) { +async function callJitsi(room, onlyAudio = false) { logEvent(onlyAudio ? events.RA_JITSI_AUDIO : events.RA_JITSI_VIDEO); - const url = await jitsiURL.call(this, { rid }); - Navigation.navigate('JitsiMeetView', { url, onlyAudio, rid }); + const url = await jitsiURL.call(this, { room }); + Navigation.navigate('JitsiMeetView', { url, onlyAudio, rid: room?.rid }); } export default callJitsi; diff --git a/app/lib/methods/canOpenRoom.js b/app/lib/methods/canOpenRoom.js index b85728727..c233899b8 100644 --- a/app/lib/methods/canOpenRoom.js +++ b/app/lib/methods/canOpenRoom.js @@ -57,7 +57,7 @@ async function open({ type, rid, name }) { export default async function canOpenRoom({ rid, path, isCall }) { try { const db = database.active; - const subsCollection = db.collections.get('subscriptions'); + const subsCollection = db.get('subscriptions'); if (isCall && !rid) { // Extract rid from a Jitsi URL @@ -75,7 +75,8 @@ export default async function canOpenRoom({ rid, path, isCall }) { name: room.name, fname: room.fname, prid: room.prid, - uids: room.uids + uids: room.uids, + usernames: room.usernames }; } catch (e) { // Do nothing diff --git a/app/lib/methods/enterpriseModules.js b/app/lib/methods/enterpriseModules.js index 5cb68e7a9..35c6672d2 100644 --- a/app/lib/methods/enterpriseModules.js +++ b/app/lib/methods/enterpriseModules.js @@ -13,7 +13,7 @@ export async function setEnterpriseModules() { try { const { server: serverId } = reduxStore.getState().server; const serversDB = database.servers; - const serversCollection = serversDB.collections.get('servers'); + const serversCollection = serversDB.get('servers'); let server; try { server = await serversCollection.find(serverId); @@ -39,7 +39,7 @@ export function getEnterpriseModules() { const enterpriseModules = await this.methodCallWrapper('license:getModules'); if (enterpriseModules) { const serversDB = database.servers; - const serversCollection = serversDB.collections.get('servers'); + const serversCollection = serversDB.get('servers'); const server = await serversCollection.find(serverId); await serversDB.action(async() => { await server.update((s) => { diff --git a/app/lib/methods/getCustomEmojis.js b/app/lib/methods/getCustomEmojis.js index 0037168df..31108fb62 100644 --- a/app/lib/methods/getCustomEmojis.js +++ b/app/lib/methods/getCustomEmojis.js @@ -1,4 +1,3 @@ -import { InteractionManager } from 'react-native'; import lt from 'semver/functions/lt'; import orderBy from 'lodash/orderBy'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; @@ -21,7 +20,7 @@ const updateEmojis = async({ update = [], remove = [], allRecords }) => { return; } const db = database.active; - const emojisCollection = db.collections.get('custom_emojis'); + const emojisCollection = db.get('custom_emojis'); let emojisToCreate = []; let emojisToUpdate = []; let emojisToDelete = []; @@ -63,7 +62,7 @@ const updateEmojis = async({ update = [], remove = [], allRecords }) => { export async function setCustomEmojis() { const db = database.active; - const emojisCollection = db.collections.get('custom_emojis'); + const emojisCollection = db.get('custom_emojis'); const allEmojis = await emojisCollection.query().fetch(); const parsed = allEmojis.reduce((ret, item) => { ret[item.name] = { @@ -86,7 +85,7 @@ export function getCustomEmojis() { try { const serverVersion = reduxStore.getState().server.version; const db = database.active; - const emojisCollection = db.collections.get('custom_emojis'); + const emojisCollection = db.get('custom_emojis'); const allRecords = await emojisCollection.query().fetch(); const updatedSince = await getUpdatedSince(allRecords); @@ -95,18 +94,16 @@ export function getCustomEmojis() { // RC 0.61.0 const result = await this.sdk.get('emoji-custom'); - InteractionManager.runAfterInteractions(async() => { - let { emojis } = result; - emojis = emojis.filter(emoji => !updatedSince || emoji._updatedAt > updatedSince); - const changedEmojis = await updateEmojis({ update: emojis, allRecords }); + let { emojis } = result; + emojis = emojis.filter(emoji => !updatedSince || emoji._updatedAt > updatedSince); + const changedEmojis = await updateEmojis({ update: emojis, allRecords }); - // `setCustomEmojis` is fired on selectServer - // We run it again only if emojis were changed - if (changedEmojis) { - setCustomEmojis(); - } - return resolve(); - }); + // `setCustomEmojis` is fired on selectServer + // We run it again only if emojis were changed + if (changedEmojis) { + setCustomEmojis(); + } + return resolve(); } else { const params = {}; if (updatedSince) { @@ -120,17 +117,15 @@ export function getCustomEmojis() { return resolve(); } - InteractionManager.runAfterInteractions(async() => { - const { emojis } = result; - const { update, remove } = emojis; - const changedEmojis = await updateEmojis({ update, remove, allRecords }); + const { emojis } = result; + const { update, remove } = emojis; + const changedEmojis = await updateEmojis({ update, remove, allRecords }); - // `setCustomEmojis` is fired on selectServer - // We run it again only if emojis were changed - if (changedEmojis) { - setCustomEmojis(); - } - }); + // `setCustomEmojis` is fired on selectServer + // We run it again only if emojis were changed + if (changedEmojis) { + setCustomEmojis(); + } } } catch (e) { log(e); diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js index 249441582..15ee221b8 100644 --- a/app/lib/methods/getPermissions.js +++ b/app/lib/methods/getPermissions.js @@ -1,12 +1,55 @@ -import { InteractionManager } from 'react-native'; import lt from 'semver/functions/lt'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; +import { Q } from '@nozbe/watermelondb'; +import coerce from 'semver/functions/coerce'; import orderBy from 'lodash/orderBy'; import database from '../database'; import log from '../../utils/log'; import reduxStore from '../createStore'; import protectedFunction from './helpers/protectedFunction'; +import { setPermissions as setPermissionsAction } from '../../actions/permissions'; + +const PERMISSIONS = [ + 'add-user-to-any-c-room', + 'add-user-to-any-p-room', + 'add-user-to-joined-room', + 'archive-room', + 'auto-translate', + 'create-invite-links', + 'delete-c', + 'delete-message', + 'delete-p', + 'edit-message', + 'edit-room', + 'force-delete-message', + 'mute-user', + 'pin-message', + 'post-readonly', + 'remove-user', + 'set-leader', + 'set-moderator', + 'set-owner', + 'set-react-when-readonly', + 'set-readonly', + 'toggle-room-e2e-encryption', + 'transfer-livechat-guest', + 'unarchive-room', + 'view-broadcast-member-list', + 'view-privileged-setting', + 'view-room-administration', + 'view-statistics', + 'view-user-administration' +]; + +export async function setPermissions() { + const db = database.active; + const permissionsCollection = db.collections.get('permissions'); + const allPermissions = await permissionsCollection.query(Q.where('id', Q.oneOf(PERMISSIONS))).fetch(); + const parsed = allPermissions.reduce((acc, item) => ({ ...acc, [item.id]: item.roles }), {}); + + reduxStore.dispatch(setPermissionsAction(parsed)); +} const getUpdatedSince = (allRecords) => { try { @@ -26,7 +69,7 @@ const updatePermissions = async({ update = [], remove = [], allRecords }) => { return; } const db = database.active; - const permissionsCollection = db.collections.get('permissions'); + const permissionsCollection = db.get('permissions'); // filter permissions let permissionsToCreate = []; @@ -65,33 +108,35 @@ const updatePermissions = async({ update = [], remove = [], allRecords }) => { await db.action(async() => { await db.batch(...batch); }); + return true; } catch (e) { log(e); } }; -export default function() { +export function getPermissions() { return new Promise(async(resolve) => { try { const serverVersion = reduxStore.getState().server.version; const db = database.active; - const permissionsCollection = db.collections.get('permissions'); + const permissionsCollection = db.get('permissions'); const allRecords = await permissionsCollection.query().fetch(); // if server version is lower than 0.73.0, fetches from old api - if (serverVersion && lt(serverVersion, '0.73.0')) { + if (serverVersion && lt(coerce(serverVersion), '0.73.0')) { // RC 0.66.0 const result = await this.sdk.get('permissions.list'); if (!result.success) { return resolve(); } - InteractionManager.runAfterInteractions(async() => { - await updatePermissions({ update: result.permissions, allRecords }); - return resolve(); - }); + const changePermissions = await updatePermissions({ update: result.permissions, allRecords }); + if (changePermissions) { + setPermissions(); + } + return resolve(); } else { const params = {}; - const updatedSince = await getUpdatedSince(allRecords); + const updatedSince = getUpdatedSince(allRecords); if (updatedSince) { params.updatedSince = updatedSince; } @@ -102,10 +147,11 @@ export default function() { return resolve(); } - InteractionManager.runAfterInteractions(async() => { - await updatePermissions({ update: result.update, remove: result.delete, allRecords }); - return resolve(); - }); + const changePermissions = await updatePermissions({ update: result.update, remove: result.delete, allRecords }); + if (changePermissions) { + setPermissions(); + } + return resolve(); } } catch (e) { log(e); diff --git a/app/lib/methods/getRoles.js b/app/lib/methods/getRoles.js index 3856d64f4..df22d382b 100644 --- a/app/lib/methods/getRoles.js +++ b/app/lib/methods/getRoles.js @@ -1,4 +1,3 @@ -import { InteractionManager } from 'react-native'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import database from '../database'; @@ -19,43 +18,41 @@ export default function() { const { roles } = result; if (roles && roles.length) { - InteractionManager.runAfterInteractions(async() => { - await db.action(async() => { - const rolesCollections = db.collections.get('roles'); - const allRolesRecords = await rolesCollections.query().fetch(); + await db.action(async() => { + const rolesCollections = db.get('roles'); + const allRolesRecords = await rolesCollections.query().fetch(); - // filter roles - let rolesToCreate = roles.filter(i1 => !allRolesRecords.find(i2 => i1._id === i2.id)); - let rolesToUpdate = allRolesRecords.filter(i1 => roles.find(i2 => i1.id === i2._id)); + // filter roles + let rolesToCreate = roles.filter(i1 => !allRolesRecords.find(i2 => i1._id === i2.id)); + let rolesToUpdate = allRolesRecords.filter(i1 => roles.find(i2 => i1.id === i2._id)); - // Create - rolesToCreate = rolesToCreate.map(role => rolesCollections.prepareCreate(protectedFunction((r) => { - r._raw = sanitizedRaw({ id: role._id }, rolesCollections.schema); - Object.assign(r, role); - }))); + // Create + rolesToCreate = rolesToCreate.map(role => rolesCollections.prepareCreate(protectedFunction((r) => { + r._raw = sanitizedRaw({ id: role._id }, rolesCollections.schema); + Object.assign(r, role); + }))); - // Update - rolesToUpdate = rolesToUpdate.map((role) => { - const newRole = roles.find(r => r._id === role.id); - return role.prepareUpdate(protectedFunction((r) => { - Object.assign(r, newRole); - })); - }); - - const allRecords = [ - ...rolesToCreate, - ...rolesToUpdate - ]; - - try { - await db.batch(...allRecords); - } catch (e) { - log(e); - } - return allRecords.length; + // Update + rolesToUpdate = rolesToUpdate.map((role) => { + const newRole = roles.find(r => r._id === role.id); + return role.prepareUpdate(protectedFunction((r) => { + Object.assign(r, newRole); + })); }); - return resolve(); + + const allRecords = [ + ...rolesToCreate, + ...rolesToUpdate + ]; + + try { + await db.batch(...allRecords); + } catch (e) { + log(e); + } + return allRecords.length; }); + return resolve(); } } catch (e) { log(e); diff --git a/app/lib/methods/getSettings.js b/app/lib/methods/getSettings.js index f433f4a9a..6935631ef 100644 --- a/app/lib/methods/getSettings.js +++ b/app/lib/methods/getSettings.js @@ -44,7 +44,7 @@ const loginSettings = [ const serverInfoUpdate = async(serverInfo, iconSetting) => { const serversDB = database.servers; const serverId = reduxStore.getState().server.server; - const serversCollection = serversDB.collections.get('servers'); + const serversCollection = serversDB.get('servers'); const server = await serversCollection.find(serverId); let info = serverInfo.reduce((allSettings, setting) => { @@ -118,7 +118,7 @@ export async function getLoginSettings({ server }) { export async function setSettings() { const db = database.active; - const settingsCollection = db.collections.get('settings'); + const settingsCollection = db.get('settings'); const settingsRecords = await settingsCollection.query().fetch(); const parsed = Object.values(settingsRecords).map(item => ({ _id: item.id, @@ -157,7 +157,7 @@ export default async function() { } await db.action(async() => { - const settingsCollection = db.collections.get('settings'); + const settingsCollection = db.get('settings'); const allSettingsRecords = await settingsCollection .query(Q.where('id', Q.oneOf(filteredSettingsIds))) .fetch(); diff --git a/app/lib/methods/getSlashCommands.js b/app/lib/methods/getSlashCommands.js index 729e8b4a9..c37e077f4 100644 --- a/app/lib/methods/getSlashCommands.js +++ b/app/lib/methods/getSlashCommands.js @@ -1,4 +1,3 @@ -import { InteractionManager } from 'react-native'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import database from '../database'; @@ -20,47 +19,45 @@ export default function() { const { commands } = result; if (commands && commands.length) { - InteractionManager.runAfterInteractions(async() => { - await db.action(async() => { - const slashCommandsCollection = db.collections.get('slash_commands'); - const allSlashCommandsRecords = await slashCommandsCollection.query().fetch(); + await db.action(async() => { + const slashCommandsCollection = db.get('slash_commands'); + const allSlashCommandsRecords = await slashCommandsCollection.query().fetch(); - // filter slash commands - let slashCommandsToCreate = commands.filter(i1 => !allSlashCommandsRecords.find(i2 => i1.command === i2.id)); - let slashCommandsToUpdate = allSlashCommandsRecords.filter(i1 => commands.find(i2 => i1.id === i2.command)); - let slashCommandsToDelete = allSlashCommandsRecords - .filter(i1 => !slashCommandsToCreate.find(i2 => i2.command === i1.id) && !slashCommandsToUpdate.find(i2 => i2.id === i1.id)); + // filter slash commands + let slashCommandsToCreate = commands.filter(i1 => !allSlashCommandsRecords.find(i2 => i1.command === i2.id)); + let slashCommandsToUpdate = allSlashCommandsRecords.filter(i1 => commands.find(i2 => i1.id === i2.command)); + let slashCommandsToDelete = allSlashCommandsRecords + .filter(i1 => !slashCommandsToCreate.find(i2 => i2.command === i1.id) && !slashCommandsToUpdate.find(i2 => i2.id === i1.id)); - // Create - slashCommandsToCreate = slashCommandsToCreate.map(command => slashCommandsCollection.prepareCreate(protectedFunction((s) => { - s._raw = sanitizedRaw({ id: command.command }, slashCommandsCollection.schema); - Object.assign(s, command); - }))); + // Create + slashCommandsToCreate = slashCommandsToCreate.map(command => slashCommandsCollection.prepareCreate(protectedFunction((s) => { + s._raw = sanitizedRaw({ id: command.command }, slashCommandsCollection.schema); + Object.assign(s, command); + }))); - // Update - slashCommandsToUpdate = slashCommandsToUpdate.map((command) => { - const newCommand = commands.find(s => s.command === command.id); - return command.prepareUpdate(protectedFunction((s) => { - Object.assign(s, newCommand); - })); - }); - - // Delete - slashCommandsToDelete = slashCommandsToDelete.map(command => command.prepareDestroyPermanently()); - - const allRecords = [ - ...slashCommandsToCreate, - ...slashCommandsToUpdate, - ...slashCommandsToDelete - ]; - - try { - await db.batch(...allRecords); - } catch (e) { - log(e); - } - return allRecords.length; + // Update + slashCommandsToUpdate = slashCommandsToUpdate.map((command) => { + const newCommand = commands.find(s => s.command === command.id); + return command.prepareUpdate(protectedFunction((s) => { + Object.assign(s, newCommand); + })); }); + + // Delete + slashCommandsToDelete = slashCommandsToDelete.map(command => command.prepareDestroyPermanently()); + + const allRecords = [ + ...slashCommandsToCreate, + ...slashCommandsToUpdate, + ...slashCommandsToDelete + ]; + + try { + await db.batch(...allRecords); + } catch (e) { + log(e); + } + return allRecords.length; }); } } catch (e) { diff --git a/app/lib/methods/getUsersPresence.js b/app/lib/methods/getUsersPresence.js index 68d9b1eaa..83e5ce815 100644 --- a/app/lib/methods/getUsersPresence.js +++ b/app/lib/methods/getUsersPresence.js @@ -72,7 +72,7 @@ export default async function getUsersPresence() { ids = []; const db = database.active; - const userCollection = db.collections.get('users'); + const userCollection = db.get('users'); users.forEach(async(user) => { try { const userRecord = await userCollection.find(user._id); diff --git a/app/lib/methods/helpers/findSubscriptionsRooms.js b/app/lib/methods/helpers/findSubscriptionsRooms.js index f1714932d..a64a88940 100644 --- a/app/lib/methods/helpers/findSubscriptionsRooms.js +++ b/app/lib/methods/helpers/findSubscriptionsRooms.js @@ -5,7 +5,7 @@ import database from '../../database'; export default async(subscriptions = [], rooms = []) => { try { const db = database.active; - const subCollection = db.collections.get('subscriptions'); + const subCollection = db.get('subscriptions'); const roomIds = rooms.filter(r => !subscriptions.find(s => s.rid === r._id)).map(r => r._id); let existingSubs = await subCollection.query(Q.where('rid', Q.oneOf(roomIds))).fetch(); diff --git a/app/lib/methods/helpers/mergeSubscriptionsRooms.js b/app/lib/methods/helpers/mergeSubscriptionsRooms.js index 1bc3f18b2..1e5afa3b2 100644 --- a/app/lib/methods/helpers/mergeSubscriptionsRooms.js +++ b/app/lib/methods/helpers/mergeSubscriptionsRooms.js @@ -1,11 +1,14 @@ import EJSON from 'ejson'; +import { lt, coerce } from 'semver'; import normalizeMessage from './normalizeMessage'; import findSubscriptionsRooms from './findSubscriptionsRooms'; import { Encryption } from '../../encryption'; +import reduxStore from '../../createStore'; // TODO: delete and update export const merge = (subscription, room) => { + const serverVersion = reduxStore.getState().server.version; subscription = EJSON.fromJSONValue(subscription); room = EJSON.fromJSONValue(room); @@ -25,9 +28,15 @@ export const merge = (subscription, room) => { subscription.usernames = room.usernames; subscription.uids = room.uids; } - // https://github.com/RocketChat/Rocket.Chat/blob/develop/app/ui-sidenav/client/roomList.js#L180 - const lastRoomUpdate = room.lm || subscription.ts || subscription._updatedAt; - subscription.roomUpdatedAt = subscription.lr ? Math.max(new Date(subscription.lr), new Date(lastRoomUpdate)) : lastRoomUpdate; + if (serverVersion && lt(coerce(serverVersion), '3.7.0')) { + const updatedAt = room?._updatedAt ? new Date(room._updatedAt) : null; + const lastMessageTs = subscription?.lastMessage?.ts ? new Date(subscription.lastMessage.ts) : null; + subscription.roomUpdatedAt = Math.max(updatedAt, lastMessageTs); + } else { + // https://github.com/RocketChat/Rocket.Chat/blob/develop/app/ui-sidenav/client/roomList.js#L180 + const lastRoomUpdate = room.lm || subscription.ts || subscription._updatedAt; + subscription.roomUpdatedAt = subscription.lr ? Math.max(new Date(subscription.lr), new Date(lastRoomUpdate)) : lastRoomUpdate; + } subscription.ro = room.ro; subscription.broadcast = room.broadcast; subscription.encrypted = room.encrypted; diff --git a/app/lib/methods/loadMissedMessages.js b/app/lib/methods/loadMissedMessages.js index 82c2e1bda..d1acaf0b4 100644 --- a/app/lib/methods/loadMissedMessages.js +++ b/app/lib/methods/loadMissedMessages.js @@ -5,7 +5,7 @@ import updateMessages from './updateMessages'; const getLastUpdate = async(rid) => { try { const db = database.active; - const subsCollection = db.collections.get('subscriptions'); + const subsCollection = db.get('subscriptions'); const sub = await subsCollection.find(rid); return sub.lastOpen.toISOString(); } catch (e) { diff --git a/app/lib/methods/loadThreadMessages.js b/app/lib/methods/loadThreadMessages.js index 4dc8a05fd..de6f244c6 100644 --- a/app/lib/methods/loadThreadMessages.js +++ b/app/lib/methods/loadThreadMessages.js @@ -1,4 +1,3 @@ -import { InteractionManager } from 'react-native'; import { Q } from '@nozbe/watermelondb'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; @@ -30,44 +29,42 @@ export default function loadThreadMessages({ tmid, rid, offset = 0 }) { let data = await load.call(this, { tmid, offset }); if (data && data.length) { - InteractionManager.runAfterInteractions(async() => { - try { - data = data.map(m => buildMessage(m)); - data = await Encryption.decryptMessages(data); - const db = database.active; - const threadMessagesCollection = db.collections.get('thread_messages'); - const allThreadMessagesRecords = await threadMessagesCollection.query(Q.where('rid', tmid)).fetch(); - let threadMessagesToCreate = data.filter(i1 => !allThreadMessagesRecords.find(i2 => i1._id === i2.id)); - let threadMessagesToUpdate = allThreadMessagesRecords.filter(i1 => data.find(i2 => i1.id === i2._id)); + try { + data = data.map(m => buildMessage(m)); + data = await Encryption.decryptMessages(data); + const db = database.active; + const threadMessagesCollection = db.get('thread_messages'); + const allThreadMessagesRecords = await threadMessagesCollection.query(Q.where('rid', tmid)).fetch(); + let threadMessagesToCreate = data.filter(i1 => !allThreadMessagesRecords.find(i2 => i1._id === i2.id)); + let threadMessagesToUpdate = allThreadMessagesRecords.filter(i1 => data.find(i2 => i1.id === i2._id)); - threadMessagesToCreate = threadMessagesToCreate.map(threadMessage => threadMessagesCollection.prepareCreate(protectedFunction((tm) => { - tm._raw = sanitizedRaw({ id: threadMessage._id }, threadMessagesCollection.schema); - Object.assign(tm, threadMessage); - tm.subscription.id = rid; + threadMessagesToCreate = threadMessagesToCreate.map(threadMessage => threadMessagesCollection.prepareCreate(protectedFunction((tm) => { + tm._raw = sanitizedRaw({ id: threadMessage._id }, threadMessagesCollection.schema); + Object.assign(tm, threadMessage); + tm.subscription.id = rid; + tm.rid = threadMessage.tmid; + delete threadMessage.tmid; + }))); + + threadMessagesToUpdate = threadMessagesToUpdate.map((threadMessage) => { + const newThreadMessage = data.find(t => t._id === threadMessage.id); + return threadMessage.prepareUpdate(protectedFunction((tm) => { + Object.assign(tm, newThreadMessage); tm.rid = threadMessage.tmid; delete threadMessage.tmid; - }))); + })); + }); - threadMessagesToUpdate = threadMessagesToUpdate.map((threadMessage) => { - const newThreadMessage = data.find(t => t._id === threadMessage.id); - return threadMessage.prepareUpdate(protectedFunction((tm) => { - Object.assign(tm, newThreadMessage); - tm.rid = threadMessage.tmid; - delete threadMessage.tmid; - })); - }); - - await db.action(async() => { - await db.batch( - ...threadMessagesToCreate, - ...threadMessagesToUpdate - ); - }); - } catch (e) { - log(e); - } - return resolve(data); - }); + await db.action(async() => { + await db.batch( + ...threadMessagesToCreate, + ...threadMessagesToUpdate + ); + }); + } catch (e) { + log(e); + } + return resolve(data); } else { return resolve([]); } diff --git a/app/lib/methods/logout.js b/app/lib/methods/logout.js index 0a1923de0..bb6950835 100644 --- a/app/lib/methods/logout.js +++ b/app/lib/methods/logout.js @@ -42,12 +42,12 @@ async function removeServerData({ server }) { const serversDB = database.servers; const userId = await UserPreferences.getStringAsync(`${ RocketChat.TOKEN_KEY }-${ server }`); - const usersCollection = serversDB.collections.get('users'); + const usersCollection = serversDB.get('users'); if (userId) { const userRecord = await usersCollection.find(userId); batch.push(userRecord.prepareDestroyPermanently()); } - const serverCollection = serversDB.collections.get('servers'); + const serverCollection = serversDB.get('servers'); const serverRecord = await serverCollection.find(server); batch.push(serverRecord.prepareDestroyPermanently()); diff --git a/app/lib/methods/readMessages.js b/app/lib/methods/readMessages.js index e813a68b4..137612648 100644 --- a/app/lib/methods/readMessages.js +++ b/app/lib/methods/readMessages.js @@ -4,7 +4,7 @@ import log from '../../utils/log'; export default async function readMessages(rid, ls, updateLastOpen = false) { try { const db = database.active; - const subscription = await db.collections.get('subscriptions').find(rid); + const subscription = await db.get('subscriptions').find(rid); // RC 0.61.0 await this.sdk.post('subscriptions.read', { rid }); diff --git a/app/lib/methods/sendFileMessage.js b/app/lib/methods/sendFileMessage.js index 923da1723..435e377cb 100644 --- a/app/lib/methods/sendFileMessage.js +++ b/app/lib/methods/sendFileMessage.js @@ -40,7 +40,7 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) { fileInfo.rid = rid; const db = database.active; - const uploadsCollection = db.collections.get('uploads'); + const uploadsCollection = db.get('uploads'); let uploadRecord; try { uploadRecord = await uploadsCollection.find(fileInfo.path); diff --git a/app/lib/methods/sendMessage.js b/app/lib/methods/sendMessage.js index a9a86ae60..9b1bc6d10 100644 --- a/app/lib/methods/sendMessage.js +++ b/app/lib/methods/sendMessage.js @@ -9,8 +9,8 @@ import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../encryption/constants'; const changeMessageStatus = async(id, tmid, status, message) => { const db = database.active; - const msgCollection = db.collections.get('messages'); - const threadMessagesCollection = db.collections.get('thread_messages'); + const msgCollection = db.get('messages'); + const threadMessagesCollection = db.get('thread_messages'); const successBatch = []; const messageRecord = await msgCollection.find(id); successBatch.push( @@ -89,10 +89,10 @@ export async function resendMessage(message, tmid) { export default async function(rid, msg, tmid, user, tshow) { try { const db = database.active; - const subsCollection = db.collections.get('subscriptions'); - const msgCollection = db.collections.get('messages'); - const threadCollection = db.collections.get('threads'); - const threadMessagesCollection = db.collections.get('thread_messages'); + const subsCollection = db.get('subscriptions'); + const msgCollection = db.get('messages'); + const threadCollection = db.get('threads'); + const threadMessagesCollection = db.get('thread_messages'); const messageId = random(17); const batch = []; @@ -151,7 +151,8 @@ export default async function(rid, msg, tmid, user, tshow) { tm.status = messagesStatus.TEMP; tm.u = { _id: user.id || '1', - username: user.username + username: user.username, + name: user.name }; tm.t = message.t; if (message.t === E2E_MESSAGE_TYPE) { @@ -175,7 +176,8 @@ export default async function(rid, msg, tmid, user, tshow) { m.status = messagesStatus.TEMP; m.u = { _id: user.id || '1', - username: user.username + username: user.username, + name: user.name }; if (tmid && tMessageRecord) { m.tmid = tmid; diff --git a/app/lib/methods/subscriptions/room.js b/app/lib/methods/subscriptions/room.js index 71ac3dfee..885c2469a 100644 --- a/app/lib/methods/subscriptions/room.js +++ b/app/lib/methods/subscriptions/room.js @@ -90,6 +90,10 @@ export default class RoomSubscription { if (ev === 'typing') { const { user } = reduxStore.getState().login; const { UI_Use_Real_Name } = reduxStore.getState().settings; + const { rooms } = reduxStore.getState().room; + if (rooms[0] !== _rid) { + return; + } const [name, typing] = ddpMessage.fields.args; const key = UI_Use_Real_Name ? 'name' : 'username'; if (name !== user[key]) { @@ -105,9 +109,9 @@ export default class RoomSubscription { try { const { _id } = ddpMessage.fields.args[0]; const db = database.active; - const msgCollection = db.collections.get('messages'); - const threadsCollection = db.collections.get('threads'); - const threadMessagesCollection = db.collections.get('thread_messages'); + const msgCollection = db.get('messages'); + const threadsCollection = db.get('threads'); + const threadMessagesCollection = db.get('thread_messages'); let deleteMessage; let deleteThread; let deleteThreadMessage; @@ -159,9 +163,9 @@ export default class RoomSubscription { } const db = database.active; - const msgCollection = db.collections.get('messages'); - const threadsCollection = db.collections.get('threads'); - const threadMessagesCollection = db.collections.get('thread_messages'); + const msgCollection = db.get('messages'); + const threadsCollection = db.get('threads'); + const threadMessagesCollection = db.get('thread_messages'); // Decrypt the message if necessary message = await Encryption.decryptMessage(message); diff --git a/app/lib/methods/subscriptions/rooms.js b/app/lib/methods/subscriptions/rooms.js index 42918da13..b79e422a6 100644 --- a/app/lib/methods/subscriptions/rooms.js +++ b/app/lib/methods/subscriptions/rooms.js @@ -32,8 +32,8 @@ const WINDOW_TIME = 500; const createOrUpdateSubscription = async(subscription, room) => { try { const db = database.active; - const subCollection = db.collections.get('subscriptions'); - const roomsCollection = db.collections.get('rooms'); + const subCollection = db.get('subscriptions'); + const roomsCollection = db.get('rooms'); if (!subscription) { try { @@ -185,7 +185,7 @@ const createOrUpdateSubscription = async(subscription, room) => { const { rooms } = store.getState().room; if (tmp.lastMessage && !rooms.includes(tmp.rid)) { const lastMessage = buildMessage(tmp.lastMessage); - const messagesCollection = db.collections.get('messages'); + const messagesCollection = db.get('messages'); let messageRecord; try { messageRecord = await messagesCollection.find(lastMessage._id); @@ -281,7 +281,7 @@ export default function subscribeRooms() { if (/subscriptions/.test(ev)) { if (type === 'removed') { try { - const subCollection = db.collections.get('subscriptions'); + const subCollection = db.get('subscriptions'); const sub = await subCollection.find(data.rid); const messages = await sub.messages.fetch(); const threads = await sub.threads.fetch(); @@ -335,7 +335,7 @@ export default function subscribeRooms() { } }; try { - const msgCollection = db.collections.get('messages'); + const msgCollection = db.get('messages'); await db.action(async() => { await msgCollection.create(protectedFunction((m) => { m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema); @@ -407,7 +407,7 @@ export default function subscribeRooms() { }; connectedListener = this.sdk.onStreamData('connected', handleConnection); - disconnectedListener = this.sdk.onStreamData('close', handleConnection); + // disconnectedListener = this.sdk.onStreamData('close', handleConnection); streamListener = this.sdk.onStreamData('stream-notify-user', handleStreamMessageReceived); try { diff --git a/app/lib/methods/updateMessages.js b/app/lib/methods/updateMessages.js index 0dc63e9ba..5f0db0f66 100644 --- a/app/lib/methods/updateMessages.js +++ b/app/lib/methods/updateMessages.js @@ -16,7 +16,7 @@ export default function updateMessages({ rid, update = [], remove = [] }) { return db.action(async() => { // Decrypt these messages update = await Encryption.decryptMessages(update); - const subCollection = db.collections.get('subscriptions'); + const subCollection = db.get('subscriptions'); let sub; try { sub = await subCollection.find(rid); @@ -26,9 +26,9 @@ export default function updateMessages({ rid, update = [], remove = [] }) { } const messagesIds = [...update.map(m => m._id), ...remove.map(m => m._id)]; - const msgCollection = db.collections.get('messages'); - const threadCollection = db.collections.get('threads'); - const threadMessagesCollection = db.collections.get('thread_messages'); + const msgCollection = db.get('messages'); + const threadCollection = db.get('threads'); + const threadMessagesCollection = db.get('thread_messages'); const allMessagesRecords = await msgCollection .query(Q.where('rid', rid), Q.where('id', Q.oneOf(messagesIds))) .fetch(); diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 4f9952b80..693c5eb78 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -32,7 +32,7 @@ import readMessages from './methods/readMessages'; import getSettings, { getLoginSettings, setSettings } from './methods/getSettings'; import getRooms from './methods/getRooms'; -import getPermissions from './methods/getPermissions'; +import { setPermissions, getPermissions } from './methods/getPermissions'; import { getCustomEmojis, setCustomEmojis } from './methods/getCustomEmojis'; import { getEnterpriseModules, setEnterpriseModules, hasLicense, isOmnichannelModuleAvailable @@ -70,7 +70,6 @@ const CERTIFICATE_KEY = 'RC_CERTIFICATE_KEY'; export const THEME_PREFERENCES_KEY = 'RC_THEME_PREFERENCES_KEY'; export const CRASH_REPORT_KEY = 'RC_CRASH_REPORT_KEY'; export const ANALYTICS_EVENTS_KEY = 'RC_ANALYTICS_EVENTS_KEY'; -const returnAnArray = obj => obj || []; const MIN_ROCKETCHAT_VERSION = '0.70.0'; const STATUSES = ['offline', 'online', 'away', 'busy']; @@ -178,9 +177,16 @@ const RocketChat = { } this.controller = new AbortController(); }, + checkAndReopen() { + return this?.sdk?.checkAndReopen(); + }, connect({ server, user, logoutOnError = false }) { return new Promise((resolve) => { - if (!this.sdk || this.sdk.client.host !== server) { + if (this?.sdk?.client?.host === server) { + return resolve(); + } else { + this.sdk?.disconnect?.(); + this.sdk = null; database.setActiveDB(server); } reduxStore.dispatch(connectRequest()); @@ -209,11 +215,6 @@ const RocketChat = { EventEmitter.emit('INQUIRY_UNSUBSCRIBE'); - if (this.sdk) { - this.sdk.disconnect(); - this.sdk = null; - } - if (this.code) { this.code = null; } @@ -241,6 +242,10 @@ const RocketChat = { sdkConnect(); + this.connectedListener = this.sdk.onStreamData('connecting', () => { + reduxStore.dispatch(connectRequest()); + }); + this.connectedListener = this.sdk.onStreamData('connected', () => { reduxStore.dispatch(connectSuccess()); }); @@ -276,7 +281,7 @@ const RocketChat = { } else if (/updateAvatar/.test(eventName)) { const { username, etag } = ddpMessage.fields.args[0]; const db = database.active; - const userCollection = db.collections.get('users'); + const userCollection = db.get('users'); try { const [userRecord] = await userCollection.query(Q.where('username', Q.eq(username))).fetch(); await db.action(async() => { @@ -290,7 +295,7 @@ const RocketChat = { } else if (/Users:NameChanged/.test(eventName)) { const userNameChanged = ddpMessage.fields.args[0]; const db = database.active; - const userCollection = db.collections.get('users'); + const userCollection = db.get('users'); try { const userRecord = await userCollection.find(userNameChanged._id); await db.action(async() => { @@ -334,7 +339,7 @@ const RocketChat = { // set Server const currentServer = { server }; const serversDB = database.servers; - const serversCollection = serversDB.collections.get('servers'); + const serversCollection = serversDB.get('servers'); try { const serverRecord = await serversCollection.find(server); currentServer.version = serverRecord.version; @@ -349,7 +354,7 @@ const RocketChat = { // set Settings const settings = ['Accounts_AvatarBlockUnauthenticatedAccess']; const db = database.active; - const settingsCollection = db.collections.get('settings'); + const settingsCollection = db.get('settings'); const settingsRecords = await settingsCollection.query(Q.where('id', Q.oneOf(settings))).fetch(); const parsed = Object.values(settingsRecords).map(item => ({ _id: item.id, @@ -363,7 +368,7 @@ const RocketChat = { // set User info const userId = await UserPreferences.getStringAsync(`${ RocketChat.TOKEN_KEY }-${ server }`); - const userCollections = serversDB.collections.get('users'); + const userCollections = serversDB.get('users'); let user = null; if (userId) { const userRecord = await userCollections.find(userId); @@ -545,7 +550,7 @@ const RocketChat = { try { const serversDB = database.servers; await serversDB.action(async() => { - const serverCollection = serversDB.collections.get('servers'); + const serverCollection = serversDB.get('servers'); const serverRecord = await serverCollection.find(server); await serverRecord.update((s) => { s.roomsUpdatedAt = null; @@ -605,7 +610,7 @@ const RocketChat = { } const db = database.active; const likeString = sanitizeLikeString(searchText); - let data = await db.collections.get('subscriptions').query( + let data = await db.get('subscriptions').query( Q.or( Q.where('name', Q.like(`%${ likeString }%`)), Q.where('fname', Q.like(`%${ likeString }%`)) @@ -621,19 +626,15 @@ const RocketChat = { data = data.slice(0, 7); - data = data.map((sub) => { - if (sub.t !== 'd') { - return { - rid: sub.rid, - name: sub.name, - fname: sub.fname, - avatarETag: sub.avatarETag, - t: sub.t, - encrypted: sub.encrypted - }; - } - return sub; - }); + data = data.map(sub => ({ + rid: sub.rid, + name: sub.name, + fname: sub.fname, + avatarETag: sub.avatarETag, + t: sub.t, + encrypted: sub.encrypted, + lastMessage: sub.lastMessage + })); return data; }, @@ -740,6 +741,7 @@ const RocketChat = { getLoginSettings, setSettings, getPermissions, + setPermissions, getCustomEmojis, setCustomEmojis, getEnterpriseModules, @@ -796,7 +798,7 @@ const RocketChat = { async getRoom(rid) { try { const db = database.active; - const room = await db.collections.get('subscriptions').find(rid); + const room = await db.get('subscriptions').find(rid); return Promise.resolve(room); } catch (error) { return Promise.reject(new Error('Room not found')); @@ -1172,10 +1174,13 @@ const RocketChat = { // RC 0.65.0 return this.sdk.get(`${ this.roomTypeToApiType(type) }.roles`, { roomId }); }, + /** + * Permissions: array of permissions' roles from redux. Example: [['owner', 'admin'], ['leader']] + * Returns an array of boolean for each permission from permissions arg + */ async hasPermission(permissions, rid) { const db = database.active; - const subsCollection = db.collections.get('subscriptions'); - const permissionsCollection = db.collections.get('permissions'); + const subsCollection = db.get('subscriptions'); let roomRoles = []; try { // get the room from database @@ -1184,31 +1189,16 @@ const RocketChat = { roomRoles = room.roles || []; } catch (error) { console.log('hasPermission -> Room not found'); - return permissions.reduce((result, permission) => { - result[permission] = false; - return result; - }, {}); + return permissions.map(() => false); } - // get permissions from database + try { - const permissionsFiltered = await permissionsCollection.query(Q.where('id', Q.oneOf(permissions))).fetch(); const shareUser = reduxStore.getState().share.user; const loginUser = reduxStore.getState().login.user; // get user roles on the server from redux const userRoles = (shareUser?.roles || loginUser?.roles) || []; - // merge both roles const mergedRoles = [...new Set([...roomRoles, ...userRoles])]; - - // return permissions in object format - // e.g. { 'edit-room': true, 'set-readonly': false } - return permissions.reduce((result, permission) => { - result[permission] = false; - const permissionFound = permissionsFiltered.find(p => p.id === permission); - if (permissionFound) { - result[permission] = returnAnArray(permissionFound.roles).some(r => mergedRoles.includes(r)); - } - return result; - }, {}); + return permissions.map(permission => permission?.some(r => mergedRoles.includes(r) ?? false)); } catch (e) { log(e); } @@ -1438,17 +1428,15 @@ const RocketChat = { query, count, offset, sort }); }, - async canAutoTranslate() { - const db = database.active; + canAutoTranslate() { try { - const AutoTranslate_Enabled = reduxStore.getState().settings && reduxStore.getState().settings.AutoTranslate_Enabled; + const { AutoTranslate_Enabled } = reduxStore.getState().settings; if (!AutoTranslate_Enabled) { return false; } - const permissionsCollection = db.collections.get('permissions'); - const autoTranslatePermission = await permissionsCollection.find('auto-translate'); - const userRoles = (reduxStore.getState().login.user && reduxStore.getState().login.user.roles) || []; - return autoTranslatePermission.roles.some(role => userRoles.includes(role)); + const autoTranslatePermission = reduxStore.getState().permissions['auto-translate']; + const userRoles = (reduxStore.getState().login?.user?.roles) ?? []; + return autoTranslatePermission?.some(role => userRoles.includes(role)); } catch (e) { log(e); return false; diff --git a/app/presentation/RoomItem/LastMessage.js b/app/presentation/RoomItem/LastMessage.js index b29cb6cd1..25fc2c08d 100644 --- a/app/presentation/RoomItem/LastMessage.js +++ b/app/presentation/RoomItem/LastMessage.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import isEqual from 'lodash/isEqual'; +import { dequal } from 'dequal'; import I18n from '../../i18n'; import styles from './styles'; @@ -45,7 +45,7 @@ const formatMsg = ({ return `${ prefix }${ lastMessage.msg }`; }; -const arePropsEqual = (oldProps, newProps) => isEqual(oldProps, newProps); +const arePropsEqual = (oldProps, newProps) => dequal(oldProps, newProps); const LastMessage = React.memo(({ lastMessage, type, showLastMessage, username, alert, useRealName, theme diff --git a/app/reducers/index.js b/app/reducers/index.js index 6211ccb6f..dfee5f3eb 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -18,6 +18,7 @@ import inviteLinks from './inviteLinks'; import createDiscussion from './createDiscussion'; import enterpriseModules from './enterpriseModules'; import encryption from './encryption'; +import permissions from './permissions'; import inquiry from '../ee/omnichannel/reducers/inquiry'; @@ -41,5 +42,6 @@ export default combineReducers({ createDiscussion, inquiry, enterpriseModules, - encryption + encryption, + permissions }); diff --git a/app/reducers/permissions.js b/app/reducers/permissions.js new file mode 100644 index 000000000..1b3a14ec2 --- /dev/null +++ b/app/reducers/permissions.js @@ -0,0 +1,14 @@ +import { PERMISSIONS } from '../actions/actionsTypes'; + +const initialState = { + permissions: {} +}; + +export default function permissions(state = initialState, action) { + switch (action.type) { + case PERMISSIONS.SET: + return action.permissions; + default: + return state; + } +} diff --git a/app/sagas/createChannel.js b/app/sagas/createChannel.js index ca4bcb9fe..613aedec2 100644 --- a/app/sagas/createChannel.js +++ b/app/sagas/createChannel.js @@ -42,7 +42,7 @@ const handleRequest = function* handleRequest({ data }) { broadcast, encrypted } = data; - logEvent(events.CREATE_CHANNEL_CREATE, { + logEvent(events.CR_CREATE, { type: type ? 'private' : 'public', readOnly, broadcast, @@ -53,7 +53,7 @@ const handleRequest = function* handleRequest({ data }) { try { const db = database.active; - const subCollection = db.collections.get('subscriptions'); + const subCollection = db.get('subscriptions'); yield db.action(async() => { await subCollection.create((s) => { s._raw = sanitizedRaw({ id: sub.rid }, subCollection.schema); @@ -66,7 +66,7 @@ const handleRequest = function* handleRequest({ data }) { yield put(createChannelSuccess(sub)); } catch (err) { - logEvent(events[data.group ? 'SELECTED_USERS_CREATE_GROUP_F' : 'CREATE_CHANNEL_CREATE_F']); + logEvent(events[data.group ? 'SELECTED_USERS_CREATE_GROUP_F' : 'CR_CREATE_F']); yield put(createChannelFailure(err)); } }; diff --git a/app/sagas/createDiscussion.js b/app/sagas/createDiscussion.js index a185e6671..746ab1583 100644 --- a/app/sagas/createDiscussion.js +++ b/app/sagas/createDiscussion.js @@ -14,7 +14,7 @@ const create = function* create(data) { }; const handleRequest = function* handleRequest({ data }) { - logEvent(events.CREATE_DISCUSSION_CREATE); + logEvent(events.CD_CREATE); try { const auth = yield select(state => state.login.isAuthenticated); if (!auth) { @@ -27,7 +27,7 @@ const handleRequest = function* handleRequest({ data }) { try { const db = database.active; - const subCollection = db.collections.get('subscriptions'); + const subCollection = db.get('subscriptions'); yield db.action(async() => { await subCollection.create((s) => { s._raw = sanitizedRaw({ id: sub.rid }, subCollection.schema); @@ -39,11 +39,11 @@ const handleRequest = function* handleRequest({ data }) { } yield put(createDiscussionSuccess(sub)); } else { - logEvent(events.CREATE_DISCUSSION_CREATE_F); + logEvent(events.CD_CREATE_F); yield put(createDiscussionFailure(result)); } } catch (err) { - logEvent(events.CREATE_DISCUSSION_CREATE_F); + logEvent(events.CD_CREATE_F); yield put(createDiscussionFailure(err)); } }; diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js index caf2a46f9..21e0695fb 100644 --- a/app/sagas/deepLinking.js +++ b/app/sagas/deepLinking.js @@ -31,6 +31,14 @@ const handleInviteLink = function* handleInviteLink({ params, requireLogin = fal } }; +const popToRoot = function popToRoot({ isMasterDetail }) { + if (isMasterDetail) { + Navigation.navigate('DrawerNavigator'); + } else { + Navigation.navigate('RoomsListView'); + } +}; + const navigate = function* navigate({ params }) { yield put(appStart({ root: ROOT_INSIDE })); if (params.path) { @@ -38,22 +46,31 @@ const navigate = function* navigate({ params }) { if (type !== 'invite') { const room = yield RocketChat.canOpenRoom(params); if (room) { - const isMasterDetail = yield select(state => state.app.isMasterDetail); - if (isMasterDetail) { - Navigation.navigate('DrawerNavigator'); - } else { - Navigation.navigate('RoomsListView'); - } const item = { name, t: roomTypes[type], roomUserId: RocketChat.getUidDirectMessage(room), ...room }; - yield goRoom({ item, isMasterDetail }); + + const isMasterDetail = yield select(state => state.app.isMasterDetail); + const focusedRooms = yield select(state => state.room.rooms); + + if (focusedRooms.includes(room.rid)) { + // if there's one room on the list or last room is the one + if (focusedRooms.length === 1 || focusedRooms[0] === room.rid) { + yield goRoom({ item, isMasterDetail }); + } else { + popToRoot({ isMasterDetail }); + yield goRoom({ item, isMasterDetail }); + } + } else { + popToRoot({ isMasterDetail }); + yield goRoom({ item, isMasterDetail }); + } if (params.isCall) { - RocketChat.callJitsi(item.rid); + RocketChat.callJitsi(item); } } } else { @@ -72,7 +89,7 @@ const fallbackNavigation = function* fallbackNavigation() { const handleOpen = function* handleOpen({ params }) { const serversDB = database.servers; - const serversCollection = serversDB.collections.get('servers'); + const serversCollection = serversDB.get('servers'); let { host } = params; if (params.isCall && !host) { @@ -121,10 +138,10 @@ const handleOpen = function* handleOpen({ params }) { } else { // search if deep link's server already exists try { - const servers = yield serversCollection.find(host); - if (servers && user) { + const hostServerRecord = yield serversCollection.find(host); + if (hostServerRecord && user) { yield localAuthenticate(host); - yield put(selectServerRequest(host)); + yield put(selectServerRequest(host, hostServerRecord.version, true, true)); yield take(types.LOGIN.SUCCESS); yield navigate({ params }); return; diff --git a/app/sagas/encryption.js b/app/sagas/encryption.js index f28c77834..395923a43 100644 --- a/app/sagas/encryption.js +++ b/app/sagas/encryption.js @@ -30,7 +30,7 @@ const handleEncryptionInit = function* handleEncryptionInit() { // Fetch server info to check E2E enable const serversDB = database.servers; - const serversCollection = serversDB.collections.get('servers'); + const serversCollection = serversDB.get('servers'); let serverInfo; try { serverInfo = yield serversCollection.find(server); diff --git a/app/sagas/init.js b/app/sagas/init.js index 5c0cb7286..03a6c5a78 100644 --- a/app/sagas/init.js +++ b/app/sagas/init.js @@ -38,7 +38,7 @@ const restore = function* restore() { yield put(appStart({ root: ROOT_OUTSIDE })); } else { const serversDB = database.servers; - const serverCollections = serversDB.collections.get('servers'); + const serverCollections = serversDB.get('servers'); let serverObj; try { diff --git a/app/sagas/login.js b/app/sagas/login.js index cf487eed4..b5c267263 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -57,7 +57,7 @@ const handleLoginRequest = function* handleLoginRequest({ credentials, logoutOnE // Saves username on server history const serversDB = database.servers; - const serversHistoryCollection = serversDB.collections.get('servers_history'); + const serversHistoryCollection = serversDB.get('servers_history'); yield serversDB.action(async() => { try { const serversHistory = await serversHistoryCollection.query(Q.where('url', server)).fetch(); @@ -145,7 +145,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) { moment.locale(toMomentLocale(user.language)); const serversDB = database.servers; - const usersCollection = serversDB.collections.get('users'); + const usersCollection = serversDB.get('users'); const u = { token: user.token, username: user.username, @@ -222,7 +222,7 @@ const handleLogout = function* handleLogout({ forcedByServer }) { } else { const serversDB = database.servers; // all servers - const serversCollection = serversDB.collections.get('servers'); + const serversCollection = serversDB.get('servers'); const servers = yield serversCollection.query().fetch(); // see if there're other logged in servers and selects first one diff --git a/app/sagas/messages.js b/app/sagas/messages.js index 2879b2b64..2674be42e 100644 --- a/app/sagas/messages.js +++ b/app/sagas/messages.js @@ -12,7 +12,7 @@ const handleReplyBroadcast = function* handleReplyBroadcast({ message }) { try { const db = database.active; const { username } = message.u; - const subsCollection = db.collections.get('subscriptions'); + const subsCollection = db.get('subscriptions'); const subscriptions = yield subsCollection.query(Q.where('name', username)).fetch(); const isMasterDetail = yield select(state => state.app.isMasterDetail); diff --git a/app/sagas/rooms.js b/app/sagas/rooms.js index 94670ea1f..f963a2948 100644 --- a/app/sagas/rooms.js +++ b/app/sagas/rooms.js @@ -15,7 +15,7 @@ import protectedFunction from '../lib/methods/helpers/protectedFunction'; const updateRooms = function* updateRooms({ server, newRoomsUpdatedAt }) { const serversDB = database.servers; - const serversCollection = serversDB.collections.get('servers'); + const serversCollection = serversDB.get('servers'); try { const serverRecord = yield serversCollection.find(server); @@ -39,7 +39,7 @@ const handleRoomsRequest = function* handleRoomsRequest({ params }) { if (params.allData) { yield put(roomsRefresh()); } else { - const serversCollection = serversDB.collections.get('servers'); + const serversCollection = serversDB.get('servers'); try { const serverRecord = yield serversCollection.find(server); ({ roomsUpdatedAt } = serverRecord); @@ -51,8 +51,8 @@ const handleRoomsRequest = function* handleRoomsRequest({ params }) { const { subscriptions } = yield mergeSubscriptionsRooms(subscriptionsResult, roomsResult); const db = database.active; - const subCollection = db.collections.get('subscriptions'); - const messagesCollection = db.collections.get('messages'); + const subCollection = db.get('subscriptions'); + const messagesCollection = db.get('messages'); const subsIds = subscriptions.map(sub => sub.rid).concat(roomsResult.remove.map(room => room._id)); if (subsIds.length) { diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js index 87fb47549..d367274a3 100644 --- a/app/sagas/selectServer.js +++ b/app/sagas/selectServer.js @@ -46,7 +46,7 @@ const getServerInfo = function* getServerInfo({ server, raiseError = true }) { } const serversDB = database.servers; - const serversCollection = serversDB.collections.get('servers'); + const serversCollection = serversDB.get('servers'); yield serversDB.action(async() => { try { const serverRecord = await serversCollection.find(server); @@ -78,7 +78,7 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch const serversDB = database.servers; yield UserPreferences.setStringAsync(RocketChat.CURRENT_SERVER, server); const userId = yield UserPreferences.getStringAsync(`${ RocketChat.TOKEN_KEY }-${ server }`); - const userCollections = serversDB.collections.get('users'); + const userCollections = serversDB.get('users'); let user = null; if (userId) { try { @@ -124,6 +124,7 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch // and block the selectServerSuccess raising multiples errors RocketChat.setSettings(); RocketChat.setCustomEmojis(); + RocketChat.setPermissions(); RocketChat.setEnterpriseModules(); let serverInfo; @@ -151,7 +152,7 @@ const handleServerRequest = function* handleServerRequest({ server, username, fr const serverInfo = yield getServerInfo({ server }); const serversDB = database.servers; - const serversHistoryCollection = serversDB.collections.get('servers_history'); + const serversHistoryCollection = serversDB.get('servers_history'); if (serverInfo) { yield RocketChat.getLoginServices(server); diff --git a/app/sagas/state.js b/app/sagas/state.js index 8b072c0a9..5b86af2ef 100644 --- a/app/sagas/state.js +++ b/app/sagas/state.js @@ -12,13 +12,14 @@ const appHasComeBackToForeground = function* appHasComeBackToForeground() { if (appRoot === ROOT_OUTSIDE) { return; } - const auth = yield select(state => state.login.isAuthenticated); - if (!auth) { + const login = yield select(state => state.login); + const server = yield select(state => state.server); + if (!login.isAuthenticated || login.isFetching || server.connecting || server.loading || server.changingServer) { return; } try { - const server = yield select(state => state.server.server); - yield localAuthenticate(server); + yield localAuthenticate(server.server); + RocketChat.checkAndReopen(); setBadgeCount(); return yield RocketChat.setUserPresenceOnline(); } catch (e) { @@ -31,14 +32,6 @@ const appHasComeBackToBackground = function* appHasComeBackToBackground() { if (appRoot === ROOT_OUTSIDE) { return; } - const auth = yield select(state => state.login.isAuthenticated); - if (!auth) { - return; - } - const localAuthenticated = yield select(state => state.login.isLocalAuthenticated); - if (!localAuthenticated) { - return; - } try { const server = yield select(state => state.server.server); yield saveLastLocalAuthenticationSession(server); diff --git a/app/stacks/InsideStack.js b/app/stacks/InsideStack.js index 75d01708e..a0af96177 100644 --- a/app/stacks/InsideStack.js +++ b/app/stacks/InsideStack.js @@ -31,6 +31,7 @@ import PickerView from '../views/PickerView'; import ThreadMessagesView from '../views/ThreadMessagesView'; import MarkdownTableView from '../views/MarkdownTableView'; import ReadReceiptsView from '../views/ReadReceiptView'; +import { themes } from '../constants/colors'; // Profile Stack import ProfileView from '../views/ProfileView'; @@ -280,19 +281,24 @@ const AdminPanelStackNavigator = () => { // DrawerNavigator const Drawer = createDrawerNavigator(); -const DrawerNavigator = () => ( - } - drawerPosition={I18nManager.isRTL ? 'right' : 'left'} - screenOptions={{ swipeEnabled: false }} - drawerType='back' - > - - - - - -); +const DrawerNavigator = () => { + const { theme } = React.useContext(ThemeContext); + + return ( + } + drawerPosition={I18nManager.isRTL ? 'right' : 'left'} + screenOptions={{ swipeEnabled: false }} + drawerType='back' + overlayColor={`rgba(0,0,0,${ themes[theme].backdropOpacity })`} + > + + + + + + ); +}; // NewMessageStackNavigator const NewMessageStack = createStackNavigator(); diff --git a/app/utils/isReadOnly.js b/app/utils/isReadOnly.js index 7d35a2836..f6f1937f6 100644 --- a/app/utils/isReadOnly.js +++ b/app/utils/isReadOnly.js @@ -1,13 +1,11 @@ import RocketChat from '../lib/rocketchat'; +import reduxStore from '../lib/createStore'; -const canPost = async({ rid }) => { - try { - const permission = await RocketChat.hasPermission(['post-readonly'], rid); - return permission && permission['post-readonly']; - } catch { - // do nothing - } - return false; +const canPostReadOnly = async({ rid }) => { + // TODO: this is not reactive. If this permission changes, the component won't be updated + const postReadOnlyPermission = reduxStore.getState().permissions['post-readonly']; + const permission = await RocketChat.hasPermission([postReadOnlyPermission], rid); + return permission[0]; }; const isMuted = (room, user) => room && room.muted && room.muted.find && !!room.muted.find(m => m === user.username); @@ -20,7 +18,7 @@ export const isReadOnly = async(room, user) => { return true; } if (room?.ro) { - const allowPost = await canPost(room); + const allowPost = await canPostReadOnly(room); if (allowPost) { return false; } diff --git a/app/utils/localAuthentication.js b/app/utils/localAuthentication.js index 2eb9c7d29..6e6d40dfb 100644 --- a/app/utils/localAuthentication.js +++ b/app/utils/localAuthentication.js @@ -17,7 +17,7 @@ import { setLocalAuthenticated } from '../actions/login'; export const saveLastLocalAuthenticationSession = async(server, serverRecord) => { const serversDB = database.servers; - const serversCollection = serversDB.collections.get('servers'); + const serversCollection = serversDB.get('servers'); await serversDB.action(async() => { try { if (!serverRecord) { @@ -91,7 +91,7 @@ export const checkHasPasscode = async({ force = true, serverRecord }) => { export const localAuthenticate = async(server) => { const serversDB = database.servers; - const serversCollection = serversDB.collections.get('servers'); + const serversCollection = serversDB.get('servers'); let serverRecord; try { @@ -102,9 +102,6 @@ export const localAuthenticate = async(server) => { // if screen lock is enabled if (serverRecord?.autoLock) { - // set isLocalAuthenticated to false - store.dispatch(setLocalAuthenticated(false)); - // Make sure splash screen has been hidden RNBootSplash.hide(); @@ -118,6 +115,9 @@ export const localAuthenticate = async(server) => { // if last authenticated session is older than configured auto lock time, authentication is required if (diffToLastSession >= serverRecord?.autoLockTime) { + // set isLocalAuthenticated to false + store.dispatch(setLocalAuthenticated(false)); + let hasBiometry = false; // if biometry is enabled on the app diff --git a/app/utils/log/events.js b/app/utils/log/events.js index 4895ad8f2..2e1e8f9a4 100644 --- a/app/utils/log/events.js +++ b/app/utils/log/events.js @@ -5,9 +5,8 @@ export default { ONBOARD_CREATE_NEW_WORKSPACE_F: 'onboard_create_new_workspace_f', // NEW SERVER VIEW - NEWSERVER_CONNECT_TO_WORKSPACE: 'newserver_connect_to_workspace', - NEWSERVER_CONNECT_TO_WORKSPACE_F: 'newserver_connect_to_workspace_f', - NEWSERVER_JOIN_OPEN_WORKSPACE: 'newserver_join_open_workspace', + NS_CONNECT_TO_WORKSPACE: 'ns_connect_to_workspace', + NS_JOIN_OPEN_WORKSPACE: 'ns_join_open_workspace', // LOGIN VIEW LOGIN_DEFAULT_LOGIN: 'login_default_login', @@ -99,20 +98,20 @@ export default { SELECTED_USERS_CREATE_GROUP_F: 'selected_users_create_group_f', // CREATE CHANNEL VIEW - CREATE_CHANNEL_CREATE: 'create_channel_create', - CREATE_CHANNEL_CREATE_F: 'create_channel_create_f', - CREATE_CHANNEL_TOGGLE_TYPE: 'create_channel_toggle_type', - CREATE_CHANNEL_TOGGLE_READ_ONLY: 'create_channel_toggle_read_only', - CREATE_CHANNEL_TOGGLE_BROADCAST: 'create_channel_toggle_broadcast', - CREATE_CHANNEL_TOGGLE_ENCRYPTED: 'create_channel_toggle_encrypted', - CREATE_CHANNEL_REMOVE_USER: 'create_channel_remove_user', + CR_CREATE: 'cr_create', + CR_CREATE_F: 'cr_create_f', + CR_TOGGLE_TYPE: 'cr_toggle_type', + CR_TOGGLE_READ_ONLY: 'cr_toggle_read_only', + CR_TOGGLE_BROADCAST: 'cr_toggle_broadcast', + CR_TOGGLE_ENCRYPTED: 'cr_toggle_encrypted', + CR_REMOVE_USER: 'cr_remove_user', // CREATE DISCUSSION VIEW - CREATE_DISCUSSION_CREATE: 'create_discussion_create', - CREATE_DISCUSSION_CREATE_F: 'create_discussion_create_f', - CREATE_DISCUSSION_SELECT_CHANNEL: 'create_discussion_select_channel', - CREATE_DISCUSSION_SELECT_USERS: 'create_discussion_select_users', - CREATE_DISCUSSION_TOGGLE_ENCRY: 'create_discussion_toggle_encry', + CD_CREATE: 'cd_create', + CD_CREATE_F: 'cd_create_f', + CD_SELECT_CHANNEL: 'cd_select_channel', + CD_SELECT_USERS: 'cd_select_users', + CD_TOGGLE_ENCRY: 'cd_toggle_encry', // PROFILE VIEW PROFILE_PICK_AVATAR: 'profile_pick_avatar', @@ -122,8 +121,9 @@ export default { PROFILE_SAVE_AVATAR_F: 'profile_save_avatar_f', PROFILE_SAVE_CHANGES: 'profile_save_changes', PROFILE_SAVE_CHANGES_F: 'profile_save_changes_f', - PROFILE_LOGOUT_OTHER_LOCATIONS: 'profile_logout_other_locations', - PROFILE_LOGOUT_OTHER_LOCATIONS_F: 'profile_logout_other_locations_f', + // PROFILE LOGOUT + PL_OTHER_LOCATIONS: 'pl_other_locations', + PL_OTHER_LOCATIONS_F: 'pl_other_locations_f', // SETTINGS VIEW SE_CONTACT_US: 'se_contact_us', @@ -297,8 +297,6 @@ export default { NP_AUDIONOTIFICATIONS_F: 'np_audio_notifications_f', NP_AUDIONOTIFICATIONVALUE: 'np_audio_notification_value', NP_AUDIONOTIFICATIONVALUE_F: 'np_audio_notification_value_f', - NP_DESKTOPNOTIFICATIONDURATION: 'np_desktopnotificationduration', - NP_DESKTOPNOTIFICATIONDURATION_F: 'np_desktopnotificationduration_f', NP_EMAILNOTIFICATIONS: 'np_email_notifications', NP_EMAILNOTIFICATIONS_F: 'np_email_notifications_f', diff --git a/app/views/AttachmentView.js b/app/views/AttachmentView.js index 3f4adfec7..c07ed71b9 100644 --- a/app/views/AttachmentView.js +++ b/app/views/AttachmentView.js @@ -121,8 +121,9 @@ class AttachmentView extends React.Component { const extension = image_url ? `.${ mime.extension(image_type) || 'jpg' }` : `.${ mime.extension(video_type) || 'mp4' }`; const documentDir = `${ RNFetchBlob.fs.dirs.DocumentDir }/`; const path = `${ documentDir + SHA256(url) + extension }`; - const file = await RNFetchBlob.config({ path }).fetch('GET', mediaAttachment).then(res => res.path()); - await CameraRoll.save(file, { album: 'Rocket.Chat' }); + const file = await RNFetchBlob.config({ path }).fetch('GET', mediaAttachment); + await CameraRoll.save(path, { album: 'Rocket.Chat' }); + await file.flush(); EventEmitter.emit(LISTENER, { message: I18n.t('saved_to_gallery') }); } catch (e) { EventEmitter.emit(LISTENER, { message: I18n.t(image_url ? 'error-save-image' : 'error-save-video') }); diff --git a/app/views/ChangePasscodeView.js b/app/views/ChangePasscodeView.js index 308e2d782..535ba71d4 100644 --- a/app/views/ChangePasscodeView.js +++ b/app/views/ChangePasscodeView.js @@ -63,12 +63,12 @@ const ChangePasscodeView = React.memo(({ theme }) => { if (!isTablet) { Orientation.lockToPortrait(); } - EventEmitter.addEventListener(CHANGE_PASSCODE_EMITTER, showChangePasscode); + const listener = EventEmitter.addEventListener(CHANGE_PASSCODE_EMITTER, showChangePasscode); return (() => { if (!isTablet) { Orientation.unlockAllOrientations(); } - EventEmitter.removeListener(CHANGE_PASSCODE_EMITTER); + EventEmitter.removeListener(CHANGE_PASSCODE_EMITTER, listener); }); }, []); diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js index 70982094d..9d1e450ff 100644 --- a/app/views/CreateChannelView.js +++ b/app/views/CreateChannelView.js @@ -4,7 +4,8 @@ import PropTypes from 'prop-types'; import { View, Text, Switch, ScrollView, StyleSheet, FlatList } from 'react-native'; -import equal from 'deep-equal'; +import { dequal } from 'dequal'; +import * as List from '../containers/List'; import TextInput from '../presentation/TextInput'; import Loading from '../containers/Loading'; @@ -31,12 +32,6 @@ const styles = StyleSheet.create({ list: { width: '100%' }, - separator: { - marginLeft: 60 - }, - formSeparator: { - marginLeft: 15 - }, input: { height: 54, paddingHorizontal: 18, @@ -133,7 +128,7 @@ class CreateChannelView extends React.Component { if (nextProps.encryptionEnabled !== encryptionEnabled) { return true; } - if (!equal(nextProps.users, users)) { + if (!dequal(nextProps.users, users)) { return true; } return false; @@ -177,7 +172,7 @@ class CreateChannelView extends React.Component { } removeUser = (user) => { - logEvent(events.CREATE_CHANNEL_REMOVE_USER); + logEvent(events.CR_REMOVE_USER); const { removeUser } = this.props; removeUser(user); } @@ -207,7 +202,7 @@ class CreateChannelView extends React.Component { value: type, label: 'Private_Channel', onValueChange: (value) => { - logEvent(events.CREATE_CHANNEL_TOGGLE_TYPE); + logEvent(events.CR_TOGGLE_TYPE); // If we set the channel as public, encrypted status should be false this.setState(({ encrypted }) => ({ type: value, encrypted: value && encrypted })); } @@ -221,7 +216,7 @@ class CreateChannelView extends React.Component { value: readOnly, label: 'Read_Only_Channel', onValueChange: (value) => { - logEvent(events.CREATE_CHANNEL_TOGGLE_READ_ONLY); + logEvent(events.CR_TOGGLE_READ_ONLY); this.setState({ readOnly: value }); }, disabled: broadcast @@ -241,7 +236,7 @@ class CreateChannelView extends React.Component { value: encrypted, label: 'Encrypted', onValueChange: (value) => { - logEvent(events.CREATE_CHANNEL_TOGGLE_ENCRYPTED); + logEvent(events.CR_TOGGLE_ENCRYPTED); this.setState({ encrypted: value }); }, disabled: !type @@ -255,7 +250,7 @@ class CreateChannelView extends React.Component { value: broadcast, label: 'Broadcast_Channel', onValueChange: (value) => { - logEvent(events.CREATE_CHANNEL_TOGGLE_BROADCAST); + logEvent(events.CR_TOGGLE_BROADCAST); this.setState({ broadcast: value, readOnly: value ? true : readOnly @@ -264,13 +259,6 @@ class CreateChannelView extends React.Component { }); } - renderSeparator = () => - - renderFormSeparator = () => { - const { theme } = this.props; - return ; - } - renderItem = ({ item }) => { const { baseUrl, user, theme } = this.props; @@ -305,7 +293,7 @@ class CreateChannelView extends React.Component { } ]} renderItem={this.renderItem} - ItemSeparatorComponent={this.renderSeparator} + ItemSeparatorComponent={List.Separator} enableEmptySections keyboardShouldPersistTaps='always' /> @@ -341,13 +329,13 @@ class CreateChannelView extends React.Component { theme={theme} underlineColorAndroid='transparent' /> - {this.renderFormSeparator()} + {this.renderType()} - {this.renderFormSeparator()} + {this.renderReadOnly()} - {this.renderFormSeparator()} + {this.renderEncrypted()} - {this.renderFormSeparator()} + {this.renderBroadcast()} diff --git a/app/views/CreateDiscussionView/SelectUsers.js b/app/views/CreateDiscussionView/SelectUsers.js index a88773ab0..8500ee15c 100644 --- a/app/views/CreateDiscussionView/SelectUsers.js +++ b/app/views/CreateDiscussionView/SelectUsers.js @@ -22,7 +22,7 @@ const SelectUsers = ({ const getUsers = debounce(async(keyword = '') => { try { const db = database.active; - const usersCollection = db.collections.get('users'); + const usersCollection = db.get('users'); const res = await RocketChat.search({ text: keyword, filterRooms: false }); let items = [...users.filter(u => selected.includes(u.name)), ...res.filter(r => !users.find(u => u.name === r.name))]; const records = await usersCollection.query(Q.where('username', Q.oneOf(items.map(u => u.name)))).fetch(); diff --git a/app/views/CreateDiscussionView/index.js b/app/views/CreateDiscussionView/index.js index 43329f102..2970ba016 100644 --- a/app/views/CreateDiscussionView/index.js +++ b/app/views/CreateDiscussionView/index.js @@ -2,7 +2,6 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { ScrollView, Text, Switch } from 'react-native'; -import isEqual from 'lodash/isEqual'; import Loading from '../../containers/Loading'; import KeyboardView from '../../presentation/KeyboardView'; @@ -63,11 +62,12 @@ class CreateChannelView extends React.Component { } componentDidUpdate(prevProps, prevState) { + const { channel, name } = this.state; const { loading, failure, error, result, isMasterDetail } = this.props; - if (!isEqual(this.state, prevState)) { + if (channel?.rid !== prevState.channel?.rid || name !== prevState.name) { this.setHeader(); } @@ -136,17 +136,17 @@ class CreateChannelView extends React.Component { }; selectChannel = ({ value }) => { - logEvent(events.CREATE_DISCUSSION_SELECT_CHANNEL); + logEvent(events.CD_SELECT_CHANNEL); this.setState({ channel: value, encrypted: value?.encrypted }); } selectUsers = ({ value }) => { - logEvent(events.CREATE_DISCUSSION_SELECT_USERS); + logEvent(events.CD_SELECT_USERS); this.setState({ users: value }); } onEncryptedChange = (value) => { - logEvent(events.CREATE_DISCUSSION_TOGGLE_ENCRY); + logEvent(events.CD_TOGGLE_ENCRY); this.setState({ encrypted: value }); } @@ -222,7 +222,7 @@ const mapStateToProps = state => ({ loading: state.createDiscussion.isFetching, result: state.createDiscussion.result, blockUnauthenticatedAccess: state.settings.Accounts_AvatarBlockUnauthenticatedAccess ?? true, - serverVersion: state.share.server.version || state.server.version, + serverVersion: state.server.version, isMasterDetail: state.app.isMasterDetail, encryptionEnabled: state.encryption.enabled }); diff --git a/app/views/DirectoryView/Options.js b/app/views/DirectoryView/Options.js index ab69b11f4..ad8de1da1 100644 --- a/app/views/DirectoryView/Options.js +++ b/app/views/DirectoryView/Options.js @@ -84,13 +84,13 @@ export default class DirectoryOptions extends PureComponent { inputRange: [0, 1], outputRange: [-326, 0] }); - const backdropOpacity = this.animatedValue.interpolate({ - inputRange: [0, 1], - outputRange: [0, 0.3] - }); const { globalUsers, toggleWorkspace, isFederationEnabled, theme } = this.props; + const backdropOpacity = this.animatedValue.interpolate({ + inputRange: [0, 1], + outputRange: [0, themes[theme].backdropOpacity] + }); return ( <> diff --git a/app/views/DirectoryView/index.js b/app/views/DirectoryView/index.js index 8be78712d..afb1214f3 100644 --- a/app/views/DirectoryView/index.js +++ b/app/views/DirectoryView/index.js @@ -4,6 +4,7 @@ import { View, FlatList, Text } from 'react-native'; import { connect } from 'react-redux'; +import * as List from '../../containers/List'; import Touch from '../../utils/touch'; import RocketChat from '../../lib/rocketchat'; @@ -182,11 +183,6 @@ class DirectoryView extends React.Component { ); } - renderSeparator = () => { - const { theme } = this.props; - return ; - } - renderItem = ({ item, index }) => { const { data, type } = this.state; const { baseUrl, user, theme } = this.props; @@ -251,7 +247,7 @@ class DirectoryView extends React.Component { keyExtractor={item => item._id} ListHeaderComponent={this.renderHeader} renderItem={this.renderItem} - ItemSeparatorComponent={this.renderSeparator} + ItemSeparatorComponent={List.Separator} keyboardShouldPersistTaps='always' ListFooterComponent={loading ? : null} onEndReached={() => this.load({})} diff --git a/app/views/LanguageView/index.js b/app/views/LanguageView/index.js index 435762d21..b2e6e1079 100644 --- a/app/views/LanguageView/index.js +++ b/app/views/LanguageView/index.js @@ -94,7 +94,7 @@ class LanguageView extends React.Component { setUser({ language: params.language }); const serversDB = database.servers; - const usersCollection = serversDB.collections.get('users'); + const usersCollection = serversDB.get('users'); await serversDB.action(async() => { try { const userRecord = await usersCollection.find(user.id); diff --git a/app/views/LoginView.js b/app/views/LoginView.js index bdffd103b..8e9791d36 100644 --- a/app/views/LoginView.js +++ b/app/views/LoginView.js @@ -4,7 +4,7 @@ import { Text, View, StyleSheet, Keyboard, Alert } from 'react-native'; import { connect } from 'react-redux'; -import equal from 'deep-equal'; +import { dequal } from 'dequal'; import sharedStyles from './Styles'; import Button from '../containers/Button'; @@ -82,7 +82,7 @@ class LoginView extends React.Component { UNSAFE_componentWillReceiveProps(nextProps) { const { error } = this.props; - if (nextProps.failure && !equal(error, nextProps.error)) { + if (nextProps.failure && !dequal(error, nextProps.error)) { Alert.alert(I18n.t('Oops'), I18n.t('Login_error')); } } diff --git a/app/views/MessagesView/index.js b/app/views/MessagesView/index.js index 678d28f29..fc840b2d6 100644 --- a/app/views/MessagesView/index.js +++ b/app/views/MessagesView/index.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FlatList, View, Text } from 'react-native'; import { connect } from 'react-redux'; -import equal from 'deep-equal'; +import { dequal } from 'dequal'; import styles from './styles'; import Message from '../../containers/message'; @@ -57,7 +57,7 @@ class MessagesView extends React.Component { if (nextState.loading !== loading) { return true; } - if (!equal(nextState.messages, messages)) { + if (!dequal(nextState.messages, messages)) { return true; } if (fileLoading !== nextState.fileLoading) { diff --git a/app/views/ModalBlockView.js b/app/views/ModalBlockView.js index 2d72fa339..85eee8492 100644 --- a/app/views/ModalBlockView.js +++ b/app/views/ModalBlockView.js @@ -1,7 +1,6 @@ import React from 'react'; import { StyleSheet, View } from 'react-native'; import PropTypes from 'prop-types'; -import isEqual from 'lodash/isEqual'; import { connect } from 'react-redux'; import { KeyboardAwareScrollView } from '@codler/react-native-keyboard-aware-scroll-view'; @@ -94,17 +93,6 @@ class ModalBlockView extends React.Component { EventEmitter.addEventListener(viewId, this.handleUpdate); } - shouldComponentUpdate(nextProps, nextState) { - if (!isEqual(nextProps, this.props)) { - return true; - } - if (!isEqual(nextState, this.state)) { - return true; - } - - return false; - } - componentDidUpdate(prevProps) { const { navigation, route } = this.props; const oldData = prevProps.route.params?.data ?? {}; diff --git a/app/views/NewMessageView.js b/app/views/NewMessageView.js index be47513bc..7e2aa2323 100644 --- a/app/views/NewMessageView.js +++ b/app/views/NewMessageView.js @@ -4,9 +4,8 @@ import { View, StyleSheet, FlatList, Text } from 'react-native'; import { connect } from 'react-redux'; -import equal from 'deep-equal'; -import orderBy from 'lodash/orderBy'; import { Q } from '@nozbe/watermelondb'; +import * as List from '../containers/List'; import Touch from '../utils/touch'; import database from '../lib/database'; @@ -27,10 +26,9 @@ import { createChannelRequest } from '../actions/createChannel'; import { goRoom } from '../utils/goRoom'; import SafeAreaView from '../containers/SafeAreaView'; +const QUERY_SIZE = 50; + const styles = StyleSheet.create({ - separator: { - marginLeft: 60 - }, button: { height: 46, flexDirection: 'row', @@ -77,40 +75,20 @@ class NewMessageView extends React.Component { }; } - shouldComponentUpdate(nextProps, nextState) { - const { search, chats } = this.state; - const { theme } = this.props; - if (nextProps.theme !== theme) { - return true; - } - if (!equal(nextState.search, search)) { - return true; - } - if (!equal(nextState.chats, chats)) { - return true; - } - return false; - } - - componentWillUnmount() { - if (this.querySubscription && this.querySubscription.unsubscribe) { - this.querySubscription.unsubscribe(); - } - } - // eslint-disable-next-line react/sort-comp init = async() => { try { const db = database.active; - const observable = await db.collections + const chats = await db.collections .get('subscriptions') - .query(Q.where('t', 'd')) - .observeWithColumns(['room_updated_at']); + .query( + Q.where('t', 'd'), + Q.experimentalTake(QUERY_SIZE), + Q.experimentalSortBy('room_updated_at', Q.desc) + ) + .fetch(); - this.querySubscription = observable.subscribe((data) => { - const chats = orderBy(data, ['roomUpdatedAt'], ['desc']); - this.setState({ chats }); - }); + this.setState({ chats }); } catch (e) { log(e); } @@ -211,10 +189,6 @@ class NewMessageView extends React.Component { ); } - renderSeparator = () => { - const { theme } = this.props; - return ; - } renderItem = ({ item, index }) => { const { search, chats } = this.state; @@ -254,7 +228,7 @@ class NewMessageView extends React.Component { keyExtractor={item => item._id} ListHeaderComponent={this.renderHeader} renderItem={this.renderItem} - ItemSeparatorComponent={this.renderSeparator} + ItemSeparatorComponent={List.Separator} contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }} keyboardShouldPersistTaps='always' /> diff --git a/app/views/NewServerView/index.js b/app/views/NewServerView/index.js index 192468ed3..27422dec8 100644 --- a/app/views/NewServerView/index.js +++ b/app/views/NewServerView/index.js @@ -132,7 +132,7 @@ class NewServerView extends React.Component { queryServerHistory = async(text) => { const db = database.servers; try { - const serversHistoryCollection = db.collections.get('servers_history'); + const serversHistoryCollection = db.get('servers_history'); let whereClause = [ Q.where('username', Q.notEq(null)), Q.experimentalSortBy('updated_at', Q.desc), @@ -174,7 +174,7 @@ class NewServerView extends React.Component { } submit = async({ fromServerHistory = false, username }) => { - logEvent(events.NEWSERVER_CONNECT_TO_WORKSPACE); + logEvent(events.NS_CONNECT_TO_WORKSPACE); const { text, certificate } = this.state; const { connectServer } = this.props; @@ -199,7 +199,7 @@ class NewServerView extends React.Component { } connectOpen = () => { - logEvent(events.NEWSERVER_JOIN_OPEN_WORKSPACE); + logEvent(events.NS_JOIN_OPEN_WORKSPACE); this.setState({ connectingOpen: true }); const { connectServer } = this.props; connectServer('https://open.rocket.chat'); diff --git a/app/views/ProfileView/index.js b/app/views/ProfileView/index.js index f4faeec1c..7e97422df 100644 --- a/app/views/ProfileView/index.js +++ b/app/views/ProfileView/index.js @@ -6,7 +6,7 @@ import prompt from 'react-native-prompt-android'; import SHA256 from 'js-sha256'; import ImagePicker from 'react-native-image-crop-picker'; import RNPickerSelect from 'react-native-picker-select'; -import isEqual from 'lodash/isEqual'; +import { dequal } from 'dequal'; import omit from 'lodash/omit'; import Touch from '../../utils/touch'; @@ -91,21 +91,11 @@ class ProfileView extends React.Component { * it's resetting the avatar right after * select some image from gallery. */ - if (!isEqual(omit(user, ['status']), omit(nextProps.user, ['status']))) { + if (!dequal(omit(user, ['status']), omit(nextProps.user, ['status']))) { this.init(nextProps.user); } } - shouldComponentUpdate(nextProps, nextState) { - if (!isEqual(nextState, this.state)) { - return true; - } - if (!isEqual(nextProps, this.props)) { - return true; - } - return false; - } - setAvatar = (avatar) => { const { Accounts_AllowUserAvatarChange } = this.props; @@ -434,7 +424,7 @@ class ProfileView extends React.Component { } logoutOtherLocations = () => { - logEvent(events.PROFILE_LOGOUT_OTHER_LOCATIONS); + logEvent(events.PL_OTHER_LOCATIONS); showConfirmationAlert({ message: I18n.t('You_will_be_logged_out_from_other_locations'), confirmationText: I18n.t('Logout'), @@ -443,7 +433,7 @@ class ProfileView extends React.Component { await RocketChat.logoutOtherLocations(); EventEmitter.emit(LISTENER, { message: I18n.t('Logged_out_of_other_clients_successfully') }); } catch { - logEvent(events.PROFILE_LOGOUT_OTHER_LOCATIONS_F); + logEvent(events.PL_OTHER_LOCATIONS_F); EventEmitter.emit(LISTENER, { message: I18n.t('Logout_failed') }); } } diff --git a/app/views/ReadReceiptView/index.js b/app/views/ReadReceiptView/index.js index b7456c77a..f41d7c873 100644 --- a/app/views/ReadReceiptView/index.js +++ b/app/views/ReadReceiptView/index.js @@ -1,9 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FlatList, View, Text } from 'react-native'; -import equal from 'deep-equal'; +import { dequal } from 'dequal'; import moment from 'moment'; import { connect } from 'react-redux'; +import * as List from '../../containers/List'; import Avatar from '../../containers/Avatar'; import styles from './styles'; @@ -55,7 +56,7 @@ class ReadReceiptView extends React.Component { if (nextState.loading !== loading) { return true; } - if (!equal(nextState.receipts, receipts)) { + if (!dequal(nextState.receipts, receipts)) { return true; } return false; @@ -121,11 +122,6 @@ class ReadReceiptView extends React.Component { ); } - renderSeparator = () => { - const { theme } = this.props; - return ; - } - render() { const { receipts, loading } = this.state; const { theme } = this.props; @@ -143,7 +139,7 @@ class ReadReceiptView extends React.Component { { const { room, joined } = this.state; + const { addUserToJoinedRoomPermission, addUserToAnyCRoomPermission, addUserToAnyPRoomPermission } = this.props; const { rid, t } = room; - let canAdd = false; + let canAddUser = false; const userInRoom = joined; - const permissions = await RocketChat.hasPermission(['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room'], rid); + const permissions = await RocketChat.hasPermission([addUserToJoinedRoomPermission, addUserToAnyCRoomPermission, addUserToAnyPRoomPermission], rid); - if (permissions) { - if (userInRoom && permissions['add-user-to-joined-room']) { - canAdd = true; - } - if (t === 'c' && permissions['add-user-to-any-c-room']) { - canAdd = true; - } - if (t === 'p' && permissions['add-user-to-any-p-room']) { - canAdd = true; - } + if (userInRoom && permissions[0]) { + canAddUser = true; } - this.setState({ canAddUser: canAdd }); + if (t === 'c' && permissions[1]) { + canAddUser = true; + } + if (t === 'p' && permissions[2]) { + canAddUser = true; + } + this.setState({ canAddUser }); } canInviteUser = async() => { const { room } = this.state; + const { createInviteLinksPermission } = this.props; const { rid } = room; - const permissions = await RocketChat.hasPermission(['create-invite-links'], rid); + const permissions = await RocketChat.hasPermission([createInviteLinksPermission], rid); - const canInviteUser = permissions && permissions['create-invite-links']; + const canInviteUser = permissions[0]; this.setState({ canInviteUser }); } canEdit = async() => { const { room } = this.state; + const { editRoomPermission } = this.props; const { rid } = room; - const permissions = await RocketChat.hasPermission(['edit-room'], rid); + const permissions = await RocketChat.hasPermission([editRoomPermission], rid); - const canEdit = permissions && permissions['edit-room']; + const canEdit = permissions[0]; this.setState({ canEdit }); } canToggleEncryption = async() => { const { room } = this.state; + const { toggleRoomE2EEncryptionPermission } = this.props; const { rid } = room; - const permissions = await RocketChat.hasPermission(['toggle-room-e2e-encryption'], rid); + const permissions = await RocketChat.hasPermission([toggleRoomE2EEncryptionPermission], rid); - const canToggleEncryption = permissions && permissions['toggle-room-e2e-encryption']; + const canToggleEncryption = permissions[0]; this.setState({ canToggleEncryption }); } canViewMembers = async() => { const { room } = this.state; + const { viewBroadcastMemberListPermission } = this.props; const { rid, t, broadcast } = room; if (broadcast) { - const viewBroadcastMemberListPermission = 'view-broadcast-member-list'; const permissions = await RocketChat.hasPermission([viewBroadcastMemberListPermission], rid); - if (!permissions[viewBroadcastMemberListPermission]) { + if (!permissions[0]) { return false; } } @@ -226,16 +236,10 @@ class RoomActionsView extends React.Component { canForwardGuest = async() => { const { room } = this.state; + const { transferLivechatGuestPermission } = this.props; const { rid } = room; - let result = true; - - const transferLivechatGuest = 'transfer-livechat-guest'; - const permissions = await RocketChat.hasPermission([transferLivechatGuest], rid); - if (!permissions[transferLivechatGuest]) { - result = false; - } - - this.setState({ canForwardGuest: result }); + const permissions = await RocketChat.hasPermission([transferLivechatGuestPermission], rid); + this.setState({ canForwardGuest: permissions[0] }); } canReturnQueue = async() => { @@ -482,7 +486,7 @@ class RoomActionsView extends React.Component { RocketChat.callJitsi(room?.rid, true)} + onPress={() => RocketChat.callJitsi(room, true)} testID='room-actions-voice' left={() => } showActionIndicator @@ -490,7 +494,7 @@ class RoomActionsView extends React.Component { RocketChat.callJitsi(room?.rid)} + onPress={() => RocketChat.callJitsi(room)} testID='room-actions-video' left={() => } showActionIndicator @@ -866,7 +870,15 @@ class RoomActionsView extends React.Component { const mapStateToProps = state => ({ jitsiEnabled: state.settings.Jitsi_Enabled || false, encryptionEnabled: state.encryption.enabled, - serverVersion: state.server.version + serverVersion: state.server.version, + addUserToJoinedRoomPermission: state.permissions['add-user-to-joined-room'], + addUserToAnyCRoomPermission: state.permissions['add-user-to-any-c-room'], + addUserToAnyPRoomPermission: state.permissions['add-user-to-any-p-room'], + createInviteLinksPermission: state.permissions['create-invite-links'], + editRoomPermission: state.permissions['edit-room'], + toggleRoomE2EEncryptionPermission: state.permissions['toggle-room-e2e-encryption'], + viewBroadcastMemberListPermission: state.permissions['view-broadcast-member-list'], + transferLivechatGuestPermission: state.permissions['transfer-livechat-guest'] }); const mapDispatchToProps = dispatch => ({ diff --git a/app/views/RoomInfoEditView/index.js b/app/views/RoomInfoEditView/index.js index 58664c6f2..04472c82e 100644 --- a/app/views/RoomInfoEditView/index.js +++ b/app/views/RoomInfoEditView/index.js @@ -4,15 +4,13 @@ import { Text, View, ScrollView, TouchableOpacity, Keyboard, Alert } from 'react-native'; import { connect } from 'react-redux'; -import equal from 'deep-equal'; import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; import ImagePicker from 'react-native-image-crop-picker'; -import isEqual from 'lodash/isEqual'; +import { dequal } from 'dequal'; import isEmpty from 'lodash/isEmpty'; import lt from 'semver/functions/lt'; import coerce from 'semver/functions/coerce'; - import database from '../../lib/database'; import { deleteRoom as deleteRoomAction } from '../../actions/room'; import KeyboardView from '../../presentation/KeyboardView'; @@ -44,14 +42,6 @@ const PERMISSION_ARCHIVE = 'archive-room'; const PERMISSION_UNARCHIVE = 'unarchive-room'; const PERMISSION_DELETE_C = 'delete-c'; const PERMISSION_DELETE_P = 'delete-p'; -const PERMISSIONS_ARRAY = [ - PERMISSION_SET_READONLY, - PERMISSION_SET_REACT_WHEN_READONLY, - PERMISSION_ARCHIVE, - PERMISSION_UNARCHIVE, - PERMISSION_DELETE_C, - PERMISSION_DELETE_P -]; class RoomInfoEditView extends React.Component { static navigationOptions = () => ({ @@ -63,7 +53,13 @@ class RoomInfoEditView extends React.Component { deleteRoom: PropTypes.func, serverVersion: PropTypes.string, encryptionEnabled: PropTypes.bool, - theme: PropTypes.string + theme: PropTypes.string, + setReadOnlyPermission: PropTypes.array, + setReactWhenReadOnlyPermission: PropTypes.array, + archiveRoomPermission: PropTypes.array, + unarchiveRoomPermission: PropTypes.array, + deleteCPermission: PropTypes.array, + deletePPermission: PropTypes.array }; constructor(props) { @@ -90,16 +86,6 @@ class RoomInfoEditView extends React.Component { this.loadRoom(); } - shouldComponentUpdate(nextProps, nextState) { - if (!equal(nextState, this.state)) { - return true; - } - if (!equal(nextProps, this.props)) { - return true; - } - return false; - } - componentWillUnmount() { if (this.querySubscription && this.querySubscription.unsubscribe) { this.querySubscription.unsubscribe(); @@ -108,14 +94,22 @@ class RoomInfoEditView extends React.Component { // eslint-disable-next-line react/sort-comp loadRoom = async() => { - const { route } = this.props; + const { + route, + setReadOnlyPermission, + setReactWhenReadOnlyPermission, + archiveRoomPermission, + unarchiveRoomPermission, + deleteCPermission, + deletePPermission + } = this.props; const rid = route.params?.rid; if (!rid) { return; } try { const db = database.active; - const sub = await db.collections.get('subscriptions').find(rid); + const sub = await db.get('subscriptions').find(rid); const observable = sub.observe(); this.querySubscription = observable.subscribe((data) => { @@ -123,8 +117,25 @@ class RoomInfoEditView extends React.Component { this.init(this.room); }); - const permissions = await RocketChat.hasPermission(PERMISSIONS_ARRAY, rid); - this.setState({ permissions }); + const result = await RocketChat.hasPermission([ + setReadOnlyPermission, + setReactWhenReadOnlyPermission, + archiveRoomPermission, + unarchiveRoomPermission, + deleteCPermission, + deletePPermission + ], rid); + + this.setState({ + permissions: { + [PERMISSION_SET_READONLY]: result[0], + [PERMISSION_SET_REACT_WHEN_READONLY]: result[1], + [PERMISSION_ARCHIVE]: result[2], + [PERMISSION_UNARCHIVE]: result[3], + [PERMISSION_DELETE_C]: result[4], + [PERMISSION_DELETE_P]: result[5] + } + }); } catch (e) { log(e); } @@ -179,7 +190,7 @@ class RoomInfoEditView extends React.Component { && room.t === 'p' === t && room.ro === ro && room.reactWhenReadOnly === reactWhenReadOnly - && isEqual(room.sysMes, systemMessages) + && dequal(room.sysMes, systemMessages) && enableSysMes === (room.sysMes && room.sysMes.length > 0) && room.encrypted === encrypted && isEmpty(avatar) @@ -239,7 +250,7 @@ class RoomInfoEditView extends React.Component { params.reactWhenReadOnly = reactWhenReadOnly; } - if (!isEqual(room.sysMes, systemMessages)) { + if (!dequal(room.sysMes, systemMessages)) { params.systemMessages = systemMessages; } @@ -666,8 +677,14 @@ class RoomInfoEditView extends React.Component { } const mapStateToProps = state => ({ - serverVersion: state.share.server.version || state.server.version, - encryptionEnabled: state.encryption.enabled + serverVersion: state.server.version, + encryptionEnabled: state.encryption.enabled, + setReadOnlyPermission: state.permissions[PERMISSION_SET_READONLY], + setReactWhenReadOnlyPermission: state.permissions[PERMISSION_SET_REACT_WHEN_READONLY], + archiveRoomPermission: state.permissions[PERMISSION_ARCHIVE], + unarchiveRoomPermission: state.permissions[PERMISSION_UNARCHIVE], + deleteCPermission: state.permissions[PERMISSION_DELETE_C], + deletePPermission: state.permissions[PERMISSION_DELETE_P] }); const mapDispatchToProps = dispatch => ({ diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js index fd61e0eae..a18476d34 100644 --- a/app/views/RoomInfoView/index.js +++ b/app/views/RoomInfoView/index.js @@ -31,7 +31,6 @@ import SafeAreaView from '../../containers/SafeAreaView'; import { goRoom } from '../../utils/goRoom'; import Navigation from '../../lib/Navigation'; -const PERMISSION_EDIT_ROOM = 'edit-room'; const getRoomTitle = (room, type, name, username, statusText, theme) => (type === 'd' ? ( <> @@ -55,7 +54,8 @@ class RoomInfoView extends React.Component { rooms: PropTypes.array, theme: PropTypes.string, isMasterDetail: PropTypes.bool, - jitsiEnabled: PropTypes.bool + jitsiEnabled: PropTypes.bool, + editRoomPermission: PropTypes.array } constructor(props) { @@ -136,7 +136,7 @@ class RoomInfoView extends React.Component { getRoleDescription = async(id) => { const db = database.active; try { - const rolesCollection = db.collections.get('roles'); + const rolesCollection = db.get('roles'); const role = await rolesCollection.find(id); if (role) { return role.description; @@ -193,7 +193,7 @@ class RoomInfoView extends React.Component { loadRoom = async() => { const { room: roomState } = this.state; - const { route } = this.props; + const { route, editRoomPermission } = this.props; let room = route.params?.room; if (room && room.observe) { this.roomObservable = room.observe(); @@ -213,8 +213,8 @@ class RoomInfoView extends React.Component { } } - const permissions = await RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid); - if (permissions[PERMISSION_EDIT_ROOM] && !room.prid) { + const permissions = await RocketChat.hasPermission([editRoomPermission], room.rid); + if (permissions[0] && !room.prid) { this.setState({ showEdit: true }, () => this.setHeader()); } } @@ -276,7 +276,7 @@ class RoomInfoView extends React.Component { videoCall = () => { const { room } = this.state; - RocketChat.callJitsi(room.rid); + RocketChat.callJitsi(room); } renderAvatar = (room, roomUser) => { @@ -369,7 +369,8 @@ class RoomInfoView extends React.Component { const mapStateToProps = state => ({ rooms: state.room.rooms, isMasterDetail: state.app.isMasterDetail, - jitsiEnabled: state.settings.Jitsi_Enabled || false + jitsiEnabled: state.settings.Jitsi_Enabled || false, + editRoomPermission: state.permissions['edit-room'] }); export default connect(mapStateToProps)(withTheme(RoomInfoView)); diff --git a/app/views/RoomMembersView/index.js b/app/views/RoomMembersView/index.js index 269814669..4caa03381 100644 --- a/app/views/RoomMembersView/index.js +++ b/app/views/RoomMembersView/index.js @@ -1,8 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { FlatList, View } from 'react-native'; +import { FlatList } from 'react-native'; import { connect } from 'react-redux'; import { Q } from '@nozbe/watermelondb'; +import * as List from '../../containers/List'; import styles from './styles'; import UserItem from '../../presentation/UserItem'; @@ -28,6 +29,12 @@ import { goRoom } from '../../utils/goRoom'; const PAGE_SIZE = 25; +const PERMISSION_MUTE_USER = 'mute-user'; +const PERMISSION_SET_LEADER = 'set-leader'; +const PERMISSION_SET_OWNER = 'set-owner'; +const PERMISSION_SET_MODERATOR = 'set-moderator'; +const PERMISSION_REMOVE_USER = 'remove-user'; + class RoomMembersView extends React.Component { static propTypes = { navigation: PropTypes.object, @@ -43,7 +50,12 @@ class RoomMembersView extends React.Component { showActionSheet: PropTypes.func, theme: PropTypes.string, isMasterDetail: PropTypes.bool, - useRealName: PropTypes.bool + useRealName: PropTypes.bool, + muteUserPermission: PropTypes.array, + setLeaderPermission: PropTypes.array, + setOwnerPermission: PropTypes.array, + setModeratorPermission: PropTypes.array, + removeUserPermission: PropTypes.array } constructor(props) { @@ -81,7 +93,20 @@ class RoomMembersView extends React.Component { this.fetchMembers(); const { room } = this.state; - this.permissions = await RocketChat.hasPermission(['mute-user', 'set-leader', 'set-owner', 'set-moderator', 'remove-user'], room.rid); + const { + muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission + } = this.props; + const result = await RocketChat.hasPermission([ + muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission + ], room.rid); + + this.permissions = { + [PERMISSION_MUTE_USER]: result[0], + [PERMISSION_SET_LEADER]: result[1], + [PERMISSION_SET_OWNER]: result[2], + [PERMISSION_SET_MODERATOR]: result[3], + [PERMISSION_REMOVE_USER]: result[4] + }; const hasSinglePermission = Object.values(this.permissions).some(p => !!p); if (hasSinglePermission) { @@ -122,7 +147,7 @@ class RoomMembersView extends React.Component { navToDirectMessage = async(item) => { try { const db = database.active; - const subsCollection = db.collections.get('subscriptions'); + const subsCollection = db.get('subscriptions'); const query = await subsCollection.query(Q.where('name', item.username)).fetch(); if (query.length) { const [room] = query; @@ -395,11 +420,6 @@ class RoomMembersView extends React.Component { this.onSearchChangeText(text)} testID='room-members-view-search' /> ) - renderSeparator = () => { - const { theme } = this.props; - return ; - } - renderItem = ({ item }) => { const { baseUrl, user, theme } = this.props; @@ -429,7 +449,7 @@ class RoomMembersView extends React.Component { renderItem={this.renderItem} style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]} keyExtractor={item => item._id} - ItemSeparatorComponent={this.renderSeparator} + ItemSeparatorComponent={List.Separator} ListHeaderComponent={this.renderSearchBar} ListFooterComponent={() => { if (isLoading) { @@ -452,7 +472,12 @@ const mapStateToProps = state => ({ baseUrl: state.server.server, user: getUserSelector(state), isMasterDetail: state.app.isMasterDetail, - useRealName: state.settings.UI_Use_Real_Name + useRealName: state.settings.UI_Use_Real_Name, + muteUserPermission: state.permissions[PERMISSION_MUTE_USER], + setLeaderPermission: state.permissions[PERMISSION_SET_LEADER], + setOwnerPermission: state.permissions[PERMISSION_SET_OWNER], + setModeratorPermission: state.permissions[PERMISSION_SET_MODERATOR], + removeUserPermission: state.permissions[PERMISSION_REMOVE_USER] }); export default connect(mapStateToProps)(withActionSheet(withTheme(RoomMembersView))); diff --git a/app/views/RoomView/Header/Header.js b/app/views/RoomView/Header/Header.js index 637d57c58..410ca1bf5 100644 --- a/app/views/RoomView/Header/Header.js +++ b/app/views/RoomView/Header/Header.js @@ -168,7 +168,7 @@ const Header = React.memo(({ theme={theme} /> - + ); }); diff --git a/app/views/RoomView/Header/RightButtons.js b/app/views/RoomView/Header/RightButtons.js index 6580351c8..37939b1e3 100644 --- a/app/views/RoomView/Header/RightButtons.js +++ b/app/views/RoomView/Header/RightButtons.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import isEqual from 'react-fast-compare'; +import { dequal } from 'dequal'; import * as HeaderButton from '../../../containers/HeaderButton'; import database from '../../../lib/database'; @@ -35,7 +35,7 @@ class RightButtonsContainer extends Component { const db = database.active; if (tmid) { try { - const threadRecord = await db.collections.get('messages').find(tmid); + const threadRecord = await db.get('messages').find(tmid); this.observeThread(threadRecord); } catch (e) { console.log('Can\'t find message to observe.'); @@ -43,7 +43,7 @@ class RightButtonsContainer extends Component { } if (rid) { try { - const subCollection = db.collections.get('subscriptions'); + const subCollection = db.get('subscriptions'); const subRecord = await subCollection.find(rid); this.observeSubscription(subRecord); } catch (e) { @@ -59,15 +59,16 @@ class RightButtonsContainer extends Component { if (nextState.isFollowingThread !== isFollowingThread) { return true; } - if (!isEqual(nextState.tunread, tunread)) { + if (!dequal(nextState.tunread, tunread)) { return true; } - if (!isEqual(nextState.tunreadUser, tunreadUser)) { + if (!dequal(nextState.tunreadUser, tunreadUser)) { return true; } - if (!isEqual(nextState.tunreadGroup, tunreadGroup)) { + if (!dequal(nextState.tunreadGroup, tunreadGroup)) { return true; } + return false; } componentWillUnmount() { diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js index 33663bbe4..05a5f2e12 100644 --- a/app/views/RoomView/Header/index.js +++ b/app/views/RoomView/Header/index.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import equal from 'deep-equal'; +import { dequal } from 'dequal'; import Header from './Header'; import LeftButtons from './LeftButtons'; @@ -65,7 +65,7 @@ class RoomHeaderView extends Component { if (nextProps.height !== height) { return true; } - if (!equal(nextProps.usersTyping, usersTyping)) { + if (!dequal(nextProps.usersTyping, usersTyping)) { return true; } if (nextProps.goRoomActionsView !== goRoomActionsView) { diff --git a/app/views/RoomView/List.js b/app/views/RoomView/List.js index 4db965d97..41d424fa3 100644 --- a/app/views/RoomView/List.js +++ b/app/views/RoomView/List.js @@ -3,7 +3,7 @@ import { FlatList, RefreshControl } from 'react-native'; import PropTypes from 'prop-types'; import { Q } from '@nozbe/watermelondb'; import moment from 'moment'; -import isEqual from 'lodash/isEqual'; +import { dequal } from 'dequal'; import styles from './styles'; import database from '../../lib/database'; @@ -89,13 +89,13 @@ class List extends React.Component { if (refreshing !== nextState.refreshing) { return true; } - if (!isEqual(hideSystemMessages, nextProps.hideSystemMessages)) { + if (!dequal(hideSystemMessages, nextProps.hideSystemMessages)) { return true; } - if (!isEqual(tunread, nextProps.tunread)) { + if (!dequal(tunread, nextProps.tunread)) { return true; } - if (!isEqual(ignored, nextProps.ignored)) { + if (!dequal(ignored, nextProps.ignored)) { return true; } return false; @@ -103,7 +103,7 @@ class List extends React.Component { componentDidUpdate(prevProps) { const { hideSystemMessages } = this.props; - if (!isEqual(hideSystemMessages, prevProps.hideSystemMessages)) { + if (!dequal(hideSystemMessages, prevProps.hideSystemMessages)) { this.reload(); } } diff --git a/app/views/RoomView/ReactionPicker.js b/app/views/RoomView/ReactionPicker.js index 556d18256..9e9c470f8 100644 --- a/app/views/RoomView/ReactionPicker.js +++ b/app/views/RoomView/ReactionPicker.js @@ -7,6 +7,8 @@ import Modal from 'react-native-modal'; import EmojiPicker from '../../containers/EmojiPicker'; import styles from './styles'; import { isAndroid } from '../../utils/deviceInfo'; +import { themes } from '../../constants/colors'; +import { withTheme } from '../../theme'; const margin = isAndroid ? 40 : 20; const maxSize = 400; @@ -20,7 +22,8 @@ class ReactionPicker extends React.Component { reactionClose: PropTypes.func, onEmojiSelected: PropTypes.func, width: PropTypes.number, - height: PropTypes.number + height: PropTypes.number, + theme: PropTypes.string }; shouldComponentUpdate(nextProps) { @@ -38,7 +41,7 @@ class ReactionPicker extends React.Component { render() { const { - width, height, show, baseUrl, reactionClose, isMasterDetail + width, height, show, baseUrl, reactionClose, isMasterDetail, theme } = this.props; let widthStyle = width - margin; @@ -58,6 +61,7 @@ class ReactionPicker extends React.Component { onBackButtonPress={reactionClose} animationIn='fadeIn' animationOut='fadeOut' + backdropOpacity={themes[theme].backdropOpacity} > ({ isMasterDetail: state.app.isMasterDetail }); -export default connect(mapStateToProps)(ReactionPicker); +export default connect(mapStateToProps)(withTheme(ReactionPicker)); diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index 38a39d979..2ee84ae25 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -7,7 +7,7 @@ import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import moment from 'moment'; import * as Haptics from 'expo-haptics'; import { Q } from '@nozbe/watermelondb'; -import isEqual from 'lodash/isEqual'; +import { dequal } from 'dequal'; import { withSafeAreaInsets } from 'react-native-safe-area-context'; import Touch from '../../utils/touch'; @@ -155,7 +155,9 @@ class RoomView extends React.Component { this.list = React.createRef(); this.joinCode = React.createRef(); this.mounted = false; - if (this.rid) { + + // we don't need to subscribe to threads + if (this.rid && !this.tmid) { this.sub = new RoomClass(this.rid); } console.timeEnd(`${ this.constructor.name } init`); @@ -168,7 +170,7 @@ class RoomView extends React.Component { const { isAuthenticated } = this.props; this.setHeader(); if (this.rid) { - this.sub.subscribe(); + this.sub?.subscribe?.(); if (isAuthenticated) { this.init(); } else { @@ -203,10 +205,10 @@ class RoomView extends React.Component { if (stateUpdated) { return true; } - if (!isEqual(nextProps.insets, insets)) { + if (!dequal(nextProps.insets, insets)) { return true; } - return roomAttrsUpdate.some(key => !isEqual(nextState.roomUpdate[key], roomUpdate[key])); + return roomAttrsUpdate.some(key => !dequal(nextState.roomUpdate[key], roomUpdate[key])); } componentDidUpdate(prevProps, prevState) { @@ -227,7 +229,7 @@ class RoomView extends React.Component { } // If it's a livechat room if (this.t === 'l') { - if (!isEqual(prevState.roomUpdate.visitor, roomUpdate.visitor)) { + if (!dequal(prevState.roomUpdate.visitor, roomUpdate.visitor)) { this.setHeader(); } } @@ -249,7 +251,7 @@ class RoomView extends React.Component { let obj; if (this.tmid) { try { - const threadsCollection = db.collections.get('threads'); + const threadsCollection = db.get('threads'); obj = await threadsCollection.find(this.tmid); } catch (e) { // Do nothing @@ -398,7 +400,7 @@ class RoomView extends React.Component { const db = database.active; try { - const subCollection = db.collections.get('subscriptions'); + const subCollection = db.get('subscriptions'); const sub = await subCollection.find(this.rid); const { room } = await RocketChat.getRoomInfo(this.rid); @@ -434,10 +436,7 @@ class RoomView extends React.Component { } } - // We run `canAutoTranslate` again in order to refetch auto translate permission - // in case of a missing connection or poor connection on room open - const canAutoTranslate = await RocketChat.canAutoTranslate(); - + const canAutoTranslate = RocketChat.canAutoTranslate(); const member = await this.getRoomMember(); this.setState({ canAutoTranslate, member, loading: false }); @@ -476,7 +475,7 @@ class RoomView extends React.Component { findAndObserveRoom = async(rid) => { try { const db = database.active; - const subCollection = await db.collections.get('subscriptions'); + const subCollection = await db.get('subscriptions'); const room = await subCollection.find(rid); this.setState({ room }); if (!this.tmid) { @@ -531,7 +530,14 @@ class RoomView extends React.Component { } onEditInit = (message) => { - this.setState({ selectedMessage: message, editing: true }); + const newMessage = { + id: message.id, + subscription: { + id: message.subscription.id + }, + msg: message?.attachments?.[0]?.description || message.msg + }; + this.setState({ selectedMessage: newMessage, editing: true }); } onEditCancel = () => { @@ -744,8 +750,8 @@ class RoomView extends React.Component { fetchThreadName = async(tmid, messageId) => { try { const db = database.active; - const threadCollection = db.collections.get('threads'); - const messageCollection = db.collections.get('messages'); + const threadCollection = db.get('threads'); + const messageCollection = db.get('messages'); const messageRecord = await messageCollection.find(messageId); let threadRecord; try { @@ -815,7 +821,7 @@ class RoomView extends React.Component { if (jitsiTimeout < Date.now()) { showErrorAlert(I18n.t('Call_already_ended')); } else { - RocketChat.callJitsi(this.rid); + RocketChat.callJitsi(room); } }; @@ -1082,6 +1088,7 @@ class RoomView extends React.Component { reactionClose={this.onReactionClose} width={width} height={height} + theme={theme} /> { - const { theme } = this.props; - return ; - } - renderServer = ({ item }) => { const { server, theme } = this.props; @@ -225,7 +202,7 @@ class ServerDropdown extends Component { }); const backdropOpacity = this.animatedValue.interpolate({ inputRange: [0, 1], - outputRange: [0, 0.6] + outputRange: [0, themes[theme].backdropOpacity] }); return ( <> @@ -266,7 +243,7 @@ class ServerDropdown extends Component { data={servers} keyExtractor={item => item.id} renderItem={this.renderServer} - ItemSeparatorComponent={this.renderSeparator} + ItemSeparatorComponent={List.Separator} keyboardShouldPersistTaps='always' /> diff --git a/app/views/RoomsListView/SortDropdown/index.js b/app/views/RoomsListView/SortDropdown/index.js index cb85edcd5..06a398a82 100644 --- a/app/views/RoomsListView/SortDropdown/index.js +++ b/app/views/RoomsListView/SortDropdown/index.js @@ -124,13 +124,13 @@ class Sort extends PureComponent { inputRange: [0, 1], outputRange: [-326, heightDestination] }); - const backdropOpacity = this.animatedValue.interpolate({ - inputRange: [0, 1], - outputRange: [0, 0.3] - }); const { sortBy, groupByType, showFavorites, showUnread, theme } = this.props; + const backdropOpacity = this.animatedValue.interpolate({ + inputRange: [0, 1], + outputRange: [0, themes[theme].backdropOpacity] + }); return ( <> diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js index 6ec23f29c..996847c91 100644 --- a/app/views/RoomsListView/index.js +++ b/app/views/RoomsListView/index.js @@ -9,7 +9,7 @@ import { RefreshControl } from 'react-native'; import { connect } from 'react-redux'; -import isEqual from 'react-fast-compare'; +import { dequal } from 'dequal'; import Orientation from 'react-native-orientation-locker'; import { Q } from '@nozbe/watermelondb'; import { withSafeAreaInsets } from 'react-native-safe-area-context'; @@ -89,7 +89,6 @@ const shouldUpdateProps = [ 'showUnread', 'useRealName', 'StoreLastMessage', - 'appState', 'theme', 'isMasterDetail', 'refreshing', @@ -126,7 +125,6 @@ class RoomsListView extends React.Component { showUnread: PropTypes.bool, refreshing: PropTypes.bool, StoreLastMessage: PropTypes.bool, - appState: PropTypes.string, theme: PropTypes.string, toggleSortDropdown: PropTypes.func, openSearchHeader: PropTypes.func, @@ -135,7 +133,6 @@ class RoomsListView extends React.Component { roomsRequest: PropTypes.func, closeServerDropdown: PropTypes.func, useRealName: PropTypes.bool, - connected: PropTypes.bool, isMasterDetail: PropTypes.bool, rooms: PropTypes.array, width: PropTypes.number, @@ -222,7 +219,7 @@ class RoomsListView extends React.Component { } // Compare changes only once - const chatsNotEqual = !isEqual(nextState.chatsUpdate, chatsUpdate); + const chatsNotEqual = !dequal(nextState.chatsUpdate, chatsUpdate); // If they aren't equal, set to update if focused if (chatsNotEqual) { @@ -253,13 +250,13 @@ class RoomsListView extends React.Component { if (nextProps.width !== width) { return true; } - if (!isEqual(nextState.search, search)) { + if (!dequal(nextState.search, search)) { return true; } - if (!isEqual(nextProps.rooms, rooms)) { + if (!dequal(nextProps.rooms, rooms)) { return true; } - if (!isEqual(nextProps.insets, insets)) { + if (!dequal(nextProps.insets, insets)) { return true; } // If it's focused and there are changes, update @@ -276,9 +273,6 @@ class RoomsListView extends React.Component { groupByType, showFavorites, showUnread, - appState, - connected, - roomsRequest, rooms, isMasterDetail, insets @@ -294,15 +288,9 @@ class RoomsListView extends React.Component { ) ) { this.getSubscriptions(); - } else if ( - appState === 'foreground' - && appState !== prevProps.appState - && connected - ) { - roomsRequest(); } // Update current item in case of another action triggers an update on rooms reducer - if (isMasterDetail && item?.rid !== rooms[0] && !isEqual(rooms, prevProps.rooms)) { + if (isMasterDetail && item?.rid !== rooms[0] && !dequal(rooms, prevProps.rooms)) { // eslint-disable-next-line react/no-did-update-set-state this.setState({ item: { rid: rooms[0] } }); } @@ -319,6 +307,9 @@ class RoomsListView extends React.Component { if (this.unsubscribeBlur) { this.unsubscribeBlur(); } + if (this.backHandler && this.backHandler.remove) { + this.backHandler.remove(); + } if (isTablet) { EventEmitter.removeListener(KEY_COMMAND, this.handleCommands); } @@ -620,7 +611,7 @@ class RoomsListView extends React.Component { const db = database.active; const result = await RocketChat.toggleFavorite(rid, !favorite); if (result.success) { - const subCollection = db.collections.get('subscriptions'); + const subCollection = db.get('subscriptions'); await db.action(async() => { try { const subRecord = await subCollection.find(rid); @@ -644,7 +635,7 @@ class RoomsListView extends React.Component { const db = database.active; const result = await RocketChat.toggleRead(isRead, rid); if (result.success) { - const subCollection = db.collections.get('subscriptions'); + const subCollection = db.get('subscriptions'); await db.action(async() => { try { const subRecord = await subCollection.find(rid); @@ -668,7 +659,7 @@ class RoomsListView extends React.Component { const db = database.active; const result = await RocketChat.hideRoom(rid, type); if (result.success) { - const subCollection = db.collections.get('subscriptions'); + const subCollection = db.get('subscriptions'); await db.action(async() => { try { const subRecord = await subCollection.find(rid); @@ -1018,7 +1009,6 @@ const mapStateToProps = state => ({ isMasterDetail: state.app.isMasterDetail, server: state.server.server, changingServer: state.server.changingServer, - connected: state.server.connected, searchText: state.rooms.searchText, loadingServer: state.server.loading, showServerDropdown: state.rooms.showServerDropdown, @@ -1029,7 +1019,6 @@ const mapStateToProps = state => ({ showFavorites: state.sortPreferences.showFavorites, showUnread: state.sortPreferences.showUnread, useRealName: state.settings.UI_Use_Real_Name, - appState: state.app.ready && state.app.foreground ? 'foreground' : 'background', StoreLastMessage: state.settings.Store_Last_Message, rooms: state.room.rooms, queueSize: getInquiryQueueSelector(state).length, diff --git a/app/views/ScreenLockConfigView.js b/app/views/ScreenLockConfigView.js index bf0f6a15c..294a854a6 100644 --- a/app/views/ScreenLockConfigView.js +++ b/app/views/ScreenLockConfigView.js @@ -71,7 +71,7 @@ class ScreenLockConfigView extends React.Component { init = async() => { const { server } = this.props; const serversDB = database.servers; - const serversCollection = serversDB.collections.get('servers'); + const serversCollection = serversDB.get('servers'); try { this.serverRecord = await serversCollection.find(server); this.setState({ diff --git a/app/views/ScreenLockedView.js b/app/views/ScreenLockedView.js index 883584056..2b036779c 100644 --- a/app/views/ScreenLockedView.js +++ b/app/views/ScreenLockedView.js @@ -31,12 +31,12 @@ const ScreenLockedView = ({ theme }) => { if (!isTablet) { Orientation.lockToPortrait(); } - EventEmitter.addEventListener(LOCAL_AUTHENTICATE_EMITTER, showScreenLock); + const listener = EventEmitter.addEventListener(LOCAL_AUTHENTICATE_EMITTER, showScreenLock); return (() => { if (!isTablet) { Orientation.unlockAllOrientations(); } - EventEmitter.removeListener(LOCAL_AUTHENTICATE_EMITTER); + EventEmitter.removeListener(LOCAL_AUTHENTICATE_EMITTER, listener); }); }, []); diff --git a/app/views/SearchMessagesView/index.js b/app/views/SearchMessagesView/index.js index 18380e55f..e11a5b83d 100644 --- a/app/views/SearchMessagesView/index.js +++ b/app/views/SearchMessagesView/index.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { View, FlatList, Text } from 'react-native'; import { Q } from '@nozbe/watermelondb'; import { connect } from 'react-redux'; -import equal from 'deep-equal'; +import { dequal } from 'dequal'; import RCTextInput from '../../containers/TextInput'; import ActivityIndicator from '../../containers/ActivityIndicator'; @@ -42,7 +42,8 @@ class SearchMessagesView extends React.Component { user: PropTypes.object, baseUrl: PropTypes.string, customEmojis: PropTypes.object, - theme: PropTypes.string + theme: PropTypes.string, + useRealName: PropTypes.bool } constructor(props) { @@ -68,7 +69,7 @@ class SearchMessagesView extends React.Component { if (nextState.searchText !== searchText) { return true; } - if (!equal(nextState.messages, messages)) { + if (!dequal(nextState.messages, messages)) { return true; } return false; @@ -83,7 +84,7 @@ class SearchMessagesView extends React.Component { // If it's a encrypted, room we'll search only on the local stored messages if (this.encrypted) { const db = database.active; - const messagesCollection = db.collections.get('messages'); + const messagesCollection = db.get('messages'); const likeString = sanitizeLikeString(searchText); return messagesCollection .query( @@ -143,7 +144,9 @@ class SearchMessagesView extends React.Component { } renderItem = ({ item }) => { - const { user, baseUrl, theme } = this.props; + const { + user, baseUrl, theme, useRealName + } = this.props; return ( {}} getCustomEmoji={this.getCustomEmoji} navToRoomInfo={this.navToRoomInfo} + useRealName={useRealName} theme={theme} /> ); @@ -206,6 +210,7 @@ class SearchMessagesView extends React.Component { const mapStateToProps = state => ({ baseUrl: state.server.server, user: getUserSelector(state), + useRealName: state.settings.UI_Use_Real_Name, customEmojis: state.customEmojis }); diff --git a/app/views/SelectServerView.js b/app/views/SelectServerView.js index e04c05247..b3345c820 100644 --- a/app/views/SelectServerView.js +++ b/app/views/SelectServerView.js @@ -29,7 +29,7 @@ class SelectServerView extends React.Component { async componentDidMount() { const serversDB = database.servers; - const serversCollection = serversDB.collections.get('servers'); + const serversCollection = serversDB.get('servers'); const servers = await serversCollection.query(Q.where('rooms_updated_at', Q.notEq(null))).fetch(); this.setState({ servers }); } diff --git a/app/views/SelectedUsersView.js b/app/views/SelectedUsersView.js index 666cee3d1..f9d14e169 100644 --- a/app/views/SelectedUsersView.js +++ b/app/views/SelectedUsersView.js @@ -1,10 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, StyleSheet, FlatList } from 'react-native'; +import { View, FlatList } from 'react-native'; import { connect } from 'react-redux'; -import equal from 'deep-equal'; import orderBy from 'lodash/orderBy'; import { Q } from '@nozbe/watermelondb'; +import * as List from '../containers/List'; import database from '../lib/database'; import RocketChat from '../lib/rocketchat'; @@ -28,12 +28,6 @@ import { import { showErrorAlert } from '../utils/info'; import SafeAreaView from '../containers/SafeAreaView'; -const styles = StyleSheet.create({ - separator: { - marginLeft: 60 - } -}); - class SelectedUsersView extends React.Component { static propTypes = { baseUrl: PropTypes.string, @@ -70,27 +64,6 @@ class SelectedUsersView extends React.Component { this.setHeader(props.route.params?.showButton); } - shouldComponentUpdate(nextProps, nextState) { - const { search, chats } = this.state; - const { users, loading, theme } = this.props; - if (nextProps.theme !== theme) { - return true; - } - if (nextProps.loading !== loading) { - return true; - } - if (!equal(nextProps.users, users)) { - return true; - } - if (!equal(nextState.search, search)) { - return true; - } - if (!equal(nextState.chats, chats)) { - return true; - } - return false; - } - componentDidUpdate(prevProps) { if (this.isGroupChat()) { const { users } = this.props; @@ -247,11 +220,6 @@ class SelectedUsersView extends React.Component { ); } - renderSeparator = () => { - const { theme } = this.props; - return ; - } - renderItem = ({ item, index }) => { const { search, chats } = this.state; const { baseUrl, user, theme } = this.props; @@ -297,7 +265,7 @@ class SelectedUsersView extends React.Component { extraData={this.props} keyExtractor={item => item._id} renderItem={this.renderItem} - ItemSeparatorComponent={this.renderSeparator} + ItemSeparatorComponent={List.Separator} ListHeaderComponent={this.renderHeader} contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }} enableEmptySections diff --git a/app/views/SettingsView/index.js b/app/views/SettingsView/index.js index bc11da7b7..b67c7f1ea 100644 --- a/app/views/SettingsView/index.js +++ b/app/views/SettingsView/index.js @@ -62,7 +62,7 @@ class SettingsView extends React.Component { checkCookiesAndLogout = async() => { const { logout, user } = this.props; const db = database.servers; - const usersCollection = db.collections.get('users'); + const usersCollection = db.get('users'); try { const userRecord = await usersCollection.find(user.id); if (!userRecord.loginEmailPassword) { diff --git a/app/views/ShareListView/index.js b/app/views/ShareListView/index.js index aa8cf7f6d..38f3aff72 100644 --- a/app/views/ShareListView/index.js +++ b/app/views/ShareListView/index.js @@ -7,7 +7,7 @@ import ShareExtension from 'rn-extensions-share'; import * as FileSystem from 'expo-file-system'; import { connect } from 'react-redux'; import * as mime from 'react-native-mime-types'; -import isEqual from 'react-fast-compare'; +import { dequal } from 'dequal'; import { Q } from '@nozbe/watermelondb'; import database from '../../lib/database'; @@ -118,7 +118,7 @@ class ShareListView extends React.Component { const { searchResults } = this.state; if (nextState.searching) { - if (!isEqual(nextState.searchResults, searchResults)) { + if (!dequal(nextState.searchResults, searchResults)) { return true; } } @@ -206,7 +206,7 @@ class ShareListView extends React.Component { ) ); } - const data = await db.collections.get('subscriptions').query(...defaultWhereClause).fetch(); + const data = await db.get('subscriptions').query(...defaultWhereClause).fetch(); return data.map(item => ({ rid: item.rid, t: item.t, @@ -226,7 +226,7 @@ class ShareListView extends React.Component { if (server) { const chats = await this.query(); - const serversCollection = serversDB.collections.get('servers'); + const serversCollection = serversDB.get('servers'); const serversCount = await serversCollection.query(Q.where('rooms_updated_at', Q.notEq(null))).fetchCount(); let serverInfo = {}; try { diff --git a/app/views/ShareView/Thumbs.js b/app/views/ShareView/Thumbs.js index 0ce0d7373..26bc54fb4 100644 --- a/app/views/ShareView/Thumbs.js +++ b/app/views/ShareView/Thumbs.js @@ -179,6 +179,7 @@ const Thumbs = React.memo(({ /> ); } + return null; }); Thumbs.propTypes = { attachments: PropTypes.array, diff --git a/app/views/ShareView/index.js b/app/views/ShareView/index.js index 03ea45724..765860036 100644 --- a/app/views/ShareView/index.js +++ b/app/views/ShareView/index.js @@ -95,7 +95,7 @@ class ShareView extends Component { getServerInfo = async() => { const { server } = this.props; const serversDB = database.servers; - const serversCollection = serversDB.collections.get('servers'); + const serversCollection = serversDB.get('servers'); try { this.serverInfo = await serversCollection.find(server); } catch (error) { @@ -235,7 +235,9 @@ class ShareView extends Component { newSelected = attachments[selectedIndex - 1] || {}; } } - this.setState({ attachments: attachments.filter(att => att.path !== item.path), selected: newSelected ?? selected }); + this.setState({ attachments: attachments.filter(att => att.path !== item.path), selected: newSelected ?? selected }, () => { + this.messagebox?.current?.forceUpdate?.(); + }); } onChangeText = (text) => { diff --git a/app/views/SidebarView/index.js b/app/views/SidebarView/index.js index 9dd63b8dc..a14b178cd 100644 --- a/app/views/SidebarView/index.js +++ b/app/views/SidebarView/index.js @@ -4,19 +4,16 @@ import { ScrollView, Text, View, TouchableWithoutFeedback } from 'react-native'; import { connect } from 'react-redux'; -import { Q } from '@nozbe/watermelondb'; -import isEqual from 'react-fast-compare'; - +import { dequal } from 'dequal'; import Avatar from '../../containers/Avatar'; import Status from '../../containers/Status/Status'; -import log, { logEvent, events } from '../../utils/log'; +import { logEvent, events } from '../../utils/log'; import I18n from '../../i18n'; import scrollPersistTaps from '../../utils/scrollPersistTaps'; import { CustomIcon } from '../../lib/Icons'; import styles from './styles'; import SidebarItem from './SidebarItem'; import { themes } from '../../constants/colors'; -import database from '../../lib/database'; import { withTheme } from '../../theme'; import { getUserSelector } from '../../selectors/login'; import SafeAreaView from '../../containers/SafeAreaView'; @@ -27,13 +24,6 @@ Separator.propTypes = { theme: PropTypes.string }; -const permissions = [ - 'view-statistics', - 'view-room-administration', - 'view-user-administration', - 'view-privileged-setting' -]; - class Sidebar extends Component { static propTypes = { baseUrl: PropTypes.string, @@ -45,32 +35,24 @@ class Sidebar extends Component { loadingServer: PropTypes.bool, useRealName: PropTypes.bool, allowStatusMessage: PropTypes.bool, - isMasterDetail: PropTypes.bool + isMasterDetail: PropTypes.bool, + viewStatisticsPermission: PropTypes.object, + viewRoomAdministrationPermission: PropTypes.object, + viewUserAdministrationPermission: PropTypes.object, + viewPrivilegedSettingPermission: PropTypes.object } constructor(props) { super(props); this.state = { - showStatus: false, - isAdmin: false + showStatus: false }; } - componentDidMount() { - this.setIsAdmin(); - } - - UNSAFE_componentWillReceiveProps(nextProps) { - const { loadingServer } = this.props; - if (loadingServer && nextProps.loadingServer !== loadingServer) { - this.setIsAdmin(); - } - } - shouldComponentUpdate(nextProps, nextState) { const { showStatus, isAdmin } = this.state; const { - Site_Name, user, baseUrl, state, isMasterDetail, useRealName, theme + Site_Name, user, baseUrl, state, isMasterDetail, useRealName, theme, viewStatisticsPermission, viewRoomAdministrationPermission, viewUserAdministrationPermission, viewPrivilegedSettingPermission } = this.props; // Drawer navigation state if (state?.index !== nextProps.state?.index) { @@ -91,7 +73,7 @@ class Sidebar extends Component { if (nextProps.theme !== theme) { return true; } - if (!isEqual(nextProps.user, user)) { + if (!dequal(nextProps.user, user)) { return true; } if (nextProps.isMasterDetail !== isMasterDetail) { @@ -103,25 +85,42 @@ class Sidebar extends Component { if (nextState.isAdmin !== isAdmin) { return true; } + if (!dequal(nextProps.viewStatisticsPermission, viewStatisticsPermission)) { + return true; + } + if (!dequal(nextProps.viewRoomAdministrationPermission, viewRoomAdministrationPermission)) { + return true; + } + if (!dequal(nextProps.viewUserAdministrationPermission, viewUserAdministrationPermission)) { + return true; + } + if (!dequal(nextProps.viewPrivilegedSettingPermission, viewPrivilegedSettingPermission)) { + return true; + } return false; } - async setIsAdmin() { - const db = database.active; - const { user } = this.props; + + getIsAdmin() { + const { + user, viewStatisticsPermission, viewRoomAdministrationPermission, viewUserAdministrationPermission, viewPrivilegedSettingPermission + } = this.props; const { roles } = user; - try { - if (roles) { - const permissionsCollection = db.collections.get('permissions'); - const permissionsFiltered = await permissionsCollection.query(Q.where('id', Q.oneOf(permissions))).fetch(); - const isAdmin = permissionsFiltered.reduce((result, permission) => ( - result || permission.roles.some(r => roles.indexOf(r) !== -1)), - false); - this.setState({ isAdmin }); - } - } catch (e) { - log(e); + const allPermissions = [viewStatisticsPermission, viewRoomAdministrationPermission, viewUserAdministrationPermission, viewPrivilegedSettingPermission]; + let isAdmin = false; + + if (roles) { + isAdmin = allPermissions.reduce((result, permission) => { + if (permission) { + return ( + result || permission.some(r => roles.indexOf(r) !== -1) + ); + } + return result; + }, + false); } + return isAdmin; } sidebarNavigate = (route) => { @@ -143,9 +142,8 @@ class Sidebar extends Component { } renderAdmin = () => { - const { isAdmin } = this.state; const { theme, isMasterDetail } = this.props; - if (!isAdmin) { + if (!this.getIsAdmin()) { return null; } const routeName = isMasterDetail ? 'AdminPanelView' : 'AdminPanelStackNavigator'; @@ -275,7 +273,11 @@ const mapStateToProps = state => ({ loadingServer: state.server.loading, useRealName: state.settings.UI_Use_Real_Name, allowStatusMessage: state.settings.Accounts_AllowUserStatusMessageChange, - isMasterDetail: state.app.isMasterDetail + isMasterDetail: state.app.isMasterDetail, + viewStatisticsPermission: state.permissions['view-statistics'], + viewRoomAdministrationPermission: state.permissions['view-room-administration'], + viewUserAdministrationPermission: state.permissions['view-user-administration'], + viewPrivilegedSettingPermission: state.permissions['view-privileged-setting'] }); export default connect(mapStateToProps)(withTheme(Sidebar)); diff --git a/app/views/ThreadMessagesView/Dropdown/index.js b/app/views/ThreadMessagesView/Dropdown/index.js index 49533e4be..fdf1acba7 100644 --- a/app/views/ThreadMessagesView/Dropdown/index.js +++ b/app/views/ThreadMessagesView/Dropdown/index.js @@ -68,7 +68,7 @@ class Dropdown extends React.Component { }); const backdropOpacity = this.animatedValue.interpolate({ inputRange: [0, 1], - outputRange: [0, 0.3] + outputRange: [0, themes[theme].backdropOpacity] }); return ( <> diff --git a/app/views/ThreadMessagesView/index.js b/app/views/ThreadMessagesView/index.js index d75328a99..417c10328 100644 --- a/app/views/ThreadMessagesView/index.js +++ b/app/views/ThreadMessagesView/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { FlatList, InteractionManager } from 'react-native'; +import { FlatList } from 'react-native'; import { connect } from 'react-redux'; import { Q } from '@nozbe/watermelondb'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; @@ -73,9 +73,7 @@ class ThreadMessagesView extends React.Component { componentDidMount() { this.mounted = true; - this.mountInteraction = InteractionManager.runAfterInteractions(() => { - this.init(); - }); + this.init(); } componentDidUpdate(prevProps) { @@ -89,12 +87,6 @@ class ThreadMessagesView extends React.Component { componentWillUnmount() { console.countReset(`${ this.constructor.name }.render calls`); - if (this.mountInteraction && this.mountInteraction.cancel) { - this.mountInteraction.cancel(); - } - if (this.syncInteraction && this.syncInteraction.cancel) { - this.syncInteraction.cancel(); - } if (this.subSubscription && this.subSubscription.unsubscribe) { this.subSubscription.unsubscribe(); } @@ -249,7 +241,7 @@ class ThreadMessagesView extends React.Component { try { const db = database.active; - const threadsCollection = db.collections.get('threads'); + const threadsCollection = db.get('threads'); const allThreadsRecords = await subscription.threads.fetch(); let threadsToCreate = []; let threadsToUpdate = []; @@ -330,10 +322,8 @@ class ThreadMessagesView extends React.Component { rid: this.rid, updatedSince: updatedSince.toISOString() }); if (result.success && result.threads) { - this.syncInteraction = InteractionManager.runAfterInteractions(() => { - const { update, remove } = result.threads; - this.updateThreads({ update, remove, lastThreadSync: updatedSince }); - }); + const { update, remove } = result.threads; + this.updateThreads({ update, remove, lastThreadSync: updatedSince }); } this.setState({ loading: false diff --git a/ios/NotificationService/Info.plist b/ios/NotificationService/Info.plist index d2f1244d4..9d9595029 100644 --- a/ios/NotificationService/Info.plist +++ b/ios/NotificationService/Info.plist @@ -4,6 +4,8 @@ AppGroup group.ios.chat.rocket + IS_OFFICIAL + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName diff --git a/ios/ReplyNotification.swift b/ios/ReplyNotification.swift index 87b8cc439..771a1056d 100644 --- a/ios/ReplyNotification.swift +++ b/ios/ReplyNotification.swift @@ -48,6 +48,9 @@ class ReplyNotification: RNNotificationEventHandler { } } } + } else { + let body = RNNotificationParser.parseNotificationResponse(response) + RNEventEmitter.sendEvent(RNNotificationOpened, body: body) } } } diff --git a/ios/RocketChatRN-Bridging-Header.h b/ios/RocketChatRN-Bridging-Header.h index 0c1ed67a4..142bd0845 100644 --- a/ios/RocketChatRN-Bridging-Header.h +++ b/ios/RocketChatRN-Bridging-Header.h @@ -7,6 +7,8 @@ #import #import #import +#import +#import #import #import #import diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj index 4d8a2f561..af7653b82 100644 --- a/ios/RocketChatRN.xcodeproj/project.pbxproj +++ b/ios/RocketChatRN.xcodeproj/project.pbxproj @@ -1477,7 +1477,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative; PRODUCT_NAME = "Rocket.Chat Experimental"; - PROVISIONING_PROFILE_SPECIFIER = "match Development chat.rocket.reactnative"; + PROVISIONING_PROFILE_SPECIFIER = "match Development chat.rocket.reactnative 1614015157"; SWIFT_OBJC_BRIDGING_HEADER = "RocketChatRN-Bridging-Header.h"; SWIFT_OBJC_INTERFACE_HEADER_NAME = "RocketChatRN-Swift.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -1582,7 +1582,7 @@ "$(inherited)", "$(SRCROOT)/../node_modules/rn-extensions-share/ios/**", "$(SRCROOT)/../node_modules/react-native-firebase/ios/RNFirebase/**", - "$PODS_CONFIGURATION_BUILD_DIR/Firebase", + $PODS_CONFIGURATION_BUILD_DIR/Firebase, ); INFOPLIST_FILE = ShareRocketChatRN/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -1599,7 +1599,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "match Development chat.rocket.reactnative.ShareExtension"; + PROVISIONING_PROFILE_SPECIFIER = "match Development chat.rocket.reactnative.ShareExtension 1614015146"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1648,7 +1648,7 @@ "$(inherited)", "$(SRCROOT)/../node_modules/rn-extensions-share/ios/**", "$(SRCROOT)/../node_modules/react-native-firebase/ios/RNFirebase/**", - "$PODS_CONFIGURATION_BUILD_DIR/Firebase", + $PODS_CONFIGURATION_BUILD_DIR/Firebase, ); INFOPLIST_FILE = ShareRocketChatRN/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -1664,7 +1664,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "chat.rocket.reactnative.ShareExtension AppStore"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore chat.rocket.reactnative.ShareExtension"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1691,12 +1691,12 @@ INFOPLIST_FILE = NotificationService/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 4.14.1; + MARKETING_VERSION = 4.15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "match Development chat.rocket.reactnative.NotificationService"; + PROVISIONING_PROFILE_SPECIFIER = "match Development chat.rocket.reactnative.NotificationService 1614015134"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OBJC_BRIDGING_HEADER = "NotificationService/NotificationService-Bridging-Header.h"; @@ -1728,11 +1728,11 @@ INFOPLIST_FILE = NotificationService/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 4.14.1; + MARKETING_VERSION = 4.15.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "chat.rocket.reactnative.NotificationService AppStore"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore chat.rocket.reactnative.NotificationService"; SKIP_INSTALL = YES; SWIFT_OBJC_BRIDGING_HEADER = "NotificationService/NotificationService-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-O"; diff --git a/ios/RocketChatRN/Info.plist b/ios/RocketChatRN/Info.plist index b6b497f45..ada5fc6c0 100644 --- a/ios/RocketChatRN/Info.plist +++ b/ios/RocketChatRN/Info.plist @@ -23,7 +23,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 4.14.1 + 4.15.0 CFBundleSignature ???? CFBundleURLTypes diff --git a/ios/ShareRocketChatRN/Info.plist b/ios/ShareRocketChatRN/Info.plist index 77e3c4e75..d545207a6 100644 --- a/ios/ShareRocketChatRN/Info.plist +++ b/ios/ShareRocketChatRN/Info.plist @@ -4,6 +4,8 @@ AppGroup group.ios.chat.rocket + IS_OFFICIAL + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -19,7 +21,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 4.14.1 + 4.15.0 CFBundleVersion 1 KeychainGroup diff --git a/package.json b/package.json index 71bfdbc1e..901e70955 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket-chat-reactnative", - "version": "0.0.1", + "version": "4.15.0", "private": true, "scripts": { "start": "react-native start", @@ -50,7 +50,7 @@ "bytebuffer": "^5.0.1", "commonmark": "git+https://github.com/RocketChat/commonmark.js.git", "commonmark-react-renderer": "git+https://github.com/RocketChat/commonmark-react-renderer.git", - "deep-equal": "2.0.3", + "dequal": "^2.0.2", "ejson": "2.2.0", "eslint-config-airbnb": "^18.1.0", "expo-apple-authentication": "^2.2.1", @@ -70,7 +70,6 @@ "pretty-bytes": "^5.3.0", "prop-types": "15.7.2", "react": "16.13.1", - "react-fast-compare": "^3.2.0", "react-native": "RocketChat/react-native#0.63.4", "react-native-animatable": "^1.3.3", "react-native-appearance": "0.3.4", @@ -121,7 +120,7 @@ "reselect": "4.0.0", "rn-extensions-share": "RocketChat/rn-extensions-share", "rn-fetch-blob": "0.12.0", - "rn-root-view": "^1.0.3", + "rn-root-view": "1.0.3", "semver": "7.3.2", "ua-parser-js": "^0.7.21", "url-parse": "^1.4.7", @@ -130,6 +129,8 @@ }, "devDependencies": { "@babel/core": "^7.8.4", + "@babel/eslint-parser": "^7.13.4", + "@babel/eslint-plugin": "^7.13.0", "@babel/plugin-proposal-decorators": "^7.8.3", "@babel/runtime": "^7.8.4", "@storybook/addon-storyshots": "5.3.19", @@ -137,7 +138,6 @@ "@types/react-native": "^0.62.7", "axios": "^0.19.2", "babel-core": "^6.26.3", - "babel-eslint": "^9.0.0", "babel-jest": "^25.1.0", "babel-plugin-transform-remove-console": "^6.9.4", "babel-runtime": "^6.26.0", diff --git a/patches/react-native-webview+10.3.2.patch b/patches/react-native-webview+10.3.2.patch index 68412ee27..08b0e9721 100644 --- a/patches/react-native-webview+10.3.2.patch +++ b/patches/react-native-webview+10.3.2.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java -index ab869cf..2aa7a9e 100644 +index ab869cf..08ce7ce 100644 --- a/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -84,6 +84,12 @@ import java.util.Map; @@ -44,7 +44,7 @@ index ab869cf..2aa7a9e 100644 } @Override -@@ -742,12 +754,54 @@ public class RNCWebViewManager extends SimpleViewManager { +@@ -742,12 +754,56 @@ public class RNCWebViewManager extends SimpleViewManager { protected static class RNCWebViewClient extends WebViewClient { @@ -93,6 +93,8 @@ index ab869cf..2aa7a9e 100644 + } + }; + task.execute(); ++ } else { ++ super.onReceivedClientCertRequest(view, request); + } + } + diff --git a/storybook/stories/Message.js b/storybook/stories/Message.js index e2f67a469..7e8e06646 100644 --- a/storybook/stories/Message.js +++ b/storybook/stories/Message.js @@ -821,6 +821,10 @@ export default ({ theme }) => { + + + + diff --git a/yarn.lock b/yarn.lock index 4a577acf4..633d519bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -76,6 +76,22 @@ semver "^5.4.1" source-map "^0.5.0" +"@babel/eslint-parser@^7.13.4": + version "7.13.4" + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.13.4.tgz#dd9df3c70f44d2fb5a6519e8e10ca06c67dca43a" + integrity sha512-WfFEd89SzqmtYox8crTLJuEXyJolZY6Uu6iJpJmw4aMu50zHbYnxzxwuVkCt2cWygw+gLkUPTtAuox7eSnrL8g== + dependencies: + eslint-scope "5.1.0" + eslint-visitor-keys "^1.3.0" + semver "7.0.0" + +"@babel/eslint-plugin@^7.13.0": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/eslint-plugin/-/eslint-plugin-7.13.0.tgz#e6d99efcd6b8551adf479e382a47218726179b1b" + integrity sha512-YGwCLc/u/uc3bU+q/fvgRQ62+TkxuyVvdmybK6ElzE49vODp+RnRe16eJzMM7EwvcRPQfQvcOSuGmzfcbZE2+w== + dependencies: + eslint-rule-composer "^0.3.0" + "@babel/generator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.4.tgz#e49eeed9fe114b62fa5b181856a43a5e32f5f243" @@ -2258,7 +2274,7 @@ "@rocket.chat/sdk@RocketChat/Rocket.Chat.js.SDK#mobile": version "1.0.0-mobile" - resolved "https://codeload.github.com/RocketChat/Rocket.Chat.js.SDK/tar.gz/0a97c818e60670d7660868ea107b96e5ebb631af" + resolved "https://codeload.github.com/RocketChat/Rocket.Chat.js.SDK/tar.gz/0241e2fc0c29827c51655f2d46d96e7a7720d2b6" dependencies: js-sha256 "^0.9.0" lru-cache "^4.1.1" @@ -3457,11 +3473,6 @@ array-equal@^1.0.0: resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= -array-filter@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" - integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= - array-filter@~0.0.0: version "0.0.1" resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" @@ -3649,13 +3660,6 @@ autoprefixer@^9.7.2: postcss "^7.0.30" postcss-value-parser "^4.1.0" -available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" - integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ== - dependencies: - array-filter "^1.0.0" - aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -3717,18 +3721,6 @@ babel-core@^6.0.0, babel-core@^6.26.0, babel-core@^6.26.3: slash "^1.0.0" source-map "^0.5.7" -babel-eslint@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-9.0.0.tgz#7d9445f81ed9f60aff38115f838970df9f2b6220" - integrity sha512-itv1MwE3TMbY0QtNfeL7wzak1mV47Uy+n6HtSOO4Xd7rvmO+tsGQSgyOEEgo6Y2vHZKZphaoelNeSVj4vkLA1g== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.0.0" - "@babel/traverse" "^7.0.0" - "@babel/types" "^7.0.0" - eslint-scope "3.7.1" - eslint-visitor-keys "^1.0.0" - babel-generator@^6.18.0, babel-generator@^6.26.0: version "6.26.1" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" @@ -5741,26 +5733,6 @@ deep-assign@^3.0.0: dependencies: is-obj "^1.0.0" -deep-equal@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0" - integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA== - dependencies: - es-abstract "^1.17.5" - es-get-iterator "^1.1.0" - is-arguments "^1.0.4" - is-date-object "^1.0.2" - is-regex "^1.0.5" - isarray "^2.0.5" - object-is "^1.1.2" - object-keys "^1.1.1" - object.assign "^4.1.0" - regexp.prototype.flags "^1.3.0" - side-channel "^1.0.2" - which-boxed-primitive "^1.0.1" - which-collection "^1.0.1" - which-typed-array "^1.1.2" - deep-equal@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" @@ -5871,6 +5843,11 @@ dequal@^1.0.0: resolved "https://registry.yarnpkg.com/dequal/-/dequal-1.0.0.tgz#41c6065e70de738541c82cdbedea5292277a017e" integrity sha512-/Nd1EQbQbI9UbSHrMiKZjFLrXSnU328iQdZKPQf78XQI6C+gutkFUeoHpG5J08Ioa6HeRbRNFpSIclh1xyG0mw== +dequal@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d" + integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug== + des.js@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" @@ -6309,7 +6286,7 @@ es-array-method-boxes-properly@^1.0.0: resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== -es-get-iterator@^1.0.2, es-get-iterator@^1.1.0: +es-get-iterator@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8" integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ== @@ -6484,10 +6461,15 @@ eslint-plugin-react@7.20.3: resolve "^1.17.0" string.prototype.matchall "^4.0.2" -eslint-scope@3.7.1: - version "3.7.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" - integrity sha1-PWPD7f2gLgbgGkUq2IyqzHzctug= +eslint-rule-composer@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" + integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== + +eslint-scope@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" + integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== dependencies: esrecurse "^4.1.0" estraverse "^4.1.1" @@ -6515,11 +6497,16 @@ eslint-utils@^1.4.3: dependencies: eslint-visitor-keys "^1.1.0" -eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: +eslint-visitor-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== +eslint-visitor-keys@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + eslint@6.8.0: version "6.8.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" @@ -7344,11 +7331,6 @@ for-own@^1.0.0: dependencies: for-in "^1.0.1" -foreach@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" - integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= - forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -8458,11 +8440,6 @@ is-arrayish@^0.3.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== -is-bigint@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4" - integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g== - is-binary-path@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" @@ -8477,11 +8454,6 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-boolean-object@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" - integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ== - is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -8525,7 +8497,7 @@ is-data-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" -is-date-object@^1.0.1, is-date-object@^1.0.2: +is-date-object@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== @@ -8665,11 +8637,6 @@ is-map@^2.0.1: resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== -is-number-object@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" - integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== - is-number@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" @@ -8767,16 +8734,6 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.1" -is-typed-array@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d" - integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ== - dependencies: - available-typed-arrays "^1.0.0" - es-abstract "^1.17.4" - foreach "^2.0.5" - has-symbols "^1.0.1" - is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -8787,16 +8744,6 @@ is-utf8@^0.2.0: resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= -is-weakmap@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" - integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== - -is-weakset@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83" - integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw== - is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -11522,7 +11469,7 @@ object-inspect@^1.7.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== -object-is@^1.0.1, object-is@^1.1.2: +object-is@^1.0.1: version "1.1.2" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6" integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ== @@ -12786,11 +12733,6 @@ react-fast-compare@^3.0.1: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.1.1.tgz#0becf31e3812fa70dc231e259f40d892d4767900" integrity sha512-SCsAORWK59BvauR2L1BTdjQbJcSGJJz03U0awektk2hshLKrITDDFTlgGCqIZpTDlPC/NFlZee6xTMzXPVLiHw== -react-fast-compare@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" - integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== - react-focus-lock@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.3.1.tgz#9d5d85899773609c7eefa4fc54fff6a0f5f2fc47" @@ -13189,7 +13131,7 @@ react-native-windows@^0.62.0-0: uuid "^3.3.2" xml-parser "^1.2.1" -react-native@RocketChat/react-native#0.63.4, react-native@^0.63.1: +react-native@RocketChat/react-native#0.63.4: version "0.63.4" resolved "https://codeload.github.com/RocketChat/react-native/tar.gz/616299bbc01eeb56cb7d5b52e543051e1ad2d136" dependencies: @@ -13832,8 +13774,6 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: rn-extensions-share@RocketChat/rn-extensions-share: version "2.4.1" resolved "https://codeload.github.com/RocketChat/rn-extensions-share/tar.gz/4d7c0e4c2f300e4fb116af7b7cc0dbbc8169150c" - dependencies: - react-native "^0.63.1" rn-fetch-blob@0.12.0: version "0.12.0" @@ -13848,7 +13788,7 @@ rn-host-detect@1.2.0: resolved "https://registry.yarnpkg.com/rn-host-detect/-/rn-host-detect-1.2.0.tgz#8b0396fc05631ec60c1cb8789e5070cdb04d0da0" integrity sha512-btNg5kzHcjZZ7t7mvvV/4wNJ9e3MPgrWivkRgWURzXL0JJ0pwWlU4zrbmdlz3HHzHOxhBhHB4D+/dbMFfu4/4A== -rn-root-view@^1.0.3: +rn-root-view@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/rn-root-view/-/rn-root-view-1.0.3.tgz#a2cddc717278cb2175fb29b7c006e407b7f0d0e2" integrity sha512-BIKm8hY5q8+pxK9B5ugYjqutoI9xn2JfxIZKWoaFmAl1bOIM4oXjwFQrRM1e6lFgzz99MN6Mf2dK3Alsywnvvw== @@ -15995,44 +15935,11 @@ whatwg-url@^7.0.0: tr46 "^1.0.1" webidl-conversions "^4.0.2" -which-boxed-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1" - integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ== - dependencies: - is-bigint "^1.0.0" - is-boolean-object "^1.0.0" - is-number-object "^1.0.3" - is-string "^1.0.4" - is-symbol "^1.0.2" - -which-collection@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" - integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== - dependencies: - is-map "^2.0.1" - is-set "^2.0.1" - is-weakmap "^2.0.1" - is-weakset "^2.0.1" - which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which-typed-array@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2" - integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ== - dependencies: - available-typed-arrays "^1.0.2" - es-abstract "^1.17.5" - foreach "^2.0.5" - function-bind "^1.1.1" - has-symbols "^1.0.1" - is-typed-array "^1.1.3" - which@1.3.1, which@^1.2.12, which@^1.2.9, which@^1.3.0, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"