[NEW] Add/Create/Remove channel on a team (#3090)

* 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

* Minor tweaks

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

* 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

* One way to refactor :P

* Next level refactor :)

* Fix create group dm

* Refactor renderItem

* Minor bug fixes

* Fix stories

Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Gerzon Z 2021-05-19 17:14:42 -04:00 committed by GitHub
parent b701913478
commit 9670fa623a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 13185 additions and 10715 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Text } from 'react-native'; import { Text, View } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
@ -20,12 +20,19 @@ export const Item = React.memo(({ item, hide, theme }) => {
theme={theme} theme={theme}
> >
<CustomIcon name={item.icon} size={20} color={item.danger ? themes[theme].dangerColor : themes[theme].bodyText} /> <CustomIcon name={item.icon} size={20} color={item.danger ? themes[theme].dangerColor : themes[theme].bodyText} />
<Text <View style={styles.titleContainer}>
numberOfLines={1} <Text
style={[styles.title, { color: item.danger ? themes[theme].dangerColor : themes[theme].bodyText }]} numberOfLines={1}
> style={[styles.title, { color: item.danger ? themes[theme].dangerColor : themes[theme].bodyText }]}
{item.title} >
</Text> {item.title}
</Text>
</View>
{ item.right ? (
<View style={styles.rightContainer}>
{item.right ? item.right() : null}
</View>
) : null }
</Button> </Button>
); );
}); });
@ -34,7 +41,8 @@ Item.propTypes = {
title: PropTypes.string, title: PropTypes.string,
icon: PropTypes.string, icon: PropTypes.string,
danger: PropTypes.bool, danger: PropTypes.bool,
onPress: PropTypes.func onPress: PropTypes.func,
right: PropTypes.func
}), }),
hide: PropTypes.func, hide: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string

View File

@ -22,6 +22,9 @@ export default StyleSheet.create({
content: { content: {
paddingTop: 16 paddingTop: 16
}, },
titleContainer: {
flex: 1
},
title: { title: {
fontSize: 16, fontSize: 16,
marginLeft: 16, marginLeft: 16,
@ -58,5 +61,8 @@ export default StyleSheet.create({
fontSize: 16, fontSize: 16,
...sharedStyles.textMedium, ...sharedStyles.textMedium,
...sharedStyles.textAlignCenter ...sharedStyles.textAlignCenter
},
rightContainer: {
paddingLeft: 12
} }
}); });

View File

