[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: {
type: 'valueAsBoolean'
},
DirectMesssage_maxUsers: {
type: 'valueAsNumber'
},
Accounts_Directory_DefaultView: {
type: 'valueAsString'
},

View File

@ -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 <Image source={{ uri: 'hashtag' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />;
} 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 }]} />;
} if (type === 'l') {
return <CustomIcon name='livechat' size={13} style={[styles.style, styles.discussion, { color }]} />;
@ -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
};

View File

@ -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',

View File

@ -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',

View File

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

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';
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({

View File

@ -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;

View File

@ -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 {

View File

@ -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;
},

View File

@ -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 <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 = {
theme: PropTypes.string,
type: PropTypes.string,
status: PropTypes.string,
prid: PropTypes.string
prid: PropTypes.string,
isGroupChat: PropTypes.bool
};
export default TypeIcon;

View File

@ -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(({
<View style={styles.titleContainer}>
<TypeIcon
type={type}
id={id}
prid={prid}
status={status}
isGroupChat={isGroupChat}
theme={theme}
/>
<Text
@ -198,6 +197,7 @@ RoomItem.propTypes = {
hideUnreadStatus: PropTypes.bool,
useRealName: PropTypes.bool,
getUserPresence: PropTypes.func,
isGroupChat: PropTypes.bool,
theme: PropTypes.string
};

View File

@ -64,8 +64,8 @@ const UserItem = ({
<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} />
<View style={styles.textContainer}>
<Text style={[styles.name, { color: themes[theme].titleText }]}>{name}</Text>
<Text style={[styles.username, { color: themes[theme].auxiliaryText }]}>@{username}</Text>
<Text style={[styles.name, { color: themes[theme].titleText }]} numberOfLines={1}>{name}</Text>
<Text style={[styles.username, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>@{username}</Text>
</View>
{icon ? <CustomIcon name={icon} size={22} style={[styles.icon, { color: themes[theme].actionTintColor }]} /> : null}
</View>

View File

@ -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;

View File

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

View File

@ -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 = {

View File

@ -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 (
<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 = () => {
@ -151,32 +179,31 @@ class NewMessageView extends React.Component {
}
renderHeader = () => {
const { theme } = this.props;
const { maxUsers, theme } = this.props;
return (
<View style={{ backgroundColor: themes[theme].auxiliaryBackground }}>
<SearchBox onChangeText={text => this.onSearchChangeText(text)} testID='new-message-view-search' />
<Touch
onPress={this.createChannel}
style={[styles.createChannelButton, { backgroundColor: themes[theme].backgroundColor }]}
testID='new-message-view-create-channel'
theme={theme}
>
<View style={[sharedStyles.separatorVertical, styles.createChannelContainer, { borderColor: themes[theme].separatorColor }]}>
<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>
<View style={styles.buttonContainer}>
{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'
})}
</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>
);
}
@ -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));

View File

@ -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,21 +384,37 @@ 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);
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);
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 = () => {
const { room } = this.state;
const { rid, blocker } = room;
@ -432,11 +463,13 @@ class RoomActionsView extends React.Component {
const { name, t, topic } = room;
const { baseUrl, user, theme } = this.props;
const avatar = RocketChat.getRoomAvatar(room);
return (
this.renderTouchableItem([
this.renderTouchableItem((
<>
<Avatar
key='avatar'
text={name}
text={avatar}
size={50}
style={styles.avatar}
type={t}
@ -445,8 +478,8 @@ class RoomActionsView extends React.Component {
token={user.token}
>
{t === 'd' && member._id ? <Status style={sharedStyles.status} id={member._id} /> : null }
</Avatar>,
<View key='name' style={styles.roomTitleContainer}>
</Avatar>
<View style={styles.roomTitleContainer}>
{room.t === 'd'
? <Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{room.fname}</Text>
: (
@ -464,9 +497,10 @@ class RoomActionsView extends React.Component {
theme={theme}
/>
{room.t === 'd' && <Markdown msg={member.statusText} style={[styles.roomDescription, { color: themes[theme].auxiliaryText }]} preview theme={theme} />}
</View>,
<DisclosureIndicator theme={theme} key='disclosure-indicator' />
], item)
</View>
{!item.disabled && <DisclosureIndicator theme={theme} />}
</>
), 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}
>
<View style={[styles.sectionItem, item.disabled && styles.sectionItemDisabled]}>
<View style={styles.sectionItem}>
{subview}
</View>
</Touch>
@ -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' ? [
<CustomIcon key='icon' name={item.icon} size={24} style={[styles.sectionItemIcon, colorDanger]} />,
<Text key='name' 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' />
];
const subview = item.type === 'danger' ? (
<>
<CustomIcon name={item.icon} size={24} style={[styles.sectionItemIcon, colorDanger]} />
<Text style={[styles.sectionItemName, colorDanger]}>{ item.name }</Text>
</>
) : (
<>
<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);
}
@ -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));

View File

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

View File

@ -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) {

View File

@ -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}
>
<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
title={title}
tmid={tmid}
@ -174,6 +174,7 @@ Header.propTypes = {
usersTyping: PropTypes.array,
widthOffset: PropTypes.number,
connecting: PropTypes.bool,
roomUserId: PropTypes.string,
goRoomActionsView: PropTypes.func
};

View File

@ -21,13 +21,15 @@ const styles = StyleSheet.create({
}
});
const Icon = React.memo(({ type, status, theme }) => {
if (type === 'd') {
const Icon = React.memo(({
roomUserId, type, status, theme
}) => {
if (type === 'd' && roomUserId) {
return <Status size={10} style={styles.status} status={status} />;
}
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

View File

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

View File

@ -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;

View File

@ -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 (
<RoomItem
@ -797,6 +806,7 @@ class RoomsListView extends React.Component {
hideChannel={this.hideChannel}
useRealName={useRealName}
getUserPresence={this.getUserPresence}
isGroupChat={isGroupChat}
/>
);
};

View File

@ -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) && (
<CustomHeaderButtons>
<Item title={I18n.t('Next')} onPress={nextAction} testID='selected-users-view-submit' />
<Item title={buttonText} onPress={nextAction} testID='selected-users-view-submit' />
</CustomHeaderButtons>
)
)
};
}
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: []
};
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) {
@ -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 (
<FlatList
data={search.length > 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));