[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 <diegolmello@gmail.com>
This commit is contained in:
Djorkaeff Alexandre 2020-04-01 09:28:54 -03:00 committed by GitHub
parent ece8f44f5a
commit 076e5e87c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 383 additions and 213 deletions

View File

@ -50,6 +50,9 @@ export default {
CROWD_Enable: { CROWD_Enable: {
type: 'valueAsBoolean' type: 'valueAsBoolean'
}, },
DirectMesssage_maxUsers: {
type: 'valueAsNumber'
},
Accounts_Directory_DefaultView: { Accounts_Directory_DefaultView: {
type: 'valueAsString' type: 'valueAsString'
}, },

View File

@ -15,7 +15,7 @@ const styles = StyleSheet.create({
}); });
const RoomTypeIcon = React.memo(({ const RoomTypeIcon = React.memo(({
type, size, style, theme type, size, isGroupChat, style, theme
}) => { }) => {
if (!type) { if (!type) {
return null; return null;
@ -31,6 +31,9 @@ const RoomTypeIcon = React.memo(({
if (type === 'c') { if (type === 'c') {
return <Image source={{ uri: 'hashtag' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />; return <Image source={{ uri: 'hashtag' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />;
} if (type === 'd') { } if (type === 'd') {
if (isGroupChat) {
return <CustomIcon name='team' size={13} style={[styles.style, styles.discussion, { color }]} />;
}
return <CustomIcon name='at' size={13} style={[styles.style, styles.discussion, { color }]} />; return <CustomIcon name='at' size={13} style={[styles.style, styles.discussion, { color }]} />;
} if (type === 'l') { } if (type === 'l') {
return <CustomIcon name='livechat' size={13} style={[styles.style, styles.discussion, { color }]} />; return <CustomIcon name='livechat' size={13} style={[styles.style, styles.discussion, { color }]} />;
@ -41,6 +44,7 @@ const RoomTypeIcon = React.memo(({
RoomTypeIcon.propTypes = { RoomTypeIcon.propTypes = {
theme: PropTypes.string, theme: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
isGroupChat: PropTypes.bool,
size: PropTypes.number, size: PropTypes.number,
style: PropTypes.object style: PropTypes.object
}; };

View File

@ -156,6 +156,7 @@ export default {
Whats_the_password_for_your_certificate: 'What\'s the password for your certificate?', Whats_the_password_for_your_certificate: 'What\'s the password for your certificate?',
Create_account: 'Create an account', Create_account: 'Create an account',
Create_Channel: 'Create Channel', Create_Channel: 'Create Channel',
Create_Direct_Messages: 'Create Direct Messages',
Create_Discussion: 'Create Discussion', Create_Discussion: 'Create Discussion',
Created_snippet: 'Created a snippet', Created_snippet: 'Created a snippet',
Create_a_new_workspace: 'Create a new workspace', Create_a_new_workspace: 'Create a new workspace',
@ -263,6 +264,7 @@ export default {
Logging_out: 'Logging out.', Logging_out: 'Logging out.',
Logout: 'Logout', Logout: 'Logout',
Max_number_of_uses: 'Max number of uses', 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',
Members: 'Members', Members: 'Members',
Mentioned_Messages: 'Mentioned Messages', Mentioned_Messages: 'Mentioned Messages',

View File

@ -153,6 +153,7 @@ export default {
Permalink: 'Link-Permanente', Permalink: 'Link-Permanente',
Create_account: 'Criar conta', Create_account: 'Criar conta',
Create_Channel: 'Criar Canal', Create_Channel: 'Criar Canal',
Create_Direct_Messages: 'Criar Mensagens Diretas',
Create_Discussion: 'Criar Discussão', Create_Discussion: 'Criar Discussão',
Created_snippet: 'Criou um snippet', Created_snippet: 'Criou um snippet',
Create_a_new_workspace: 'Criar nova área de trabalho', Create_a_new_workspace: 'Criar nova área de trabalho',
@ -247,6 +248,7 @@ export default {
Logout: 'Sair', Logout: 'Sair',
Logging_out: 'Saindo.', Logging_out: 'Saindo.',
Max_number_of_uses: 'Número máximo de usos', 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', Members: 'Membros',
Mentioned_Messages: 'Mensagens mencionadas', Mentioned_Messages: 'Mensagens mencionadas',
mentioned: 'mencionado', mentioned: 'mencionado',

View File

@ -91,4 +91,8 @@ export default class Subscription extends Model {
@field('hide_unread_status') hideUnreadStatus; @field('hide_unread_status') hideUnreadStatus;
@json('sys_mes', sanitizer) sysMes; @json('sys_mes', sanitizer) sysMes;
@json('uids', sanitizer) uids;
@json('usernames', sanitizer) usernames;
} }

View File

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

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb'; import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({ export default appSchema({
version: 6, version: 7,
tables: [ tables: [
tableSchema({ tableSchema({
name: 'subscriptions', name: 'subscriptions',
@ -40,7 +40,9 @@ export default appSchema({
{ name: 'auto_translate', type: 'boolean', isOptional: true }, { name: 'auto_translate', type: 'boolean', isOptional: true },
{ name: 'auto_translate_language', type: 'string' }, { name: 'auto_translate_language', type: 'string' },
{ name: 'hide_unread_status', type: 'boolean', isOptional: true }, { 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({ tableSchema({

View File

@ -21,6 +21,8 @@ export const merge = (subscription, room) => {
subscription.archived = room.archived || false; subscription.archived = room.archived || false;
subscription.joinCodeRequired = room.joinCodeRequired; subscription.joinCodeRequired = room.joinCodeRequired;
subscription.jitsiTimeout = room.jitsiTimeout; subscription.jitsiTimeout = room.jitsiTimeout;
subscription.usernames = room.usernames;
subscription.uids = room.uids;
} }
subscription.ro = room.ro; subscription.ro = room.ro;
subscription.broadcast = room.broadcast; subscription.broadcast = room.broadcast;

View File

@ -71,7 +71,9 @@ const createOrUpdateSubscription = async(subscription, room) => {
jitsiTimeout: s.jitsiTimeout, jitsiTimeout: s.jitsiTimeout,
autoTranslate: s.autoTranslate, autoTranslate: s.autoTranslate,
autoTranslateLanguage: s.autoTranslateLanguage, autoTranslateLanguage: s.autoTranslateLanguage,
lastMessage: s.lastMessage lastMessage: s.lastMessage,
usernames: s.usernames,
uids: s.uids
}; };
} catch (error) { } catch (error) {
try { try {

View File

@ -607,6 +607,14 @@ const RocketChat = {
return this.sdk.post('im.create', { username }); 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({ createDiscussion({
prid, pmid, t_name, reply, users prid, pmid, t_name, reply, users
}) { }) {
@ -784,12 +792,27 @@ const RocketChat = {
// RC 0.72.0 // RC 0.72.0
return this.sdk.get('rooms.info', { roomId }); return this.sdk.get('rooms.info', { roomId });
}, },
getRoomMemberId(rid, currentUserId) {
if (rid === `${ currentUserId }${ currentUserId }`) { getUidDirectMessage(room, userId) {
return currentUserId; // 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) { toggleBlockUser(rid, blocked, block) {
if (block) { if (block) {
// RC 0.49.0 // RC 0.49.0
@ -1121,9 +1144,16 @@ const RocketChat = {
}, },
getRoomTitle(room) { getRoomTitle(room) {
const { UI_Use_Real_Name: useRealName } = reduxStore.getState().settings; 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; return ((room.prid || useRealName) && room.fname) || room.name;
}, },
getRoomAvatar(room) { getRoomAvatar(room) {
if (RocketChat.isGroupChat(room)) {
return room.uids.length + room.usernames.join();
}
return room.prid ? room.fname : room.name; return room.prid ? room.fname : room.name;
}, },

View File

@ -6,19 +6,20 @@ import RoomTypeIcon from '../../containers/RoomTypeIcon';
import styles from './styles'; import styles from './styles';
const TypeIcon = React.memo(({ const TypeIcon = React.memo(({
theme, type, prid, status theme, type, prid, status, isGroupChat
}) => { }) => {
if (type === 'd') { if (type === 'd' && !isGroupChat) {
return <Status style={styles.status} size={10} status={status} />; return <Status style={styles.status} size={10} status={status} />;
} }
return <RoomTypeIcon theme={theme} type={prid ? 'discussion' : type} />; return <RoomTypeIcon theme={theme} type={prid ? 'discussion' : type} isGroupChat={isGroupChat} />;
}); });
TypeIcon.propTypes = { TypeIcon.propTypes = {
theme: PropTypes.string, theme: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
status: PropTypes.string, status: PropTypes.string,
prid: PropTypes.string prid: PropTypes.string,
isGroupChat: PropTypes.bool
}; };
export default TypeIcon; export default TypeIcon;

View File

@ -40,12 +40,11 @@ const arePropsEqual = (oldProps, newProps) => {
}; };
const RoomItem = React.memo(({ 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(() => { useEffect(() => {
if (type === 'd' && rid) { if (type === 'd') {
const uid = rid.replace(userId, ''); getUserPresence(id);
getUserPresence(uid);
} }
}, []); }, []);
@ -104,9 +103,9 @@ const RoomItem = React.memo(({
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<TypeIcon <TypeIcon
type={type} type={type}
id={id}
prid={prid} prid={prid}
status={status} status={status}
isGroupChat={isGroupChat}
theme={theme} theme={theme}
/> />
<Text <Text
@ -198,6 +197,7 @@ RoomItem.propTypes = {
hideUnreadStatus: PropTypes.bool, hideUnreadStatus: PropTypes.bool,
useRealName: PropTypes.bool, useRealName: PropTypes.bool,
getUserPresence: PropTypes.func, getUserPresence: PropTypes.func,
isGroupChat: PropTypes.bool,
theme: PropTypes.string theme: PropTypes.string
}; };

View File

@ -64,8 +64,8 @@ const UserItem = ({
<View style={[styles.container, styles.button, style]}> <View style={[styles.container, styles.button, style]}>
<Avatar text={username} size={30} type='d' style={styles.avatar} baseUrl={baseUrl} userId={user.id} token={user.token} /> <Avatar text={username} size={30} type='d' style={styles.avatar} baseUrl={baseUrl} userId={user.id} token={user.token} />
<View style={styles.textContainer}> <View style={styles.textContainer}>
<Text style={[styles.name, { color: themes[theme].titleText }]}>{name}</Text> <Text style={[styles.name, { color: themes[theme].titleText }]} numberOfLines={1}>{name}</Text>
<Text style={[styles.username, { color: themes[theme].auxiliaryText }]}>@{username}</Text> <Text style={[styles.username, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>@{username}</Text>
</View> </View>
{icon ? <CustomIcon name={icon} size={22} style={[styles.icon, { color: themes[theme].actionTintColor }]} /> : null} {icon ? <CustomIcon name={icon} size={22} style={[styles.icon, { color: themes[theme].actionTintColor }]} /> : null}
</View> </View>

View File

@ -5,11 +5,18 @@ import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { CREATE_CHANNEL, LOGIN } from '../actions/actionsTypes'; import { CREATE_CHANNEL, LOGIN } from '../actions/actionsTypes';
import { createChannelSuccess, createChannelFailure } from '../actions/createChannel'; import { createChannelSuccess, createChannelFailure } from '../actions/createChannel';
import { showErrorAlert } from '../utils/info';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import Navigation from '../lib/Navigation';
import database from '../lib/database'; import database from '../lib/database';
import I18n from '../i18n';
const create = function* create(data) { const createChannel = function createChannel(data) {
return yield RocketChat.createChannel(data); return RocketChat.createChannel(data);
};
const createGroupChat = function createGroupChat() {
return RocketChat.createGroupChat();
}; };
const handleRequest = function* handleRequest({ data }) { const handleRequest = function* handleRequest({ data }) {
@ -18,7 +25,13 @@ const handleRequest = function* handleRequest({ data }) {
if (!auth) { if (!auth) {
yield take(LOGIN.SUCCESS); 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 { try {
const db = database.active; 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() { const root = function* root() {
yield takeLatest(CREATE_CHANNEL.REQUEST, handleRequest); yield takeLatest(CREATE_CHANNEL.REQUEST, handleRequest);
yield takeLatest(CREATE_CHANNEL.SUCCESS, handleSuccess);
yield takeLatest(CREATE_CHANNEL.FAILURE, handleFailure);
}; };
export default root; export default root;

View File

@ -16,7 +16,6 @@ import KeyboardView from '../presentation/KeyboardView';
import scrollPersistTaps from '../utils/scrollPersistTaps'; import scrollPersistTaps from '../utils/scrollPersistTaps';
import I18n from '../i18n'; import I18n from '../i18n';
import UserItem from '../presentation/UserItem'; import UserItem from '../presentation/UserItem';
import { showErrorAlert } from '../utils/info';
import { CustomHeaderButtons, Item } from '../containers/HeaderButton'; import { CustomHeaderButtons, Item } from '../containers/HeaderButton';
import StatusBar from '../containers/StatusBar'; import StatusBar from '../containers/StatusBar';
import { SWITCH_TRACK_COLOR, themes } from '../constants/colors'; import { SWITCH_TRACK_COLOR, themes } from '../constants/colors';
@ -100,7 +99,6 @@ class CreateChannelView extends React.Component {
error: PropTypes.object, error: PropTypes.object,
failure: PropTypes.bool, failure: PropTypes.bool,
isFetching: PropTypes.bool, isFetching: PropTypes.bool,
result: PropTypes.object,
users: PropTypes.array.isRequired, users: PropTypes.array.isRequired,
user: PropTypes.shape({ user: PropTypes.shape({
id: PropTypes.string, id: PropTypes.string,
@ -125,9 +123,7 @@ class CreateChannelView extends React.Component {
const { const {
channelName, type, readOnly, broadcast channelName, type, readOnly, broadcast
} = this.state; } = this.state;
const { const { users, isFetching, theme } = this.props;
error, failure, isFetching, result, users, theme
} = this.props;
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
return true; return true;
} }
@ -143,43 +139,15 @@ class CreateChannelView extends React.Component {
if (nextState.broadcast !== broadcast) { if (nextState.broadcast !== broadcast) {
return true; return true;
} }
if (nextProps.failure !== failure) {
return true;
}
if (nextProps.isFetching !== isFetching) { if (nextProps.isFetching !== isFetching) {
return true; return true;
} }
if (!equal(nextProps.error, error)) {
return true;
}
if (!equal(nextProps.result, result)) {
return true;
}
if (!equal(nextProps.users, users)) { if (!equal(nextProps.users, users)) {
return true; return true;
} }
return false; 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) => { onChangeText = (channelName) => {
const { navigation } = this.props; const { navigation } = this.props;
navigation.setParams({ showSubmit: channelName.trim().length > 0 }); navigation.setParams({ showSubmit: channelName.trim().length > 0 });
@ -365,10 +333,7 @@ class CreateChannelView extends React.Component {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
baseUrl: state.server.server, baseUrl: state.server.server,
error: state.createChannel.error,
failure: state.createChannel.failure,
isFetching: state.createChannel.isFetching, isFetching: state.createChannel.isFetching,
result: state.createChannel.result,
users: state.selectedUsers.users, users: state.selectedUsers.users,
user: getUserSelector(state) user: getUserSelector(state)
}); });

View File

@ -180,7 +180,10 @@ class DirectoryView extends React.Component {
let style; let style;
if (index === data.length - 1) { if (index === data.length - 1) {
style = sharedStyles.separatorBottom; style = {
...sharedStyles.separatorBottom,
borderColor: themes[theme].separatorColor
};
} }
const commonProps = { const commonProps = {

View File

@ -25,6 +25,7 @@ import { withTheme } from '../theme';
import { themedHeader } from '../utils/navigation'; import { themedHeader } from '../utils/navigation';
import { getUserSelector } from '../selectors/login'; import { getUserSelector } from '../selectors/login';
import Navigation from '../lib/Navigation'; import Navigation from '../lib/Navigation';
import { createChannelRequest } from '../actions/createChannel';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
safeAreaView: { safeAreaView: {
@ -33,24 +34,21 @@ const styles = StyleSheet.create({
separator: { separator: {
marginLeft: 60 marginLeft: 60
}, },
createChannelButton: { button: {
marginTop: 25
},
createDiscussionButton: {
marginBottom: 25
},
createChannelContainer: {
height: 46, height: 46,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center' alignItems: 'center'
}, },
createChannelIcon: { buttonIcon: {
marginLeft: 18, marginLeft: 18,
marginRight: 16 marginRight: 16
}, },
createChannelText: { buttonText: {
fontSize: 17, fontSize: 17,
...sharedStyles.textRegular ...sharedStyles.textRegular
},
buttonContainer: {
paddingVertical: 25
} }
}); });
@ -68,6 +66,8 @@ class NewMessageView extends React.Component {
id: PropTypes.string, id: PropTypes.string,
token: PropTypes.string token: PropTypes.string
}), }),
createChannel: PropTypes.func,
maxUsers: PropTypes.number,
theme: PropTypes.string theme: PropTypes.string
}; };
@ -143,7 +143,35 @@ class NewMessageView extends React.Component {
createChannel = () => { createChannel = () => {
const { navigation } = this.props; 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 (
<Touch
onPress={onPress}
style={{ backgroundColor: themes[theme].backgroundColor }}
testID={testID}
theme={theme}
>
<View style={[first ? sharedStyles.separatorVertical : sharedStyles.separatorBottom, styles.button, { borderColor: themes[theme].separatorColor }]}>
<CustomIcon style={[styles.buttonIcon, { color: themes[theme].tintColor }]} size={24} name={icon} />
<Text style={[styles.buttonText, { color: themes[theme].tintColor }]}>{title}</Text>
</View>
</Touch>
);
} }
createDiscussion = () => { createDiscussion = () => {
@ -151,32 +179,31 @@ class NewMessageView extends React.Component {
} }
renderHeader = () => { renderHeader = () => {
const { theme } = this.props; const { maxUsers, theme } = this.props;
return ( return (
<View style={{ backgroundColor: themes[theme].auxiliaryBackground }}> <View style={{ backgroundColor: themes[theme].auxiliaryBackground }}>
<SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='new-message-view-search' /> <SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='new-message-view-search' />
<Touch <View style={styles.buttonContainer}>
onPress={this.createChannel} {this.renderButton({
style={[styles.createChannelButton, { backgroundColor: themes[theme].backgroundColor }]} onPress: this.createChannel,
testID='new-message-view-create-channel' title: I18n.t('Create_Channel'),
theme={theme} icon: 'hashtag',
> testID: 'new-message-view-create-channel',
<View style={[sharedStyles.separatorVertical, styles.createChannelContainer, { borderColor: themes[theme].separatorColor }]}> first: true
<CustomIcon style={[styles.createChannelIcon, { color: themes[theme].tintColor }]} size={24} name='hashtag' /> })}
<Text style={[styles.createChannelText, { color: themes[theme].tintColor }]}>{I18n.t('Create_Channel')}</Text> {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'
})}
</View> </View>
</Touch>
<Touch
onPress={this.createDiscussion}
style={[styles.createDiscussionButton, { backgroundColor: themes[theme].backgroundColor }]}
testID='new-message-view-create-discussion'
theme={theme}
>
<View style={[sharedStyles.separatorBottom, styles.createChannelContainer, { borderColor: themes[theme].separatorColor }]}>
<CustomIcon style={[styles.createChannelIcon, { color: themes[theme].tintColor }]} size={24} name='chat' />
<Text style={[styles.createChannelText, { color: themes[theme].tintColor }]}>{I18n.t('Create_Discussion')}</Text>
</View>
</Touch>
</View> </View>
); );
} }
@ -248,7 +275,12 @@ class NewMessageView extends React.Component {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
baseUrl: state.server.server, baseUrl: state.server.server,
maxUsers: state.settings.DirectMesssage_maxUsers || 1,
user: getUserSelector(state) user: getUserSelector(state)
}); });
export default connect(mapStateToProps)(withTheme(NewMessageView)); const mapDispatchToProps = dispatch => ({
createChannel: params => dispatch(createChannelRequest(params))
});
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(NewMessageView));

View File

@ -8,6 +8,7 @@ import { SafeAreaView } from 'react-navigation';
import _ from 'lodash'; import _ from 'lodash';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import { setLoading as setLoadingAction } from '../../actions/selectedUsers';
import { leaveRoom as leaveRoomAction } from '../../actions/room'; import { leaveRoom as leaveRoomAction } from '../../actions/room';
import styles from './styles'; import styles from './styles';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
@ -49,6 +50,7 @@ class RoomActionsView extends React.Component {
}), }),
leaveRoom: PropTypes.func, leaveRoom: PropTypes.func,
jitsiEnabled: PropTypes.bool, jitsiEnabled: PropTypes.bool,
setLoadingInvite: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string
} }
@ -190,6 +192,7 @@ class RoomActionsView extends React.Component {
const { const {
rid, t, blocker rid, t, blocker
} = room; } = room;
const isGroupChat = RocketChat.isGroupChat(room);
const notificationsAction = { const notificationsAction = {
icon: 'bell', icon: 'bell',
@ -223,6 +226,7 @@ class RoomActionsView extends React.Component {
params: { params: {
rid, t, room, member rid, t, room, member
}, },
disabled: isGroupChat,
testID: 'room-actions-info' testID: 'room-actions-info'
}], }],
renderItem: this.renderRoomInfo 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({ sections.push({
data: [ data: [
{ {
@ -320,9 +335,9 @@ class RoomActionsView extends React.Component {
name: I18n.t('Add_users'), name: I18n.t('Add_users'),
route: 'SelectedUsersView', route: 'SelectedUsersView',
params: { params: {
nextActionID: 'ADD_USER',
rid, rid,
title: I18n.t('Add_users') title: I18n.t('Add_users'),
nextAction: this.addUser
}, },
testID: 'room-actions-add-user' testID: 'room-actions-add-user'
}); });
@ -369,21 +384,37 @@ class RoomActionsView extends React.Component {
updateRoomMember = async() => { updateRoomMember = async() => {
const { room } = this.state; const { room } = this.state;
const { rid } = room;
const { user } = this.props; const { user } = this.props;
try { try {
const roomUserId = RocketChat.getRoomMemberId(rid, user.id); if (!RocketChat.isGroupChat(room)) {
const roomUserId = RocketChat.getUidDirectMessage(room, user.id);
const result = await RocketChat.getUserInfo(roomUserId); const result = await RocketChat.getUserInfo(roomUserId);
if (result.success) { if (result.success) {
this.setState({ member: result.user }); this.setState({ member: result.user });
} }
}
} catch (e) { } catch (e) {
log(e); log(e);
this.setState({ member: {} }); this.setState({ member: {} });
} }
} }
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 = () => { toggleBlockUser = () => {
const { room } = this.state; const { room } = this.state;
const { rid, blocker } = room; const { rid, blocker } = room;
@ -432,11 +463,13 @@ class RoomActionsView extends React.Component {
const { name, t, topic } = room; const { name, t, topic } = room;
const { baseUrl, user, theme } = this.props; const { baseUrl, user, theme } = this.props;
const avatar = RocketChat.getRoomAvatar(room);
return ( return (
this.renderTouchableItem([ this.renderTouchableItem((
<>
<Avatar <Avatar
key='avatar' text={avatar}
text={name}
size={50} size={50}
style={styles.avatar} style={styles.avatar}
type={t} type={t}
@ -445,8 +478,8 @@ class RoomActionsView extends React.Component {
token={user.token} token={user.token}
> >
{t === 'd' && member._id ? <Status style={sharedStyles.status} id={member._id} /> : null } {t === 'd' && member._id ? <Status style={sharedStyles.status} id={member._id} /> : null }
</Avatar>, </Avatar>
<View key='name' style={styles.roomTitleContainer}> <View style={styles.roomTitleContainer}>
{room.t === 'd' {room.t === 'd'
? <Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{room.fname}</Text> ? <Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{room.fname}</Text>
: ( : (
@ -464,9 +497,10 @@ class RoomActionsView extends React.Component {
theme={theme} theme={theme}
/> />
{room.t === 'd' && <Markdown msg={member.statusText} style={[styles.roomDescription, { color: themes[theme].auxiliaryText }]} preview theme={theme} />} {room.t === 'd' && <Markdown msg={member.statusText} style={[styles.roomDescription, { color: themes[theme].auxiliaryText }]} preview theme={theme} />}
</View>, </View>
<DisclosureIndicator theme={theme} key='disclosure-indicator' /> {!item.disabled && <DisclosureIndicator theme={theme} />}
], item) </>
), item)
); );
} }
@ -478,10 +512,11 @@ class RoomActionsView extends React.Component {
style={{ backgroundColor: themes[theme].backgroundColor }} style={{ backgroundColor: themes[theme].backgroundColor }}
accessibilityLabel={item.name} accessibilityLabel={item.name}
accessibilityTraits='button' accessibilityTraits='button'
enabled={!item.disabled}
testID={item.testID} testID={item.testID}
theme={theme} theme={theme}
> >
<View style={[styles.sectionItem, item.disabled && styles.sectionItemDisabled]}> <View style={styles.sectionItem}>
{subview} {subview}
</View> </View>
</Touch> </Touch>
@ -491,15 +526,19 @@ class RoomActionsView extends React.Component {
renderItem = ({ item }) => { renderItem = ({ item }) => {
const { theme } = this.props; const { theme } = this.props;
const colorDanger = { color: themes[theme].dangerColor }; const colorDanger = { color: themes[theme].dangerColor };
const subview = item.type === 'danger' ? [ const subview = item.type === 'danger' ? (
<CustomIcon key='icon' name={item.icon} size={24} style={[styles.sectionItemIcon, colorDanger]} />, <>
<Text key='name' style={[styles.sectionItemName, colorDanger]}>{ item.name }</Text> <CustomIcon name={item.icon} size={24} style={[styles.sectionItemIcon, colorDanger]} />
] : [ <Text style={[styles.sectionItemName, colorDanger]}>{ item.name }</Text>
<CustomIcon key='left-icon' name={item.icon} size={24} style={[styles.sectionItemIcon, { color: themes[theme].bodyText }]} />, </>
<Text key='name' style={[styles.sectionItemName, { color: themes[theme].bodyText }]}>{ item.name }</Text>, ) : (
item.description ? <Text key='description' style={[styles.sectionItemDescription, { color: themes[theme].auxiliaryText }]}>{ item.description }</Text> : null, <>
<DisclosureIndicator theme={theme} key='disclosure-indicator' /> <CustomIcon name={item.icon} size={24} style={[styles.sectionItemIcon, { color: themes[theme].bodyText }]} />
]; <Text style={[styles.sectionItemName, { color: themes[theme].bodyText }]}>{ item.name }</Text>
{item.description ? <Text style={[styles.sectionItemDescription, { color: themes[theme].auxiliaryText }]}>{ item.description }</Text> : null}
<DisclosureIndicator theme={theme} />
</>
);
return this.renderTouchableItem(subview, item); return this.renderTouchableItem(subview, item);
} }
@ -542,7 +581,8 @@ const mapStateToProps = state => ({
}); });
const mapDispatchToProps = dispatch => ({ 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)); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(RoomActionsView));

View File

@ -14,9 +14,6 @@ export default StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center' alignItems: 'center'
}, },
sectionItemDisabled: {
opacity: 0.3
},
sectionItemIcon: { sectionItemIcon: {
width: 56, width: 56,
textAlign: 'center' textAlign: 'center'

View File

@ -77,22 +77,21 @@ class RoomInfoView extends React.Component {
this.rid = props.navigation.getParam('rid'); this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t'); this.t = props.navigation.getParam('t');
this.state = { this.state = {
room: room || {}, room: room || { rid: this.rid, t: this.t },
roomUser: roomUser || {}, roomUser: roomUser || {},
parsedRoles: [] parsedRoles: []
}; };
} }
async componentDidMount() { async componentDidMount() {
const { roomUser } = this.state; const { roomUser, room: roomState } = this.state;
if (this.t === 'd' && !_.isEmpty(roomUser)) { if (this.t === 'd' && !_.isEmpty(roomUser)) {
return; return;
} }
if (this.t === 'd') { if (this.t === 'd') {
const { user } = this.props;
const roomUserId = RocketChat.getRoomMemberId(this.rid, user.id);
try { try {
const roomUserId = RocketChat.getUidDirectMessage(roomState);
const result = await RocketChat.getUserInfo(roomUserId); const result = await RocketChat.getUserInfo(roomUserId);
if (result.success) { if (result.success) {
const { roles } = result.user; const { roles } = result.user;
@ -110,6 +109,7 @@ class RoomInfoView extends React.Component {
} }
return; return;
} }
const { navigation } = this.props; const { navigation } = this.props;
let room = navigation.getParam('room'); let room = navigation.getParam('room');
if (room && room.observe) { if (room && room.observe) {

View File

@ -125,7 +125,7 @@ HeaderTitle.propTypes = {
}; };
const Header = React.memo(({ 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; const portrait = height > width;
let scale = 1; let scale = 1;
@ -146,7 +146,7 @@ const Header = React.memo(({
disabled={tmid} disabled={tmid}
> >
<View style={[styles.titleContainer, tmid && styles.threadContainer]}> <View style={[styles.titleContainer, tmid && styles.threadContainer]}>
<Icon type={prid ? 'discussion' : type} status={status} theme={theme} /> <Icon type={prid ? 'discussion' : type} status={status} roomUserId={roomUserId} theme={theme} />
<HeaderTitle <HeaderTitle
title={title} title={title}
tmid={tmid} tmid={tmid}
@ -174,6 +174,7 @@ Header.propTypes = {
usersTyping: PropTypes.array, usersTyping: PropTypes.array,
widthOffset: PropTypes.number, widthOffset: PropTypes.number,
connecting: PropTypes.bool, connecting: PropTypes.bool,
roomUserId: PropTypes.string,
goRoomActionsView: PropTypes.func goRoomActionsView: PropTypes.func
}; };

View File

@ -21,13 +21,15 @@ const styles = StyleSheet.create({
} }
}); });
const Icon = React.memo(({ type, status, theme }) => { const Icon = React.memo(({
if (type === 'd') { roomUserId, type, status, theme
}) => {
if (type === 'd' && roomUserId) {
return <Status size={10} style={styles.status} status={status} />; return <Status size={10} style={styles.status} status={status} />;
} }
let colorStyle = {}; let colorStyle = {};
if (type === 'd') { if (type === 'd' && roomUserId) {
colorStyle = { color: STATUS_COLORS[status] }; colorStyle = { color: STATUS_COLORS[status] };
} else { } else {
colorStyle = { color: isAndroid && theme === 'light' ? themes[theme].buttonText : themes[theme].auxiliaryText }; colorStyle = { color: isAndroid && theme === 'light' ? themes[theme].buttonText : themes[theme].auxiliaryText };
@ -42,6 +44,8 @@ const Icon = React.memo(({ type, status, theme }) => {
icon = 'hashtag'; icon = 'hashtag';
} else if (type === 'l') { } else if (type === 'l') {
icon = 'livechat'; icon = 'livechat';
} else if (type === 'd') {
icon = 'team';
} else { } else {
icon = 'lock'; icon = 'lock';
} }
@ -62,6 +66,7 @@ const Icon = React.memo(({ type, status, theme }) => {
}); });
Icon.propTypes = { Icon.propTypes = {
roomUserId: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
status: PropTypes.string, status: PropTypes.string,
theme: PropTypes.string theme: PropTypes.string

View File

@ -23,6 +23,7 @@ class RoomHeaderView extends Component {
statusText: PropTypes.string, statusText: PropTypes.string,
connecting: PropTypes.bool, connecting: PropTypes.bool,
theme: PropTypes.string, theme: PropTypes.string,
roomUserId: PropTypes.string,
widthOffset: PropTypes.number, widthOffset: PropTypes.number,
goRoomActionsView: PropTypes.func goRoomActionsView: PropTypes.func
}; };
@ -69,7 +70,7 @@ class RoomHeaderView extends Component {
render() { render() {
const { 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; } = this.props;
return ( return (
@ -85,6 +86,7 @@ class RoomHeaderView extends Component {
theme={theme} theme={theme}
usersTyping={usersTyping} usersTyping={usersTyping}
widthOffset={widthOffset} widthOffset={widthOffset}
roomUserId={roomUserId}
goRoomActionsView={goRoomActionsView} goRoomActionsView={goRoomActionsView}
connecting={connecting} connecting={connecting}
/> />
@ -95,13 +97,12 @@ class RoomHeaderView extends Component {
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
let status; let status;
let statusText; let statusText;
const { rid, type } = ownProps; const { roomUserId, type } = ownProps;
if (type === 'd') { if (type === 'd') {
const user = getUserSelector(state); const user = getUserSelector(state);
if (user.id) { if (user.id) {
const userId = rid.replace(user.id, '').trim(); if (state.activeUsers[roomUserId]) {
if (state.activeUsers[userId]) { ({ status, statusText } = state.activeUsers[roomUserId]);
({ status, statusText } = state.activeUsers[userId]);
} }
} }
} }

View File

@ -84,6 +84,7 @@ class RoomView extends React.Component {
const toggleFollowThread = navigation.getParam('toggleFollowThread', () => {}); const toggleFollowThread = navigation.getParam('toggleFollowThread', () => {});
const goRoomActionsView = navigation.getParam('goRoomActionsView', () => {}); const goRoomActionsView = navigation.getParam('goRoomActionsView', () => {});
const unreadsCount = navigation.getParam('unreadsCount', null); const unreadsCount = navigation.getParam('unreadsCount', null);
const roomUserId = navigation.getParam('roomUserId');
if (!rid) { if (!rid) {
return { return {
...themedHeader(screenProps.theme) ...themedHeader(screenProps.theme)
@ -100,6 +101,7 @@ class RoomView extends React.Component {
subtitle={subtitle} subtitle={subtitle}
type={t} type={t}
widthOffset={tmid ? 95 : 130} widthOffset={tmid ? 95 : 130}
roomUserId={roomUserId}
goRoomActionsView={goRoomActionsView} goRoomActionsView={goRoomActionsView}
/> />
), ),
@ -382,13 +384,16 @@ class RoomView extends React.Component {
getRoomMember = async() => { getRoomMember = async() => {
const { room } = this.state; const { room } = this.state;
const { rid, t } = room; const { t } = room;
if (t === 'd') { if (t === 'd' && !RocketChat.isGroupChat(room)) {
const { user } = this.props; const { user, navigation } = this.props;
try { try {
const roomUserId = RocketChat.getRoomMemberId(rid, user.id); const roomUserId = RocketChat.getUidDirectMessage(room, user.id);
navigation.setParams({ roomUserId });
const result = await RocketChat.getUserInfo(roomUserId); const result = await RocketChat.getUserInfo(roomUserId);
if (result.success) { if (result.success) {
return result.user; return result.user;

View File

@ -412,7 +412,9 @@ class RoomsListView extends React.Component {
key: item._id, key: item._id,
rid: item.rid, rid: item.rid,
type: item.t, type: item.t,
prid: item.prid prid: item.prid,
uids: item.uids,
usernames: item.usernames
})); }));
// unread // unread
@ -526,6 +528,11 @@ class RoomsListView extends React.Component {
getUserPresence = uid => RocketChat.getUserPresence(uid) getUserPresence = uid => RocketChat.getUserPresence(uid)
getUidDirectMessage = (room) => {
const { user: { id } } = this.props;
return RocketChat.getUidDirectMessage(room, id);
}
goRoom = (item) => { goRoom = (item) => {
const { navigation } = this.props; const { navigation } = this.props;
this.cancelSearch(); this.cancelSearch();
@ -535,6 +542,7 @@ class RoomsListView extends React.Component {
name: this.getRoomTitle(item), name: this.getRoomTitle(item),
t: item.t, t: item.t,
prid: item.prid, prid: item.prid,
roomUserId: this.getUidDirectMessage(item),
room: item room: item
}); });
} }
@ -764,7 +772,8 @@ class RoomsListView extends React.Component {
theme, theme,
split split
} = this.props; } = this.props;
const id = item.rid.replace(userId, '').trim(); const id = this.getUidDirectMessage(item);
const isGroupChat = RocketChat.isGroupChat(item);
return ( return (
<RoomItem <RoomItem
@ -797,6 +806,7 @@ class RoomsListView extends React.Component {
hideChannel={this.hideChannel} hideChannel={this.hideChannel}
useRealName={useRealName} useRealName={useRealName}
getUserPresence={this.getUserPresence} getUserPresence={this.getUserPresence}
isGroupChat={isGroupChat}
/> />
); );
}; };

View File

@ -7,14 +7,10 @@ import equal from 'deep-equal';
import { orderBy } from 'lodash'; import { orderBy } from 'lodash';
import { Q } from '@nozbe/watermelondb'; 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 database from '../lib/database';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import UserItem from '../presentation/UserItem'; import UserItem from '../presentation/UserItem';
import Loading from '../containers/Loading'; import Loading from '../containers/Loading';
import debounce from '../utils/debounce';
import I18n from '../i18n'; import I18n from '../i18n';
import log from '../utils/log'; import log from '../utils/log';
import SearchBox from '../containers/SearchBox'; import SearchBox from '../containers/SearchBox';
@ -26,6 +22,12 @@ import { animateNextTransition } from '../utils/layoutAnimation';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { themedHeader } from '../utils/navigation'; import { themedHeader } from '../utils/navigation';
import { getUserSelector } from '../selectors/login'; 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({ const styles = StyleSheet.create({
safeAreaView: { safeAreaView: {
@ -38,47 +40,55 @@ const styles = StyleSheet.create({
class SelectedUsersView extends React.Component { class SelectedUsersView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => { 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', () => {}); const nextAction = navigation.getParam('nextAction', () => {});
return { return {
...themedHeader(screenProps.theme), ...themedHeader(screenProps.theme),
title, title,
headerRight: ( headerRight: (
(!maxUsers || showButton) && (
<CustomHeaderButtons> <CustomHeaderButtons>
<Item title={I18n.t('Next')} onPress={nextAction} testID='selected-users-view-submit' /> <Item title={buttonText} onPress={nextAction} testID='selected-users-view-submit' />
</CustomHeaderButtons> </CustomHeaderButtons>
) )
)
}; };
} }
static propTypes = { static propTypes = {
navigation: PropTypes.object,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
addUser: PropTypes.func.isRequired, addUser: PropTypes.func.isRequired,
removeUser: PropTypes.func.isRequired, removeUser: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired, reset: PropTypes.func.isRequired,
users: PropTypes.array, users: PropTypes.array,
loading: PropTypes.bool, loading: PropTypes.bool,
setLoadingInvite: PropTypes.func,
user: PropTypes.shape({ user: PropTypes.shape({
id: PropTypes.string, id: PropTypes.string,
token: PropTypes.string token: PropTypes.string,
username: PropTypes.string,
name: PropTypes.string
}), }),
navigation: PropTypes.object,
theme: PropTypes.string theme: PropTypes.string
}; };
constructor(props) { constructor(props) {
super(props); super(props);
this.init(); this.init();
const maxUsers = props.navigation.getParam('maxUsers');
this.state = { this.state = {
maxUsers,
search: [], search: [],
chats: [] chats: []
}; };
const { user } = this.props;
if (this.isGroupChat()) {
props.addUser({ _id: user.id, name: user.username, fname: user.name });
} }
componentDidMount() {
const { navigation } = this.props;
navigation.setParams({ nextAction: this.nextAction });
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
@ -102,6 +112,19 @@ class SelectedUsersView extends React.Component {
return false; 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() { componentWillUnmount() {
const { reset } = this.props; const { reset } = this.props;
reset(); reset();
@ -132,30 +155,6 @@ class SelectedUsersView extends React.Component {
this.search(text); 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) => { search = async(text) => {
const result = await RocketChat.search({ text, filterRooms: false }); const result = await RocketChat.search({ text, filterRooms: false });
this.setState({ this.setState({
@ -163,16 +162,33 @@ class SelectedUsersView extends React.Component {
}); });
} }
isGroupChat = () => {
const { maxUsers } = this.state;
return maxUsers > 2;
}
isChecked = (username) => { isChecked = (username) => {
const { users } = this.props; const { users } = this.props;
return users.findIndex(el => el.name === username) !== -1; return users.findIndex(el => el.name === username) !== -1;
} }
toggleUser = (user) => { 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(); animateNextTransition();
if (!this.isChecked(user.name)) { 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); addUser(user);
} else { } else {
removeUser(user); removeUser(user);
@ -274,9 +290,14 @@ class SelectedUsersView extends React.Component {
renderList = () => { renderList = () => {
const { search, chats } = this.state; const { search, chats } = this.state;
const { theme } = this.props; const { theme } = this.props;
const data = (search.length > 0 ? search : chats)
// filter DM between multiple users
.filter(sub => !RocketChat.isGroupChat(sub));
return ( return (
<FlatList <FlatList
data={search.length > 0 ? search : chats} data={data}
extraData={this.props} extraData={this.props}
keyExtractor={item => item._id} keyExtractor={item => item._id}
renderItem={this.renderItem} renderItem={this.renderItem}
@ -315,8 +336,7 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
addUser: user => dispatch(addUserAction(user)), addUser: user => dispatch(addUserAction(user)),
removeUser: user => dispatch(removeUserAction(user)), removeUser: user => dispatch(removeUserAction(user)),
reset: () => dispatch(resetAction()), reset: () => dispatch(resetAction())
setLoadingInvite: loading => dispatch(setLoadingAction(loading))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(SelectedUsersView)); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(SelectedUsersView));