[NEW] Convert/Move Channel to Team (#3164)

* Added Create Team

* Added actionTypes, actions, ENG strings for Teams and updated NewMessageView

* Added createTeam sagas, createTeam reducer, new Team string and update CreateChannelView

* Remove unnecessary actionTypes, reducers and sagas, e2e tests and navigation to team view

* Minor tweaks

* Show TeamChannelsView only if joined the team

* Minor tweak

* Added AddChannelTeamView

* Added permissions, translations strings for teams,  deleteTeamRoom and addTeamRooms, AddExistingChannelView, updated CreateChannelView, TeamChannelsView

* Refactor touch component and update removeRoom and deleteRoom methods

* Minor tweaks

* Minor tweaks for removing channels and addExistingChannelView

* Added missing events and fixed channels list

* Minor tweaks for refactored touch component

* Added SelectListView and logic for leaving team

* Added addTeamMember and removeTeamMember

* Minor tweak

* Added deleteTeam function

* Minor tweak

* Minor tweaks

* Remove unnecesary changes, update TeamChannelsView, AddExistingChannelView, AddChannelTeamView, createChannel, goRoom and Touchable

* Remove unnecesary prop

* Add screens to ModalStack, events, autoJoin, update createChannel, addRoomsToTeam and Touchable

* Minor tweak

* Update loadMessagesForRoom.js

* Updated schema, tag component, touch, AddChannelTeamView, AddExistingChannelView, ActionSheet Item

* Fix unnecessary changes

* Add i18n, update createChannel, AddExistingChannelTeamView, AddChannelTeamView, RightButton and TeamChannelsView

* Updated styles, added tag story

* Minor tweak

* Minor tweaks

* Auto-join tweak

* Minor tweaks

* Minor tweak on search

* Minor refactor to ListItem, add SelectListView to ModalStack, update handleLeaveTeam

* Minor tweaks

* Update SelectListView

* Update handleLeaveTeam, remove unnecessary method, add story

* Minor tweak

* Minor visual tweaks

* Update SelectListView.js

* Update index.js

* Update RoomMembersView

* Updated SelectListView, RoomActionsView, leaveTeam method and string translations

* Update SelectListVIew

* Minor tweak

* Update SelectListView

* Minor tweak

* Minor tweaks

* Fix for List.Item subtitles being pushed down by title's flex

* Minor tweaks

* Update RoomActionsView

* Use showConfirmationAlert and showErrorAlert

* Remove addTeamMember, update removeTeamMember

* Update Alert

* Minor tweaks

* Minor tweaks

* Minor tweak

* Update showActionSheet on RoomMembersView

* Remove team main from query and move code around

* Fetch roles

* Update RoomMembersView and SelectListView

* Update rocketchat.js

* Updated leaveTeam and handleRemoveFromTeam

* Fix validation

* Remove unnecessary function

* Update RoomActionsView

* Update en.json

* updated deleteTeam function and permissions

* Added showConfirmationAlert

* Added string translations for teams

* Fix permission

* Added moveChannelToTeam and convertToTeam functionality

* Fix SelectListView RadioButton

* Fix moveToTeam

* Added searchBar to SelectListVIew

* Update RoomView , SelectListVIew and string translation for error

Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Gerzon Z 2021-06-02 09:44:19 -04:00 committed by GitHub
parent 2b51f37384
commit 5fd7981d07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 270 additions and 16 deletions

View File

@ -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;
}

View File

@ -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 teams context, however, all channels members, which are not members of the respective team, will still have access to this channel, but will not be added as teams members. \n\nAll channels management will still be made by the owners of this channel.\n\nTeams members and even teams owners, if not a member of this channel, can not have access to the channels content. \n\nPlease notice that the Teams 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"

View File

@ -17,6 +17,7 @@ const PERMISSIONS = [
'archive-room',
'auto-translate',
'create-invite-links',
'create-team',
'delete-c',
'delete-message',
'delete-p',

View File

@ -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

View File

@ -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
? (
<>
<List.Item
title='Convert_to_Team'
onPress={() => this.onPressTouchable({
event: this.convertToTeam
})}
testID='room-actions-convert-to-team'
left={() => <List.Icon name='teams' />}
showActionIndicator
/>
<List.Separator />
</>
)
: null}
{['c', 'p'].includes(t) && canMoveToTeam
? (
<>
<List.Item
title='Move_Channel_to_Team'
onPress={() => this.onPressTouchable({
event: this.moveToTeam
})}
testID='room-actions-convert-to-team'
left={() => <List.Icon name='channel-move-to-team' />}
showActionIndicator
/>
<List.Separator />
</>
)
: 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 => ({

View File

@ -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;
}

View File

@ -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) {

View File

@ -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 (
<View style={{ backgroundColor: themes[theme].auxiliaryBackground }}>
<SearchBox onChangeText={text => this.search(text)} testID='select-list-view-search' onCancelPress={() => this.setState({ isSearching: false })} />
</View>
);
}
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.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 = () => <RadioButton selected={selected.includes(item.rid)} color={themes[theme].actionTintColor} size={ICON_SIZE} />;
const showCheck = () => <List.Icon name={checked} color={themes[theme].actionTintColor} />;
return (
<>
<List.Separator />
@ -107,25 +147,24 @@ class SelectListView extends React.Component {
onPress={() => (item.alert ? this.showAlert() : this.toggleItem(item.rid))}
alert={item.alert}
left={() => <List.Icon name={icon} color={themes[theme].controlText} />}
right={() => (checked ? <List.Icon name={checked} color={themes[theme].actionTintColor} /> : null)}
right={() => (this.isRadio ? showRadio() : showCheck())}
/>
</>
);
}
render() {
const { data } = this.state;
const { data, isSearching, dataFiltered } = this.state;
const { theme } = this.props;
return (
<SafeAreaView testID='select-list-view'>
<StatusBar />
<FlatList
data={data}
data={!isSearching ? data : dataFiltered}
extraData={this.state}
keyExtractor={item => item.rid}
renderItem={this.renderItem}
ListHeaderComponent={this.renderInfoText}
ListHeaderComponent={this.isSearch ? this.renderSearch : this.renderInfoText}
contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }}
keyboardShouldPersistTaps='always'
/>