diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 52cde22be..4c88d8e5c 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -44348,589 +44348,180 @@ exports[`Storyshots Room Item Alerts 1`] = ` } > - - - - - - - - -  - - - rocket.cat - - - - - - - - - - -  - - - Read - - - - - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - - + > + + - - - -  - - - unread - +  + + - 1 + rocket.cat @@ -45165,202 +44756,222 @@ exports[`Storyshots Room Item Alerts 1`] = ` } > - + > + + - - - -  - - - unread - +  + + - +999 + unread + + + 1 + + @@ -45594,202 +45205,222 @@ exports[`Storyshots Room Item Alerts 1`] = ` } > - + > + + - - - -  - - - user mentions - +  + + - 1 + unread + + + +999 + + @@ -46023,202 +45654,222 @@ exports[`Storyshots Room Item Alerts 1`] = ` } > - + > + + - - - -  - - - group mentions - +  + + - 1 + user mentions + + + 1 + + @@ -46452,202 +46103,222 @@ exports[`Storyshots Room Item Alerts 1`] = ` } > - + > + + - - - -  - - - thread unread - +  + + - 1 + group mentions + + + 1 + + @@ -46881,202 +46552,222 @@ exports[`Storyshots Room Item Alerts 1`] = ` } > - + > + + - - - -  - - - thread unread user - +  + + - 1 + thread unread + + + 1 + + @@ -47310,202 +47001,222 @@ exports[`Storyshots Room Item Alerts 1`] = ` } > - + > + + - - - -  - - - thread unread group - +  + + - 1 + thread unread user + + + 1 + + @@ -47739,202 +47450,222 @@ exports[`Storyshots Room Item Alerts 1`] = ` } > - + > + + - - - -  - - - user mentions priority 1 - +  + + - 1 + thread unread group + + + 1 + + @@ -48168,202 +47899,222 @@ exports[`Storyshots Room Item Alerts 1`] = ` } > - + > + + - - - -  - - - group mentions priority 2 - +  + + - 1 + user mentions priority 1 + + + 1 + + @@ -48597,202 +48348,671 @@ exports[`Storyshots Room Item Alerts 1`] = ` } > - + > + + + + + + +  + + + group mentions priority 2 + + + + 1 + + + + + + + + + + +  + + + Read + + + + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + - -  - - - thread unread priority 3 - + + + + +  + + - 1 + thread unread priority 3 + + + 1 + + @@ -49039,157 +49259,177 @@ exports[`Storyshots Room Item Basic 1`] = ` } > - + > + + - - - + -  - - - rocket.cat - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + rocket.cat + + @@ -49435,2020 +49675,173 @@ exports[`Storyshots Room Item Last Message 1`] = ` } > - - - - - - - - -  - - - rocket.cat - - - 10:00 - - - - - No Message - - - - - - - - - - -  - - - Read - - - - - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - - - - - - - -  - - - rocket.cat - - - 10:00 - - - - - 2 - - - - - - - - - - - -  - - - Read - - - - - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - - - - - - - - -  - - - rocket.cat - - - 10:00 - - - - - You: 1 - - - - - - - - - - - -  - - - Read - - - - - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - - - - - - - - -  - - - rocket.cat - - - 10:00 - - - - - Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries - - - - - - - - - - - -  - - - Read - - - - - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - - - - - - - - -  - - - rocket.cat - - - 10:00 - - - - - Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries - + + + + + +  + + + rocket.cat + + + 10:00 + + + + - 1 + No Message @@ -51701,252 +50137,173 @@ exports[`Storyshots Room Item Last Message 1`] = ` } > - - - - - - -  - - - rocket.cat - - - 10:00 - - - - - Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries - + + + + + +  + + + rocket.cat + + + 10:00 + + + + - +999 + 2 @@ -52199,252 +50599,173 @@ exports[`Storyshots Room Item Last Message 1`] = ` } > - - - - - - -  - - - rocket.cat - - - 10:00 - - - - - Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries - + + + + + +  + + + rocket.cat + + + 10:00 + + + + - 1 + You: 1 @@ -52470,6 +50834,2022 @@ exports[`Storyshots Room Item Last Message 1`] = ` + + + + + +  + + + Read + + + + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + + + + + + + + + +  + + + rocket.cat + + + 10:00 + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + + + + + + + + + +  + + + Read + + + + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + + + + + + + + + +  + + + rocket.cat + + + 10:00 + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + 1 + + + + + + + + + + + + + +  + + + Read + + + + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + + + + + + + + + +  + + + rocket.cat + + + 10:00 + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + +999 + + + + + + + + + + + + + +  + + + Read + + + + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + + + + + + + + + +  + + + rocket.cat + + + 10:00 + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + 1 + + + + + + + + `; @@ -52710,157 +53090,177 @@ exports[`Storyshots Room Item Type 1`] = ` } > - + > + + - - - + -  - - - rocket.cat - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + rocket.cat + + @@ -53093,145 +53493,165 @@ exports[`Storyshots Room Item Type 1`] = ` } > - + > + + - - - + +  + + -  - - - rocket.cat - + ] + } + > + rocket.cat + + @@ -53464,145 +53884,165 @@ exports[`Storyshots Room Item Type 1`] = ` } > - + > + + - - - + +  + + -  - - - rocket.cat - + ] + } + > + rocket.cat + + @@ -53835,145 +54275,165 @@ exports[`Storyshots Room Item Type 1`] = ` } > - + > + + - - - + +  + + -  - - - rocket.cat - + ] + } + > + rocket.cat + + @@ -54206,145 +54666,165 @@ exports[`Storyshots Room Item Type 1`] = ` } > - + > + + - - - + +  + + -  - - - rocket.cat - + ] + } + > + rocket.cat + + @@ -54577,145 +55057,165 @@ exports[`Storyshots Room Item Type 1`] = ` } > - + > + + - - - + +  + + -  - - - rocket.cat - + ] + } + > + rocket.cat + + @@ -54948,145 +55448,165 @@ exports[`Storyshots Room Item Type 1`] = ` } > - + > + + - - - + +  + + -  - - - rocket.cat - + ] + } + > + rocket.cat + + @@ -55332,157 +55852,177 @@ exports[`Storyshots Room Item User 1`] = ` } > - + > + + - - - + -  - - - diego.mello - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + diego.mello + + @@ -55715,157 +56255,177 @@ exports[`Storyshots Room Item User 1`] = ` } > - + > + + - - - + -  - - - Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + @@ -56111,157 +56671,177 @@ exports[`Storyshots Room Item User status 1`] = ` } > - + > + + - - - + -  - - - rocket.cat - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + rocket.cat + + @@ -56494,157 +57074,177 @@ exports[`Storyshots Room Item User status 1`] = ` } > - + > + + - - - + -  - - - rocket.cat - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + rocket.cat + + @@ -56877,157 +57477,177 @@ exports[`Storyshots Room Item User status 1`] = ` } > - + > + + - - - + -  - - - rocket.cat - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + rocket.cat + + @@ -57260,157 +57880,177 @@ exports[`Storyshots Room Item User status 1`] = ` } > - + > + + - - - + -  - - - rocket.cat - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + rocket.cat + + @@ -57643,157 +58283,177 @@ exports[`Storyshots Room Item User status 1`] = ` } > - + > + + - - - + -  - - - rocket.cat - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + rocket.cat + + @@ -58026,157 +58686,177 @@ exports[`Storyshots Room Item User status 1`] = ` } > - + > + + - - - + -  - - - rocket.cat - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + rocket.cat + + diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 06e3167eb..2854728ab 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -719,5 +719,8 @@ "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": "Add Existing", + "Add_Existing_Channel": "Add Existing Channel", + "Remove_from_Team": "Remove from Team", + "Auto-join": "Auto-join" } diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js index 09b91aa63..1f4bcb9cb 100644 --- a/app/lib/methods/getPermissions.js +++ b/app/lib/methods/getPermissions.js @@ -13,6 +13,7 @@ const PERMISSIONS = [ 'add-user-to-any-c-room', 'add-user-to-any-p-room', 'add-user-to-joined-room', + 'add-team-channel', 'archive-room', 'auto-translate', 'create-invite-links', @@ -21,11 +22,13 @@ const PERMISSIONS = [ 'delete-p', 'edit-message', 'edit-room', + '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 +41,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() { diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 965f1c211..b98ab4958 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -734,7 +734,7 @@ const RocketChat = { const params = { name, users, - type: type ? 1 : 0, + type, room: { readOnly, extraData: { @@ -746,6 +746,22 @@ const RocketChat = { // RC 3.13.0 return this.post('teams.create', params); }, + addTeamRooms({ rooms, teamId }) { + const params = { + rooms: rooms.length ? [...rooms] : [rooms], + teamId + }; + // RC 3.13.0 + return this.post('teams.addRooms', params); + }, + deleteTeamRoom({ rid, teamId }) { + const params = { + roomId: rid, + teamId + }; + // RC 3.13.0 + return this.post('teams.removeRoom', params); + }, joinRoom(roomId, joinCode, type) { // TODO: join code // RC 0.48.0 diff --git a/app/presentation/RoomItem/RoomItem.js b/app/presentation/RoomItem/RoomItem.js index b3922787b..6d8c31c20 100644 --- a/app/presentation/RoomItem/RoomItem.js +++ b/app/presentation/RoomItem/RoomItem.js @@ -42,6 +42,7 @@ const RoomItem = ({ testID, swipeEnabled, onPress, + onLongPress, toggleFav, toggleRead, hideChannel, @@ -49,6 +50,7 @@ const RoomItem = ({ }) => ( { + 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 @@ -237,8 +249,9 @@ class Touchable extends React.Component { transform: [{ translateX: this.transX }] }} > - {children} - + diff --git a/app/presentation/RoomItem/index.js b/app/presentation/RoomItem/index.js index 80bcf063b..ee131867d 100644 --- a/app/presentation/RoomItem/index.js +++ b/app/presentation/RoomItem/index.js @@ -25,6 +25,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, @@ -112,6 +113,11 @@ class RoomItemContainer extends React.Component { return onPress(item); } + onLongPress = () => { + const { item, onLongPress } = this.props; + return onLongPress(item); + } + render() { const { item, @@ -160,6 +166,7 @@ class RoomItemContainer extends React.Component { isGroupChat={this.isGroupChat} isRead={isRead} onPress={this.onPress} + onLongPress={this.onLongPress} date={date} accessibilityLabel={accessibilityLabel} width={width} diff --git a/app/sagas/createChannel.js b/app/sagas/createChannel.js index f8e96aeb1..5cd1c5d0c 100644 --- a/app/sagas/createChannel.js +++ b/app/sagas/createChannel.js @@ -25,6 +25,10 @@ const createTeam = function createTeam(data) { return RocketChat.createTeam(data); }; +const addTeamRoom = function addRoomToTeam(params) { + return RocketChat.addTeamRoom(params); +}; + const handleRequest = function* handleRequest({ data }) { try { const auth = yield select(state => state.login.isAuthenticated); @@ -40,6 +44,7 @@ const handleRequest = function* handleRequest({ data }) { broadcast, encrypted } = data; + // TODO: Create event CT_CREATE logEvent(events.CR_CREATE, { type, readOnly, @@ -67,14 +72,22 @@ const handleRequest = function* handleRequest({ data }) { encrypted }); sub = yield call(createChannel, data); - } + if (data.teamId) { + // TODO: Log when adding room to team + const channels = yield call(addTeamRoom, { rooms: sub.rid, teamId: data.teamId }); + if (channels.success) { + sub.teamId = channels.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.team ? sub.team.roomId : 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); }); }); diff --git a/app/stacks/InsideStack.js b/app/stacks/InsideStack.js index 9052b1310..75758960b 100644 --- a/app/stacks/InsideStack.js +++ b/app/stacks/InsideStack.js @@ -72,6 +72,7 @@ import CreateDiscussionView from '../views/CreateDiscussionView'; import QueueListView from '../ee/omnichannel/views/QueueListView'; import AddChannelTeamView from '../views/AddChannelTeamView'; +import AddExistingChannelView from '../views/AddExistingChannelView'; // ChatsStackNavigator const ChatsStack = createStackNavigator(); @@ -180,6 +181,11 @@ const ChatsStackNavigator = () => { component={AddChannelTeamView} options={AddChannelTeamView.navigationOptions} /> + { navigationMethod = Navigation.replace; } - 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 - }); + 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 { + 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 }) => { diff --git a/app/utils/touch.js b/app/utils/touch.js index d5c22d4df..ae9d01bb0 100644 --- a/app/utils/touch.js +++ b/app/utils/touch.js @@ -15,13 +15,14 @@ class Touch extends React.Component { render() { const { - children, onPress, theme, underlayColor, ...props + children, onPress, onLongPress, theme, underlayColor, ...props } = this.props; return ( ( - - ) + headerTitle: I18n.t('Add_Channel_to_Team') }; if (isMasterDetail) { @@ -74,7 +71,7 @@ class AddChannelTeamView extends React.Component { return ( onPress()} + onPress={onPress} style={{ backgroundColor: themes[theme].backgroundColor }} testID={testID} theme={theme} @@ -88,21 +85,22 @@ class AddChannelTeamView extends React.Component { } render() { - const { navigation } = this.props; + const { navigation, route } = this.props; + const { teamChannels } = route?.params; return ( {this.renderButton({ - onPress: navigation.navigate('NewMessageStackNavigator', { screen: 'CreateChannelView', isTeam: false }), + 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'), + onPress: () => navigation.navigate('AddExistingChannelView', { teamId: this.teamId, teamChannels }), title: I18n.t('Add_Existing'), icon: 'team', testID: 'add-channel-team-view-create-channel' diff --git a/app/views/AddExistingChannelView.js b/app/views/AddExistingChannelView.js new file mode 100644 index 000000000..d8c4da808 --- /dev/null +++ b/app/views/AddExistingChannelView.js @@ -0,0 +1,256 @@ +/* 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 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 + }; + + 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 = () => ; + } else { + options.headerLeft = () => navigation.pop()} tintColor={themes[theme].headerTintColor} />; + } + + options.headerRight = () => selected.length > 0 && ( + + + + ); + + navigation.setOptions(options); + } + + // eslint-disable-next-line react/sort-comp + init = async() => { + try { + const db = database.active; + const channels = await db.collections + .get('subscriptions') + .query( + Q.where('t', 'p'), + Q.where('team_id', ''), + Q.experimentalTake(QUERY_SIZE), + Q.experimentalSortBy('room_updated_at', Q.desc) + ) + .fetch(); + this.setState({ channels }); + } 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 { + // TODO: Log request + const result = await RocketChat.addTeamRooms({ rooms: selected, teamId: this.teamId }); + if (result.success) { + this.setState({ loading: false }); + goRoom(result, isMasterDetail); + } + } catch (e) { + // TODO: Log error + this.setState({ loading: false }); + } + } + + renderChannel = ({ + onPress, testID, title, icon, checked + }) => { + const { theme } = this.props; + return ( + + + + + {title} + + {checked ? : null} + + + ); + } + + renderHeader = () => { + const { theme } = this.props; + return ( + + this.onSearchChangeText(text)} testID='add-existing-channel-view-search' /> + + ); + } + + 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 ( + 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 ( + + + {this.renderList()} + + + ); + } +} + +const mapStateToProps = state => ({ + isMasterDetail: state.app.isMasterDetail +}); + +export default connect(mapStateToProps, null)(withTheme(AddExistingChannelView)); diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js index 54d70d80b..de2804c33 100644 --- a/app/views/CreateChannelView.js +++ b/app/views/CreateChannelView.js @@ -87,13 +87,15 @@ class CreateChannelView extends React.Component { id: PropTypes.string, token: PropTypes.string }), - theme: PropTypes.string + theme: PropTypes.string, + teamId: PropTypes.string }; constructor(props) { super(props); const { route } = this.props; this.isTeam = route?.params?.isTeam || false; + this.teamId = route?.params?.teamId; this.state = { channelName: '', type: true, @@ -173,7 +175,7 @@ class CreateChannelView extends React.Component { // create channel or team create({ - name: channelName, users, type, readOnly, broadcast, encrypted, isTeam: this.isTeam + name: channelName, users, type, readOnly, broadcast, encrypted, isTeam: this.isTeam, teamId: this.teamId }); Review.pushPositiveEvent(); diff --git a/app/views/RoomView/RightButtons.js b/app/views/RoomView/RightButtons.js index 398f868b8..c2c1a43ae 100644 --- a/app/views/RoomView/RightButtons.js +++ b/app/views/RoomView/RightButtons.js @@ -114,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', { @@ -122,7 +122,7 @@ class RightButtonsContainer extends Component { params: { teamId } }); } else { - navigation.navigate('TeamChannelsView', { teamId }); + navigation.navigate('TeamChannelsView', { teamId, rid }); } } diff --git a/app/views/TeamChannelsView.js b/app/views/TeamChannelsView.js index 83c206298..4fa8bb2c0 100644 --- a/app/views/TeamChannelsView.js +++ b/app/views/TeamChannelsView.js @@ -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,16 @@ class TeamChannelsView extends React.Component { theme: PropTypes.string, useRealName: PropTypes.bool, width: PropTypes.number, - StoreLastMessage: PropTypes.bool + StoreLastMessage: PropTypes.bool, + addTeamChannelPermission: 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 +66,11 @@ class TeamChannelsView extends React.Component { isSearching: false, searchText: '', search: [], - end: false + end: false, + showCreate: false }; this.loadTeam(); + this.setHeader(); } componentDidMount() { @@ -70,6 +78,7 @@ class TeamChannelsView extends React.Component { } loadTeam = async() => { + const { addTeamChannelPermission } = this.props; const db = database.active; try { const subCollection = db.get('subscriptions'); @@ -82,6 +91,11 @@ 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()); + } } catch { const { navigation } = this.props; navigation.pop(); @@ -135,8 +149,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; @@ -201,15 +215,11 @@ class TeamChannelsView extends React.Component { options.headerRight = () => ( - navigation.navigate('AddChannelTeamView')} /> + { showCreate + ? navigation.navigate('AddChannelTeamView', { teamId: this.teamId, teamChannels: data })} /> + : null} ); - return options; - } - - setHeader = () => { - const { navigation } = this.props; - const options = this.getHeader(); navigation.setOptions(options); } @@ -288,6 +298,54 @@ 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.removeFromTeam(item.id, this.teamId) + }, + { + title: I18n.t('Delete'), + icon: 'delete', + danger: true, + onPress: this.delete + } + ]) + + delete = () => { + const { room } = this.state; + 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(room.rid, room.t) + } + ], + { cancelable: false } + ); + } + + showChannelActions = (item) => { + logEvent(events.ROOM_SHOW_BOX_ACTIONS); + const { showActionSheet } = this.props; + showActionSheet({ options: this.options(item) }); + } + renderItem = ({ item }) => { const { StoreLastMessage, @@ -303,6 +361,7 @@ class TeamChannelsView extends React.Component { showLastMessage={StoreLastMessage} onPress={this.onPressItem} width={width} + onLongPress={this.showChannelActions} useRealName={useRealName} getRoomTitle={this.getRoomTitle} getRoomAvatar={this.getRoomAvatar} @@ -366,7 +425,12 @@ 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'] }); -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)))));