From 076e5e87c64524210f106022056b2cf19aa33ef4 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Wed, 1 Apr 2020 09:28:54 -0300 Subject: [PATCH] [NEW] Direct Message between multiple users (#1958) * [WIP] DM between multiple users * [WIP][NEW] Create new DM between multiple users * [IMPROVEMENT] Improve createChannel Sagas * [IMPROVEMENT] Selected Users view * [IMPROVEMENT] Room Actions of Group DM * [NEW] Create new DM between multiple users * [NEW] Group DM avatar * [FIX] Directory border * [IMPROVEMENT] Use isGroupChat * [CHORE] Remove legacy getRoomMemberId * [NEW] RoomTypeIcon * [FIX] No use legacy method on RoomInfoView * [FIX] Blink header when create new DM * [FIX] Only show create direct message option when allowed * [FIX] RoomInfoView * pt-BR * Few fixes * Create button name * Show create button only after a user is selected * Fix max users issues Co-authored-by: Diego Mello --- app/constants/settings.js | 3 + app/containers/RoomTypeIcon.js | 6 +- app/i18n/locales/en.js | 2 + app/i18n/locales/pt-BR.js | 2 + app/lib/database/model/Subscription.js | 4 + app/lib/database/model/migrations.js | 12 ++ app/lib/database/schema/app.js | 6 +- .../helpers/mergeSubscriptionsRooms.js | 2 + app/lib/methods/subscriptions/rooms.js | 4 +- app/lib/rocketchat.js | 38 ++++- app/presentation/RoomItem/TypeIcon.js | 9 +- app/presentation/RoomItem/index.js | 10 +- app/presentation/UserItem.js | 4 +- app/sagas/createChannel.js | 33 +++- app/views/CreateChannelView.js | 37 +---- app/views/DirectoryView/index.js | 5 +- app/views/NewMessageView.js | 100 ++++++++---- app/views/RoomActionsView/index.js | 146 +++++++++++------- app/views/RoomActionsView/styles.js | 3 - app/views/RoomInfoView/index.js | 8 +- app/views/RoomView/Header/Header.js | 5 +- app/views/RoomView/Header/Icon.js | 11 +- app/views/RoomView/Header/index.js | 11 +- app/views/RoomView/index.js | 13 +- app/views/RoomsListView/index.js | 14 +- app/views/SelectedUsersView.js | 108 +++++++------ 26 files changed, 383 insertions(+), 213 deletions(-) diff --git a/app/constants/settings.js b/app/constants/settings.js index f01c79ae..122d38f4 100644 --- a/app/constants/settings.js +++ b/app/constants/settings.js @@ -50,6 +50,9 @@ export default { CROWD_Enable: { type: 'valueAsBoolean' }, + DirectMesssage_maxUsers: { + type: 'valueAsNumber' + }, Accounts_Directory_DefaultView: { type: 'valueAsString' }, diff --git a/app/containers/RoomTypeIcon.js b/app/containers/RoomTypeIcon.js index 6933f358..4e4d0b1d 100644 --- a/app/containers/RoomTypeIcon.js +++ b/app/containers/RoomTypeIcon.js @@ -15,7 +15,7 @@ const styles = StyleSheet.create({ }); const RoomTypeIcon = React.memo(({ - type, size, style, theme + type, size, isGroupChat, style, theme }) => { if (!type) { return null; @@ -31,6 +31,9 @@ const RoomTypeIcon = React.memo(({ if (type === 'c') { return ; } if (type === 'd') { + if (isGroupChat) { + return ; + } return ; } if (type === 'l') { return ; @@ -41,6 +44,7 @@ const RoomTypeIcon = React.memo(({ RoomTypeIcon.propTypes = { theme: PropTypes.string, type: PropTypes.string, + isGroupChat: PropTypes.bool, size: PropTypes.number, style: PropTypes.object }; diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index c767abaa..ba77ab0a 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -156,6 +156,7 @@ export default { Whats_the_password_for_your_certificate: 'What\'s the password for your certificate?', Create_account: 'Create an account', Create_Channel: 'Create Channel', + Create_Direct_Messages: 'Create Direct Messages', Create_Discussion: 'Create Discussion', Created_snippet: 'Created a snippet', Create_a_new_workspace: 'Create a new workspace', @@ -263,6 +264,7 @@ export default { Logging_out: 'Logging out.', Logout: 'Logout', Max_number_of_uses: 'Max number of uses', + Max_number_of_users_allowed_is_number: 'Max number of users allowed is {{maxUsers}}', members: 'members', Members: 'Members', Mentioned_Messages: 'Mentioned Messages', diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js index ce010f49..768fdbf3 100644 --- a/app/i18n/locales/pt-BR.js +++ b/app/i18n/locales/pt-BR.js @@ -153,6 +153,7 @@ export default { Permalink: 'Link-Permanente', Create_account: 'Criar conta', Create_Channel: 'Criar Canal', + Create_Direct_Messages: 'Criar Mensagens Diretas', Create_Discussion: 'Criar Discussão', Created_snippet: 'Criou um snippet', Create_a_new_workspace: 'Criar nova área de trabalho', @@ -247,6 +248,7 @@ export default { Logout: 'Sair', Logging_out: 'Saindo.', Max_number_of_uses: 'Número máximo de usos', + Max_number_of_users_allowed_is_number: 'Número máximo de usuários é {{maxUsers}}', Members: 'Membros', Mentioned_Messages: 'Mensagens mencionadas', mentioned: 'mencionado', diff --git a/app/lib/database/model/Subscription.js b/app/lib/database/model/Subscription.js index d40193c5..37bab8c0 100644 --- a/app/lib/database/model/Subscription.js +++ b/app/lib/database/model/Subscription.js @@ -91,4 +91,8 @@ export default class Subscription extends Model { @field('hide_unread_status') hideUnreadStatus; @json('sys_mes', sanitizer) sysMes; + + @json('uids', sanitizer) uids; + + @json('usernames', sanitizer) usernames; } diff --git a/app/lib/database/model/migrations.js b/app/lib/database/model/migrations.js index 14313585..c8d81524 100644 --- a/app/lib/database/model/migrations.js +++ b/app/lib/database/model/migrations.js @@ -62,6 +62,18 @@ export default schemaMigrations({ ] }) ] + }, + { + toVersion: 7, + steps: [ + addColumns({ + table: 'subscriptions', + columns: [ + { name: 'uids', type: 'string', isOptional: true }, + { name: 'usernames', type: 'string', isOptional: true } + ] + }) + ] } ] }); diff --git a/app/lib/database/schema/app.js b/app/lib/database/schema/app.js index 20741b57..a16cb547 100644 --- a/app/lib/database/schema/app.js +++ b/app/lib/database/schema/app.js @@ -1,7 +1,7 @@ import { appSchema, tableSchema } from '@nozbe/watermelondb'; export default appSchema({ - version: 6, + version: 7, tables: [ tableSchema({ name: 'subscriptions', @@ -40,7 +40,9 @@ export default appSchema({ { name: 'auto_translate', type: 'boolean', isOptional: true }, { name: 'auto_translate_language', type: 'string' }, { name: 'hide_unread_status', type: 'boolean', isOptional: true }, - { name: 'sys_mes', type: 'string', isOptional: true } + { name: 'sys_mes', type: 'string', isOptional: true }, + { name: 'uids', type: 'string', isOptional: true }, + { name: 'usernames', type: 'string', isOptional: true } ] }), tableSchema({ diff --git a/app/lib/methods/helpers/mergeSubscriptionsRooms.js b/app/lib/methods/helpers/mergeSubscriptionsRooms.js index 6af040f5..ba26294c 100644 --- a/app/lib/methods/helpers/mergeSubscriptionsRooms.js +++ b/app/lib/methods/helpers/mergeSubscriptionsRooms.js @@ -21,6 +21,8 @@ export const merge = (subscription, room) => { subscription.archived = room.archived || false; subscription.joinCodeRequired = room.joinCodeRequired; subscription.jitsiTimeout = room.jitsiTimeout; + subscription.usernames = room.usernames; + subscription.uids = room.uids; } subscription.ro = room.ro; subscription.broadcast = room.broadcast; diff --git a/app/lib/methods/subscriptions/rooms.js b/app/lib/methods/subscriptions/rooms.js index 7ba99097..e954e6f1 100644 --- a/app/lib/methods/subscriptions/rooms.js +++ b/app/lib/methods/subscriptions/rooms.js @@ -71,7 +71,9 @@ const createOrUpdateSubscription = async(subscription, room) => { jitsiTimeout: s.jitsiTimeout, autoTranslate: s.autoTranslate, autoTranslateLanguage: s.autoTranslateLanguage, - lastMessage: s.lastMessage + lastMessage: s.lastMessage, + usernames: s.usernames, + uids: s.uids }; } catch (error) { try { diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 15c5f904..66b50b48 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -607,6 +607,14 @@ const RocketChat = { return this.sdk.post('im.create', { username }); }, + createGroupChat() { + let { users } = reduxStore.getState().selectedUsers; + users = users.map(u => u.name); + + // RC 3.1.0 + return this.sdk.methodCall('createDirectMessage', ...users); + }, + createDiscussion({ prid, pmid, t_name, reply, users }) { @@ -784,12 +792,27 @@ const RocketChat = { // RC 0.72.0 return this.sdk.get('rooms.info', { roomId }); }, - getRoomMemberId(rid, currentUserId) { - if (rid === `${ currentUserId }${ currentUserId }`) { - return currentUserId; + + getUidDirectMessage(room, userId) { + // legacy method + if (!room.uids && room.rid && room.t === 'd') { + return room.rid.replace(userId, '').trim(); } - return rid.replace(currentUserId, '').trim(); + + if (RocketChat.isGroupChat(room)) { + return false; + } + + const me = room && room.uids && room.uids.find(uid => uid === userId); + const other = room && room.uids && room.uids.filter(uid => uid !== userId); + + return other && other.length ? other[0] : me; }, + + isGroupChat(room) { + return (room.uids && room.uids.length > 2) || (room.usernames && room.usernames.length > 2); + }, + toggleBlockUser(rid, blocked, block) { if (block) { // RC 0.49.0 @@ -1121,9 +1144,16 @@ const RocketChat = { }, getRoomTitle(room) { const { UI_Use_Real_Name: useRealName } = reduxStore.getState().settings; + const { username } = reduxStore.getState().login.user; + if (RocketChat.isGroupChat(room) && !(room.name && room.name.length)) { + return room.usernames.filter(u => u !== username).sort((u1, u2) => u1.localeCompare(u2)).join(', '); + } return ((room.prid || useRealName) && room.fname) || room.name; }, getRoomAvatar(room) { + if (RocketChat.isGroupChat(room)) { + return room.uids.length + room.usernames.join(); + } return room.prid ? room.fname : room.name; }, diff --git a/app/presentation/RoomItem/TypeIcon.js b/app/presentation/RoomItem/TypeIcon.js index 0f807942..19537c07 100644 --- a/app/presentation/RoomItem/TypeIcon.js +++ b/app/presentation/RoomItem/TypeIcon.js @@ -6,19 +6,20 @@ import RoomTypeIcon from '../../containers/RoomTypeIcon'; import styles from './styles'; const TypeIcon = React.memo(({ - theme, type, prid, status + theme, type, prid, status, isGroupChat }) => { - if (type === 'd') { + if (type === 'd' && !isGroupChat) { return ; } - return ; + return ; }); TypeIcon.propTypes = { theme: PropTypes.string, type: PropTypes.string, status: PropTypes.string, - prid: PropTypes.string + prid: PropTypes.string, + isGroupChat: PropTypes.bool }; export default TypeIcon; diff --git a/app/presentation/RoomItem/index.js b/app/presentation/RoomItem/index.js index 2db35a8f..a284ca95 100644 --- a/app/presentation/RoomItem/index.js +++ b/app/presentation/RoomItem/index.js @@ -40,12 +40,11 @@ const arePropsEqual = (oldProps, newProps) => { }; const RoomItem = React.memo(({ - onPress, width, favorite, toggleFav, isRead, rid, toggleRead, hideChannel, testID, unread, userMentions, name, _updatedAt, alert, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, hideUnreadStatus, lastMessage, status, avatar, useRealName, getUserPresence, theme + onPress, width, favorite, toggleFav, isRead, rid, toggleRead, hideChannel, testID, unread, userMentions, name, _updatedAt, alert, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, hideUnreadStatus, lastMessage, status, avatar, useRealName, getUserPresence, isGroupChat, theme }) => { useEffect(() => { - if (type === 'd' && rid) { - const uid = rid.replace(userId, ''); - getUserPresence(uid); + if (type === 'd') { + getUserPresence(id); } }, []); @@ -104,9 +103,9 @@ const RoomItem = React.memo(({ - {name} - @{username} + {name} + @{username} {icon ? : null} diff --git a/app/sagas/createChannel.js b/app/sagas/createChannel.js index 7047505d..86a5b613 100644 --- a/app/sagas/createChannel.js +++ b/app/sagas/createChannel.js @@ -5,11 +5,18 @@ import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { CREATE_CHANNEL, LOGIN } from '../actions/actionsTypes'; import { createChannelSuccess, createChannelFailure } from '../actions/createChannel'; +import { showErrorAlert } from '../utils/info'; import RocketChat from '../lib/rocketchat'; +import Navigation from '../lib/Navigation'; import database from '../lib/database'; +import I18n from '../i18n'; -const create = function* create(data) { - return yield RocketChat.createChannel(data); +const createChannel = function createChannel(data) { + return RocketChat.createChannel(data); +}; + +const createGroupChat = function createGroupChat() { + return RocketChat.createGroupChat(); }; const handleRequest = function* handleRequest({ data }) { @@ -18,7 +25,13 @@ const handleRequest = function* handleRequest({ data }) { if (!auth) { yield take(LOGIN.SUCCESS); } - const sub = yield call(create, data); + + let sub; + if (data.group) { + sub = yield call(createGroupChat); + } else { + sub = yield call(createChannel, data); + } try { const db = database.active; @@ -39,8 +52,22 @@ const handleRequest = function* handleRequest({ data }) { } }; +const handleSuccess = function handleSuccess({ data }) { + const { rid, t } = data; + Navigation.navigate('RoomView', { rid, t, name: RocketChat.getRoomTitle(data) }); +}; + +const handleFailure = function handleFailure({ err }) { + setTimeout(() => { + const msg = err.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') }); + showErrorAlert(msg); + }, 300); +}; + const root = function* root() { yield takeLatest(CREATE_CHANNEL.REQUEST, handleRequest); + yield takeLatest(CREATE_CHANNEL.SUCCESS, handleSuccess); + yield takeLatest(CREATE_CHANNEL.FAILURE, handleFailure); }; export default root; diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js index 02ecfc66..4709afd6 100644 --- a/app/views/CreateChannelView.js +++ b/app/views/CreateChannelView.js @@ -16,7 +16,6 @@ import KeyboardView from '../presentation/KeyboardView'; import scrollPersistTaps from '../utils/scrollPersistTaps'; import I18n from '../i18n'; import UserItem from '../presentation/UserItem'; -import { showErrorAlert } from '../utils/info'; import { CustomHeaderButtons, Item } from '../containers/HeaderButton'; import StatusBar from '../containers/StatusBar'; import { SWITCH_TRACK_COLOR, themes } from '../constants/colors'; @@ -100,7 +99,6 @@ class CreateChannelView extends React.Component { error: PropTypes.object, failure: PropTypes.bool, isFetching: PropTypes.bool, - result: PropTypes.object, users: PropTypes.array.isRequired, user: PropTypes.shape({ id: PropTypes.string, @@ -125,9 +123,7 @@ class CreateChannelView extends React.Component { const { channelName, type, readOnly, broadcast } = this.state; - const { - error, failure, isFetching, result, users, theme - } = this.props; + const { users, isFetching, theme } = this.props; if (nextProps.theme !== theme) { return true; } @@ -143,43 +139,15 @@ class CreateChannelView extends React.Component { if (nextState.broadcast !== broadcast) { return true; } - if (nextProps.failure !== failure) { - return true; - } if (nextProps.isFetching !== isFetching) { return true; } - if (!equal(nextProps.error, error)) { - return true; - } - if (!equal(nextProps.result, result)) { - return true; - } if (!equal(nextProps.users, users)) { return true; } return false; } - componentDidUpdate(prevProps) { - const { - isFetching, failure, error, result, navigation - } = this.props; - - if (!isFetching && isFetching !== prevProps.isFetching) { - setTimeout(() => { - if (failure) { - const msg = error.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') }); - showErrorAlert(msg); - } else { - const { type } = this.state; - const { rid, name } = result; - navigation.navigate('RoomView', { rid, name, t: type ? 'p' : 'c' }); - } - }, 300); - } - } - onChangeText = (channelName) => { const { navigation } = this.props; navigation.setParams({ showSubmit: channelName.trim().length > 0 }); @@ -365,10 +333,7 @@ class CreateChannelView extends React.Component { const mapStateToProps = state => ({ baseUrl: state.server.server, - error: state.createChannel.error, - failure: state.createChannel.failure, isFetching: state.createChannel.isFetching, - result: state.createChannel.result, users: state.selectedUsers.users, user: getUserSelector(state) }); diff --git a/app/views/DirectoryView/index.js b/app/views/DirectoryView/index.js index e54f966c..3828a747 100644 --- a/app/views/DirectoryView/index.js +++ b/app/views/DirectoryView/index.js @@ -180,7 +180,10 @@ class DirectoryView extends React.Component { let style; if (index === data.length - 1) { - style = sharedStyles.separatorBottom; + style = { + ...sharedStyles.separatorBottom, + borderColor: themes[theme].separatorColor + }; } const commonProps = { diff --git a/app/views/NewMessageView.js b/app/views/NewMessageView.js index 69f61f1d..cecd0988 100644 --- a/app/views/NewMessageView.js +++ b/app/views/NewMessageView.js @@ -25,6 +25,7 @@ import { withTheme } from '../theme'; import { themedHeader } from '../utils/navigation'; import { getUserSelector } from '../selectors/login'; import Navigation from '../lib/Navigation'; +import { createChannelRequest } from '../actions/createChannel'; const styles = StyleSheet.create({ safeAreaView: { @@ -33,24 +34,21 @@ const styles = StyleSheet.create({ separator: { marginLeft: 60 }, - createChannelButton: { - marginTop: 25 - }, - createDiscussionButton: { - marginBottom: 25 - }, - createChannelContainer: { + button: { height: 46, flexDirection: 'row', alignItems: 'center' }, - createChannelIcon: { + buttonIcon: { marginLeft: 18, marginRight: 16 }, - createChannelText: { + buttonText: { fontSize: 17, ...sharedStyles.textRegular + }, + buttonContainer: { + paddingVertical: 25 } }); @@ -68,6 +66,8 @@ class NewMessageView extends React.Component { id: PropTypes.string, token: PropTypes.string }), + createChannel: PropTypes.func, + maxUsers: PropTypes.number, theme: PropTypes.string }; @@ -143,7 +143,35 @@ class NewMessageView extends React.Component { createChannel = () => { const { navigation } = this.props; - navigation.navigate('SelectedUsersViewCreateChannel', { nextActionID: 'CREATE_CHANNEL', title: I18n.t('Select_Users') }); + navigation.navigate('SelectedUsersViewCreateChannel', { nextAction: () => navigation.navigate('CreateChannelView') }); + } + + createGroupChat = () => { + const { createChannel, maxUsers, navigation } = this.props; + navigation.navigate('SelectedUsersViewCreateChannel', { + nextAction: () => createChannel({ group: true }), + buttonText: I18n.t('Create'), + maxUsers + }); + } + + renderButton = ({ + onPress, testID, title, icon, first + }) => { + const { theme } = this.props; + return ( + + + + {title} + + + ); } createDiscussion = () => { @@ -151,32 +179,31 @@ class NewMessageView extends React.Component { } renderHeader = () => { - const { theme } = this.props; + const { maxUsers, theme } = this.props; return ( this.onSearchChangeText(text)} testID='new-message-view-search' /> - - - - {I18n.t('Create_Channel')} - - - - - - {I18n.t('Create_Discussion')} - - + + {this.renderButton({ + onPress: this.createChannel, + title: I18n.t('Create_Channel'), + icon: 'hashtag', + testID: 'new-message-view-create-channel', + first: true + })} + {maxUsers > 2 ? this.renderButton({ + onPress: this.createGroupChat, + title: I18n.t('Create_Direct_Messages'), + icon: 'team', + testID: 'new-message-view-create-direct-message' + }) : null} + {this.renderButton({ + onPress: this.createDiscussion, + title: I18n.t('Create_Discussion'), + icon: 'chat', + testID: 'new-message-view-create-discussion' + })} + ); } @@ -248,7 +275,12 @@ class NewMessageView extends React.Component { const mapStateToProps = state => ({ baseUrl: state.server.server, + maxUsers: state.settings.DirectMesssage_maxUsers || 1, user: getUserSelector(state) }); -export default connect(mapStateToProps)(withTheme(NewMessageView)); +const mapDispatchToProps = dispatch => ({ + createChannel: params => dispatch(createChannelRequest(params)) +}); + +export default connect(mapStateToProps, mapDispatchToProps)(withTheme(NewMessageView)); diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index ba184f1d..7fe261a8 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -8,6 +8,7 @@ import { SafeAreaView } from 'react-navigation'; import _ from 'lodash'; import Touch from '../../utils/touch'; +import { setLoading as setLoadingAction } from '../../actions/selectedUsers'; import { leaveRoom as leaveRoomAction } from '../../actions/room'; import styles from './styles'; import sharedStyles from '../Styles'; @@ -49,6 +50,7 @@ class RoomActionsView extends React.Component { }), leaveRoom: PropTypes.func, jitsiEnabled: PropTypes.bool, + setLoadingInvite: PropTypes.func, theme: PropTypes.string } @@ -190,6 +192,7 @@ class RoomActionsView extends React.Component { const { rid, t, blocker } = room; + const isGroupChat = RocketChat.isGroupChat(room); const notificationsAction = { icon: 'bell', @@ -223,6 +226,7 @@ class RoomActionsView extends React.Component { params: { rid, t, room, member }, + disabled: isGroupChat, testID: 'room-actions-info' }], renderItem: this.renderRoomInfo @@ -286,7 +290,18 @@ class RoomActionsView extends React.Component { }); } - if (t === 'd') { + if (isGroupChat) { + sections[2].data.unshift({ + icon: 'team', + name: I18n.t('Members'), + description: membersCount > 0 ? `${ membersCount } ${ I18n.t('members') }` : null, + route: 'RoomMembersView', + params: { rid, room }, + testID: 'room-actions-members' + }); + } + + if (t === 'd' && !isGroupChat) { sections.push({ data: [ { @@ -320,9 +335,9 @@ class RoomActionsView extends React.Component { name: I18n.t('Add_users'), route: 'SelectedUsersView', params: { - nextActionID: 'ADD_USER', rid, - title: I18n.t('Add_users') + title: I18n.t('Add_users'), + nextAction: this.addUser }, testID: 'room-actions-add-user' }); @@ -369,14 +384,15 @@ class RoomActionsView extends React.Component { updateRoomMember = async() => { const { room } = this.state; - const { rid } = room; const { user } = this.props; try { - const roomUserId = RocketChat.getRoomMemberId(rid, user.id); - const result = await RocketChat.getUserInfo(roomUserId); - if (result.success) { - this.setState({ member: result.user }); + if (!RocketChat.isGroupChat(room)) { + const roomUserId = RocketChat.getUidDirectMessage(room, user.id); + const result = await RocketChat.getUserInfo(roomUserId); + if (result.success) { + this.setState({ member: result.user }); + } } } catch (e) { log(e); @@ -384,6 +400,21 @@ class RoomActionsView extends React.Component { } } + addUser = async() => { + const { room } = this.state; + const { setLoadingInvite, navigation } = this.props; + const { rid } = room; + try { + setLoadingInvite(true); + await RocketChat.addUsersToRoom(rid); + navigation.pop(); + } catch (e) { + log(e); + } finally { + setLoadingInvite(false); + } + } + toggleBlockUser = () => { const { room } = this.state; const { rid, blocker } = room; @@ -432,41 +463,44 @@ class RoomActionsView extends React.Component { const { name, t, topic } = room; const { baseUrl, user, theme } = this.props; + const avatar = RocketChat.getRoomAvatar(room); + return ( - this.renderTouchableItem([ - - {t === 'd' && member._id ? : null } - , - - {room.t === 'd' - ? {room.fname} - : ( - - - {room.prid ? room.fname : room.name} - - ) - } - - {room.t === 'd' && } - , - - ], item) + this.renderTouchableItem(( + <> + + {t === 'd' && member._id ? : null } + + + {room.t === 'd' + ? {room.fname} + : ( + + + {room.prid ? room.fname : room.name} + + ) + } + + {room.t === 'd' && } + + {!item.disabled && } + + ), item) ); } @@ -478,10 +512,11 @@ class RoomActionsView extends React.Component { style={{ backgroundColor: themes[theme].backgroundColor }} accessibilityLabel={item.name} accessibilityTraits='button' + enabled={!item.disabled} testID={item.testID} theme={theme} > - + {subview} @@ -491,15 +526,19 @@ class RoomActionsView extends React.Component { renderItem = ({ item }) => { const { theme } = this.props; const colorDanger = { color: themes[theme].dangerColor }; - const subview = item.type === 'danger' ? [ - , - { item.name } - ] : [ - , - { item.name }, - item.description ? { item.description } : null, - - ]; + const subview = item.type === 'danger' ? ( + <> + + { item.name } + + ) : ( + <> + + { item.name } + {item.description ? { item.description } : null} + + + ); return this.renderTouchableItem(subview, item); } @@ -542,7 +581,8 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ - leaveRoom: (rid, t) => dispatch(leaveRoomAction(rid, t)) + leaveRoom: (rid, t) => dispatch(leaveRoomAction(rid, t)), + setLoadingInvite: loading => dispatch(setLoadingAction(loading)) }); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(RoomActionsView)); diff --git a/app/views/RoomActionsView/styles.js b/app/views/RoomActionsView/styles.js index 1461cba3..8021dfb2 100644 --- a/app/views/RoomActionsView/styles.js +++ b/app/views/RoomActionsView/styles.js @@ -14,9 +14,6 @@ export default StyleSheet.create({ flexDirection: 'row', alignItems: 'center' }, - sectionItemDisabled: { - opacity: 0.3 - }, sectionItemIcon: { width: 56, textAlign: 'center' diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js index 610d49f6..184109ea 100644 --- a/app/views/RoomInfoView/index.js +++ b/app/views/RoomInfoView/index.js @@ -77,22 +77,21 @@ class RoomInfoView extends React.Component { this.rid = props.navigation.getParam('rid'); this.t = props.navigation.getParam('t'); this.state = { - room: room || {}, + room: room || { rid: this.rid, t: this.t }, roomUser: roomUser || {}, parsedRoles: [] }; } async componentDidMount() { - const { roomUser } = this.state; + const { roomUser, room: roomState } = this.state; if (this.t === 'd' && !_.isEmpty(roomUser)) { return; } if (this.t === 'd') { - const { user } = this.props; - const roomUserId = RocketChat.getRoomMemberId(this.rid, user.id); try { + const roomUserId = RocketChat.getUidDirectMessage(roomState); const result = await RocketChat.getUserInfo(roomUserId); if (result.success) { const { roles } = result.user; @@ -110,6 +109,7 @@ class RoomInfoView extends React.Component { } return; } + const { navigation } = this.props; let room = navigation.getParam('room'); if (room && room.observe) { diff --git a/app/views/RoomView/Header/Header.js b/app/views/RoomView/Header/Header.js index f8594665..2a668f0e 100644 --- a/app/views/RoomView/Header/Header.js +++ b/app/views/RoomView/Header/Header.js @@ -125,7 +125,7 @@ HeaderTitle.propTypes = { }; const Header = React.memo(({ - title, subtitle, type, status, usersTyping, width, height, prid, tmid, widthOffset, connecting, goRoomActionsView, theme + title, subtitle, type, status, usersTyping, width, height, prid, tmid, widthOffset, connecting, goRoomActionsView, roomUserId, theme }) => { const portrait = height > width; let scale = 1; @@ -146,7 +146,7 @@ const Header = React.memo(({ disabled={tmid} > - + { - if (type === 'd') { +const Icon = React.memo(({ + roomUserId, type, status, theme +}) => { + if (type === 'd' && roomUserId) { return ; } let colorStyle = {}; - if (type === 'd') { + if (type === 'd' && roomUserId) { colorStyle = { color: STATUS_COLORS[status] }; } else { colorStyle = { color: isAndroid && theme === 'light' ? themes[theme].buttonText : themes[theme].auxiliaryText }; @@ -42,6 +44,8 @@ const Icon = React.memo(({ type, status, theme }) => { icon = 'hashtag'; } else if (type === 'l') { icon = 'livechat'; + } else if (type === 'd') { + icon = 'team'; } else { icon = 'lock'; } @@ -62,6 +66,7 @@ const Icon = React.memo(({ type, status, theme }) => { }); Icon.propTypes = { + roomUserId: PropTypes.string, type: PropTypes.string, status: PropTypes.string, theme: PropTypes.string diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js index a6497a07..7e91d3af 100644 --- a/app/views/RoomView/Header/index.js +++ b/app/views/RoomView/Header/index.js @@ -23,6 +23,7 @@ class RoomHeaderView extends Component { statusText: PropTypes.string, connecting: PropTypes.bool, theme: PropTypes.string, + roomUserId: PropTypes.string, widthOffset: PropTypes.number, goRoomActionsView: PropTypes.func }; @@ -69,7 +70,7 @@ class RoomHeaderView extends Component { render() { const { - window, title, subtitle, type, prid, tmid, widthOffset, status = 'offline', statusText, connecting, usersTyping, goRoomActionsView, theme + window, title, subtitle, type, prid, tmid, widthOffset, status = 'offline', statusText, connecting, usersTyping, goRoomActionsView, roomUserId, theme } = this.props; return ( @@ -85,6 +86,7 @@ class RoomHeaderView extends Component { theme={theme} usersTyping={usersTyping} widthOffset={widthOffset} + roomUserId={roomUserId} goRoomActionsView={goRoomActionsView} connecting={connecting} /> @@ -95,13 +97,12 @@ class RoomHeaderView extends Component { const mapStateToProps = (state, ownProps) => { let status; let statusText; - const { rid, type } = ownProps; + const { roomUserId, type } = ownProps; if (type === 'd') { const user = getUserSelector(state); if (user.id) { - const userId = rid.replace(user.id, '').trim(); - if (state.activeUsers[userId]) { - ({ status, statusText } = state.activeUsers[userId]); + if (state.activeUsers[roomUserId]) { + ({ status, statusText } = state.activeUsers[roomUserId]); } } } diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index d43d4c91..41d14e6f 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -84,6 +84,7 @@ class RoomView extends React.Component { const toggleFollowThread = navigation.getParam('toggleFollowThread', () => {}); const goRoomActionsView = navigation.getParam('goRoomActionsView', () => {}); const unreadsCount = navigation.getParam('unreadsCount', null); + const roomUserId = navigation.getParam('roomUserId'); if (!rid) { return { ...themedHeader(screenProps.theme) @@ -100,6 +101,7 @@ class RoomView extends React.Component { subtitle={subtitle} type={t} widthOffset={tmid ? 95 : 130} + roomUserId={roomUserId} goRoomActionsView={goRoomActionsView} /> ), @@ -382,13 +384,16 @@ class RoomView extends React.Component { getRoomMember = async() => { const { room } = this.state; - const { rid, t } = room; + const { t } = room; - if (t === 'd') { - const { user } = this.props; + if (t === 'd' && !RocketChat.isGroupChat(room)) { + const { user, navigation } = this.props; try { - const roomUserId = RocketChat.getRoomMemberId(rid, user.id); + const roomUserId = RocketChat.getUidDirectMessage(room, user.id); + + navigation.setParams({ roomUserId }); + const result = await RocketChat.getUserInfo(roomUserId); if (result.success) { return result.user; diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js index bc9a09bb..d8b6e472 100644 --- a/app/views/RoomsListView/index.js +++ b/app/views/RoomsListView/index.js @@ -412,7 +412,9 @@ class RoomsListView extends React.Component { key: item._id, rid: item.rid, type: item.t, - prid: item.prid + prid: item.prid, + uids: item.uids, + usernames: item.usernames })); // unread @@ -526,6 +528,11 @@ class RoomsListView extends React.Component { getUserPresence = uid => RocketChat.getUserPresence(uid) + getUidDirectMessage = (room) => { + const { user: { id } } = this.props; + return RocketChat.getUidDirectMessage(room, id); + } + goRoom = (item) => { const { navigation } = this.props; this.cancelSearch(); @@ -535,6 +542,7 @@ class RoomsListView extends React.Component { name: this.getRoomTitle(item), t: item.t, prid: item.prid, + roomUserId: this.getUidDirectMessage(item), room: item }); } @@ -764,7 +772,8 @@ class RoomsListView extends React.Component { theme, split } = this.props; - const id = item.rid.replace(userId, '').trim(); + const id = this.getUidDirectMessage(item); + const isGroupChat = RocketChat.isGroupChat(item); return ( ); }; diff --git a/app/views/SelectedUsersView.js b/app/views/SelectedUsersView.js index 530b7e3d..1a1ac044 100644 --- a/app/views/SelectedUsersView.js +++ b/app/views/SelectedUsersView.js @@ -7,14 +7,10 @@ import equal from 'deep-equal'; import { orderBy } from 'lodash'; import { Q } from '@nozbe/watermelondb'; -import { - addUser as addUserAction, removeUser as removeUserAction, reset as resetAction, setLoading as setLoadingAction -} from '../actions/selectedUsers'; import database from '../lib/database'; import RocketChat from '../lib/rocketchat'; import UserItem from '../presentation/UserItem'; import Loading from '../containers/Loading'; -import debounce from '../utils/debounce'; import I18n from '../i18n'; import log from '../utils/log'; import SearchBox from '../containers/SearchBox'; @@ -26,6 +22,12 @@ import { animateNextTransition } from '../utils/layoutAnimation'; import { withTheme } from '../theme'; import { themedHeader } from '../utils/navigation'; import { getUserSelector } from '../selectors/login'; +import { + reset as resetAction, + addUser as addUserAction, + removeUser as removeUserAction +} from '../actions/selectedUsers'; +import { showErrorAlert } from '../utils/info'; const styles = StyleSheet.create({ safeAreaView: { @@ -38,47 +40,55 @@ const styles = StyleSheet.create({ class SelectedUsersView extends React.Component { static navigationOptions = ({ navigation, screenProps }) => { - const title = navigation.getParam('title'); + const title = navigation.getParam('title', I18n.t('Select_Users')); + const buttonText = navigation.getParam('buttonText', I18n.t('Next')); + const showButton = navigation.getParam('showButton', false); + const maxUsers = navigation.getParam('maxUsers'); const nextAction = navigation.getParam('nextAction', () => {}); return { ...themedHeader(screenProps.theme), title, headerRight: ( - - - + (!maxUsers || showButton) && ( + + + + ) ) }; } static propTypes = { - navigation: PropTypes.object, baseUrl: PropTypes.string, addUser: PropTypes.func.isRequired, removeUser: PropTypes.func.isRequired, reset: PropTypes.func.isRequired, users: PropTypes.array, loading: PropTypes.bool, - setLoadingInvite: PropTypes.func, user: PropTypes.shape({ id: PropTypes.string, - token: PropTypes.string + token: PropTypes.string, + username: PropTypes.string, + name: PropTypes.string }), + navigation: PropTypes.object, theme: PropTypes.string }; constructor(props) { super(props); this.init(); + + const maxUsers = props.navigation.getParam('maxUsers'); this.state = { + maxUsers, search: [], chats: [] }; - } - - componentDidMount() { - const { navigation } = this.props; - navigation.setParams({ nextAction: this.nextAction }); + const { user } = this.props; + if (this.isGroupChat()) { + props.addUser({ _id: user.id, name: user.username, fname: user.name }); + } } shouldComponentUpdate(nextProps, nextState) { @@ -102,6 +112,19 @@ class SelectedUsersView extends React.Component { return false; } + componentDidUpdate(prevProps) { + if (this.isGroupChat()) { + const { users, navigation } = this.props; + if (prevProps.users.length !== users.length) { + if (users.length) { + navigation.setParams({ showButton: true }); + } else { + navigation.setParams({ showButton: false }); + } + } + } + } + componentWillUnmount() { const { reset } = this.props; reset(); @@ -132,30 +155,6 @@ class SelectedUsersView extends React.Component { this.search(text); } - nextAction = async() => { - const { navigation, setLoadingInvite } = this.props; - const nextActionID = navigation.getParam('nextActionID'); - if (nextActionID === 'CREATE_CHANNEL') { - navigation.navigate('CreateChannelView'); - } else { - const rid = navigation.getParam('rid'); - try { - setLoadingInvite(true); - await RocketChat.addUsersToRoom(rid); - navigation.pop(); - } catch (e) { - log(e); - } finally { - setLoadingInvite(false); - } - } - } - - // eslint-disable-next-line react/sort-comp - updateState = debounce(() => { - this.forceUpdate(); - }, 1000); - search = async(text) => { const result = await RocketChat.search({ text, filterRooms: false }); this.setState({ @@ -163,16 +162,33 @@ class SelectedUsersView extends React.Component { }); } + isGroupChat = () => { + const { maxUsers } = this.state; + return maxUsers > 2; + } + isChecked = (username) => { const { users } = this.props; return users.findIndex(el => el.name === username) !== -1; } toggleUser = (user) => { - const { addUser, removeUser } = this.props; + const { maxUsers } = this.state; + const { + addUser, removeUser, users, user: { username } + } = this.props; + + // Disallow removing self user from the direct message group + if (this.isGroupChat() && username === user.name) { + return; + } animateNextTransition(); if (!this.isChecked(user.name)) { + if (this.isGroupChat() && users.length === maxUsers) { + return showErrorAlert(I18n.t('Max_number_of_users_allowed_is_number', { maxUsers }), I18n.t('Oops')); + } + addUser(user); } else { removeUser(user); @@ -274,9 +290,14 @@ class SelectedUsersView extends React.Component { renderList = () => { const { search, chats } = this.state; const { theme } = this.props; + + const data = (search.length > 0 ? search : chats) + // filter DM between multiple users + .filter(sub => !RocketChat.isGroupChat(sub)); + return ( 0 ? search : chats} + data={data} extraData={this.props} keyExtractor={item => item._id} renderItem={this.renderItem} @@ -315,8 +336,7 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ addUser: user => dispatch(addUserAction(user)), removeUser: user => dispatch(removeUserAction(user)), - reset: () => dispatch(resetAction()), - setLoadingInvite: loading => dispatch(setLoadingAction(loading)) + reset: () => dispatch(resetAction()) }); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(SelectedUsersView));