diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js index d23268f39..589aa6bd1 100644 --- a/app/lib/methods/getPermissions.js +++ b/app/lib/methods/getPermissions.js @@ -18,6 +18,10 @@ const PERMISSIONS = [ 'archive-room', 'auto-translate', 'create-invite-links', + 'create-c', + 'create-p', + 'create-d', + 'start-discussion', 'create-team', 'delete-c', 'delete-message', diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index fa8a47b18..e0c16ac6c 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -1390,17 +1390,19 @@ const RocketChat = { * Returns an array of boolean for each permission from permissions arg */ async hasPermission(permissions, rid) { - const db = database.active; - const subsCollection = db.get('subscriptions'); let roomRoles = []; - try { - // get the room from database - const room = await subsCollection.find(rid); - // get room roles - roomRoles = room.roles || []; - } catch (error) { - console.log('hasPermission -> Room not found'); - return permissions.map(() => false); + if (rid) { + const db = database.active; + const subsCollection = db.get('subscriptions'); + try { + // get the room from database + const room = await subsCollection.find(rid); + // get room roles + roomRoles = room.roles || []; + } catch (error) { + console.log('hasPermission -> Room not found'); + return permissions.map(() => false); + } } try { @@ -1547,11 +1549,13 @@ const RocketChat = { messageId }); }, - searchMessages(roomId, searchText) { + searchMessages(roomId, searchText, count, offset) { // RC 0.60.0 return this.sdk.get('chat.search', { roomId, - searchText + searchText, + count, + offset }); }, toggleFollowMessage(mid, follow) { diff --git a/app/views/AuthLoadingView.js b/app/views/AuthLoadingView.tsx similarity index 81% rename from app/views/AuthLoadingView.js rename to app/views/AuthLoadingView.tsx index e97c10d94..ef6e7b943 100644 --- a/app/views/AuthLoadingView.js +++ b/app/views/AuthLoadingView.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'; -import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import I18n from '../i18n'; @@ -23,25 +22,25 @@ const styles = StyleSheet.create({ } }); -const AuthLoadingView = React.memo(({ theme, text }) => ( +interface IAuthLoadingView { + theme: string; + text: string; +} + +const AuthLoadingView = React.memo(({ theme, text }: IAuthLoadingView) => ( - {text && ( + {text ? ( <> {`${text}\n${I18n.t('Please_wait')}`} - )} + ) : null} )); -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ text: state.app.text }); -AuthLoadingView.propTypes = { - theme: PropTypes.string, - text: PropTypes.string -}; - export default connect(mapStateToProps)(withTheme(AuthLoadingView)); diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js index af17961fb..0aa3c7035 100644 --- a/app/views/CreateChannelView.js +++ b/app/views/CreateChannelView.js @@ -21,6 +21,7 @@ import { Review } from '../utils/review'; import { getUserSelector } from '../selectors/login'; import { events, logEvent } from '../utils/log'; import SafeAreaView from '../containers/SafeAreaView'; +import RocketChat from '../lib/rocketchat'; import sharedStyles from './Styles'; const styles = StyleSheet.create({ @@ -79,10 +80,13 @@ class CreateChannelView extends React.Component { users: PropTypes.array.isRequired, user: PropTypes.shape({ id: PropTypes.string, - token: PropTypes.string + token: PropTypes.string, + roles: PropTypes.array }), theme: PropTypes.string, - teamId: PropTypes.string + teamId: PropTypes.string, + createPublicChannelPermission: PropTypes.array, + createPrivateChannelPermission: PropTypes.array }; constructor(props) { @@ -96,14 +100,20 @@ class CreateChannelView extends React.Component { readOnly: false, encrypted: false, broadcast: false, - isTeam + isTeam, + permissions: [] }; this.setHeader(); } + componentDidMount() { + this.handleHasPermission(); + } + shouldComponentUpdate(nextProps, nextState) { - const { channelName, type, readOnly, broadcast, encrypted } = this.state; - const { users, isFetching, encryptionEnabled, theme } = this.props; + const { channelName, type, readOnly, broadcast, encrypted, permissions } = this.state; + const { users, isFetching, encryptionEnabled, theme, createPublicChannelPermission, createPrivateChannelPermission } = + this.props; if (nextProps.theme !== theme) { return true; } @@ -122,18 +132,37 @@ class CreateChannelView extends React.Component { if (nextState.broadcast !== broadcast) { return true; } + if (nextState.permissions !== permissions) { + return true; + } if (nextProps.isFetching !== isFetching) { return true; } if (nextProps.encryptionEnabled !== encryptionEnabled) { return true; } + if (!dequal(nextProps.createPublicChannelPermission, createPublicChannelPermission)) { + return true; + } + if (!dequal(nextProps.createPrivateChannelPermission, createPrivateChannelPermission)) { + return true; + } if (!dequal(nextProps.users, users)) { return true; } return false; } + componentDidUpdate(prevProps) { + const { createPublicChannelPermission, createPrivateChannelPermission } = this.props; + if ( + !dequal(createPublicChannelPermission, prevProps.createPublicChannelPermission) || + !dequal(createPrivateChannelPermission, prevProps.createPrivateChannelPermission) + ) { + this.handleHasPermission(); + } + } + setHeader = () => { const { navigation } = this.props; const { isTeam } = this.state; @@ -208,12 +237,21 @@ class CreateChannelView extends React.Component { ); }; + handleHasPermission = async () => { + const { createPublicChannelPermission, createPrivateChannelPermission } = this.props; + const permissions = [createPublicChannelPermission, createPrivateChannelPermission]; + const permissionsToCreate = await RocketChat.hasPermission(permissions); + this.setState({ permissions: permissionsToCreate }); + }; + renderType() { - const { type, isTeam } = this.state; + const { type, isTeam, permissions } = this.state; + const isDisabled = permissions.filter(r => r === true).length <= 1; return this.renderSwitch({ id: 'type', - value: type, + value: permissions[1] ? type : false, + disabled: isDisabled, label: isTeam ? 'Private_Team' : 'Private_Channel', onValueChange: value => { logEvent(events.CR_TOGGLE_TYPE); @@ -373,7 +411,9 @@ const mapStateToProps = state => ({ isFetching: state.createChannel.isFetching, encryptionEnabled: state.encryption.enabled, users: state.selectedUsers.users, - user: getUserSelector(state) + user: getUserSelector(state), + createPublicChannelPermission: state.permissions['create-c'], + createPrivateChannelPermission: state.permissions['create-p'] }); const mapDispatchToProps = dispatch => ({ diff --git a/app/views/NewMessageView.js b/app/views/NewMessageView.js index 4b210e7cb..020588ffa 100644 --- a/app/views/NewMessageView.js +++ b/app/views/NewMessageView.js @@ -3,8 +3,9 @@ import PropTypes from 'prop-types'; import { FlatList, StyleSheet, Text, View } from 'react-native'; import { connect } from 'react-redux'; import { Q } from '@nozbe/watermelondb'; - +import { dequal } from 'dequal'; import * as List from '../containers/List'; + import Touch from '../utils/touch'; import database from '../lib/database'; import RocketChat from '../lib/rocketchat'; @@ -57,13 +58,19 @@ class NewMessageView extends React.Component { baseUrl: PropTypes.string, user: PropTypes.shape({ id: PropTypes.string, - token: PropTypes.string + token: PropTypes.string, + roles: PropTypes.array }), create: PropTypes.func, maxUsers: PropTypes.number, theme: PropTypes.string, isMasterDetail: PropTypes.bool, - serverVersion: PropTypes.string + serverVersion: PropTypes.string, + createTeamPermission: PropTypes.array, + createDirectMessagePermission: PropTypes.array, + createPublicChannelPermission: PropTypes.array, + createPrivateChannelPermission: PropTypes.array, + createDiscussionPermission: PropTypes.array }; constructor(props) { @@ -71,7 +78,8 @@ class NewMessageView extends React.Component { this.init(); this.state = { search: [], - chats: [] + chats: [], + permissions: [] }; } @@ -90,6 +98,30 @@ class NewMessageView extends React.Component { } }; + componentDidMount() { + this.handleHasPermission(); + } + + componentDidUpdate(prevProps) { + const { + createTeamPermission, + createPublicChannelPermission, + createPrivateChannelPermission, + createDirectMessagePermission, + createDiscussionPermission + } = this.props; + + if ( + !dequal(createTeamPermission, prevProps.createTeamPermission) || + !dequal(createPublicChannelPermission, prevProps.createPublicChannelPermission) || + !dequal(createPrivateChannelPermission, prevProps.createPrivateChannelPermission) || + !dequal(createDirectMessagePermission, prevProps.createDirectMessagePermission) || + !dequal(createDiscussionPermission, prevProps.createDiscussionPermission) + ) { + this.handleHasPermission(); + } + } + onSearchChangeText(text) { this.search(text); } @@ -161,20 +193,43 @@ class NewMessageView extends React.Component { Navigation.navigate('CreateDiscussionView'); }; + handleHasPermission = async () => { + const { + createTeamPermission, + createDirectMessagePermission, + createPublicChannelPermission, + createPrivateChannelPermission, + createDiscussionPermission + } = this.props; + const permissions = [ + createPublicChannelPermission, + createPrivateChannelPermission, + createTeamPermission, + createDirectMessagePermission, + createDiscussionPermission + ]; + const permissionsToCreate = await RocketChat.hasPermission(permissions); + this.setState({ permissions: permissionsToCreate }); + }; + renderHeader = () => { const { maxUsers, theme, serverVersion } = this.props; + const { permissions } = this.state; + return ( this.onSearchChangeText(text)} testID='new-message-view-search' /> - {this.renderButton({ - onPress: this.createChannel, - title: I18n.t('Create_Channel'), - icon: 'channel-public', - testID: 'new-message-view-create-channel', - first: true - })} - {compareServerVersion(serverVersion, '3.13.0', methods.greaterThanOrEqualTo) + {permissions[0] || permissions[1] + ? this.renderButton({ + onPress: this.createChannel, + title: I18n.t('Create_Channel'), + icon: 'channel-public', + testID: 'new-message-view-create-channel', + first: true + }) + : null} + {compareServerVersion(serverVersion, '3.13.0', methods.greaterThanOrEqualTo) && permissions[2] ? this.renderButton({ onPress: this.createTeam, title: I18n.t('Create_Team'), @@ -182,7 +237,7 @@ class NewMessageView extends React.Component { testID: 'new-message-view-create-team' }) : null} - {maxUsers > 2 + {maxUsers > 2 && permissions[3] ? this.renderButton({ onPress: this.createGroupChat, title: I18n.t('Create_Direct_Messages'), @@ -190,12 +245,14 @@ class NewMessageView extends React.Component { testID: 'new-message-view-create-direct-message' }) : null} - {this.renderButton({ - onPress: this.createDiscussion, - title: I18n.t('Create_Discussion'), - icon: 'discussions', - testID: 'new-message-view-create-discussion' - })} + {permissions[4] + ? this.renderButton({ + onPress: this.createDiscussion, + title: I18n.t('Create_Discussion'), + icon: 'discussions', + testID: 'new-message-view-create-discussion' + }) + : null} ); @@ -261,7 +318,12 @@ const mapStateToProps = state => ({ isMasterDetail: state.app.isMasterDetail, baseUrl: state.server.server, maxUsers: state.settings.DirectMesssage_maxUsers || 1, - user: getUserSelector(state) + user: getUserSelector(state), + createTeamPermission: state.permissions['create-team'], + createDirectMessagePermission: state.permissions['create-d'], + createPublicChannelPermission: state.permissions['create-c'], + createPrivateChannelPermission: state.permissions['create-p'], + createDiscussionPermission: state.permissions['start-discussion'] }); const mapDispatchToProps = dispatch => ({ diff --git a/app/views/RoomView/RightButtons.js b/app/views/RoomView/RightButtons.js index 70e18bd68..9bbf717fd 100644 --- a/app/views/RoomView/RightButtons.js +++ b/app/views/RoomView/RightButtons.js @@ -20,7 +20,8 @@ class RightButtonsContainer extends Component { navigation: PropTypes.object, isMasterDetail: PropTypes.bool, toggleFollowThread: PropTypes.func, - joined: PropTypes.bool + joined: PropTypes.bool, + encrypted: PropTypes.bool }; constructor(props) { @@ -137,11 +138,14 @@ class RightButtonsContainer extends Component { goSearchView = () => { logEvent(events.ROOM_GO_SEARCH); - const { rid, t, navigation, isMasterDetail } = this.props; + const { rid, t, navigation, isMasterDetail, encrypted } = this.props; if (isMasterDetail) { - navigation.navigate('ModalStackNavigator', { screen: 'SearchMessagesView', params: { rid, showCloseModal: true } }); + navigation.navigate('ModalStackNavigator', { + screen: 'SearchMessagesView', + params: { rid, showCloseModal: true, encrypted } + }); } else { - navigation.navigate('SearchMessagesView', { rid, t }); + navigation.navigate('SearchMessagesView', { rid, t, encrypted }); } }; diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index 95e5da195..713be5615 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -362,6 +362,7 @@ class RoomView extends React.Component { const t = room?.t; const teamMain = room?.teamMain; const teamId = room?.teamId; + const encrypted = room?.encrypted; const { id: userId, token } = user; const avatar = room?.name; const visitor = room?.visitor; @@ -424,6 +425,7 @@ class RoomView extends React.Component { teamMain={teamMain} joined={joined} t={t} + encrypted={encrypted} navigation={navigation} toggleFollowThread={this.toggleFollowThread} /> diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js index 5f4d12553..4c9b48269 100644 --- a/app/views/RoomsListView/index.js +++ b/app/views/RoomsListView/index.js @@ -89,7 +89,12 @@ const shouldUpdateProps = [ 'refreshing', 'queueSize', 'inquiryEnabled', - 'encryptionBanner' + 'encryptionBanner', + 'createTeamPermission', + 'createDirectMessagePermission', + 'createPublicChannelPermission', + 'createPrivateChannelPermission', + 'createDiscussionPermission' ]; const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, @@ -106,7 +111,7 @@ class RoomsListView extends React.Component { username: PropTypes.string, token: PropTypes.string, statusLivechat: PropTypes.string, - roles: PropTypes.object + roles: PropTypes.array }), server: PropTypes.string, searchText: PropTypes.string, @@ -135,6 +140,11 @@ class RoomsListView extends React.Component { queueSize: PropTypes.number, inquiryEnabled: PropTypes.bool, encryptionBanner: PropTypes.string, + createTeamPermission: PropTypes.array, + createDirectMessagePermission: PropTypes.array, + createPublicChannelPermission: PropTypes.array, + createPrivateChannelPermission: PropTypes.array, + createDiscussionPermission: PropTypes.array, initAdd: PropTypes.func }; @@ -152,7 +162,8 @@ class RoomsListView extends React.Component { loading: true, chatsUpdate: [], chats: [], - item: {} + item: {}, + canCreateRoom: false }; this.setHeader(); this.getSubscriptions(); @@ -160,6 +171,7 @@ class RoomsListView extends React.Component { componentDidMount() { const { navigation, closeServerDropdown } = this.props; + this.handleHasPermission(); this.mounted = true; if (isTablet) { @@ -203,7 +215,7 @@ class RoomsListView extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - const { chatsUpdate, searching, item } = this.state; + const { chatsUpdate, searching, item, canCreateRoom } = this.state; // eslint-disable-next-line react/destructuring-assignment const propsUpdated = shouldUpdateProps.some(key => nextProps[key] !== this.props[key]); if (propsUpdated) { @@ -222,6 +234,10 @@ class RoomsListView extends React.Component { return true; } + if (nextState.canCreateRoom !== canCreateRoom) { + return true; + } + if (nextState.item?.rid !== item?.rid) { return true; } @@ -257,7 +273,20 @@ class RoomsListView extends React.Component { } componentDidUpdate(prevProps) { - const { sortBy, groupByType, showFavorites, showUnread, rooms, isMasterDetail, insets } = this.props; + const { + sortBy, + groupByType, + showFavorites, + showUnread, + rooms, + isMasterDetail, + insets, + createTeamPermission, + createPublicChannelPermission, + createPrivateChannelPermission, + createDirectMessagePermission, + createDiscussionPermission + } = this.props; const { item } = this.state; if ( @@ -278,6 +307,17 @@ class RoomsListView extends React.Component { if (insets.left !== prevProps.insets.left || insets.right !== prevProps.insets.right) { this.setHeader(); } + + if ( + !dequal(createTeamPermission, prevProps.createTeamPermission) || + !dequal(createPublicChannelPermission, prevProps.createPublicChannelPermission) || + !dequal(createPrivateChannelPermission, prevProps.createPrivateChannelPermission) || + !dequal(createDirectMessagePermission, prevProps.createDirectMessagePermission) || + !dequal(createDiscussionPermission, prevProps.createDiscussionPermission) + ) { + this.handleHasPermission(); + this.setHeader(); + } } componentWillUnmount() { @@ -297,10 +337,31 @@ class RoomsListView extends React.Component { console.countReset(`${this.constructor.name}.render calls`); } + handleHasPermission = async () => { + const { + createTeamPermission, + createDirectMessagePermission, + createPublicChannelPermission, + createPrivateChannelPermission, + createDiscussionPermission + } = this.props; + const permissions = [ + createPublicChannelPermission, + createPrivateChannelPermission, + createTeamPermission, + createDirectMessagePermission, + createDiscussionPermission + ]; + const permissionsToCreate = await RocketChat.hasPermission(permissions); + const canCreateRoom = permissionsToCreate.filter(r => r === true).length > 0; + this.setState({ canCreateRoom }, () => this.setHeader()); + }; + getHeader = () => { - const { searching } = this.state; + const { searching, canCreateRoom } = this.state; const { navigation, isMasterDetail, insets } = this.props; const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: searching ? 0 : 3 }); + return { headerTitleAlign: 'left', headerLeft: () => @@ -327,7 +388,9 @@ class RoomsListView extends React.Component { headerRight: () => searching ? null : ( - + {canCreateRoom ? ( + + ) : null} @@ -963,7 +1026,12 @@ const mapStateToProps = state => ({ rooms: state.room.rooms, queueSize: getInquiryQueueSelector(state).length, inquiryEnabled: state.inquiry.enabled, - encryptionBanner: state.encryption.banner + encryptionBanner: state.encryption.banner, + createTeamPermission: state.permissions['create-team'], + createDirectMessagePermission: state.permissions['create-d'], + createPublicChannelPermission: state.permissions['create-c'], + createPrivateChannelPermission: state.permissions['create-p'], + createDiscussionPermission: state.permissions['start-discussion'] }); const mapDispatchToProps = dispatch => ({ diff --git a/app/views/SearchMessagesView/index.js b/app/views/SearchMessagesView/index.js index f8131ec1d..c36425edd 100644 --- a/app/views/SearchMessagesView/index.js +++ b/app/views/SearchMessagesView/index.js @@ -24,8 +24,11 @@ import database from '../../lib/database'; import { sanitizeLikeString } from '../../lib/database/utils'; import getThreadName from '../../lib/methods/getThreadName'; import getRoomInfo from '../../lib/methods/getRoomInfo'; +import { isIOS } from '../../utils/deviceInfo'; +import { compareServerVersion, methods } from '../../lib/utils'; import styles from './styles'; +const QUERY_SIZE = 50; class SearchMessagesView extends React.Component { static navigationOptions = ({ navigation, route }) => { const options = { @@ -43,6 +46,7 @@ class SearchMessagesView extends React.Component { route: PropTypes.object, user: PropTypes.object, baseUrl: PropTypes.string, + serverVersion: PropTypes.string, customEmojis: PropTypes.object, theme: PropTypes.string, useRealName: PropTypes.bool @@ -55,6 +59,7 @@ class SearchMessagesView extends React.Component { messages: [], searchText: '' }; + this.offset = 0; this.rid = props.route.params?.rid; this.t = props.route.params?.t; this.encrypted = props.route.params?.encrypted; @@ -88,6 +93,9 @@ class SearchMessagesView extends React.Component { // Handle encrypted rooms search messages searchMessages = async searchText => { + if (!searchText) { + return []; + } // If it's a encrypted, room we'll search only on the local stored messages if (this.encrypted) { const db = database.active; @@ -103,25 +111,33 @@ class SearchMessagesView extends React.Component { .fetch(); } // If it's not a encrypted room, search messages on the server - const result = await RocketChat.searchMessages(this.rid, searchText); + const result = await RocketChat.searchMessages(this.rid, searchText, QUERY_SIZE, this.offset); if (result.success) { return result.messages; } }; - search = debounce(async searchText => { - this.setState({ searchText, loading: true, messages: [] }); - + getMessages = async (searchText, debounced) => { try { const messages = await this.searchMessages(searchText); - this.setState({ - messages: messages || [], + this.setState(prevState => ({ + messages: debounced ? messages : [...prevState.messages, ...messages], loading: false - }); + })); } catch (e) { this.setState({ loading: false }); log(e); } + }; + + search = searchText => { + this.offset = 0; + this.setState({ searchText, loading: true, messages: [] }); + this.searchDebounced(searchText); + }; + + searchDebounced = debounce(async searchText => { + await this.getMessages(searchText, true); }, 1000); getCustomEmoji = name => { @@ -168,6 +184,23 @@ class SearchMessagesView extends React.Component { } }; + onEndReached = async () => { + const { serverVersion } = this.props; + const { searchText, messages, loading } = this.state; + if ( + messages.length < this.offset || + this.encrypted || + loading || + compareServerVersion(serverVersion, '3.17.0', methods.lowerThan) + ) { + return; + } + this.setState({ loading: true }); + this.offset += QUERY_SIZE; + + await this.getMessages(searchText); + }; + renderEmpty = () => { const { theme } = this.props; return ( @@ -212,8 +245,10 @@ class SearchMessagesView extends React.Component { renderItem={this.renderItem} style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]} keyExtractor={item => item._id} - onEndReached={this.load} + onEndReached={this.onEndReached} ListFooterComponent={loading ? : null} + onEndReachedThreshold={0.5} + removeClippedSubviews={isIOS} {...scrollPersistTaps} /> ); @@ -243,6 +278,7 @@ class SearchMessagesView extends React.Component { } const mapStateToProps = state => ({ + serverVersion: state.server.version, baseUrl: state.server.server, user: getUserSelector(state), useRealName: state.settings.UI_Use_Real_Name,