diff --git a/app/containers/RoomHeader/index.js b/app/containers/RoomHeader/index.js index 4eeab701f2..7d4d22de87 100644 --- a/app/containers/RoomHeader/index.js +++ b/app/containers/RoomHeader/index.js @@ -32,7 +32,7 @@ class RoomHeaderContainer extends Component { shouldComponentUpdate(nextProps) { const { - type, title, subtitle, status, statusText, connecting, connected, onPress, usersTyping, width, height + type, title, subtitle, status, statusText, connecting, connected, onPress, usersTyping, width, height, teamMain } = this.props; if (nextProps.type !== type) { return true; @@ -67,6 +67,9 @@ class RoomHeaderContainer extends Component { if (nextProps.onPress !== onPress) { return true; } + if (nextProps.teamMain !== teamMain) { + return true; + } return false; } diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 29a3b2422f..090bf5cd05 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -14,7 +14,7 @@ "error-delete-protected-role": "Cannot delete a protected role", "error-department-not-found": "Department not found", "error-direct-message-file-upload-not-allowed": "File sharing not allowed in direct messages", - "error-duplicate-channel-name": "A channel with name {{channel_name}} exists", + "error-duplicate-channel-name": "A channel with name {{room_name}} exists", "error-email-domain-blacklisted": "The email domain is blacklisted", "error-email-send-failed": "Error trying to send email: {{message}}", "error-save-image": "Error while saving image", @@ -182,6 +182,7 @@ "delete": "delete", "Delete": "Delete", "DELETE": "DELETE", + "move": "move", "deleting_room": "deleting room", "description": "description", "Description": "Description", @@ -731,6 +732,7 @@ "invalid-room": "Invalid room", "You_are_leaving_the_team": "You are leaving the team '{{team}}'", "Leave_Team": "Leave Team", + "Select_Team": "Select Team", "Select_Team_Channels": "Select the Team's channels you would like to leave.", "Cannot_leave": "Cannot leave", "Cannot_remove": "Cannot remove", @@ -746,8 +748,16 @@ "Remove_Member": "Remove Member", "leaving_team": "leaving team", "removing_team": "removing from team", + "moving_channel_to_team": "moving channel to team", "deleting_team": "deleting team", "member-does-not-exist": "Member does not exist", + "Convert": "Convert", + "Convert_to_Team": "Convert to Team", + "Convert_to_Team_Warning": "This can't be undone. Once you convert a channel to a team, you can not turn it back to a channel.", + "Move_to_Team": "Move to Team", + "Move_Channel_to_Team": "Move Channel to Team", + "Move_Channel_Paragraph": "Moving a channel inside a team means that this channel will be added in the team’s context, however, all channel’s members, which are not members of the respective team, will still have access to this channel, but will not be added as team’s members. \n\nAll channel’s management will still be made by the owners of this channel.\n\nTeam’s members and even team’s owners, if not a member of this channel, can not have access to the channel’s content. \n\nPlease notice that the Team’s owner will be able remove members from the Channel.", + "Move_to_Team_Warning": "After reading the previous intructions about this behavior, do you still want to move this channel to the selected team?", "Load_More": "Load More", "Load_Newer": "Load Newer", "Load_Older": "Load Older" diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js index 99a18a6c08..2b7da47652 100644 --- a/app/lib/methods/getPermissions.js +++ b/app/lib/methods/getPermissions.js @@ -17,6 +17,7 @@ const PERMISSIONS = [ 'archive-room', 'auto-translate', 'create-invite-links', + 'create-team', 'delete-c', 'delete-message', 'delete-p', diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 4774b42722..f174aa1eb4 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -798,6 +798,20 @@ const RocketChat = { // RC 3.13.0 return this.sdk.get('teams.listRoomsOfUser', { teamId, userId }); }, + convertChannelToTeam({ rid, name, type }) { + const params = { + ...(type === 'c' + ? { + channelId: rid, + channelName: name + } + : { + roomId: rid, + roomName: name + }) + }; + return this.sdk.post(type === 'c' ? 'channels.convertToTeam' : 'groups.convertToTeam', params); + }, joinRoom(roomId, joinCode, type) { // TODO: join code // RC 0.48.0 diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index 89d34ed074..7199843c8c 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -5,8 +5,9 @@ import { } from 'react-native'; import { connect } from 'react-redux'; import isEmpty from 'lodash/isEmpty'; -import { compareServerVersion, methods } from '../../lib/utils'; +import { Q } from '@nozbe/watermelondb'; +import { compareServerVersion, methods } from '../../lib/utils'; import Touch from '../../utils/touch'; import { setLoading as setLoadingAction } from '../../actions/selectedUsers'; import { leaveRoom as leaveRoomAction, closeRoom as closeRoomAction } from '../../actions/room'; @@ -61,7 +62,9 @@ class RoomActionsView extends React.Component { editRoomPermission: PropTypes.array, toggleRoomE2EEncryptionPermission: PropTypes.array, viewBroadcastMemberListPermission: PropTypes.array, - transferLivechatGuestPermission: PropTypes.array + transferLivechatGuestPermission: PropTypes.array, + createTeamPermission: PropTypes.array, + addTeamChannelPermission: PropTypes.array } constructor(props) { @@ -83,7 +86,9 @@ class RoomActionsView extends React.Component { canForwardGuest: false, canReturnQueue: false, canEdit: false, - canToggleEncryption: false + canToggleEncryption: false, + canCreateTeam: false, + canAddChannelToTeam: false }; if (room && room.observe && room.rid) { this.roomObservable = room.observe(); @@ -132,9 +137,11 @@ class RoomActionsView extends React.Component { const canEdit = await this.canEdit(); const canToggleEncryption = await this.canToggleEncryption(); const canViewMembers = await this.canViewMembers(); + const canCreateTeam = await this.canCreateTeam(); + const canAddChannelToTeam = await this.canAddChannelToTeam(); this.setState({ - canAutoTranslate, canAddUser, canInviteUser, canEdit, canToggleEncryption, canViewMembers + canAutoTranslate, canAddUser, canInviteUser, canEdit, canToggleEncryption, canViewMembers, canCreateTeam, canAddChannelToTeam }); // livechat permissions @@ -210,6 +217,26 @@ class RoomActionsView extends React.Component { return canEdit; } + canCreateTeam = async() => { + const { room } = this.state; + const { createTeamPermission } = this.props; + const { rid } = room; + const permissions = await RocketChat.hasPermission([createTeamPermission], rid); + + const canCreateTeam = permissions[0]; + return canCreateTeam; + } + + canAddChannelToTeam = async() => { + const { room } = this.state; + const { addTeamChannelPermission } = this.props; + const { rid } = room; + const permissions = await RocketChat.hasPermission([addTeamChannelPermission], rid); + + const canAddChannelToTeam = permissions[0]; + return canAddChannelToTeam; + } + canToggleEncryption = async() => { const { room } = this.state; const { toggleRoomE2EEncryptionPermission } = this.props; @@ -464,6 +491,111 @@ class RoomActionsView extends React.Component { } } + handleConvertToTeam = async() => { + try { + const { room } = this.state; + const { navigation } = this.props; + const result = await RocketChat.convertChannelToTeam({ rid: room.rid, name: room.name, type: room.t }); + + if (result.success) { + navigation.navigate('RoomView'); + } + } catch (e) { + log(e); + } + } + + convertToTeam = () => { + showConfirmationAlert({ + title: I18n.t('Confirmation'), + message: I18n.t('Convert_to_Team_Warning'), + confirmationText: I18n.t('Convert'), + onPress: () => this.handleConvertToTeam() + }); + } + + handleMoveToTeam = async(selected) => { + try { + const { room } = this.state; + const { navigation } = this.props; + const result = await RocketChat.addRoomsToTeam({ teamId: selected.teamId, rooms: [room.rid] }); + if (result.success) { + navigation.navigate('RoomView'); + } + } catch (e) { + log(e); + showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t('moving_channel_to_team') })); + } + } + + moveToTeam = async() => { + try { + const { navigation } = this.props; + const db = database.active; + const subCollection = db.get('subscriptions'); + const teamRooms = await subCollection.query( + Q.where('team_main', Q.notEq(null)) + ); + + if (teamRooms.length) { + navigation.navigate('SelectListView', { + title: 'Move_to_Team', + infoText: 'Move_Channel_Paragraph', + nextAction: () => { + navigation.push('SelectListView', { + title: 'Select_Team', + data: teamRooms, + isRadio: true, + isSearch: true, + onSearch: onChangeText => this.searchTeam(onChangeText), + nextAction: selected => showConfirmationAlert({ + title: I18n.t('Confirmation'), + message: I18n.t('Move_to_Team_Warning'), + confirmationText: I18n.t('Yes_action_it', { action: I18n.t('move') }), + onPress: () => this.handleMoveToTeam(selected) + }) + + }); + } + }); + } + } catch (e) { + log(e); + } + } + + searchTeam = async(onChangeText) => { + try { + const { addTeamChannelPermission, createTeamPermission } = this.props; + const QUERY_SIZE = 50; + const db = database.active; + const teams = await db.collections + .get('subscriptions') + .query( + Q.where('team_main', Q.notEq(null)), + Q.where('name', Q.like(`%${ onChangeText }%`)), + Q.experimentalTake(QUERY_SIZE), + Q.experimentalSortBy('room_updated_at', Q.desc) + ); + + const asyncFilter = async(teamArray) => { + const results = await Promise.all(teamArray.map(async(team) => { + const permissions = await RocketChat.hasPermission([addTeamChannelPermission, createTeamPermission], team.rid); + if (!permissions[0]) { + return false; + } + return true; + })); + + return teamArray.filter((_v, index) => results[index]); + }; + const teamsFiltered = await asyncFilter(teams); + return teamsFiltered; + } catch (e) { + log(e); + } + } + renderRoomInfo = () => { const { room, member } = this.state; const { @@ -635,6 +767,50 @@ class RoomActionsView extends React.Component { } } + teamChannelActions = (t, room) => { + const { canEdit, canCreateTeam, canAddChannelToTeam } = this.state; + const canConvertToTeam = canEdit && canCreateTeam && !room.teamMain; + const canMoveToTeam = canEdit && canAddChannelToTeam && !room.teamId; + + return ( + <> + {['c', 'p'].includes(t) && canConvertToTeam + ? ( + <> + this.onPressTouchable({ + event: this.convertToTeam + })} + testID='room-actions-convert-to-team' + left={() => } + showActionIndicator + /> + + + ) + : null} + + {['c', 'p'].includes(t) && canMoveToTeam + ? ( + <> + this.onPressTouchable({ + event: this.moveToTeam + })} + testID='room-actions-convert-to-team' + left={() => } + showActionIndicator + /> + + + ) + : null} + + ); + } + render() { const { room, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate, canForwardGuest, canReturnQueue @@ -836,6 +1012,8 @@ class RoomActionsView extends React.Component { ) : null} + { this.teamChannelActions(t, room) } + {['l'].includes(t) && !this.isOmnichannelPreview ? ( <> @@ -922,7 +1100,9 @@ const mapStateToProps = state => ({ 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'] + transferLivechatGuestPermission: state.permissions['transfer-livechat-guest'], + createTeamPermission: state.permissions['create-team'], + addTeamChannelPermission: state.permissions['add-team-channel'] }); const mapDispatchToProps = dispatch => ({ diff --git a/app/views/RoomView/RightButtons.js b/app/views/RoomView/RightButtons.js index f61488b5bb..5b283b4ad0 100644 --- a/app/views/RoomView/RightButtons.js +++ b/app/views/RoomView/RightButtons.js @@ -59,6 +59,10 @@ class RightButtonsContainer extends Component { const { isFollowingThread, tunread, tunreadUser, tunreadGroup } = this.state; + const { teamId } = this.props; + if (nextProps.teamId !== teamId) { + return true; + } if (nextState.isFollowingThread !== isFollowingThread) { return true; } diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index 18d123ec41..13de89c87d 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -85,7 +85,7 @@ const stateAttrsUpdate = [ 'member', 'showingBlockingLoader' ]; -const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'tunread', 'muted', 'ignored', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname', 'roles', 'bannerClosed', 'visitor', 'joinCodeRequired']; +const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'tunread', 'muted', 'ignored', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname', 'roles', 'bannerClosed', 'visitor', 'joinCodeRequired', 'teamMain', 'teamId']; class RoomView extends React.Component { static propTypes = { @@ -254,7 +254,10 @@ class RoomView extends React.Component { this.setHeader(); } } - if (((roomUpdate.fname !== prevState.roomUpdate.fname) || (roomUpdate.name !== prevState.roomUpdate.name)) && !this.tmid) { + if ((roomUpdate.teamMain !== prevState.roomUpdate.teamMain) || (roomUpdate.teamId !== prevState.roomUpdate.teamId)) { + this.setHeader(); + } + if (((roomUpdate.fname !== prevState.roomUpdate.fname) || (roomUpdate.name !== prevState.roomUpdate.name) || (roomUpdate.teamMain !== prevState.roomUpdate.teamMain) || (roomUpdate.teamId !== prevState.roomUpdate.teamId)) && !this.tmid) { this.setHeader(); } if (insets.left !== prevProps.insets.left || insets.right !== prevProps.insets.right) { diff --git a/app/views/SelectListView.js b/app/views/SelectListView.js index bcbb0923f7..a3ad3f8898 100644 --- a/app/views/SelectListView.js +++ b/app/views/SelectListView.js @@ -4,7 +4,9 @@ import { View, StyleSheet, FlatList, Text } from 'react-native'; import { connect } from 'react-redux'; +import { RadioButton } from 'react-native-ui-lib'; +import log from '../utils/log'; import * as List from '../containers/List'; import sharedStyles from './Styles'; import I18n from '../i18n'; @@ -14,6 +16,9 @@ import { themes } from '../constants/colors'; import { withTheme } from '../theme'; import SafeAreaView from '../containers/SafeAreaView'; import { animateNextTransition } from '../utils/layoutAnimation'; +import { ICON_SIZE } from '../containers/List/constants'; +import SearchBox from '../containers/SearchBox'; + const styles = StyleSheet.create({ buttonText: { @@ -38,8 +43,13 @@ class SelectListView extends React.Component { this.infoText = props.route?.params?.infoText; this.nextAction = props.route?.params?.nextAction; this.showAlert = props.route?.params?.showAlert; + this.isSearch = props.route?.params?.isSearch; + this.onSearch = props.route?.params?.onSearch; + this.isRadio = props.route?.params?.isRadio; this.state = { data, + dataFiltered: [], + isSearching: false, selected: [] }; this.setHeader(); @@ -75,6 +85,25 @@ class SelectListView extends React.Component { ); } + renderSearch = () => { + const { theme } = this.props; + return ( + + this.search(text)} testID='select-list-view-search' onCancelPress={() => this.setState({ isSearching: false })} /> + + ); + } + + search = async(text) => { + try { + this.setState({ isSearching: true }); + const result = await this.onSearch(text); + this.setState({ dataFiltered: result }); + } catch (e) { + log(e); + } + } + isChecked = (rid) => { const { selected } = this.state; return selected.includes(rid); @@ -84,7 +113,11 @@ class SelectListView extends React.Component { const { selected } = this.state; animateNextTransition(); - if (!this.isChecked(rid)) { + if (this.isRadio) { + if (!this.isChecked(rid)) { + this.setState({ selected: [rid] }, () => this.setHeader()); + } + } else if (!this.isChecked(rid)) { this.setState({ selected: [...selected, rid] }, () => this.setHeader()); } else { const filterSelected = selected.filter(el => el !== rid); @@ -94,9 +127,16 @@ class SelectListView extends React.Component { renderItem = ({ item }) => { const { theme } = this.props; - const icon = item.t === 'p' ? 'channel-private' : 'channel-public'; + const { selected } = this.state; + + const channelIcon = item.t === 'p' ? 'channel-private' : 'channel-public'; + const teamIcon = item.t === 'p' ? 'teams-private' : 'teams'; + const icon = item.teamMain ? teamIcon : channelIcon; const checked = this.isChecked(item.rid) ? 'check' : null; + const showRadio = () => ; + const showCheck = () => ; + return ( <> @@ -107,25 +147,24 @@ class SelectListView extends React.Component { onPress={() => (item.alert ? this.showAlert() : this.toggleItem(item.rid))} alert={item.alert} left={() => } - right={() => (checked ? : null)} + right={() => (this.isRadio ? showRadio() : showCheck())} /> ); } render() { - const { data } = this.state; + const { data, isSearching, dataFiltered } = this.state; const { theme } = this.props; - return ( item.rid} renderItem={this.renderItem} - ListHeaderComponent={this.renderInfoText} + ListHeaderComponent={this.isSearch ? this.renderSearch : this.renderInfoText} contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }} keyboardShouldPersistTaps='always' />