@ -30,6 +30,7 @@ const RoomTypeIcon = React.memo(({
return <Status style={[iconStyle, { color: STATUS_COLORS[status] ?? STATUS_COLORS.offline }]} size={size} status={status} />; return <Status style={[iconStyle, { color: STATUS_COLORS[status] ?? STATUS_COLORS.offline }]} size={size} status={status} />;
} }
// TODO: move this to a separate function
let icon = 'channel-private'; let icon = 'channel-private';
if (teamMain) { if (teamMain) {
icon = `teams${ type === 'p' ? '-private' : '' }`; icon = `teams${ type === 'p' ? '-private' : '' }`;

View File

@ -435,6 +435,7 @@
"Review_app_unable_store": "Unable to open {{store}}", "Review_app_unable_store": "Unable to open {{store}}",
"Review_this_app": "Review this app", "Review_this_app": "Review this app",
"Remove": "Remove", "Remove": "Remove",
"remove": "remove",
"Roles": "Roles", "Roles": "Roles",
"Room_actions": "Room actions", "Room_actions": "Room actions",
"Room_changed_announcement": "Room announcement changed to: {{announcement}} by {{userBy}}", "Room_changed_announcement": "Room announcement changed to: {{announcement}} by {{userBy}}",
@ -716,5 +717,14 @@
"Read_Only_Team": "Read Only Team", "Read_Only_Team": "Read Only Team",
"Broadcast_Team": "Broadcast Team", "Broadcast_Team": "Broadcast Team",
"creating_team": "creating team", "creating_team": "creating team",
"team-name-already-exists": "A team with that name already exists" "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"
}

View File

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

View File

@ -95,10 +95,19 @@ const RocketChat = {
}, },
canOpenRoom, canOpenRoom,
createChannel({ createChannel({
name, users, type, readOnly, broadcast, encrypted name, users, type, readOnly, broadcast, encrypted, teamId
}) { }) {
// RC 0.51.0 const params = {
return this.methodCallWrapper(type ? 'createPrivateGroup' : 'createChannel', name, users, readOnly, {}, { broadcast, encrypted }); name,
members: users,
readOnly,
extraData: {
broadcast,
encrypted,
...(teamId && { teamId })
}
};
return this.post(type ? 'groups.create' : 'channels.create', params);
}, },
async getWebsocketInfo({ server }) { async getWebsocketInfo({ server }) {
const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) }); const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
@ -648,7 +657,8 @@ const RocketChat = {
avatarETag: sub.avatarETag, avatarETag: sub.avatarETag,
t: sub.t, t: sub.t,
encrypted: sub.encrypted, encrypted: sub.encrypted,
lastMessage: sub.lastMessage lastMessage: sub.lastMessage,
...(sub.teamId && { teamId: sub.teamId })
})); }));
return data; return data;
@ -751,6 +761,18 @@ const RocketChat = {
// RC 3.13.0 // RC 3.13.0
return this.post('teams.create', params); return this.post('teams.create', params);
}, },
addRoomsToTeam({ teamId, rooms }) {
// RC 3.13.0
return this.post('teams.addRooms', { teamId, rooms });
},
removeTeamRoom({ roomId, teamId }) {
// RC 3.13.0
return this.post('teams.removeRoom', { roomId, teamId });
},
updateTeamRoom({ roomId, isDefault }) {
// RC 3.13.0
return this.post('teams.updateRoom', { roomId, isDefault });
},
joinRoom(roomId, joinCode, type) { joinRoom(roomId, joinCode, type) {
// TODO: join code // TODO: join code
// RC 0.48.0 // RC 0.48.0

View File

@ -10,6 +10,8 @@ import LastMessage from './LastMessage';
import Title from './Title'; import Title from './Title';
import UpdatedAt from './UpdatedAt'; import UpdatedAt from './UpdatedAt';
import Touchable from './Touchable'; import Touchable from './Touchable';
import Tag from './Tag';
import I18n from '../../i18n';
const RoomItem = ({ const RoomItem = ({
rid, rid,
@ -42,13 +44,16 @@ const RoomItem = ({
testID, testID,
swipeEnabled, swipeEnabled,
onPress, onPress,
onLongPress,
toggleFav, toggleFav,
toggleRead, toggleRead,
hideChannel, hideChannel,
teamMain teamMain,
autoJoin
}) => ( }) => (
<Touchable <Touchable
onPress={onPress} onPress={onPress}
onLongPress={onLongPress}
width={width} width={width}
favorite={favorite} favorite={favorite}
toggleFav={toggleFav} toggleFav={toggleFav}
@ -88,6 +93,9 @@ const RoomItem = ({
hideUnreadStatus={hideUnreadStatus} hideUnreadStatus={hideUnreadStatus}
alert={alert} alert={alert}
/> />
{
autoJoin ? <Tag name={I18n.t('Auto-join')} /> : null
}
<UpdatedAt <UpdatedAt
date={date} date={date}
theme={theme} theme={theme}
@ -132,6 +140,9 @@ const RoomItem = ({
hideUnreadStatus={hideUnreadStatus} hideUnreadStatus={hideUnreadStatus}
alert={alert} alert={alert}
/> />
{
autoJoin ? <Tag name={I18n.t('Auto-join')} /> : null
}
<UnreadBadge <UnreadBadge
unread={unread} unread={unread}
userMentions={userMentions} userMentions={userMentions}
@ -181,7 +192,9 @@ RoomItem.propTypes = {
toggleFav: PropTypes.func, toggleFav: PropTypes.func,
toggleRead: PropTypes.func, toggleRead: PropTypes.func,
onPress: PropTypes.func, onPress: PropTypes.func,
hideChannel: PropTypes.func onLongPress: PropTypes.func,
hideChannel: PropTypes.func,
autoJoin: PropTypes.bool
}; };
RoomItem.defaultProps = { RoomItem.defaultProps = {

View File

@ -0,0 +1,30 @@
import React from 'react';
import { Text, View } from 'react-native';
import PropTypes from 'prop-types';
import { themes } from '../../constants/colors';
import { useTheme } from '../../theme';
import styles from './styles';
const Tag = React.memo(({ name }) => {
const { theme } = useTheme();
return (
<View style={[styles.tagContainer, { backgroundColor: themes[theme].borderColor }]}>
<Text
style={[
styles.tagText, { color: themes[theme].infoText }
]}
numberOfLines={1}
>
{name}
</Text>
</View>
);
});
Tag.propTypes = {
name: PropTypes.string
};
export default Tag;

View File

@ -1,7 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Animated } from 'react-native'; import { Animated } from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler'; import {
LongPressGestureHandler, PanGestureHandler, State
} from 'react-native-gesture-handler';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import { import {
@ -17,6 +19,7 @@ class Touchable extends React.Component {
static propTypes = { static propTypes = {
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
onPress: PropTypes.func, onPress: PropTypes.func,
onLongPress: PropTypes.func,
testID: PropTypes.string, testID: PropTypes.string,
width: PropTypes.number, width: PropTypes.number,
favorite: PropTypes.bool, favorite: PropTypes.bool,
@ -59,6 +62,12 @@ class Touchable extends React.Component {
} }
} }
onLongPressHandlerStateChange = ({ nativeEvent }) => {
if (nativeEvent.state === State.ACTIVE) {
this.onLongPress();
}
}
_handleRelease = (nativeEvent) => { _handleRelease = (nativeEvent) => {
const { translationX } = nativeEvent; const { translationX } = nativeEvent;
@ -203,54 +212,70 @@ class Touchable extends React.Component {
} }
}; };
onLongPress = () => {
const { rowState } = this.state;
const { onLongPress } = this.props;
if (rowState !== 0) {
this.close();
return;
}
if (onLongPress) {
onLongPress();
}
};
render() { render() {
const { const {
testID, isRead, width, favorite, children, theme, isFocused, swipeEnabled testID, isRead, width, favorite, children, theme, isFocused, swipeEnabled
} = this.props; } = this.props;
return ( return (
<LongPressGestureHandler onHandlerStateChange={this.onLongPressHandlerStateChange}>
<PanGestureHandler
minDeltaX={20}
onGestureEvent={this._onGestureEvent}
onHandlerStateChange={this._onHandlerStateChange}
enabled={swipeEnabled}
>
<Animated.View> <Animated.View>
<LeftActions <PanGestureHandler
transX={this.transXReverse} minDeltaX={20}
isRead={isRead} onGestureEvent={this._onGestureEvent}
width={width} onHandlerStateChange={this._onHandlerStateChange}
onToggleReadPress={this.onToggleReadPress} enabled={swipeEnabled}
theme={theme}
/>
<RightActions
transX={this.transXReverse}
favorite={favorite}
width={width}
toggleFav={this.toggleFav}
onHidePress={this.onHidePress}
theme={theme}
/>
<Animated.View
style={{
transform: [{ translateX: this.transX }]
}}
> >
<Touch <Animated.View>
onPress={this.onPress} <LeftActions
theme={theme} transX={this.transXReverse}
testID={testID} isRead={isRead}
style={{ width={width}
backgroundColor: isFocused ? themes[theme].chatComponentBackground : themes[theme].backgroundColor onToggleReadPress={this.onToggleReadPress}
}} theme={theme}
> />
{children} <RightActions
</Touch> transX={this.transXReverse}
</Animated.View> favorite={favorite}
</Animated.View> width={width}
toggleFav={this.toggleFav}
onHidePress={this.onHidePress}
theme={theme}
/>
<Animated.View
style={{
transform: [{ translateX: this.transX }]
}}
>
<Touch
onPress={this.onPress}
theme={theme}
testID={testID}
style={{
backgroundColor: isFocused ? themes[theme].chatComponentBackground : themes[theme].backgroundColor
}}
>
{children}
</Touch>
</Animated.View>
</Animated.View>
</PanGestureHandler> </PanGestureHandler>
</Animated.View>
</LongPressGestureHandler>
); );
} }
} }

View File

@ -16,7 +16,8 @@ const attrs = [
'theme', 'theme',
'isFocused', 'isFocused',
'forceUpdate', 'forceUpdate',
'showLastMessage' 'showLastMessage',
'autoJoin'
]; ];
class RoomItemContainer extends React.Component { class RoomItemContainer extends React.Component {
@ -25,6 +26,7 @@ class RoomItemContainer extends React.Component {
showLastMessage: PropTypes.bool, showLastMessage: PropTypes.bool,
id: PropTypes.string, id: PropTypes.string,
onPress: PropTypes.func, onPress: PropTypes.func,
onLongPress: PropTypes.func,
username: PropTypes.string, username: PropTypes.string,
avatarSize: PropTypes.number, avatarSize: PropTypes.number,
width: PropTypes.number, width: PropTypes.number,
@ -41,7 +43,8 @@ class RoomItemContainer extends React.Component {
getRoomAvatar: PropTypes.func, getRoomAvatar: PropTypes.func,
getIsGroupChat: PropTypes.func, getIsGroupChat: PropTypes.func,
getIsRead: PropTypes.func, getIsRead: PropTypes.func,
swipeEnabled: PropTypes.bool swipeEnabled: PropTypes.bool,
autoJoin: PropTypes.bool
}; };
static defaultProps = { static defaultProps = {
@ -112,6 +115,11 @@ class RoomItemContainer extends React.Component {
return onPress(item); return onPress(item);
} }
onLongPress = () => {
const { item, onLongPress } = this.props;
return onLongPress(item);
}
render() { render() {
const { const {
item, item,
@ -129,7 +137,8 @@ class RoomItemContainer extends React.Component {
showLastMessage, showLastMessage,
username, username,
useRealName, useRealName,
swipeEnabled swipeEnabled,
autoJoin
} = this.props; } = this.props;
const name = getRoomTitle(item); const name = getRoomTitle(item);
const testID = `rooms-list-view-item-${ name }`; const testID = `rooms-list-view-item-${ name }`;
@ -160,6 +169,7 @@ class RoomItemContainer extends React.Component {
isGroupChat={this.isGroupChat} isGroupChat={this.isGroupChat}
isRead={isRead} isRead={isRead}
onPress={this.onPress} onPress={this.onPress}
onLongPress={this.onLongPress}
date={date} date={date}
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
width={width} width={width}
@ -189,6 +199,7 @@ class RoomItemContainer extends React.Component {
tunreadGroup={item.tunreadGroup} tunreadGroup={item.tunreadGroup}
swipeEnabled={swipeEnabled} swipeEnabled={swipeEnabled}
teamMain={item.teamMain} teamMain={item.teamMain}
autoJoin={autoJoin}
/> />
); );
} }

View File

@ -96,5 +96,16 @@ export default StyleSheet.create({
height: '100%', height: '100%',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center' justifyContent: 'center'
},
tagContainer: {
alignSelf: 'center',
alignItems: 'center',
borderRadius: 4,
marginHorizontal: 4
},
tagText: {
fontSize: 13,
paddingHorizontal: 4,
...sharedStyles.textSemibold
} }
}); });

View File

@ -40,18 +40,26 @@ const handleRequest = function* handleRequest({ data }) {
broadcast, broadcast,
encrypted encrypted
} = data; } = data;
logEvent(events.CR_CREATE, { logEvent(events.CT_CREATE, {
type, type,
readOnly, readOnly,
broadcast, broadcast,
encrypted encrypted
}); });
sub = yield call(createTeam, data); const result = yield call(createTeam, data);
sub = {
rid: result?.team?.roomId,
...result.team,
t: result.team.type ? 'p' : 'c'
};
} else if (data.group) { } else if (data.group) {
logEvent(events.SELECTED_USERS_CREATE_GROUP); logEvent(events.SELECTED_USERS_CREATE_GROUP);
const result = yield call(createGroupChat); const result = yield call(createGroupChat);
if (result.success) { if (result.success) {
({ room: sub } = result); sub = {
rid: result.room?._id,
...result.room
};
} }
} else { } else {
const { const {
@ -66,33 +74,26 @@ const handleRequest = function* handleRequest({ data }) {
broadcast, broadcast,
encrypted encrypted
}); });
sub = yield call(createChannel, data); const result = yield call(createChannel, data);
sub = {
rid: result?.channel?._id || result?.group?._id,
...result?.channel,
...result?.group
};
} }
try { try {
const db = database.active; const db = database.active;
const subCollection = db.get('subscriptions'); const subCollection = db.get('subscriptions');
yield db.action(async() => { yield db.action(async() => {
await subCollection.create((s) => { await subCollection.create((s) => {
s._raw = sanitizedRaw({ id: sub.team ? sub.team.roomId : sub.rid }, subCollection.schema); s._raw = sanitizedRaw({ id: sub.rid }, subCollection.schema);
Object.assign(s, sub); Object.assign(s, sub);
}); });
}); });
} catch { } catch {
// do nothing // do nothing
} }
yield put(createChannelSuccess(sub));
let successParams = {};
if (data.isTeam) {
successParams = {
...sub.team,
rid: sub.team.roomId,
t: sub.team.type ? 'p' : 'c'
};
} else {
successParams = data;
}
yield put(createChannelSuccess(successParams));
} catch (err) { } catch (err) {
logEvent(events[data.group ? 'SELECTED_USERS_CREATE_GROUP_F' : 'CR_CREATE_F']); logEvent(events[data.group ? 'SELECTED_USERS_CREATE_GROUP_F' : 'CR_CREATE_F']);
yield put(createChannelFailure(err)); yield put(createChannelFailure(err));

View File

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

View File

@ -61,6 +61,8 @@ import { setKeyCommands, deleteKeyCommands } from '../../commands';
import ShareView from '../../views/ShareView'; import ShareView from '../../views/ShareView';
import QueueListView from '../../ee/omnichannel/views/QueueListView'; import QueueListView from '../../ee/omnichannel/views/QueueListView';
import AddChannelTeamView from '../../views/AddChannelTeamView';
import AddExistingChannelView from '../../views/AddExistingChannelView';
// ChatsStackNavigator // ChatsStackNavigator
const ChatsStack = createStackNavigator(); const ChatsStack = createStackNavigator();
@ -141,6 +143,16 @@ const ModalStackNavigator = React.memo(({ navigation }) => {
component={InviteUsersView} component={InviteUsersView}
options={InviteUsersView.navigationOptions} options={InviteUsersView.navigationOptions}
/> />
<ModalStack.Screen
name='AddChannelTeamView'
component={AddChannelTeamView}
options={AddChannelTeamView.navigationOptions}
/>
<ModalStack.Screen
name='AddExistingChannelView'
component={AddExistingChannelView}
options={AddExistingChannelView.navigationOptions}
/>
<ModalStack.Screen <ModalStack.Screen
name='InviteUsersEditView' name='InviteUsersEditView'
component={InviteUsersEditView} component={InviteUsersEditView}

View File

@ -99,14 +99,22 @@ export default {
SELECTED_USERS_CREATE_GROUP: 'selected_users_create_group', SELECTED_USERS_CREATE_GROUP: 'selected_users_create_group',
SELECTED_USERS_CREATE_GROUP_F: 'selected_users_create_group_f', SELECTED_USERS_CREATE_GROUP_F: 'selected_users_create_group_f',
// ADD EXISTING CHANNEL VIEW
EXISTING_CHANNEL_ADD_CHANNEL: 'existing_channel_add_channel',
EXISTING_CHANNEL_REMOVE_CHANNEL: 'existing_channel_remove_channel',
// CREATE CHANNEL VIEW // CREATE CHANNEL VIEW
CR_CREATE: 'cr_create', CR_CREATE: 'cr_create',
CT_CREATE: 'ct_create',
CR_CREATE_F: 'cr_create_f', CR_CREATE_F: 'cr_create_f',
CT_CREATE_F: 'ct_create_f',
CR_TOGGLE_TYPE: 'cr_toggle_type', CR_TOGGLE_TYPE: 'cr_toggle_type',
CR_TOGGLE_READ_ONLY: 'cr_toggle_read_only', CR_TOGGLE_READ_ONLY: 'cr_toggle_read_only',
CR_TOGGLE_BROADCAST: 'cr_toggle_broadcast', CR_TOGGLE_BROADCAST: 'cr_toggle_broadcast',
CR_TOGGLE_ENCRYPTED: 'cr_toggle_encrypted', CR_TOGGLE_ENCRYPTED: 'cr_toggle_encrypted',
CR_REMOVE_USER: 'cr_remove_user', 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 // CREATE DISCUSSION VIEW
CD_CREATE: 'cd_create', CD_CREATE: 'cd_create',

View File

@ -0,0 +1,72 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import * as List from '../containers/List';
import StatusBar from '../containers/StatusBar';
import { useTheme } from '../theme';
import * as HeaderButton from '../containers/HeaderButton';
import SafeAreaView from '../containers/SafeAreaView';
import I18n from '../i18n';
const setHeader = (navigation, isMasterDetail) => {
const options = {
headerTitle: I18n.t('Add_Channel_to_Team')
};
if (isMasterDetail) {
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
}
navigation.setOptions(options);
};
const AddChannelTeamView = ({
navigation, route, isMasterDetail
}) => {
const { teamId, teamChannels } = route.params;
const { theme } = useTheme();
useEffect(() => {
setHeader(navigation, isMasterDetail);
}, []);
return (
<SafeAreaView testID='add-channel-team-view'>
<StatusBar />
<List.Container>
<List.Separator />
<List.Item
title='Create_New'
onPress={() => navigation.navigate('NewMessageStackNavigator', { screen: 'SelectedUsersViewCreateChannel', params: { nextAction: () => navigation.navigate('CreateChannelView', { teamId }) } })}
testID='add-channel-team-view-create-channel'
left={() => <List.Icon name='team' />}
right={() => <List.Icon name='chevron-right' />}
theme={theme}
/>
<List.Separator />
<List.Item
title='Add_Existing'
onPress={() => navigation.navigate('AddExistingChannelView', { teamId, teamChannels })}
testID='add-channel-team-view-create-channel'
left={() => <List.Icon name='channel-public' />}
right={() => <List.Icon name='chevron-right' />}
theme={theme}
/>
<List.Separator />
</List.Container>
</SafeAreaView>
);
};
AddChannelTeamView.propTypes = {
route: PropTypes.object,
navigation: PropTypes.object,
isMasterDetail: PropTypes.bool
};
const mapStateToProps = state => ({
isMasterDetail: state.app.isMasterDetail
});
export default connect(mapStateToProps)(AddChannelTeamView);

View File

@ -0,0 +1,209 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
View, FlatList
} from 'react-native';
import { connect } from 'react-redux';
import { Q } from '@nozbe/watermelondb';
import * as List from '../containers/List';
import database from '../lib/database';
import RocketChat from '../lib/rocketchat';
import I18n from '../i18n';
import log, { events, logEvent } from '../utils/log';
import SearchBox from '../containers/SearchBox';
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 = 50;
class AddExistingChannelView extends React.Component {
static propTypes = {
navigation: PropTypes.object,
route: PropTypes.object,
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 } = this.props;
const { selected } = this.state;
const options = {
headerTitle: I18n.t('Add_Existing_Channel')
};
if (isMasterDetail) {
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
}
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);
}
init = async() => {
try {
const { addTeamChannelPermission } = this.props;
const db = database.active;
const channels = await db.collections
.get('subscriptions')
.query(
Q.where('team_id', ''),
Q.where('t', Q.oneOf(['c', '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.addRoomsToTeam({ 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 });
}
}
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.EXISTING_CHANNEL_ADD_CHANNEL);
this.setState({ selected: [...selected, rid] }, () => this.setHeader());
} else {
logEvent(events.EXISTING_CHANNEL_REMOVE_CHANNEL);
const filterSelected = selected.filter(el => el !== rid);
this.setState({ selected: filterSelected }, () => this.setHeader());
}
}
renderItem = ({ item }) => {
const isChecked = this.isChecked(item.rid);
// TODO: reuse logic inside RoomTypeIcon
const icon = item.t === 'p' && !item.teamId ? 'channel-private' : 'channel-public';
return (
<List.Item
title={RocketChat.getRoomTitle(item)}
translateTitle={false}
onPress={() => this.toggleChannel(item.rid)}
testID='add-existing-channel-view-item'
left={() => <List.Icon name={icon} />}
right={() => (isChecked ? <List.Icon name='check' /> : null)}
/>
);
}
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='add-existing-channel-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)(withTheme(AddExistingChannelView));

View File

@ -83,13 +83,15 @@ class CreateChannelView extends React.Component {
id: PropTypes.string, id: PropTypes.string,
token: PropTypes.string token: PropTypes.string
}), }),
theme: PropTypes.string theme: PropTypes.string,
teamId: PropTypes.string
}; };
constructor(props) { constructor(props) {
super(props); super(props);
const { route } = this.props; const { route } = this.props;
const isTeam = route?.params?.isTeam || false; const isTeam = route?.params?.isTeam || false;
this.teamId = route?.params?.teamId;
this.state = { this.state = {
channelName: '', channelName: '',
type: true, type: true,
@ -180,7 +182,7 @@ class CreateChannelView extends React.Component {
// create channel or team // create channel or team
create({ create({
name: channelName, users, type, readOnly, broadcast, encrypted, isTeam name: channelName, users, type, readOnly, broadcast, encrypted, isTeam, teamId: this.teamId
}); });
Review.pushPositiveEvent(); Review.pushPositiveEvent();

View File

@ -60,7 +60,7 @@ class NewMessageView extends React.Component {
id: PropTypes.string, id: PropTypes.string,
token: PropTypes.string token: PropTypes.string
}), }),
createChannel: PropTypes.func, create: PropTypes.func,
maxUsers: PropTypes.number, maxUsers: PropTypes.number,
theme: PropTypes.string, theme: PropTypes.string,
isMasterDetail: PropTypes.bool isMasterDetail: PropTypes.bool
@ -124,9 +124,9 @@ class NewMessageView extends React.Component {
createGroupChat = () => { createGroupChat = () => {
logEvent(events.NEW_MSG_CREATE_GROUP_CHAT); logEvent(events.NEW_MSG_CREATE_GROUP_CHAT);
const { createChannel, maxUsers, navigation } = this.props; const { create, maxUsers, navigation } = this.props;
navigation.navigate('SelectedUsersViewCreateChannel', { navigation.navigate('SelectedUsersViewCreateChannel', {
nextAction: () => createChannel({ group: true }), nextAction: () => create({ group: true }),
buttonText: I18n.t('Create'), buttonText: I18n.t('Create'),
maxUsers maxUsers
}); });

View File

@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import { Keyboard } from 'react-native'; import { Keyboard, Alert } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import { withSafeAreaInsets } from 'react-native-safe-area-context'; import { withSafeAreaInsets } from 'react-native-safe-area-context';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { FlatList } from 'react-native-gesture-handler'; import { FlatList } from 'react-native-gesture-handler';
import { HeaderBackButton } from '@react-navigation/stack';
import StatusBar from '../containers/StatusBar'; import StatusBar from '../containers/StatusBar';
import RoomHeader from '../containers/RoomHeader'; import RoomHeader from '../containers/RoomHeader';
@ -23,11 +22,14 @@ import RoomItem, { ROW_HEIGHT } from '../presentation/RoomItem';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { withDimensions } from '../dimensions'; import { withDimensions } from '../dimensions';
import { isIOS } from '../utils/deviceInfo'; import { isIOS } from '../utils/deviceInfo';
import { themes } from '../constants/colors';
import debounce from '../utils/debounce'; import debounce from '../utils/debounce';
import { showErrorAlert } from '../utils/info'; import { showErrorAlert } from '../utils/info';
import { goRoom } from '../utils/goRoom'; import { goRoom } from '../utils/goRoom';
import I18n from '../i18n'; import I18n from '../i18n';
import { withActionSheet } from '../containers/ActionSheet';
import { deleteRoom as deleteRoomAction } from '../actions/room';
import { CustomIcon } from '../lib/Icons';
import { themes } from '../constants/colors';
const API_FETCH_COUNT = 25; const API_FETCH_COUNT = 25;
@ -47,7 +49,11 @@ class TeamChannelsView extends React.Component {
theme: PropTypes.string, theme: PropTypes.string,
useRealName: PropTypes.bool, useRealName: PropTypes.bool,
width: PropTypes.number, width: PropTypes.number,
StoreLastMessage: PropTypes.bool StoreLastMessage: PropTypes.bool,
addTeamChannelPermission: PropTypes.array,
removeTeamChannelPermission: PropTypes.array,
showActionSheet: PropTypes.func,
deleteRoom: PropTypes.func
} }
constructor(props) { constructor(props) {
@ -60,9 +66,11 @@ class TeamChannelsView extends React.Component {
isSearching: false, isSearching: false,
searchText: '', searchText: '',
search: [], search: [],
end: false end: false,
showCreate: false
}; };
this.loadTeam(); this.loadTeam();
this.setHeader();
} }
componentDidMount() { componentDidMount() {
@ -70,6 +78,9 @@ class TeamChannelsView extends React.Component {
} }
loadTeam = async() => { loadTeam = async() => {
const { addTeamChannelPermission } = this.props;
const { loading, data } = this.state;
const db = database.active; const db = database.active;
try { try {
const subCollection = db.get('subscriptions'); const subCollection = db.get('subscriptions');
@ -82,6 +93,15 @@ class TeamChannelsView extends React.Component {
if (!this.team) { if (!this.team) {
throw new Error(); throw new Error();
} }
const permissions = await RocketChat.hasPermission([addTeamChannelPermission], this.team.rid);
if (permissions[0]) {
this.setState({ showCreate: true }, () => this.setHeader());
}
if (loading && data.length) {
this.setState({ loading: false });
}
} catch { } catch {
const { navigation } = this.props; const { navigation } = this.props;
navigation.pop(); navigation.pop();
@ -115,14 +135,11 @@ class TeamChannelsView extends React.Component {
loadingMore: false, loadingMore: false,
end: result.rooms.length < API_FETCH_COUNT end: result.rooms.length < API_FETCH_COUNT
}; };
const rooms = result.rooms.map((room) => {
const record = this.teamChannels?.find(c => c.rid === room._id);
return record ?? room;
});
if (isSearching) { if (isSearching) {
newState.search = [...search, ...rooms]; newState.search = [...search, ...result.rooms];
} else { } else {
newState.data = [...data, ...rooms]; newState.data = [...data, ...result.rooms];
} }
this.setState(newState); this.setState(newState);
@ -135,18 +152,16 @@ class TeamChannelsView extends React.Component {
} }
}, 300) }, 300)
getHeader = () => { setHeader = () => {
const { isSearching } = this.state; const { isSearching, showCreate, data } = this.state;
const { const { navigation, isMasterDetail, insets } = this.props;
navigation, isMasterDetail, insets, theme
} = this.props;
const { team } = this; const { team } = this;
if (!team) { if (!team) {
return; return;
} }
const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 1 }); const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 2 });
if (isSearching) { if (isSearching) {
return { return {
@ -188,27 +203,16 @@ class TeamChannelsView extends React.Component {
if (isMasterDetail) { if (isMasterDetail) {
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />; options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
} else {
options.headerLeft = () => (
<HeaderBackButton
labelVisible={false}
onPress={() => navigation.pop()}
tintColor={themes[theme].headerTintColor}
/>
);
} }
options.headerRight = () => ( options.headerRight = () => (
<HeaderButton.Container> <HeaderButton.Container>
{ showCreate
? <HeaderButton.Item iconName='create' onPress={() => navigation.navigate('AddChannelTeamView', { teamId: this.teamId, teamChannels: data })} />
: null}
<HeaderButton.Item iconName='search' onPress={this.onSearchPress} /> <HeaderButton.Item iconName='search' onPress={this.onSearchPress} />
</HeaderButton.Container> </HeaderButton.Container>
); );
return options;
}
setHeader = () => {
const { navigation } = this.props;
const options = this.getHeader();
navigation.setOptions(options); navigation.setOptions(options);
} }
@ -287,6 +291,115 @@ class TeamChannelsView extends React.Component {
} }
}, 1000, true); }, 1000, true);
options = (item) => {
const { theme } = this.props;
const isAutoJoinChecked = item.teamDefault;
const autoJoinIcon = isAutoJoinChecked ? 'checkbox-checked' : 'checkbox-unchecked';
const autoJoinIconColor = isAutoJoinChecked ? themes[theme].tintActive : themes[theme].auxiliaryTintColor;
return ([
{
title: I18n.t('Auto-join'),
icon: item.t === 'p' ? 'channel-private' : 'channel-public',
onPress: () => this.toggleAutoJoin(item),
right: () => <CustomIcon name={autoJoinIcon} size={20} color={autoJoinIconColor} />
},
{
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)
}
]);
}
toggleAutoJoin = async(item) => {
try {
const { data } = this.state;
const result = await RocketChat.updateTeamRoom({ roomId: item._id, isDefault: !item.teamDefault });
if (result.success) {
const newData = data.map((i) => {
if (i._id === item._id) {
i.teamDefault = !i.teamDefault;
}
return i;
});
this.setState({ data: newData });
}
} catch (e) {
log(e);
}
}
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._id, teamId: this.team.teamId });
if (result.success) {
const newData = data.filter(room => result.room._id !== room._id);
this.setState({ data: newData });
}
} 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._id, 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 }) => { renderItem = ({ item }) => {
const { const {
StoreLastMessage, StoreLastMessage,
@ -302,10 +415,12 @@ class TeamChannelsView extends React.Component {
showLastMessage={StoreLastMessage} showLastMessage={StoreLastMessage}
onPress={this.onPressItem} onPress={this.onPressItem}
width={width} width={width}
onLongPress={this.showChannelActions}
useRealName={useRealName} useRealName={useRealName}
getRoomTitle={this.getRoomTitle} getRoomTitle={this.getRoomTitle}
getRoomAvatar={this.getRoomAvatar} getRoomAvatar={this.getRoomAvatar}
swipeEnabled={false} swipeEnabled={false}
autoJoin={item.teamDefault}
/> />
); );
}; };
@ -365,7 +480,13 @@ const mapStateToProps = state => ({
user: getUserSelector(state), user: getUserSelector(state),
useRealName: state.settings.UI_Use_Real_Name, useRealName: state.settings.UI_Use_Real_Name,
isMasterDetail: state.app.isMasterDetail, 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

@ -3,7 +3,6 @@ import React from 'react';
import { ScrollView, Dimensions } from 'react-native'; import { ScrollView, Dimensions } from 'react-native';
import { storiesOf } from '@storybook/react-native'; import { storiesOf } from '@storybook/react-native';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
// import moment from 'moment';
import { themes } from '../../app/constants/colors'; import { themes } from '../../app/constants/colors';
import RoomItemComponent from '../../app/presentation/RoomItem/RoomItem'; import RoomItemComponent from '../../app/presentation/RoomItem/RoomItem';
@ -94,6 +93,15 @@ stories.add('Alerts', () => (
</> </>
)); ));
stories.add('Tag', () => (
<>
<RoomItem autoJoin />
<RoomItem showLastMessage autoJoin />
<RoomItem name={longText} autoJoin />
<RoomItem name={longText} autoJoin showLastMessage />
</>
));
stories.add('Last Message', () => ( stories.add('Last Message', () => (
<> <>
<RoomItem <RoomItem