Compare commits

...

22 Commits

Author SHA1 Message Date
Gerzon Z a305676fd6 Added deleteTeam function 2021-05-07 08:01:27 -04:00
Gerzon Z 9cf3e68721 Merge branch 'new.delete-team' into new.add-remove-users-teams 2021-05-06 09:07:53 -04:00
Gerzon Z 04ea01b324 Minor tweak 2021-05-06 09:05:11 -04:00
Gerzon Z b961fa05d9 Added addTeamMember and removeTeamMember 2021-05-06 08:57:53 -04:00
Gerzon Z 4b6e691d8a Added SelectListView and logic for leaving team 2021-05-06 05:22:34 -04:00
Gerzon Z 4bac054105 Minor tweaks for refactored touch component 2021-05-05 08:20:48 -04:00
Gerzon Z 7d2924e2e9 Added missing events and fixed channels list 2021-05-05 05:06:45 -04:00
Gerzon Z 4761e21de8 Minor tweaks for removing channels and addExistingChannelView 2021-05-05 03:46:18 -04:00
Gerzon Z 69ae247329 Minor tweaks 2021-05-05 02:37:57 -04:00
Gerzon Z c3ffa37323 Refactor touch component and update removeRoom and deleteRoom methods 2021-05-04 14:02:01 -04:00
Gerzon Z bb0632b689 Added permissions, translations strings for teams, deleteTeamRoom and addTeamRooms, AddExistingChannelView, updated CreateChannelView, TeamChannelsView 2021-04-30 13:09:10 -04:00
Gerzon Z 37421d395a Merge branch 'develop' into new.add-remove-channel-teams 2021-04-28 23:08:15 -04:00
Gerzon Z 0cf173d7ba Added AddChannelTeamView 2021-04-26 14:06:22 -04:00
Gerzon Z 69214bee93 Minor tweak 2021-04-26 14:03:05 -04:00
Gerzon Z 1ff77118f5 Show TeamChannelsView only if joined the team 2021-04-26 10:21:04 -04:00
Gerzon Z 16fd57527a Minor tweaks 2021-04-26 10:08:24 -04:00
Gerzon Z a245af6c1c Remove unnecessary actionTypes, reducers and sagas, e2e tests and navigation to team view 2021-04-23 08:28:01 -04:00
Gerzon Z c8b8680541 Added createTeam sagas, createTeam reducer, new Team string and update CreateChannelView 2021-04-20 19:07:33 -04:00
Gerzon Z 8b82bd456e Added actionTypes, actions, ENG strings for Teams and updated NewMessageView 2021-04-20 05:30:30 -04:00
Gerzon Z 585a9aea9a
Merge branch 'develop' into new.create-team 2021-04-19 18:59:59 -04:00
Gerzon Z c8a88c9b02
Merge branch 'develop' into new.create-team 2021-04-19 15:25:01 -04:00
Gerzon Z 2f5f247915 Added Create Team 2021-04-19 15:21:27 -04:00
37 changed files with 10315 additions and 7776 deletions

File diff suppressed because it is too large Load Diff

View File

@ -83,8 +83,7 @@ const Button = React.memo(({
}) => (
<Touch
onPress={() => onPress(props.title)}
style={{ backgroundColor: backgroundColor || themes[props.theme].backgroundColor }}
underlayColor={underlayColor}
style={({ pressed }) => [{ backgroundColor: pressed ? underlayColor || themes[props.theme].bannerBackground : backgroundColor || themes[props.theme].backgroundColor }]}
enabled={!props.disabled}
theme={props.theme}
>

View File

@ -7,6 +7,7 @@ import { connect } from 'react-redux';
import { Base64 } from 'js-base64';
import * as AppleAuthentication from 'expo-apple-authentication';
import { transparentize } from 'color2k';
import { withTheme } from '../theme';
import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors';
@ -344,10 +345,8 @@ class LoginServices extends React.PureComponent {
<Touch
key={service.name}
onPress={onPress}
style={[styles.serviceButton, { backgroundColor }]}
style={({ pressed }) => [styles.serviceButton, { backgroundColor: pressed ? transparentize(themes[theme].buttonText, 0.5) : backgroundColor }]}
theme={theme}
activeOpacity={0.5}
underlayColor={themes[theme].buttonText}
>
<View style={styles.serviceButtonContainer}>
{service.authType === 'oauth' || service.authType === 'apple' ? <CustomIcon name={icon} size={24} color={themes[theme].titleText} style={styles.serviceIcon} /> : null}

View File

@ -14,7 +14,7 @@ const Button = React.memo(({
return (
<Touch
style={[styles.buttonView, { backgroundColor: 'transparent' }]}
style={({ pressed }) => [styles.buttonView, { backgroundColor: pressed ? 'transparent' : themes[theme].bannerBackground }]}
underlayColor={themes[theme].passcodeButtonActive}
rippleColor={themes[theme].passcodeButtonActive}
enabled={!disabled}

View File

@ -290,6 +290,7 @@
"last_message": "last message",
"Leave_channel": "Leave channel",
"leaving_room": "leaving room",
"Leave": "Leave",
"leave": "leave",
"Legal": "Legal",
"Light": "Light",
@ -435,6 +436,7 @@
"Review_app_unable_store": "Unable to open {{store}}",
"Review_this_app": "Review this app",
"Remove": "Remove",
"remove": "remove",
"Roles": "Roles",
"Room_actions": "Room actions",
"Room_changed_announcement": "Room announcement changed to: {{announcement}} by {{userBy}}",
@ -709,5 +711,33 @@
"This_room_encryption_has_been_disabled_by__username_": "This room's encryption has been disabled by {{username}}",
"Teams": "Teams",
"No_team_channels_found": "No channels found",
"Team_not_found": "Team not found"
"Team_not_found": "Team not found",
"Create_Team": "Create Team",
"Team_Name": "Team Name",
"Private_Team": "Private Team",
"Read_Only_Team": "Read Only Team",
"Broadcast_Team": "Broadcast Team",
"creating_team" : "creating team",
"team-name-already-exists": "A team with that name already exists",
"Add_Channel_to_Team": "Add Channel to Team",
"Create_New": "Create New",
"Add_Existing": "Add Existing",
"Add_Existing_Channel": "Add Existing Channel",
"Remove_from_Team": "Remove from Team",
"Auto-join": "Auto-join",
"Delete_Team_Room_Warning": "Woud you like to remove this channel from the team? The channel will be moved back to the workspace",
"Confirmation": "Confirmation",
"invalid-room": "Invalid room",
"You_are_leaving_the_team": "You are leaving the team '{{team}}'",
"Leave_Team": "Leave Team",
"Select_Teams": "Select the Team's channels you would like to leave.",
"Cannot_leave": "Cannot leave",
"Last_owner_team_room": "You are the last owner of this channel. Once you leave the team, the channel will be kept inside the team but you will be managing it from outside.",
"last-owner-can-not-be-removed": "Last owner cannot be removed",
"Removing_user_from_this_Team": "You are removing {{user}} from this Team",
"Remove_User_Teams": "Select channels you want the user to be removed from.",
"Remove_Member": "Remove Member",
"Error": "Error",
"Delete_Team": "Delete Team",
"Delete_Team_Warning": "You are deleting this team."
}

View File

@ -13,6 +13,8 @@ const PERMISSIONS = [
'add-user-to-any-c-room',
'add-user-to-any-p-room',
'add-user-to-joined-room',
'add-team-member',
'add-team-channel',
'archive-room',
'auto-translate',
'create-invite-links',
@ -21,11 +23,14 @@ const PERMISSIONS = [
'delete-p',
'edit-message',
'edit-room',
'edit-team-member',
'edit-team-channel',
'force-delete-message',
'mute-user',
'pin-message',
'post-readonly',
'remove-user',
'remove-team-channel',
'set-leader',
'set-moderator',
'set-owner',
@ -38,7 +43,9 @@ const PERMISSIONS = [
'view-privileged-setting',
'view-room-administration',
'view-statistics',
'view-user-administration'
'view-user-administration',
'view-all-teams',
'view-all-team-channels'
];
export async function setPermissions() {

View File

@ -1,13 +1,20 @@
import log from '../../utils/log';
import updateMessages from './updateMessages';
async function load({ rid: roomId, latest, t }) {
let params = { roomId, count: 50 };
async function load({
rid: roomId, latest, t, team
}) {
let params = { roomId: roomId || team.roomId, count: 50 };
let apiType;
if (latest) {
params = { ...params, latest: new Date(latest).toISOString() };
}
const apiType = this.roomTypeToApiType(t);
if (team.type) {
apiType = this.roomTypeToApiType('p');
} else {
apiType = this.roomTypeToApiType(t || 'c');
}
if (!apiType) {
return [];
}

View File

@ -728,7 +728,54 @@ const RocketChat = {
prid, pmid, t_name, reply, users, encrypted
});
},
createTeam({
name, users, type, readOnly, broadcast, encrypted
}) {
const params = {
name,
users,
type,
room: {
readOnly,
extraData: {
broadcast,
encrypted
}
}
};
// RC 3.13.0
return this.post('teams.create', params);
},
addTeamRooms({ rooms, teamId }) {
const params = {
rooms: Array.isArray(rooms) ? rooms : [rooms],
teamId
};
// RC 3.13.0
return this.post('teams.addRooms', params);
},
removeTeamRoom({ roomId, teamId }) {
// RC 3.13.0
return this.post('teams.removeRoom', { roomId, teamId });
},
leaveTeam({ teamName }) {
// RC 3.13.0
return this.post('teams.leave', { teamName });
},
addTeamMember(teamName) {
const { users } = reduxStore.getState().selectedUsers;
const members = users.map(u => ({ userId: u._id, roles: ['member'] }));
// RC 3.13.0
return this.post('teams.addMembers', { teamName, members });
},
removeTeamMember({ teamName, userId }) {
// RC 3.13.0
return this.post('teams.removeMember', { teamName, userId });
},
deleteTeam({ teamName }) {
// RC 3.13.0
return this.post('teams.delete', { teamName });
},
joinRoom(roomId, joinCode, type) {
// TODO: join code
// RC 0.48.0

View File

@ -24,7 +24,6 @@ const RoomItem = ({
status,
useRealName,
theme,
isFocused,
isGroupChat,
isRead,
date,
@ -42,6 +41,7 @@ const RoomItem = ({
testID,
swipeEnabled,
onPress,
onLongPress,
toggleFav,
toggleRead,
hideChannel,
@ -49,6 +49,7 @@ const RoomItem = ({
}) => (
<Touchable
onPress={onPress}
onLongPress={onLongPress}
width={width}
favorite={favorite}
toggleFav={toggleFav}
@ -59,7 +60,6 @@ const RoomItem = ({
testID={testID}
type={type}
theme={theme}
isFocused={isFocused}
swipeEnabled={swipeEnabled}
>
<Wrapper
@ -161,7 +161,6 @@ RoomItem.propTypes = {
status: PropTypes.string,
useRealName: PropTypes.bool,
theme: PropTypes.string,
isFocused: PropTypes.bool,
isGroupChat: PropTypes.bool,
isRead: PropTypes.bool,
teamMain: PropTypes.bool,
@ -181,6 +180,7 @@ RoomItem.propTypes = {
toggleFav: PropTypes.func,
toggleRead: PropTypes.func,
onPress: PropTypes.func,
onLongPress: PropTypes.func,
hideChannel: PropTypes.func
};

View File

@ -17,6 +17,7 @@ class Touchable extends React.Component {
static propTypes = {
type: PropTypes.string.isRequired,
onPress: PropTypes.func,
onLongPress: PropTypes.func,
testID: PropTypes.string,
width: PropTypes.number,
favorite: PropTypes.bool,
@ -27,7 +28,6 @@ class Touchable extends React.Component {
hideChannel: PropTypes.func,
children: PropTypes.element,
theme: PropTypes.string,
isFocused: PropTypes.bool,
swipeEnabled: PropTypes.bool
}
@ -203,9 +203,21 @@ class Touchable extends React.Component {
}
};
onLongPress = () => {
const { rowState } = this.state;
if (rowState !== 0) {
this.close();
return;
}
const { onLongPress } = this.props;
if (onLongPress) {
onLongPress();
}
};
render() {
const {
testID, isRead, width, favorite, children, theme, isFocused, swipeEnabled
testID, isRead, width, favorite, children, theme, swipeEnabled
} = this.props;
return (
@ -239,11 +251,10 @@ class Touchable extends React.Component {
>
<Touch
onPress={this.onPress}
onLongPress={this.onLongPress}
theme={theme}
testID={testID}
style={{
backgroundColor: isFocused ? themes[theme].chatComponentBackground : themes[theme].backgroundColor
}}
style={({ pressed }) => [{ backgroundColor: pressed ? themes[theme].chatComponentBackground : themes[theme].backgroundColor }]}
>
{children}
</Touch>

View File

@ -14,7 +14,6 @@ const attrs = [
'status',
'connected',
'theme',
'isFocused',
'forceUpdate',
'showLastMessage'
];
@ -25,6 +24,7 @@ class RoomItemContainer extends React.Component {
showLastMessage: PropTypes.bool,
id: PropTypes.string,
onPress: PropTypes.func,
onLongPress: PropTypes.func,
username: PropTypes.string,
avatarSize: PropTypes.number,
width: PropTypes.number,
@ -36,7 +36,6 @@ class RoomItemContainer extends React.Component {
getUserPresence: PropTypes.func,
connected: PropTypes.bool,
theme: PropTypes.string,
isFocused: PropTypes.bool,
getRoomTitle: PropTypes.func,
getRoomAvatar: PropTypes.func,
getIsGroupChat: PropTypes.func,
@ -112,6 +111,11 @@ class RoomItemContainer extends React.Component {
return onPress(item);
}
onLongPress = () => {
const { item, onLongPress } = this.props;
return onLongPress(item);
}
render() {
const {
item,
@ -123,7 +127,6 @@ class RoomItemContainer extends React.Component {
toggleRead,
hideChannel,
theme,
isFocused,
avatarSize,
status,
showLastMessage,
@ -160,6 +163,7 @@ class RoomItemContainer extends React.Component {
isGroupChat={this.isGroupChat}
isRead={isRead}
onPress={this.onPress}
onLongPress={this.onLongPress}
date={date}
accessibilityLabel={accessibilityLabel}
width={width}
@ -171,7 +175,6 @@ class RoomItemContainer extends React.Component {
testID={testID}
type={item.t}
theme={theme}
isFocused={isFocused}
size={avatarSize}
prid={item.prid}
status={status}

View File

@ -21,6 +21,14 @@ const createGroupChat = function createGroupChat() {
return RocketChat.createGroupChat();
};
const createTeam = function createTeam(data) {
return RocketChat.createTeam(data);
};
const addTeamRoom = function addRoomToTeam(params) {
return RocketChat.addTeamRooms(params);
};
const handleRequest = function* handleRequest({ data }) {
try {
const auth = yield select(state => state.login.isAuthenticated);
@ -29,7 +37,21 @@ const handleRequest = function* handleRequest({ data }) {
}
let sub;
if (data.group) {
if (data.isTeam) {
const {
type,
readOnly,
broadcast,
encrypted
} = data;
logEvent(events.CT_CREATE, {
type,
readOnly,
broadcast,
encrypted
});
sub = yield call(createTeam, data);
} else if (data.group) {
logEvent(events.SELECTED_USERS_CREATE_GROUP);
const result = yield call(createGroupChat);
if (result.success) {
@ -49,14 +71,22 @@ const handleRequest = function* handleRequest({ data }) {
encrypted
});
sub = yield call(createChannel, data);
}
if (data.teamId) {
logEvent(events.CT_ADD_ROOM_TO_TEAM);
const channels = yield call(addTeamRoom, { rooms: sub.rid, teamId: data.teamId });
if (channels.success) {
sub.teamId = channels.rooms[0].teamId;
sub.isTeamChannel = true;
}
}
}
try {
const db = database.active;
const subCollection = db.get('subscriptions');
yield db.action(async() => {
await subCollection.create((s) => {
s._raw = sanitizedRaw({ id: sub.rid }, subCollection.schema);
s._raw = sanitizedRaw({ id: sub.team ? sub.team.roomId : sub.rid, team_id: sub.teamId }, subCollection.schema);
Object.assign(s, sub);
});
});
@ -76,12 +106,12 @@ const handleSuccess = function* handleSuccess({ data }) {
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
}
goRoom({ item: data, isMasterDetail });
goRoom({ item: data.team ? data.team : data, isMasterDetail });
};
const handleFailure = function handleFailure({ err }) {
setTimeout(() => {
const msg = err.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') });
const msg = err.data ? I18n.t(err.data.error) : err.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') });
showErrorAlert(msg);
}, 300);
};

View File

@ -71,6 +71,9 @@ import ShareView from '../views/ShareView';
import CreateDiscussionView from '../views/CreateDiscussionView';
import QueueListView from '../ee/omnichannel/views/QueueListView';
import AddChannelTeamView from '../views/AddChannelTeamView';
import AddExistingChannelView from '../views/AddExistingChannelView';
import SelectListView from '../views/SelectListView';
// ChatsStackNavigator
const ChatsStack = createStackNavigator();
@ -91,6 +94,11 @@ const ChatsStackNavigator = () => {
component={RoomActionsView}
options={RoomActionsView.navigationOptions}
/>
<ChatsStack.Screen
name='SelectListView'
component={SelectListView}
options={SelectListView.navigationOptions}
/>
<ChatsStack.Screen
name='RoomInfoView'
component={RoomInfoView}
@ -174,6 +182,16 @@ const ChatsStackNavigator = () => {
component={TeamChannelsView}
options={TeamChannelsView.navigationOptions}
/>
<ChatsStack.Screen
name='AddChannelTeamView'
component={AddChannelTeamView}
options={AddChannelTeamView.navigationOptions}
/>
<ChatsStack.Screen
name='AddExistingChannelView'
component={AddExistingChannelView}
options={AddExistingChannelView.navigationOptions}
/>
<ChatsStack.Screen
name='MarkdownTableView'
component={MarkdownTableView}

View File

@ -8,17 +8,36 @@ const navigate = ({ item, isMasterDetail, ...props }) => {
navigationMethod = Navigation.replace;
}
navigationMethod('RoomView', {
rid: item.rid,
name: RocketChat.getRoomTitle(item),
t: item.t,
prid: item.prid,
room: item,
search: item.search,
visitor: item.visitor,
roomUserId: RocketChat.getUidDirectMessage(item),
...props
});
if (item.isTeamChannel) {
// TODO: Refactor
Navigation.navigate('TeamChannelsView');
Navigation.push('RoomView', {
rid: item.roomId || item.rid,
name: RocketChat.getRoomTitle(item),
t: item.type ? 'p' : item.t,
prid: item.prid,
room: item,
search: item.search,
visitor: item.visitor,
roomUserId: RocketChat.getUidDirectMessage(item),
teamId: item.teamId,
...props
});
} else if (item.rooms) {
Navigation.navigate('TeamChannelsView');
} else {
navigationMethod('RoomView', {
rid: item.roomId || item.rid,
name: RocketChat.getRoomTitle(item),
t: item.type ? 'p' : item.t,
prid: item.prid,
room: item,
search: item.search,
visitor: item.visitor,
roomUserId: RocketChat.getUidDirectMessage(item),
...props
});
}
};
export const goRoom = async({ item = {}, isMasterDetail = false, ...props }) => {

View File

@ -88,6 +88,7 @@ export default {
// NEW MESSAGE VIEW
NEW_MSG_CREATE_CHANNEL: 'new_msg_create_channel',
NEW_MSG_CREATE_TEAM: 'new_msg_create_team',
NEW_MSG_CREATE_GROUP_CHAT: 'new_msg_create_group_chat',
NEW_MSG_CREATE_DISCUSSION: 'new_msg_create_discussion',
NEW_MSG_CHAT_WITH_USER: 'new_msg_chat_with_user',
@ -100,12 +101,16 @@ export default {
// CREATE CHANNEL VIEW
CR_CREATE: 'cr_create',
CT_CREATE: 'ct_create',
CR_CREATE_F: 'cr_create_f',
CT_CREATE_F: 'ct_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',
CT_ADD_ROOM_TO_TEAM: 'ct_add_room_to_team',
CT_ADD_ROOM_TO_TEAM_F: 'ct_add_room_to_team_f',
// CREATE DISCUSSION VIEW
CD_CREATE: 'cd_create',

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { RectButton } from 'react-native-gesture-handler';
import { Pressable } from 'react-native';
import { themes } from '../constants/colors';
class Touch extends React.Component {
@ -15,20 +15,21 @@ class Touch extends React.Component {
render() {
const {
children, onPress, theme, underlayColor, ...props
children, onPress, onLongPress, theme, style, ...props
} = this.props;
return (
<RectButton
<Pressable
ref={this.getRef}
onPress={onPress}
onLongPress={onLongPress}
activeOpacity={1}
underlayColor={underlayColor || themes[theme].bannerBackground}
rippleColor={themes[theme].bannerBackground}
style={style}
android_ripple={{ color: themes[theme].bannerBackground }}
{...props}
>
{children}
</RectButton>
</Pressable>
);
}
}
@ -36,8 +37,10 @@ class Touch extends React.Component {
Touch.propTypes = {
children: PropTypes.node,
onPress: PropTypes.func,
onLongPress: PropTypes.func,
theme: PropTypes.string,
underlayColor: PropTypes.string
underlayColor: PropTypes.string,
style: PropTypes.object
};
export default Touch;

View File

@ -0,0 +1,123 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { HeaderBackButton } from '@react-navigation/stack';
import sharedStyles from './Styles';
import { CustomIcon } from '../lib/Icons';
import Touch from '../utils/touch';
import StatusBar from '../containers/StatusBar';
import { withTheme } from '../theme';
import * as HeaderButton from '../containers/HeaderButton';
import SafeAreaView from '../containers/SafeAreaView';
import { withDimensions } from '../dimensions';
import { themes } from '../constants/colors';
import I18n from '../i18n';
const styles = StyleSheet.create({
button: {
height: 46,
flexDirection: 'row',
alignItems: 'center'
},
buttonIcon: {
marginLeft: 18,
marginRight: 16
},
buttonText: {
fontSize: 17,
...sharedStyles.textRegular
},
buttonContainer: {
paddingVertical: 25
}
});
class AddChannelTeamView extends React.Component {
constructor(props) {
super(props);
this.teamId = props.route.params?.teamId;
this.setHeader();
}
setHeader = () => {
const { navigation, isMasterDetail, theme } = this.props;
const options = {
headerShown: true,
headerTitleAlign: 'center',
headerTitle: I18n.t('Add_Channel_to_Team')
};
if (isMasterDetail) {
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
} else {
options.headerLeft = () => (
<HeaderBackButton
labelVisible={false}
onPress={() => navigation.pop()}
tintColor={themes[theme].headerTintColor}
/>
);
}
navigation.setOptions(options);
}
renderButton = ({
onPress, testID, title, icon, first
}) => {
const { theme } = this.props;
return (
<Touch
onPress={onPress}
style={({ pressed }) => [{
backgroundColor: pressed ? themes[theme].chatComponentBackground : themes[theme].backgroundColor
}]}
testID={testID}
theme={theme}
>
<View style={[first ? sharedStyles.separatorVertical : sharedStyles.separatorBottom, styles.button, { borderColor: themes[theme].separatorColor }]}>
<CustomIcon style={[styles.buttonIcon, { color: themes[theme].tintColor }]} size={24} name={icon} />
<Text style={[styles.buttonText, { color: themes[theme].tintColor }]}>{title}</Text>
</View>
</Touch>
);
}
render() {
const { navigation, route } = this.props;
const { teamChannels } = route?.params;
return (
<SafeAreaView testID='add-channel-team-view'>
<StatusBar />
<View style={styles.buttonContainer}>
{this.renderButton({
onPress: () => navigation.navigate('NewMessageStackNavigator', { screen: 'SelectedUsersViewCreateChannel', params: { nextAction: () => navigation.navigate('CreateChannelView', { teamId: this.teamId }) } }),
title: I18n.t('Create_New'),
icon: 'channel-public',
testID: 'add-channel-team-view-create-channel',
first: true
})}
{this.renderButton({
onPress: () => navigation.navigate('AddExistingChannelView', { teamId: this.teamId, teamChannels }),
title: I18n.t('Add_Existing'),
icon: 'team',
testID: 'add-channel-team-view-create-channel'
})}
</View>
</SafeAreaView>
);
}
}
AddChannelTeamView.propTypes = {
route: PropTypes.object,
navigation: PropTypes.object,
isMasterDetail: PropTypes.bool,
theme: PropTypes.string
};
export default withDimensions(withTheme(AddChannelTeamView));

View File

@ -0,0 +1,265 @@
/* eslint-disable no-mixed-spaces-and-tabs */
import React from 'react';
import PropTypes from 'prop-types';
import {
View, StyleSheet, FlatList, Text
} from 'react-native';
import { connect } from 'react-redux';
import { Q } from '@nozbe/watermelondb';
import { HeaderBackButton } from '@react-navigation/stack';
import * as List from '../containers/List';
import Touch from '../utils/touch';
import database from '../lib/database';
import RocketChat from '../lib/rocketchat';
import sharedStyles from './Styles';
import I18n from '../i18n';
import log, { events, logEvent } from '../utils/log';
import SearchBox from '../containers/SearchBox';
import { CustomIcon } from '../lib/Icons';
import * as HeaderButton from '../containers/HeaderButton';
import StatusBar from '../containers/StatusBar';
import { themes } from '../constants/colors';
import { withTheme } from '../theme';
import SafeAreaView from '../containers/SafeAreaView';
import { animateNextTransition } from '../utils/layoutAnimation';
import { goRoom } from '../utils/goRoom';
import Loading from '../containers/Loading';
const QUERY_SIZE = 15;
const styles = StyleSheet.create({
button: {
height: 46,
flexDirection: 'row',
alignItems: 'center'
},
buttonIcon: {
marginLeft: 18,
marginRight: 16
},
buttonText: {
fontSize: 17,
...sharedStyles.textRegular
},
textContainer: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
marginRight: 15
},
icon: {
marginHorizontal: 15,
alignSelf: 'center'
}
});
class AddExistingChannelView extends React.Component {
static propTypes = {
navigation: PropTypes.object,
route: PropTypes.object,
user: PropTypes.shape({
id: PropTypes.string,
token: PropTypes.string
}),
theme: PropTypes.string,
isMasterDetail: PropTypes.bool,
addTeamChannelPermission: PropTypes.array
};
constructor(props) {
super(props);
this.init();
this.teamId = props.route?.params?.teamId;
this.state = {
search: [],
channels: [],
selected: [],
loading: false
};
this.setHeader();
}
setHeader = () => {
const { navigation, isMasterDetail, theme } = this.props;
const { selected } = this.state;
const options = {
headerShown: true,
headerTitleAlign: 'center',
headerTitle: I18n.t('Add_Existing_Channel')
};
if (isMasterDetail) {
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
} else {
options.headerLeft = () => <HeaderBackButton labelVisible={false} onPress={() => navigation.pop()} tintColor={themes[theme].headerTintColor} />;
}
options.headerRight = () => selected.length > 0 && (
<HeaderButton.Container>
<HeaderButton.Item title={I18n.t('Create')} onPress={this.submit} testID='add-existing-channel-view-submit' />
</HeaderButton.Container>
);
navigation.setOptions(options);
}
// eslint-disable-next-line react/sort-comp
init = async() => {
try {
const { addTeamChannelPermission } = this.props;
const db = database.active;
const channels = await db.collections
.get('subscriptions')
.query(
Q.and(Q.where('team_id', ''), Q.or(Q.where('t', 'c'), Q.where('t', 'p'))),
Q.experimentalTake(QUERY_SIZE),
Q.experimentalSortBy('room_updated_at', Q.desc)
)
.fetch();
const filteredChannels = channels.filter(async(channel) => {
const permissions = await RocketChat.hasPermission([addTeamChannelPermission], channel.rid);
if (!permissions[0]) {
return;
}
return channel;
});
this.setState({ channels: filteredChannels });
} catch (e) {
log(e);
}
}
onSearchChangeText(text) {
this.search(text);
}
dismiss = () => {
const { navigation } = this.props;
return navigation.pop();
}
search = async(text) => {
const result = await RocketChat.search({ text, filterUsers: false });
this.setState({
search: result
});
}
submit = async() => {
const { selected } = this.state;
const { isMasterDetail } = this.props;
this.setState({ loading: true });
try {
logEvent(events.CT_ADD_ROOM_TO_TEAM);
const result = await RocketChat.addTeamRooms({ rooms: selected, teamId: this.teamId });
if (result.success) {
this.setState({ loading: false });
goRoom({ item: result, isMasterDetail });
}
} catch (e) {
logEvent(events.CT_ADD_ROOM_TO_TEAM_F);
this.setState({ loading: false });
}
}
renderChannel = ({
onPress, testID, title, icon, checked
}) => {
const { theme } = this.props;
return (
<Touch
onPress={onPress}
style={{ backgroundColor: themes[theme].backgroundColor }}
testID={testID}
theme={theme}
>
<View style={[styles.button, { borderColor: themes[theme].separatorColor, marginVertical: 4 }]}>
<CustomIcon style={[styles.buttonIcon, { color: themes[theme].controlText }]} size={24} name={icon} />
<View style={styles.textContainer}>
<Text style={[styles.buttonText, { color: themes[theme].bodyText }]}>{title}</Text>
</View>
{checked ? <CustomIcon name={checked} size={22} style={[styles.icon, { color: themes[theme].actionTintColor }]} /> : null}
</View>
</Touch>
);
}
renderHeader = () => {
const { theme } = this.props;
return (
<View style={{ backgroundColor: themes[theme].auxiliaryBackground }}>
<SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='add-existing-channel-view-search' />
</View>
);
}
isChecked = (rid) => {
const { selected } = this.state;
return selected.includes(rid);
}
toggleChannel = (rid) => {
const { selected } = this.state;
animateNextTransition();
if (!this.isChecked(rid)) {
// logEvent(events.SELECTED_USERS_ADD_USER);
this.setState({ selected: [...selected, rid] }, () => this.setHeader());
} else {
// logEvent(events.SELECTED_USERS_REMOVE_USER);
const filterSelected = selected.filter(el => el !== rid);
this.setState({ selected: filterSelected }, () => this.setHeader());
}
}
renderItem = ({ item }) => (
<>
{this.renderChannel({
onPress: () => this.toggleChannel(item.rid),
title: item.name,
icon: item.t === 'p' && !item.teamId ? 'channel-private' : 'channel-public',
checked: this.isChecked(item.rid) ? 'check' : null,
testID: 'add-existing-channel-view-item'
})}
</>
)
renderList = () => {
const { search, channels } = this.state;
const { theme } = this.props;
return (
<FlatList
data={search.length > 0 ? search : channels}
extraData={this.state}
keyExtractor={item => item._id}
ListHeaderComponent={this.renderHeader}
renderItem={this.renderItem}
ItemSeparatorComponent={List.Separator}
contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }}
keyboardShouldPersistTaps='always'
/>
);
}
render() {
const { loading } = this.state;
return (
<SafeAreaView testID='new-message-view'>
<StatusBar />
{this.renderList()}
<Loading visible={loading} />
</SafeAreaView>
);
}
}
const mapStateToProps = state => ({
isMasterDetail: state.app.isMasterDetail,
addTeamChannelPermission: state.permissions['add-team-channel']
});
export default connect(mapStateToProps, null)(withTheme(AddExistingChannelView));

View File

@ -69,11 +69,12 @@ const styles = StyleSheet.create({
class CreateChannelView extends React.Component {
static navigationOptions = () => ({
title: I18n.t('Create_Channel')
});
title: this.isTeam ? I18n.t('Create_Team') : I18n.t('Create_Channel')
})
static propTypes = {
navigation: PropTypes.object,
route: PropTypes.object,
baseUrl: PropTypes.string,
create: PropTypes.func.isRequired,
removeUser: PropTypes.func.isRequired,
@ -86,15 +87,22 @@ class CreateChannelView extends React.Component {
id: PropTypes.string,
token: PropTypes.string
}),
theme: PropTypes.string
theme: PropTypes.string,
teamId: PropTypes.string
};
state = {
channelName: '',
type: true,
readOnly: false,
encrypted: false,
broadcast: false
constructor(props) {
super(props);
const { route } = this.props;
this.isTeam = route?.params?.isTeam || false;
this.teamId = route?.params?.teamId;
this.state = {
channelName: '',
type: true,
readOnly: false,
encrypted: false,
broadcast: false
};
}
shouldComponentUpdate(nextProps, nextState) {
@ -154,7 +162,9 @@ class CreateChannelView extends React.Component {
const {
channelName, type, readOnly, broadcast, encrypted
} = this.state;
const { users: usersProps, isFetching, create } = this.props;
const {
users: usersProps, isFetching, create
} = this.props;
if (!channelName.trim() || isFetching) {
return;
@ -163,9 +173,9 @@ class CreateChannelView extends React.Component {
// transform users object into array of usernames
const users = usersProps.map(user => user.name);
// create channel
// create channel or team
create({
name: channelName, users, type, readOnly, broadcast, encrypted
name: channelName, users, type, readOnly, broadcast, encrypted, isTeam: this.isTeam, teamId: this.teamId
});
Review.pushPositiveEvent();
@ -197,10 +207,11 @@ class CreateChannelView extends React.Component {
renderType() {
const { type } = this.state;
return this.renderSwitch({
id: 'type',
value: type,
label: 'Private_Channel',
label: this.isTeam ? 'Private_Team' : 'Private_Channel',
onValueChange: (value) => {
logEvent(events.CR_TOGGLE_TYPE);
// If we set the channel as public, encrypted status should be false
@ -211,10 +222,11 @@ class CreateChannelView extends React.Component {
renderReadOnly() {
const { readOnly, broadcast } = this.state;
return this.renderSwitch({
id: 'readonly',
value: readOnly,
label: 'Read_Only_Channel',
label: this.isTeam ? 'Read_Only_Team' : 'Read_Only_Channel',
onValueChange: (value) => {
logEvent(events.CR_TOGGLE_READ_ONLY);
this.setState({ readOnly: value });
@ -245,10 +257,11 @@ class CreateChannelView extends React.Component {
renderBroadcast() {
const { broadcast, readOnly } = this.state;
return this.renderSwitch({
id: 'broadcast',
value: broadcast,
label: 'Broadcast_Channel',
label: this.isTeam ? 'Broadcast_Team' : 'Broadcast_Channel',
onValueChange: (value) => {
logEvent(events.CR_TOGGLE_BROADCAST);
this.setState({
@ -302,7 +315,9 @@ class CreateChannelView extends React.Component {
render() {
const { channelName } = this.state;
const { users, isFetching, theme } = this.props;
const {
users, isFetching, theme
} = this.props;
const userCount = users.length;
return (
@ -312,18 +327,18 @@ class CreateChannelView extends React.Component {
keyboardVerticalOffset={128}
>
<StatusBar />
<SafeAreaView testID='create-channel-view'>
<SafeAreaView testID={this.isTeam ? 'create-team-view' : 'create-channel-view'}>
<ScrollView {...scrollPersistTaps}>
<View style={[sharedStyles.separatorVertical, { borderColor: themes[theme].separatorColor }]}>
<TextInput
autoFocus
style={[styles.input, { backgroundColor: themes[theme].backgroundColor }]}
label={I18n.t('Channel_Name')}
label={this.isTeam ? I18n.t('Team_Name') : I18n.t('Channel_Name')}
value={channelName}
onChangeText={this.onChangeText}
placeholder={I18n.t('Channel_Name')}
placeholder={this.isTeam ? I18n.t('Team_Name') : I18n.t('Channel_Name')}
returnKeyType='done'
testID='create-channel-name'
testID={this.isTeam ? 'create-team-name' : 'create-channel-name'}
autoCorrect={false}
autoCapitalize='none'
theme={theme}

View File

@ -67,7 +67,7 @@ export default class DirectoryOptions extends PureComponent {
return (
<Touch
onPress={() => changeType(itemType)}
style={styles.dropdownItemButton}
style={({ pressed }) => [{ backgroundColor: pressed ? themes[theme].bannerBackground : themes[theme].backgroundColor }, styles.dropdownItemButton]}
theme={theme}
>
<View style={styles.dropdownItemContainer}>

View File

@ -169,7 +169,7 @@ class DirectoryView extends React.Component {
/>
<Touch
onPress={this.toggleDropdown}
style={styles.dropdownItemButton}
style={({ pressed }) => [{ backgroundColor: pressed ? themes[theme].bannerBackground : themes[theme].backgroundColor }, styles.dropdownItemButton]}
testID='directory-view-dropdown'
theme={theme}
>

View File

@ -116,6 +116,12 @@ class NewMessageView extends React.Component {
navigation.navigate('SelectedUsersViewCreateChannel', { nextAction: () => navigation.navigate('CreateChannelView') });
}
createTeam = () => {
logEvent(events.NEW_MSG_CREATE_TEAM);
const { navigation } = this.props;
navigation.navigate('SelectedUsersViewCreateChannel', { nextAction: () => navigation.navigate('CreateChannelView', { isTeam: true }) });
}
createGroupChat = () => {
logEvent(events.NEW_MSG_CREATE_GROUP_CHAT);
const { createChannel, maxUsers, navigation } = this.props;
@ -142,7 +148,9 @@ class NewMessageView extends React.Component {
return (
<Touch
onPress={onPress}
style={{ backgroundColor: themes[theme].backgroundColor }}
style={({ pressed }) => [{
backgroundColor: pressed ? themes[theme].bannerBackground : themes[theme].backgroundColor
}]}
testID={testID}
theme={theme}
>
@ -172,6 +180,12 @@ class NewMessageView extends React.Component {
testID: 'new-message-view-create-channel',
first: true
})}
{this.renderButton({
onPress: this.createTeam,
title: I18n.t('Create_Team'),
icon: 'team',
testID: 'new-message-view-create-team'
})}
{maxUsers > 2 ? this.renderButton({
onPress: this.createGroupChat,
title: I18n.t('Create_Direct_Messages'),
@ -253,7 +267,7 @@ const mapStateToProps = state => ({
});
const mapDispatchToProps = dispatch => ({
createChannel: params => dispatch(createChannelRequest(params))
create: params => dispatch(createChannelRequest(params))
});
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(NewMessageView));

View File

@ -309,7 +309,7 @@ class ProfileView extends React.Component {
key={key}
testID={key}
onPress={onPress}
style={[styles.avatarButton, { opacity: disabled ? 0.5 : 1 }, { backgroundColor: themes[theme].borderColor }]}
style={({ pressed }) => [styles.avatarButton, { opacity: disabled ? 0.5 : 1 }, { backgroundColor: pressed ? themes[theme].borderColor : themes[theme].bannerBackground }]}
enabled={!disabled}
theme={theme}
>

View File

@ -5,6 +5,7 @@ import {
} from 'react-native';
import { connect } from 'react-redux';
import isEmpty from 'lodash/isEmpty';
import { Q } from '@nozbe/watermelondb';
import { compareServerVersion, methods } from '../../lib/utils';
import Touch from '../../utils/touch';
@ -60,7 +61,8 @@ class RoomActionsView extends React.Component {
editRoomPermission: PropTypes.array,
toggleRoomE2EEncryptionPermission: PropTypes.array,
viewBroadcastMemberListPermission: PropTypes.array,
transferLivechatGuestPermission: PropTypes.array
transferLivechatGuestPermission: PropTypes.array,
addTeamMemberPermission: PropTypes.array
}
constructor(props) {
@ -170,12 +172,19 @@ class RoomActionsView extends React.Component {
canAddUser = async() => {
const { room, joined } = this.state;
const { addUserToJoinedRoomPermission, addUserToAnyCRoomPermission, addUserToAnyPRoomPermission } = this.props;
const {
addUserToJoinedRoomPermission, addUserToAnyCRoomPermission, addUserToAnyPRoomPermission, addTeamMemberPermission
} = this.props;
const { rid, t } = room;
let canAddUser = false;
let permissions;
const userInRoom = joined;
const permissions = await RocketChat.hasPermission([addUserToJoinedRoomPermission, addUserToAnyCRoomPermission, addUserToAnyPRoomPermission], rid);
if (room.teamMain) {
permissions = await RocketChat.hasPermission([addTeamMemberPermission], rid);
} else {
permissions = await RocketChat.hasPermission([addUserToJoinedRoomPermission, addUserToAnyCRoomPermission, addUserToAnyPRoomPermission], rid);
}
if (userInRoom && permissions[0]) {
canAddUser = true;
@ -320,6 +329,31 @@ class RoomActionsView extends React.Component {
setLoadingInvite(true);
await RocketChat.addUsersToRoom(rid);
navigation.pop();
} catch (e) {
log(e);
Alert.alert(
I18n.t('Confirmation'),
I18n.t('Removing_user_from_this_Team'),
[
{
text: I18n.t('OK'),
style: 'cancel'
}
],
{ cancelable: false }
);
} finally {
setLoadingInvite(false);
}
}
addMemberToTeam = async() => {
const { room } = this.state;
const { setLoadingInvite, navigation } = this.props;
try {
setLoadingInvite(true);
await RocketChat.addTeamMember(room.name);
navigation.pop();
} catch (e) {
log(e);
} finally {
@ -412,6 +446,56 @@ class RoomActionsView extends React.Component {
);
}
_leave = async(teamName) => {
try {
const { navigation } = this.props;
const result = await RocketChat.leaveTeam({ teamName });
// Add isMasterDetail
if (result.success) {
navigation.navigate('RoomsListView');
}
} catch (e) {
log(e);
}
}
leaveTeam = async() => {
const { room } = this.state;
const { navigation } = this.props;
try {
const db = database.active;
const subCollection = db.get('subscriptions');
const teamChannels = await subCollection.query(
Q.where('team_id', Q.eq(room.teamId))
);
if (teamChannels) {
navigation.navigate('SelectListView', {
title: 'Leave_Team', room, teamChannels, teamName: room.name, subtitle: 'Select_Teams'
});
} else {
Alert.alert(
I18n.t('Confirmation'),
I18n.t('You_are_leaving_the_team', { team: RocketChat.getRoomTitle(room) }),
[
{
text: I18n.t('Cancel'),
style: 'cancel'
},
{
text: I18n.t('Yes_action_it', { action: I18n.t('leave') }),
style: 'destructive',
onPress: () => this._leave(room.name)
}
]
);
}
} catch (e) {
log(e);
}
}
renderRoomInfo = () => {
const { room, member } = this.state;
const {
@ -433,7 +517,7 @@ class RoomActionsView extends React.Component {
rid, t, room, member
}
})}
style={{ backgroundColor: themes[theme].backgroundColor }}
style={({ pressed }) => [{ backgroundColor: pressed ? themes[theme].bannerBackground : themes[theme].backgroundColor }]}
accessibilityLabel={I18n.t('Room_Info')}
accessibilityTraits='button'
enabled={!isGroupChat}
@ -568,9 +652,9 @@ class RoomActionsView extends React.Component {
<List.Section>
<List.Separator />
<List.Item
title='Leave_channel'
title={room.teamId && room.teamMain ? 'Leave' : 'Leave_channel'}
onPress={() => this.onPressTouchable({
event: this.leaveChannel
event: room.teamId && room.teamMain ? this.leaveTeam : this.leaveChannel
})}
testID='room-actions-leave-channel'
left={() => <List.Icon name='logout' color={themes[theme].dangerColor} />}
@ -629,7 +713,7 @@ class RoomActionsView extends React.Component {
params: {
rid,
title: I18n.t('Add_users'),
nextAction: this.addUser
nextAction: room.teamId ? this.addMemberToTeam : this.addUser
}
})}
testID='room-actions-add-user'
@ -881,6 +965,7 @@ const mapStateToProps = state => ({
encryptionEnabled: state.encryption.enabled,
serverVersion: state.server.version,
addUserToJoinedRoomPermission: state.permissions['add-user-to-joined-room'],
addTeamMemberPermission: state.permissions['add-team-member'],
addUserToAnyCRoomPermission: state.permissions['add-user-to-any-c-room'],
addUserToAnyPRoomPermission: state.permissions['add-user-to-any-p-room'],
createInviteLinksPermission: state.permissions['create-invite-links'],

View File

@ -8,6 +8,7 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit';
import ImagePicker from 'react-native-image-crop-picker';
import { dequal } from 'dequal';
import isEmpty from 'lodash/isEmpty';
import { Q } from '@nozbe/watermelondb';
import { compareServerVersion, methods } from '../../lib/utils';
import database from '../../lib/database';
@ -41,6 +42,7 @@ const PERMISSION_ARCHIVE = 'archive-room';
const PERMISSION_UNARCHIVE = 'unarchive-room';
const PERMISSION_DELETE_C = 'delete-c';
const PERMISSION_DELETE_P = 'delete-p';
const PERMISSION_EDIT_TEAM_CHANNEL = 'edit-team-channel';
class RoomInfoEditView extends React.Component {
static navigationOptions = () => ({
@ -48,6 +50,7 @@ class RoomInfoEditView extends React.Component {
})
static propTypes = {
navigation: PropTypes.object,
route: PropTypes.object,
deleteRoom: PropTypes.func,
serverVersion: PropTypes.string,
@ -58,7 +61,8 @@ class RoomInfoEditView extends React.Component {
archiveRoomPermission: PropTypes.array,
unarchiveRoomPermission: PropTypes.array,
deleteCPermission: PropTypes.array,
deletePPermission: PropTypes.array
deletePPermission: PropTypes.array,
editTeamChannelPermission: PropTypes.array
};
constructor(props) {
@ -100,7 +104,8 @@ class RoomInfoEditView extends React.Component {
archiveRoomPermission,
unarchiveRoomPermission,
deleteCPermission,
deletePPermission
deletePPermission,
editTeamChannelPermission
} = this.props;
const rid = route.params?.rid;
if (!rid) {
@ -116,25 +121,51 @@ class RoomInfoEditView extends React.Component {
this.init(this.room);
});
const result = await RocketChat.hasPermission([
setReadOnlyPermission,
setReactWhenReadOnlyPermission,
archiveRoomPermission,
unarchiveRoomPermission,
deleteCPermission,
deletePPermission
], rid);
let result;
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]
}
});
if (this.room.teamId) {
result = await RocketChat.hasPermission([
setReadOnlyPermission,
setReactWhenReadOnlyPermission,
archiveRoomPermission,
unarchiveRoomPermission,
deleteCPermission,
deletePPermission,
editTeamChannelPermission
], 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],
[PERMISSION_EDIT_TEAM_CHANNEL]: result[6]
}
});
} else {
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);
}
@ -284,13 +315,37 @@ class RoomInfoEditView extends React.Component {
}, 100);
}
deleteTeam = async(teamName) => {
const { navigation } = this.props;
const { room } = this.state;
try {
const result = await RocketChat.deleteTeam({ teamName });
if (result.success) {
const db = database.active;
const subCollection = db.get('subscriptions');
const teamChannels = await subCollection.query(
Q.and(Q.where('team_id', Q.eq(this.room.teamId), Q.where('name', Q.notEq(this.room.name))))
);
if (teamChannels.length) {
navigation.navigate('SelectListView', {
title: 'Delete_Team', teamChannels: this.teamChannels, teamName: room.name, subtitle: 'Select_Teams', delete: this.delete
});
} else {
navigation.navigate('RoomsListView');
}
}
} catch (e) {
log(e);
}
}
delete = () => {
const { room } = this.state;
const { deleteRoom } = this.props;
Alert.alert(
I18n.t('Are_you_sure_question_mark'),
I18n.t('Delete_Room_Warning'),
I18n.t('Confirmation'),
I18n.t('Delete_Team_Warning'),
[
{
text: I18n.t('Cancel'),
@ -299,7 +354,7 @@ class RoomInfoEditView extends React.Component {
{
text: I18n.t('Yes_action_it', { action: I18n.t('delete') }),
style: 'destructive',
onPress: () => deleteRoom(room.rid, room.t)
onPress: () => (this.room.teamId ? this.deleteTeam(room.name) : deleteRoom(room.rid, room.t))
}
],
{ cancelable: false }
@ -678,7 +733,8 @@ const mapStateToProps = state => ({
archiveRoomPermission: state.permissions[PERMISSION_ARCHIVE],
unarchiveRoomPermission: state.permissions[PERMISSION_UNARCHIVE],
deleteCPermission: state.permissions[PERMISSION_DELETE_C],
deletePPermission: state.permissions[PERMISSION_DELETE_P]
deletePPermission: state.permissions[PERMISSION_DELETE_P],
editTeamChannelPermission: state.permissions[PERMISSION_EDIT_TEAM_CHANNEL]
});
const mapDispatchToProps = dispatch => ({

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FlatList } from 'react-native';
import { FlatList, Alert } from 'react-native';
import { connect } from 'react-redux';
import { Q } from '@nozbe/watermelondb';
import * as List from '../../containers/List';
@ -34,6 +34,7 @@ const PERMISSION_SET_LEADER = 'set-leader';
const PERMISSION_SET_OWNER = 'set-owner';
const PERMISSION_SET_MODERATOR = 'set-moderator';
const PERMISSION_REMOVE_USER = 'remove-user';
const PERMISSION_EDIT_TEAM_MEMBER = 'edit-team-member';
class RoomMembersView extends React.Component {
static propTypes = {
@ -55,7 +56,8 @@ class RoomMembersView extends React.Component {
setLeaderPermission: PropTypes.array,
setOwnerPermission: PropTypes.array,
setModeratorPermission: PropTypes.array,
removeUserPermission: PropTypes.array
removeUserPermission: PropTypes.array,
editTeamMemberPermission: PropTypes.array
}
constructor(props) {
@ -94,18 +96,44 @@ class RoomMembersView extends React.Component {
const { room } = this.state;
const {
muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission
muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission, editTeamMemberPermission
} = this.props;
const result = await RocketChat.hasPermission([
muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission
], room.rid);
let result;
if (room.teamId) {
result = await RocketChat.hasPermission([
muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission, editTeamMemberPermission
], 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],
[PERMISSION_EDIT_TEAM_MEMBER]: result[5]
};
} else {
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]
};
}
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]
[PERMISSION_REMOVE_USER]: result[4],
[PERMISSION_EDIT_TEAM_MEMBER]: result[5]
};
const hasSinglePermission = Object.values(this.permissions).some(p => !!p);
@ -163,6 +191,56 @@ class RoomMembersView extends React.Component {
}
}
handleRemoveFromTeam = async(selectedUser) => {
const { navigation } = this.props;
const { room } = this.state;
const db = database.active;
const subCollection = db.get('subscriptions');
const teamChannels = await subCollection.query(
Q.where('team_id', Q.eq(room.teamId))
);
if (teamChannels) {
navigation.navigate('SelectListView', {
title: 'Remove_Member', subtitle: 'Remove_User_Teams', teamChannels, selectedUser
});
} else {
Alert.alert(
I18n.t('Confirmation'),
I18n.t('Removing_user_from_this_Team', { user: selectedUser.username }),
[
{
text: I18n.t('Cancel'),
style: 'cancel'
},
{
text: I18n.t('Yes_action_it', { action: I18n.t('remove') }),
style: 'destructive',
onPress: () => this.removeFromTeam(selectedUser)
}
],
{ cancelable: false }
);
}
}
removeFromTeam = async(selectedUser) => {
try {
const { members, membersFiltered, room } = this.state;
const userId = selectedUser._id;
const result = await RocketChat.removeTeamMember({ teamName: room.name, userId });
if (result.success) {
const message = I18n.t('User_has_been_removed_from_s', { s: RocketChat.getRoomTitle(room) });
EventEmitter.emit(LISTENER, { message });
this.setState({
members: members.filter(member => member._id !== userId),
membersFiltered: membersFiltered.filter(member => member._id !== userId)
});
}
} catch (e) {
log(e);
}
}
onPressUser = (selectedUser) => {
const { room } = this.state;
const { showActionSheet, user } = this.props;
@ -173,6 +251,46 @@ class RoomMembersView extends React.Component {
onPress: () => this.navToDirectMessage(selectedUser)
}];
// Ignore
if (selectedUser._id !== user.id) {
const { ignored } = room;
const isIgnored = ignored?.includes?.(selectedUser._id);
options.push({
icon: 'ignore',
title: I18n.t(isIgnored ? 'Unignore' : 'Ignore'),
onPress: () => this.handleIgnore(selectedUser, !isIgnored)
});
}
if (this.permissions['mute-user']) {
const { muted = [] } = room;
const userIsMuted = muted.find?.(m => m === selectedUser.username);
selectedUser.muted = !!userIsMuted;
options.push({
icon: userIsMuted ? 'audio' : 'audio-disabled',
title: I18n.t(userIsMuted ? 'Unmute' : 'Mute'),
onPress: () => {
showConfirmationAlert({
message: I18n.t(`The_user_${ userIsMuted ? 'will' : 'wont' }_be_able_to_type_in_roomName`, {
roomName: RocketChat.getRoomTitle(room)
}),
confirmationText: I18n.t(userIsMuted ? 'Unmute' : 'Mute'),
onPress: () => this.handleMute(selectedUser)
});
}
});
}
// Remove from team
if (this.permissions['edit-team-member']) {
options.push({
icon: 'close',
danger: true,
title: I18n.t('Remove_from_Team'),
onPress: () => this.handleRemoveFromTeam(selectedUser)
});
}
// Owner
if (this.permissions['set-owner']) {
const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id);
@ -206,36 +324,6 @@ class RoomMembersView extends React.Component {
});
}
// Ignore
if (selectedUser._id !== user.id) {
const { ignored } = room;
const isIgnored = ignored?.includes?.(selectedUser._id);
options.push({
icon: 'ignore',
title: I18n.t(isIgnored ? 'Unignore' : 'Ignore'),
onPress: () => this.handleIgnore(selectedUser, !isIgnored)
});
}
if (this.permissions['mute-user']) {
const { muted = [] } = room;
const userIsMuted = muted.find?.(m => m === selectedUser.username);
selectedUser.muted = !!userIsMuted;
options.push({
icon: userIsMuted ? 'audio' : 'audio-disabled',
title: I18n.t(userIsMuted ? 'Unmute' : 'Mute'),
onPress: () => {
showConfirmationAlert({
message: I18n.t(`The_user_${ userIsMuted ? 'will' : 'wont' }_be_able_to_type_in_roomName`, {
roomName: RocketChat.getRoomTitle(room)
}),
confirmationText: I18n.t(userIsMuted ? 'Unmute' : 'Mute'),
onPress: () => this.handleMute(selectedUser)
});
}
});
}
// Remove from room
if (this.permissions['remove-user']) {
options.push({
@ -292,6 +380,7 @@ class RoomMembersView extends React.Component {
this.setState({ isLoading: true });
try {
const membersResult = await RocketChat.getRoomMembers(rid, allUsers, members.length, PAGE_SIZE);
console.log({ membersResult });
const newMembers = membersResult.records;
this.setState({
members: members.concat(newMembers || []),
@ -477,7 +566,8 @@ const mapStateToProps = state => ({
setLeaderPermission: state.permissions[PERMISSION_SET_LEADER],
setOwnerPermission: state.permissions[PERMISSION_SET_OWNER],
setModeratorPermission: state.permissions[PERMISSION_SET_MODERATOR],
removeUserPermission: state.permissions[PERMISSION_REMOVE_USER]
removeUserPermission: state.permissions[PERMISSION_REMOVE_USER],
editTeamMemberPermission: state.permissions['edit-team-member']
});
export default connect(mapStateToProps)(withActionSheet(withTheme(RoomMembersView)));

View File

@ -18,7 +18,8 @@ class RightButtonsContainer extends Component {
teamId: PropTypes.bool,
navigation: PropTypes.object,
isMasterDetail: PropTypes.bool,
toggleFollowThread: PropTypes.func
toggleFollowThread: PropTypes.func,
joined: PropTypes.bool
};
constructor(props) {
@ -113,7 +114,7 @@ class RightButtonsContainer extends Component {
goTeamChannels = () => {
logEvent(events.ROOM_GO_TEAM_CHANNELS);
const {
navigation, isMasterDetail, teamId
navigation, isMasterDetail, teamId, rid
} = this.props;
if (isMasterDetail) {
navigation.navigate('ModalStackNavigator', {
@ -121,7 +122,7 @@ class RightButtonsContainer extends Component {
params: { teamId }
});
} else {
navigation.navigate('TeamChannelsView', { teamId });
navigation.navigate('TeamChannelsView', { teamId, rid });
}
}
@ -163,7 +164,7 @@ class RightButtonsContainer extends Component {
isFollowingThread, tunread, tunreadUser, tunreadGroup
} = this.state;
const {
t, tmid, threadsEnabled, teamId
t, tmid, threadsEnabled, teamId, joined
} = this.props;
if (t === 'l') {
return null;
@ -181,7 +182,7 @@ class RightButtonsContainer extends Component {
}
return (
<HeaderButton.Container>
{teamId ? (
{teamId && joined ? (
<HeaderButton.Item
iconName='channel-public'
onPress={this.goTeamChannels}

View File

@ -301,7 +301,7 @@ class RoomView extends React.Component {
setHeader = () => {
const {
room, unreadsCount, roomUserId
room, unreadsCount, roomUserId, joined
} = this.state;
const {
navigation, isMasterDetail, theme, baseUrl, user, insets, route
@ -331,7 +331,7 @@ class RoomView extends React.Component {
let numIconsRight = 2;
if (tmid) {
numIconsRight = 1;
} else if (teamId) {
} else if (teamId && joined) {
numIconsRight = 3;
}
const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight });
@ -380,6 +380,7 @@ class RoomView extends React.Component {
rid={rid}
tmid={tmid}
teamId={teamId}
joined={joined}
t={t}
navigation={navigation}
toggleFollowThread={this.toggleFollowThread}
@ -1032,6 +1033,7 @@ class RoomView extends React.Component {
renderActions = () => {
const { room, readOnly } = this.state;
const { user } = this.props;
return (
<>
<MessageActions

View File

@ -894,7 +894,6 @@ class RoomsListView extends React.Component {
return this.renderSectionHeader(item.rid);
}
const { item: currentItem } = this.state;
const {
user: { username },
StoreLastMessage,
@ -925,7 +924,6 @@ class RoomsListView extends React.Component {
getIsGroupChat={this.isGroupChat}
getIsRead={this.isRead}
visitor={item.visitor}
isFocused={currentItem?.rid === item.rid}
/>
);
};
@ -948,7 +946,6 @@ class RoomsListView extends React.Component {
if (loading) {
return <ActivityIndicator theme={theme} />;
}
return (
<FlatList
ref={this.getScrollRef}

299
app/views/SelectListView.js Normal file
View File

@ -0,0 +1,299 @@
/* eslint-disable no-mixed-spaces-and-tabs */
import React from 'react';
import PropTypes from 'prop-types';
import {
View, StyleSheet, FlatList, Text, Alert, Pressable
} from 'react-native';
import { connect } from 'react-redux';
import { HeaderBackButton } from '@react-navigation/stack';
import * as List from '../containers/List';
import Touch from '../utils/touch';
import { leaveRoom as leaveRoomAction } from '../actions/room';
import RocketChat from '../lib/rocketchat';
import sharedStyles from './Styles';
import I18n from '../i18n';
import { CustomIcon } from '../lib/Icons';
import * as HeaderButton from '../containers/HeaderButton';
import StatusBar from '../containers/StatusBar';
import { themes } from '../constants/colors';
import { withTheme } from '../theme';
import SafeAreaView from '../containers/SafeAreaView';
import { animateNextTransition } from '../utils/layoutAnimation';
import Loading from '../containers/Loading';
import { LISTENER } from '../containers/Toast';
import EventEmitter from '../utils/events';
import log from '../utils/log';
const styles = StyleSheet.create({
button: {
height: 46,
flexDirection: 'row',
alignItems: 'center'
},
buttonIcon: {
marginLeft: 18,
marginRight: 16
},
buttonText: {
fontSize: 17,
...sharedStyles.textRegular
},
textContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginRight: 15
},
icon: {
marginHorizontal: 15,
alignSelf: 'center'
}
});
class SelectListView extends React.Component {
static propTypes = {
navigation: PropTypes.object,
route: PropTypes.object,
user: PropTypes.shape({
id: PropTypes.string,
token: PropTypes.string
}),
theme: PropTypes.string,
isMasterDetail: PropTypes.bool,
leaveRoom: PropTypes.func
};
constructor(props) {
super(props);
const teamChannels = props.route?.params?.teamChannels;
this.title = props.route?.params?.title;
this.subtitle = props.route?.params?.subtitle;
this.teamName = props.route?.params?.teamName;
this.room = props.route?.params?.room;
this.delete = props.route?.params?.delete;
this.state = {
data: teamChannels,
selected: [],
loading: false
};
this.setHeader();
}
setHeader = () => {
const { navigation, isMasterDetail, theme } = this.props;
const options = {
headerShown: true,
headerTitleAlign: 'center',
headerTitle: I18n.t(this.title)
};
if (isMasterDetail) {
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
} else {
options.headerLeft = () => <HeaderBackButton labelVisible={false} onPress={() => navigation.pop()} tintColor={themes[theme].headerTintColor} />;
}
options.headerRight = () => (
<HeaderButton.Container>
<HeaderButton.Item title={I18n.t('Next')} onPress={this.delete ? this.delete : this.submit} testID='select-list-view-submit' />
</HeaderButton.Container>
);
navigation.setOptions(options);
}
submit = async() => {
const { selected } = this.state;
const { navigation, leaveRoom } = this.props;
this.setState({ loading: true });
try {
// logEvent(events.CT_ADD_ROOM_TO_TEAM);
const result = await RocketChat.leaveTeam({ teamName: this.teamName });
if (selected) {
selected.map(room => leaveRoom(room.rid, room.t));
}
if (result.success) {
this.setState({ loading: false });
navigation.navigate('RoomsListView');
}
} catch (e) {
// logEvent(events.CT_ADD_ROOM_TO_TEAM_F);
this.setState({ loading: false });
Alert.alert(
I18n.t('Cannot_leave'),
I18n.t(e.data.error),
[
{
text: 'OK',
style: 'cancel'
}
]
);
}
}
renderHeader = () => {
const { theme } = this.props;
return (
<View style={{ backgroundColor: themes[theme].backgroundColor }}>
<Text style={[styles.buttonText, { color: themes[theme].bodyText, margin: 16 }]}>{I18n.t('Select_Teams')}</Text>
</View>
);
}
showAlert = () => {
Alert.alert(
I18n.t('Cannot_leave'),
I18n.t('Last_owner_team_room'),
[
{
text: 'OK',
style: 'cancel'
}
]
);
}
leaveChannel = () => {
const { room } = this.state;
const { leaveRoom } = this.props;
Alert.alert(
I18n.t('Are_you_sure_question_mark'),
I18n.t('Are_you_sure_you_want_to_leave_the_room', { room: RocketChat.getRoomTitle(room) }),
[
{
text: I18n.t('Cancel'),
style: 'cancel'
},
{
text: I18n.t('Yes_action_it', { action: I18n.t('leave') }),
style: 'destructive',
onPress: () => leaveRoom(room.rid, room.t)
}
]
);
}
removeFromTeam = async(selectedUser) => {
try {
const { data, room } = this.state;
const userId = selectedUser._id;
const result = await RocketChat.removeTeamMember({ teamName: room.name, userId });
if (result.success) {
const message = I18n.t('User_has_been_removed_from_s', { s: RocketChat.getRoomTitle(room) });
EventEmitter.emit(LISTENER, { message });
this.setState({ data: data.filter(member => member._id !== userId) });
}
} catch (e) {
log(e);
}
}
renderChannel = ({
onPress, testID, title, icon, checked, alert
}) => {
const { theme } = this.props;
return (
<Touch
onPress={onPress}
style={{ backgroundColor: themes[theme].backgroundColor }}
testID={testID}
theme={theme}
>
<View style={[styles.button, { borderColor: themes[theme].separatorColor, marginVertical: 4 }]}>
<CustomIcon style={[styles.buttonIcon, { color: themes[theme].controlText }]} size={24} name={icon} />
<View style={styles.textContainer}>
<Text style={[styles.buttonText, { color: themes[theme].bodyText }]}>{title}</Text>
{ alert
? (
<Pressable onPress={() => this.showAlert()}>
<CustomIcon style={[styles.buttonIcon, { color: themes[theme].dangerColor, transform: [{ rotateY: '180deg' }] }]} size={24} name={alert} />
</Pressable>
) : null}
</View>
{checked ? <CustomIcon name={checked} size={22} style={[styles.icon, { color: themes[theme].actionTintColor }]} /> : null}
</View>
</Touch>
);
}
isChecked = (rid) => {
const { selected } = this.state;
return selected.includes(rid);
}
toggleChannel = (rid, roles) => {
const { selected } = this.state;
if (roles) {
this.showAlert();
return;
}
animateNextTransition();
if (!this.isChecked(rid)) {
this.setState({ selected: [...selected, rid] }, () => this.setHeader());
} else {
const filterSelected = selected.filter(el => el !== rid);
this.setState({ selected: filterSelected }, () => this.setHeader());
}
}
renderItem = ({ item }) => (
<>
{this.renderChannel({
onPress: () => this.toggleChannel(item.rid, item.roles),
title: item.name,
icon: item.t === 'p' ? 'channel-private' : 'channel-public',
checked: this.isChecked(item.rid, item.roles) ? 'check' : null,
testID: 'select-list-view-item',
alert: item.roles ? 'info' : null
})}
</>
)
renderList = () => {
const { data } = this.state;
const { theme } = this.props;
return (
<FlatList
data={data}
extraData={this.state}
keyExtractor={item => item._id}
renderItem={this.renderItem}
ListHeaderComponent={this.renderHeader}
ItemSeparatorComponent={List.Separator}
contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }}
keyboardShouldPersistTaps='always'
/>
);
}
render() {
const { loading } = this.state;
return (
<SafeAreaView testID='new-message-view'>
<StatusBar />
{this.renderList()}
<Loading visible={loading} />
</SafeAreaView>
);
}
}
const mapStateToProps = state => ({
isMasterDetail: state.app.isMasterDetail
});
const mapDispatchToProps = dispatch => ({
leaveRoom: (rid, t) => dispatch(leaveRoomAction(rid, t))
});
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(SelectListView));

View File

@ -50,7 +50,7 @@ class SelectedUsersView extends React.Component {
constructor(props) {
super(props);
this.init();
this.flatlist = React.createRef();
const maxUsers = props.route.params?.maxUsers;
this.state = {
maxUsers,
@ -190,9 +190,13 @@ class SelectedUsersView extends React.Component {
if (users.length === 0) {
return null;
}
const ITEM_WIDTH = 250;
return (
<FlatList
data={users}
ref={ref => this.flatlist = ref}
onContentSizeChange={() => this.flatlist.scrollToEnd()}
getItemLayout={(_, index) => ({ length: ITEM_WIDTH, offset: ITEM_WIDTH * index, index })}
keyExtractor={item => item._id}
style={[sharedStyles.separatorTop, { borderColor: themes[theme].separatorColor }]}
contentContainerStyle={{ marginVertical: 5 }}

View File

@ -15,7 +15,7 @@ const Item = React.memo(({
testID={testID}
onPress={onPress}
theme={theme}
style={[styles.item, current && { backgroundColor: themes[theme].borderColor }]}
style={({ pressed }) => [styles.item, current && { backgroundColor: pressed ? themes[theme].borderColor : themes[theme].bannerBackground }]}
>
<View style={styles.itemHorizontal}>
{left}

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Keyboard } from 'react-native';
import { Keyboard, Alert } from 'react-native';
import PropTypes from 'prop-types';
import { Q } from '@nozbe/watermelondb';
import { withSafeAreaInsets } from 'react-native-safe-area-context';
@ -28,6 +28,8 @@ import debounce from '../utils/debounce';
import { showErrorAlert } from '../utils/info';
import { goRoom } from '../utils/goRoom';
import I18n from '../i18n';
import { withActionSheet } from '../containers/ActionSheet';
import { deleteRoom as deleteRoomAction } from '../actions/room';
const API_FETCH_COUNT = 25;
@ -47,12 +49,17 @@ class TeamChannelsView extends React.Component {
theme: PropTypes.string,
useRealName: PropTypes.bool,
width: PropTypes.number,
StoreLastMessage: PropTypes.bool
StoreLastMessage: PropTypes.bool,
addTeamChannelPermission: PropTypes.array,
removeTeamChannelPermission: PropTypes.array,
showActionSheet: PropTypes.func,
deleteRoom: PropTypes.func
}
constructor(props) {
super(props);
this.teamId = props.route.params?.teamId;
this.rid = props.route.params?.rid;
this.state = {
loading: true,
loadingMore: false,
@ -60,9 +67,11 @@ class TeamChannelsView extends React.Component {
isSearching: false,
searchText: '',
search: [],
end: false
end: false,
showCreate: false
};
this.loadTeam();
this.setHeader();
}
componentDidMount() {
@ -70,6 +79,9 @@ class TeamChannelsView extends React.Component {
}
loadTeam = async() => {
const { addTeamChannelPermission } = this.props;
const { loading } = this.state;
const db = database.active;
try {
const subCollection = db.get('subscriptions');
@ -82,6 +94,14 @@ class TeamChannelsView extends React.Component {
if (!this.team) {
throw new Error();
}
const permissions = await RocketChat.hasPermission([addTeamChannelPermission], this.team.rid);
if (permissions[0]) {
this.setState({ showCreate: true }, () => this.setHeader());
}
if (loading) {
this.setState({ loading: false });
}
} catch {
const { navigation } = this.props;
navigation.pop();
@ -135,8 +155,8 @@ class TeamChannelsView extends React.Component {
}
}, 300)
getHeader = () => {
const { isSearching } = this.state;
setHeader = () => {
const { isSearching, showCreate, data } = this.state;
const {
navigation, isMasterDetail, insets, theme
} = this.props;
@ -146,7 +166,7 @@ class TeamChannelsView extends React.Component {
return;
}
const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 1 });
const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 2 });
if (isSearching) {
return {
@ -201,14 +221,11 @@ class TeamChannelsView extends React.Component {
options.headerRight = () => (
<HeaderButton.Container>
<HeaderButton.Item iconName='search' onPress={this.onSearchPress} />
{ showCreate
? <HeaderButton.Item iconName='create' onPress={() => navigation.navigate('AddChannelTeamView', { teamId: this.teamId, teamChannels: data })} />
: null}
</HeaderButton.Container>
);
return options;
}
setHeader = () => {
const { navigation } = this.props;
const options = this.getHeader();
navigation.setOptions(options);
}
@ -287,6 +304,93 @@ class TeamChannelsView extends React.Component {
}
}, 1000, true);
options = item => ([
{
title: I18n.t('Auto-join'),
icon: item.t === 'p' ? 'channel-private' : 'channel-public'
// onPress: this.autoJoin
},
{
title: I18n.t('Remove_from_Team'),
icon: 'close',
danger: true,
onPress: () => this.remove(item)
},
{
title: I18n.t('Delete'),
icon: 'delete',
danger: true,
onPress: () => this.delete(item)
}
])
remove = (item) => {
Alert.alert(
I18n.t('Confirmation'),
I18n.t('Delete_Team_Room_Warning'),
[
{
text: I18n.t('Cancel'),
style: 'cancel'
},
{
text: I18n.t('Yes_action_it', { action: I18n.t('remove') }),
style: 'destructive',
onPress: () => this.removeRoom(item)
}
],
{ cancelable: false }
);
}
removeRoom = async(item) => {
try {
const { data } = this.state;
const result = await RocketChat.removeTeamRoom({ roomId: item.rid, teamId: this.team.teamId });
if (result.success) {
const newData = data.filter(room => result.room._id !== room.rid);
this.setState({ loading: true, data: newData }, () => {
this.load();
this.loadTeam();
});
}
} catch (e) {
log(e);
}
}
delete = (item) => {
const { deleteRoom } = this.props;
Alert.alert(
I18n.t('Are_you_sure_question_mark'),
I18n.t('Delete_Room_Warning'),
[
{
text: I18n.t('Cancel'),
style: 'cancel'
},
{
text: I18n.t('Yes_action_it', { action: I18n.t('delete') }),
style: 'destructive',
onPress: () => deleteRoom(item.rid, item.t)
}
],
{ cancelable: false }
);
}
showChannelActions = async(item) => {
logEvent(events.ROOM_SHOW_BOX_ACTIONS);
const { showActionSheet, removeTeamChannelPermission } = this.props;
const permissions = await RocketChat.hasPermission([removeTeamChannelPermission], this.team.rid);
if (!permissions[0]) {
return;
}
showActionSheet({ options: this.options(item) });
}
renderItem = ({ item }) => {
const {
StoreLastMessage,
@ -302,6 +406,7 @@ class TeamChannelsView extends React.Component {
showLastMessage={StoreLastMessage}
onPress={this.onPressItem}
width={width}
onLongPress={this.showChannelActions}
useRealName={useRealName}
getRoomTitle={this.getRoomTitle}
getRoomAvatar={this.getRoomAvatar}
@ -365,7 +470,13 @@ const mapStateToProps = state => ({
user: getUserSelector(state),
useRealName: state.settings.UI_Use_Real_Name,
isMasterDetail: state.app.isMasterDetail,
StoreLastMessage: state.settings.Store_Last_Message
StoreLastMessage: state.settings.Store_Last_Message,
addTeamChannelPermission: state.permissions['add-team-channel'],
removeTeamChannelPermission: state.permissions['remove-team-channel']
});
export default connect(mapStateToProps)(withDimensions(withSafeAreaInsets(withTheme(TeamChannelsView))));
const mapDispatchToProps = dispatch => ({
deleteRoom: (rid, t) => dispatch(deleteRoomAction(rid, t))
});
export default connect(mapStateToProps, mapDispatchToProps)(withDimensions(withSafeAreaInsets(withTheme(withActionSheet(TeamChannelsView)))));

View File

@ -26,7 +26,7 @@ const styles = StyleSheet.create({
const DropdownItem = React.memo(({
theme, onPress, iconName, text
}) => (
<Touch theme={theme} onPress={onPress} style={{ backgroundColor: themes[theme].backgroundColor }}>
<Touch theme={theme} onPress={onPress} style={({ pressed }) => [{ backgroundColor: pressed ? themes[theme].bannerBackground : themes[theme].backgroundColor }]}>
<View style={styles.container}>
<Text style={[styles.text, { color: themes[theme].auxiliaryText }]}>{text}</Text>
{iconName ? <CustomIcon name={iconName} size={22} color={themes[theme].auxiliaryText} /> : null}

View File

@ -42,6 +42,11 @@ const data = {
name: `detox-private-${ value }`
}
},
teams: {
private: {
name: `detox-team-${ value }`
}
},
registeringUser: {
username: `newuser${ value }`,
password: `password${ value }`,

View File

@ -57,6 +57,26 @@ const createChannelIfNotExists = async (channelname) => {
}
}
const createTeamIfNotExists = async (teamname) => {
console.log(`Creating private team ${teamname}`)
try {
const team = await rocketchat.post('teams.create', {
"name": teamname,
"type": 1
})
return team
} catch (createError) {
try { //Maybe it exists already?
const team = rocketchat.get(`teams.info?teamName=${teamname}`)
return team
} catch (infoError) {
console.log(JSON.stringify(createError))
console.log(JSON.stringify(infoError))
throw "Failed to find or create private team"
}
}
}
const createGroupIfNotExists = async (groupname) => {
console.log(`Creating private group ${groupname}`)
try {
@ -133,6 +153,13 @@ const setup = async () => {
}
}
for (var teamKey in data.teams) {
if (data.teams.hasOwnProperty(teamKey)) {
const team = data.teams[teamKey]
await createTeamIfNotExists(team.name)
}
}
return
}

View File

@ -0,0 +1,82 @@
const {
device, expect, element, by, waitFor
} = require('detox');
const data = require('../../data');
const { tapBack, sleep, navigateToLogin, login, tryTapping } = require('../../helpers/app');
describe('Create team screen', () => {
before(async() => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password);
});
describe('New Message', async() => {
before(async() => {
await element(by.id('rooms-list-view-create-channel')).tap();
});
describe('Render', async() => {
it('should have team button', async() => {
await waitFor(element(by.id('new-message-view-create-team'))).toBeVisible().withTimeout(2000);
});
})
describe('Usage', async() => {
it('should navigate to select users', async() => {
await element(by.id('new-message-view-create-team')).tap();
await waitFor(element(by.id('select-users-view'))).toExist().withTimeout(5000);
});
})
});
describe('Select Users', async() => {
it('should search users', async() => {
await element(by.id('select-users-view-search')).replaceText('rocket.cat');
await waitFor(element(by.id(`select-users-view-item-rocket.cat`))).toBeVisible().withTimeout(10000);
});
it('should select/unselect user', async() => {
// Spotlight issues
await element(by.id('select-users-view-item-rocket.cat')).tap();
await waitFor(element(by.id('selected-user-rocket.cat'))).toBeVisible().withTimeout(10000);
await element(by.id('selected-user-rocket.cat')).tap();
await waitFor(element(by.id('selected-user-rocket.cat'))).toBeNotVisible().withTimeout(10000);
// Spotlight issues
await element(by.id('select-users-view-item-rocket.cat')).tap();
await waitFor(element(by.id('selected-user-rocket.cat'))).toBeVisible().withTimeout(10000);
});
it('should create team', async() => {
await element(by.id('selected-users-view-submit')).tap();
await waitFor(element(by.id('create-team-view'))).toExist().withTimeout(10000);
});
})
describe('Create Team', async() => {
describe('Usage', async() => {
it('should get invalid team name', async() => {
await element(by.id('create-team-name')).typeText(`${data.teams.private.name}`);
await element(by.id('create-channel-submit')).tap();
await element(by.text('OK')).tap();
});
it('should create private team', async() => {
const room = `private${ data.random }`;
await element(by.id('create-team-name')).replaceText('');
await element(by.id('create-team-name')).typeText(room);
await element(by.id('create-channel-submit')).tap();
await waitFor(element(by.id('room-view'))).toExist().withTimeout(20000);
await expect(element(by.id('room-view'))).toExist();
await waitFor(element(by.id(`room-view-title-${ room }`))).toExist().withTimeout(6000);
await expect(element(by.id(`room-view-title-${ room }`))).toExist();
await tapBack();
await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(10000);
await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toExist().withTimeout(6000);
await expect(element(by.id(`rooms-list-view-item-${ room }`))).toExist();
});
})
});
});