diff --git a/app/constants/settings.js b/app/constants/settings.js
index 2b26af320..f01c79ae2 100644
--- a/app/constants/settings.js
+++ b/app/constants/settings.js
@@ -14,6 +14,9 @@ export default {
Accounts_AllowUserProfileChange: {
type: 'valueAsBoolean'
},
+ Accounts_AllowUserStatusMessageChange: {
+ type: 'valueAsBoolean'
+ },
Accounts_AllowUsernameChange: {
type: 'valueAsBoolean'
},
diff --git a/app/containers/HeaderButton.js b/app/containers/HeaderButton.js
index f696f2afd..7cb5375f1 100644
--- a/app/containers/HeaderButton.js
+++ b/app/containers/HeaderButton.js
@@ -42,7 +42,7 @@ export const CloseModalButton = React.memo(({ navigation, testID, onPress = () =
));
-export const CloseShareExtensionButton = React.memo(({ onPress, testID }) => (
+export const CancelModalButton = React.memo(({ onPress, testID }) => (
{isIOS
?
@@ -79,7 +79,7 @@ CloseModalButton.propTypes = {
testID: PropTypes.string.isRequired,
onPress: PropTypes.func
};
-CloseShareExtensionButton.propTypes = {
+CancelModalButton.propTypes = {
onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired
};
diff --git a/app/containers/ListItem.js b/app/containers/ListItem.js
index dcdaa6585..faccf4c0b 100644
--- a/app/containers/ListItem.js
+++ b/app/containers/ListItem.js
@@ -33,9 +33,10 @@ const styles = StyleSheet.create({
});
const Content = React.memo(({
- title, subtitle, disabled, testID, right, color, theme
+ title, subtitle, disabled, testID, left, right, color, theme
}) => (
+ {left ? left() : null}
{title}
{subtitle
@@ -79,6 +80,7 @@ Item.propTypes = {
Content.propTypes = {
title: PropTypes.string.isRequired,
subtitle: PropTypes.string,
+ left: PropTypes.func,
right: PropTypes.func,
disabled: PropTypes.bool,
testID: PropTypes.string,
diff --git a/app/containers/MessageBox/UploadModal.js b/app/containers/MessageBox/UploadModal.js
index e6443dc0c..877347854 100644
--- a/app/containers/MessageBox/UploadModal.js
+++ b/app/containers/MessageBox/UploadModal.js
@@ -225,7 +225,7 @@ class UploadModal extends Component {
hideModalContentWhileAnimating
avoidKeyboard
>
-
+
{I18n.t('Upload_file_question_mark')}
diff --git a/app/containers/Status/Status.js b/app/containers/Status/Status.js
index 669b8102a..5f053591c 100644
--- a/app/containers/Status/Status.js
+++ b/app/containers/Status/Status.js
@@ -4,7 +4,7 @@ import { View } from 'react-native';
import { STATUS_COLORS, themes } from '../../constants/colors';
const Status = React.memo(({
- status, size, style, theme
+ status, size, style, theme, ...props
}) => (
));
Status.propTypes = {
diff --git a/app/containers/Status/index.js b/app/containers/Status/index.js
index b6fec37b6..431762b64 100644
--- a/app/containers/Status/index.js
+++ b/app/containers/Status/index.js
@@ -26,7 +26,7 @@ class StatusContainer extends React.PureComponent {
}
const mapStateToProps = (state, ownProps) => ({
- status: state.meteor.connected ? state.activeUsers[ownProps.id] : 'offline'
+ status: state.meteor.connected ? (state.activeUsers[ownProps.id] && state.activeUsers[ownProps.id].status) : 'offline'
});
export default connect(mapStateToProps)(withTheme(StatusContainer));
diff --git a/app/containers/TextInput.js b/app/containers/TextInput.js
index 6915dbb6f..dc071971f 100644
--- a/app/containers/TextInput.js
+++ b/app/containers/TextInput.js
@@ -65,6 +65,7 @@ export default class RCTextInput extends React.PureComponent {
testID: PropTypes.string,
iconLeft: PropTypes.string,
placeholder: PropTypes.string,
+ left: PropTypes.element,
theme: PropTypes.string
}
@@ -116,7 +117,7 @@ export default class RCTextInput extends React.PureComponent {
render() {
const { showPassword } = this.state;
const {
- label, error, loading, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, testID, placeholder, theme, ...inputProps
+ label, left, error, loading, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, testID, placeholder, theme, ...inputProps
} = this.props;
const { dangerColor } = themes[theme];
return (
@@ -166,6 +167,7 @@ export default class RCTextInput extends React.PureComponent {
{iconLeft ? this.iconLeft : null}
{secureTextEntry ? this.iconPassword : null}
{loading ? this.loading : null}
+ {left}
{error && error.reason ? {error.reason} : null}
diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js
index e2d047143..c767abaab 100644
--- a/app/i18n/locales/en.js
+++ b/app/i18n/locales/en.js
@@ -10,6 +10,7 @@ export default {
'error-could-not-change-email': 'Could not change email',
'error-could-not-change-name': 'Could not change name',
'error-could-not-change-username': 'Could not change username',
+ 'error-could-not-change-status': 'Could not change status',
'error-delete-protected-role': 'Cannot delete a protected role',
'error-department-not-found': 'Department not found',
'error-direct-message-file-upload-not-allowed': 'File sharing not allowed in direct messages',
@@ -159,6 +160,7 @@ export default {
Created_snippet: 'Created a snippet',
Create_a_new_workspace: 'Create a new workspace',
Create: 'Create',
+ Custom_Status: 'Custom Status',
Dark: 'Dark',
Dark_level: 'Dark Level',
Default: 'Default',
@@ -177,6 +179,7 @@ export default {
Discussions: 'Discussions',
Discussion_Desc: 'Help keeping an overview about what\'s going on! By creating a discussion, a sub-channel of the one you selected is created and both are linked.',
Discussion_name: 'Discussion name',
+ Done: 'Done',
Dont_Have_An_Account: 'Don\'t you have an account?',
Do_you_have_an_account: 'Do you have an account?',
Do_you_have_a_certificate: 'Do you have a certificate?',
@@ -184,6 +187,7 @@ export default {
edit: 'edit',
edited: 'edited',
Edit: 'Edit',
+ Edit_Status: 'Edit Status',
Edit_Invite: 'Edit Invite',
Email_or_password_field_is_empty: 'Email or password field is empty',
Email: 'Email',
@@ -416,6 +420,9 @@ export default {
Servers: 'Servers',
Server_version: 'Server version: {{version}}',
Set_username_subtitle: 'The username is used to allow others to mention you in messages',
+ Set_custom_status: 'Set custom status',
+ Set_status: 'Set status',
+ Status_saved_successfully: 'Status saved successfully!',
Settings: 'Settings',
Settings_succesfully_changed: 'Settings succesfully changed!',
Share: 'Share',
@@ -497,6 +504,7 @@ export default {
Voice_call: 'Voice call',
Websocket_disabled: 'Websocket is disabled for this server.\n{{contact}}',
Welcome: 'Welcome',
+ What_are_you_doing_right_now: 'What are you doing right now?',
Whats_your_2fa: 'What\'s your 2FA code?',
Without_Servers: 'Without Servers',
Workspaces: 'Workspaces',
diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js
index 806cac9db..ce010f49f 100644
--- a/app/i18n/locales/pt-BR.js
+++ b/app/i18n/locales/pt-BR.js
@@ -173,6 +173,7 @@ export default {
Discussions: 'Discussões',
Discussion_Desc: 'Ajude a manter uma visão geral sobre o que está acontecendo! Ao criar uma discussão, um sub-canal do que você selecionou é criado e os dois são vinculados.',
Discussion_name: 'Nome da discussão',
+ Done: 'Pronto',
Dont_Have_An_Account: 'Não tem uma conta?',
Do_you_have_an_account: 'Você tem uma conta?',
Do_you_really_want_to_key_this_room_question_mark: 'Você quer realmente {{key}} esta sala?',
@@ -180,6 +181,7 @@ export default {
edited: 'editado',
Edit: 'Editar',
Edit_Invite: 'Editar convite',
+ Edit_Status: 'Editar Status',
Email_or_password_field_is_empty: 'Email ou senha estão vazios',
Email: 'Email',
email: 'e-mail',
@@ -449,6 +451,7 @@ export default {
Websocket_disabled: 'Websocket está desativado para esse servidor.\n{{contact}}',
Welcome: 'Bem vindo',
Whats_your_2fa: 'Qual seu código de autenticação?',
+ What_are_you_doing_right_now: 'O que você está fazendo agora?',
Without_Servers: 'Sem Servidores',
Workspaces: 'Workspaces',
Yes_action_it: 'Sim, {{action}}!',
diff --git a/app/index.js b/app/index.js
index b18fba467..9efe0e600 100644
--- a/app/index.js
+++ b/app/index.js
@@ -298,11 +298,21 @@ const CreateDiscussionStack = createStackNavigator({
cardStyle
});
+const StatusStack = createStackNavigator({
+ StatusView: {
+ getScreen: () => require('./views/StatusView').default
+ }
+}, {
+ defaultNavigationOptions: defaultHeader,
+ cardStyle
+});
+
const InsideStackModal = createStackNavigator({
Main: ChatsDrawer,
NewMessageStack,
AttachmentStack,
ModalBlockStack,
+ StatusStack,
CreateDiscussionStack,
JitsiMeetView: {
getScreen: () => require('./views/JitsiMeetView').default
@@ -395,6 +405,9 @@ const SidebarStack = createStackNavigator({
},
AdminPanelView: {
getScreen: () => require('./views/AdminPanelView').default
+ },
+ StatusView: {
+ getScreen: () => require('./views/StatusView').default
}
}, {
defaultNavigationOptions: defaultHeader,
diff --git a/app/lib/database/index.js b/app/lib/database/index.js
index 7745eb5b1..c17b18b21 100644
--- a/app/lib/database/index.js
+++ b/app/lib/database/index.js
@@ -23,6 +23,8 @@ import appSchema from './schema/app';
import migrations from './model/migrations';
+import serversMigrations from './model/serversMigrations';
+
import { isIOS } from '../../utils/deviceInfo';
const appGroupPath = isIOS ? `${ RNFetchBlob.fs.syncPathAppGroup('group.ios.chat.rocket') }/` : '';
@@ -36,7 +38,8 @@ class DB {
serversDB: new Database({
adapter: new SQLiteAdapter({
dbName: `${ appGroupPath }default.db`,
- schema: serversSchema
+ schema: serversSchema,
+ migrations: serversMigrations
}),
modelClasses: [Server, User],
actionsEnabled: true
diff --git a/app/lib/database/model/User.js b/app/lib/database/model/User.js
index eea82afd7..5535ef440 100644
--- a/app/lib/database/model/User.js
+++ b/app/lib/database/model/User.js
@@ -16,5 +16,7 @@ export default class User extends Model {
@field('status') status;
+ @field('statusText') statusText;
+
@json('roles', sanitizer) roles;
}
diff --git a/app/lib/database/model/serversMigrations.js b/app/lib/database/model/serversMigrations.js
new file mode 100644
index 000000000..d11b76432
--- /dev/null
+++ b/app/lib/database/model/serversMigrations.js
@@ -0,0 +1,17 @@
+import { schemaMigrations, addColumns } from '@nozbe/watermelondb/Schema/migrations';
+
+export default schemaMigrations({
+ migrations: [
+ {
+ toVersion: 3,
+ steps: [
+ addColumns({
+ table: 'users',
+ columns: [
+ { name: 'statusText', type: 'string', isOptional: true }
+ ]
+ })
+ ]
+ }
+ ]
+});
diff --git a/app/lib/database/schema/servers.js b/app/lib/database/schema/servers.js
index 33af6c1d2..ec4980d1c 100644
--- a/app/lib/database/schema/servers.js
+++ b/app/lib/database/schema/servers.js
@@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({
- version: 2,
+ version: 3,
tables: [
tableSchema({
name: 'users',
@@ -11,6 +11,7 @@ export default appSchema({
{ name: 'name', type: 'string', isOptional: true },
{ name: 'language', type: 'string', isOptional: true },
{ name: 'status', type: 'string', isOptional: true },
+ { name: 'statusText', type: 'string', isOptional: true },
{ name: 'roles', type: 'string', isOptional: true }
]
}),
diff --git a/app/lib/methods/getUsersPresence.js b/app/lib/methods/getUsersPresence.js
index e4a87856c..2d8fe5926 100644
--- a/app/lib/methods/getUsersPresence.js
+++ b/app/lib/methods/getUsersPresence.js
@@ -45,7 +45,10 @@ export default async function getUsersPresence() {
const result = await this.sdk.get('users.presence', params);
if (result.success) {
const activeUsers = result.users.reduce((ret, item) => {
- ret[item._id] = item.status;
+ ret[item._id] = {
+ status: item.status,
+ statusText: item.statusText
+ };
return ret;
}, {});
InteractionManager.runAfterInteractions(() => {
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index 1bb3791ff..15c5f9040 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -241,12 +241,12 @@ const RocketChat = {
}, 10000);
}
const userStatus = ddpMessage.fields.args[0];
- const [id,, status] = userStatus;
- this.activeUsers[id] = STATUSES[status];
+ const [id,, status, statusText] = userStatus;
+ this.activeUsers[id] = { status: STATUSES[status], statusText };
const { user: loggedUser } = reduxStore.getState().login;
if (loggedUser && loggedUser.id === id) {
- reduxStore.dispatch(setUser({ status: STATUSES[status] }));
+ reduxStore.dispatch(setUser({ status: STATUSES[status], statusText }));
}
}
}));
@@ -378,6 +378,7 @@ const RocketChat = {
name: result.me.name,
language: result.me.language,
status: result.me.status,
+ statusText: result.me.statusText,
customFields: result.me.customFields,
emails: result.me.emails,
roles: result.me.roles
@@ -741,6 +742,10 @@ const RocketChat = {
setUserPresenceDefaultStatus(status) {
return this.sdk.methodCall('UserPresence:setDefaultStatus', status);
},
+ setUserStatus(message) {
+ // RC 1.2.0
+ return this.sdk.post('users.setStatus', { message });
+ },
setReaction(emoji, messageId) {
// RC 0.62.2
return this.sdk.post('chat.react', { emoji, messageId });
@@ -1056,7 +1061,7 @@ const RocketChat = {
}
if (ddpMessage.cleared && user && user.id === ddpMessage.id) {
- reduxStore.dispatch(setUser({ status: 'offline' }));
+ reduxStore.dispatch(setUser({ status: { status: 'offline' } }));
}
if (!this._setUserTimer) {
@@ -1071,9 +1076,9 @@ const RocketChat = {
}
if (!ddpMessage.fields) {
- this.activeUsers[ddpMessage.id] = 'offline';
+ this.activeUsers[ddpMessage.id] = { status: 'offline' };
} else if (ddpMessage.fields.status) {
- this.activeUsers[ddpMessage.id] = ddpMessage.fields.status;
+ this.activeUsers[ddpMessage.id] = { status: ddpMessage.fields.status };
}
},
getUsersPresence,
diff --git a/app/presentation/RoomItem/index.js b/app/presentation/RoomItem/index.js
index 9993220dc..2db35a8f8 100644
--- a/app/presentation/RoomItem/index.js
+++ b/app/presentation/RoomItem/index.js
@@ -210,7 +210,7 @@ RoomItem.defaultProps = {
const mapStateToProps = (state, ownProps) => ({
status:
state.meteor.connected && ownProps.type === 'd'
- ? state.activeUsers[ownProps.id]
+ ? state.activeUsers[ownProps.id] && state.activeUsers[ownProps.id].status
: 'offline'
});
diff --git a/app/sagas/login.js b/app/sagas/login.js
index a5bb8736a..1eb504722 100644
--- a/app/sagas/login.js
+++ b/app/sagas/login.js
@@ -101,6 +101,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
name: user.name,
language: user.language,
status: user.status,
+ statusText: user.statusText,
roles: user.roles
};
yield serversDB.action(async() => {
diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js
index 076b2f1c6..9d4c04b1b 100644
--- a/app/sagas/selectServer.js
+++ b/app/sagas/selectServer.js
@@ -78,6 +78,7 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
name: userRecord.name,
language: userRecord.language,
status: userRecord.status,
+ statusText: userRecord.statusText,
roles: userRecord.roles
};
} catch (e) {
diff --git a/app/tablet.js b/app/tablet.js
index e530c74b7..89dc9ab74 100644
--- a/app/tablet.js
+++ b/app/tablet.js
@@ -112,17 +112,11 @@ export const initTabletNav = (setState) => {
KeyCommands.deleteKeyCommands([...defaultCommands, ...keyCommands]);
setState({ inside: false, showModal: false });
}
- if (routeName === 'ModalBlockView') {
+ if (routeName === 'ModalBlockView' || routeName === 'StatusView' || routeName === 'CreateDiscussionView') {
modalRef.dispatch(NavigationActions.navigate({ routeName, params }));
setState({ showModal: true });
return null;
}
- if (routeName === 'CreateDiscussionView') {
- modalRef.dispatch(NavigationActions.navigate({ routeName, params }));
- setState({ showModal: true });
- return null;
- }
-
if (routeName === 'RoomView') {
const resetAction = StackActions.reset({
index: 0,
diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js
index 7f60e6e67..ba184f1d7 100644
--- a/app/views/RoomActionsView/index.js
+++ b/app/views/RoomActionsView/index.js
@@ -5,6 +5,7 @@ import {
} from 'react-native';
import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation';
+import _ from 'lodash';
import Touch from '../../utils/touch';
import { leaveRoom as leaveRoomAction } from '../../actions/room';
@@ -25,6 +26,7 @@ import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation';
import { CloseModalButton } from '../../containers/HeaderButton';
import { getUserSelector } from '../../selectors/login';
+import Markdown from '../../containers/markdown';
class RoomActionsView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => {
@@ -54,12 +56,13 @@ class RoomActionsView extends React.Component {
super(props);
this.mounted = false;
const room = props.navigation.getParam('room');
+ const member = props.navigation.getParam('member');
this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t');
this.state = {
room: room || { rid: this.rid, t: this.t },
membersCount: 0,
- member: {},
+ member: member || {},
joined: !!room,
canViewMembers: false,
canAutoTranslate: false,
@@ -81,7 +84,7 @@ class RoomActionsView extends React.Component {
async componentDidMount() {
this.mounted = true;
- const { room } = this.state;
+ const { room, member } = this.state;
if (!room.id) {
try {
const result = await RocketChat.getChannelInfo(room.rid);
@@ -102,7 +105,7 @@ class RoomActionsView extends React.Component {
} catch (e) {
log(e);
}
- } else if (room.t === 'd') {
+ } else if (room.t === 'd' && _.isEmpty(member)) {
this.updateRoomMember();
}
@@ -181,7 +184,7 @@ class RoomActionsView extends React.Component {
get sections() {
const {
- room, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate
+ room, member, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate
} = this.state;
const { jitsiEnabled } = this.props;
const {
@@ -217,7 +220,9 @@ class RoomActionsView extends React.Component {
name: I18n.t('Room_Info'),
route: 'RoomInfoView',
// forward room only if room isn't joined
- params: { rid, t, room },
+ params: {
+ rid, t, room, member
+ },
testID: 'room-actions-info'
}],
renderItem: this.renderRoomInfo
@@ -451,7 +456,14 @@ class RoomActionsView extends React.Component {
)
}
- {t === 'd' ? `@${ name }` : topic}
+
+ {room.t === 'd' && }
,
], item)
diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js
index 7b0d14368..610d49f6e 100644
--- a/app/views/RoomInfoView/index.js
+++ b/app/views/RoomInfoView/index.js
@@ -4,6 +4,7 @@ import { View, Text, ScrollView } from 'react-native';
import { BorderlessButton } from 'react-native-gesture-handler';
import { connect } from 'react-redux';
import moment from 'moment';
+import _ from 'lodash';
import { SafeAreaView } from 'react-navigation';
import { CustomIcon } from '../../lib/Icons';
import Status from '../../containers/Status';
@@ -24,11 +25,12 @@ import { getUserSelector } from '../../selectors/login';
import Markdown from '../../containers/markdown';
const PERMISSION_EDIT_ROOM = 'edit-room';
-const getRoomTitle = (room, type, name, username, theme) => (type === 'd'
+const getRoomTitle = (room, type, name, username, statusText, theme) => (type === 'd'
? (
<>
{ name }
{username && {`@${ username }`}}
+ {!!statusText && }
>
)
: (
@@ -71,16 +73,22 @@ class RoomInfoView extends React.Component {
constructor(props) {
super(props);
const room = props.navigation.getParam('room');
+ const roomUser = props.navigation.getParam('member');
this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t');
this.state = {
room: room || {},
- roomUser: {},
+ roomUser: roomUser || {},
parsedRoles: []
};
}
async componentDidMount() {
+ const { roomUser } = 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);
@@ -342,7 +350,7 @@ class RoomInfoView extends React.Component {
>
{this.renderAvatar(room, roomUser)}
- { getRoomTitle(room, this.t, roomUser && roomUser.name, roomUser && roomUser.username, theme) }
+ { getRoomTitle(room, this.t, roomUser && roomUser.name, roomUser && roomUser.username, roomUser && roomUser.statusText, theme) }
{isDirect ? this.renderButtons() : null}
{isDirect ? this.renderDirect() : this.renderChannel()}
diff --git a/app/views/RoomView/Banner.js b/app/views/RoomView/Banner.js
new file mode 100644
index 000000000..a023fa1c6
--- /dev/null
+++ b/app/views/RoomView/Banner.js
@@ -0,0 +1,65 @@
+import React, { useState } from 'react';
+import { View, Text } from 'react-native';
+import PropTypes from 'prop-types';
+import { ScrollView, BorderlessButton } from 'react-native-gesture-handler';
+import Modal from 'react-native-modal';
+
+import Markdown from '../../containers/markdown';
+
+import { themes } from '../../constants/colors';
+import styles from './styles';
+
+const Banner = React.memo(({
+ text, title, theme
+}) => {
+ const [showModal, openModal] = useState(false);
+
+ const toggleModal = () => openModal(prevState => !prevState);
+
+ if (text) {
+ return (
+ <>
+
+
+
+
+
+ {title}
+
+
+
+
+
+ >
+ );
+ }
+
+ return null;
+}, (prevProps, nextProps) => prevProps.text === nextProps.text && prevProps.theme === nextProps.theme);
+
+Banner.propTypes = {
+ text: PropTypes.string,
+ title: PropTypes.string,
+ theme: PropTypes.string
+};
+
+export default Banner;
diff --git a/app/views/RoomView/Header/Header.js b/app/views/RoomView/Header/Header.js
index bc6818588..f85946656 100644
--- a/app/views/RoomView/Header/Header.js
+++ b/app/views/RoomView/Header/Header.js
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
- View, Text, StyleSheet, ScrollView, TouchableOpacity
+ View, Text, StyleSheet, TouchableOpacity
} from 'react-native';
import I18n from '../../../i18n';
@@ -11,18 +11,17 @@ import Icon from './Icon';
import { themes } from '../../../constants/colors';
import Markdown from '../../../containers/markdown';
-const androidMarginLeft = isTablet ? 0 : 10;
+const androidMarginLeft = isTablet ? 0 : 4;
const TITLE_SIZE = 16;
const styles = StyleSheet.create({
container: {
flex: 1,
- height: '100%',
marginRight: isAndroid ? 15 : 5,
- marginLeft: isAndroid ? androidMarginLeft : -12
+ marginLeft: isAndroid ? androidMarginLeft : -10
},
titleContainer: {
- flex: 6,
+ alignItems: 'center',
flexDirection: 'row'
},
threadContainer: {
@@ -35,36 +34,54 @@ const styles = StyleSheet.create({
scroll: {
alignItems: 'center'
},
- typing: {
+ subtitle: {
...sharedStyles.textRegular,
- fontSize: 12,
- flex: 4
+ fontSize: 12
},
typingUsers: {
...sharedStyles.textSemibold
}
});
-const Typing = React.memo(({ usersTyping, theme }) => {
- let usersText;
- if (!usersTyping.length) {
+const SubTitle = React.memo(({ usersTyping, subtitle, theme }) => {
+ if (!subtitle && !usersTyping.length) {
return null;
- } else if (usersTyping.length === 2) {
- usersText = usersTyping.join(` ${ I18n.t('and') } `);
- } else {
- usersText = usersTyping.join(', ');
}
- return (
-
- {usersText}
- { usersTyping.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }...
-
- );
+
+ // typing
+ if (usersTyping.length) {
+ let usersText;
+ if (usersTyping.length === 2) {
+ usersText = usersTyping.join(` ${ I18n.t('and') } `);
+ } else {
+ usersText = usersTyping.join(', ');
+ }
+ return (
+
+ {usersText}
+ { usersTyping.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }...
+
+ );
+ }
+
+ // subtitle
+ if (subtitle) {
+ return (
+
+ );
+ }
});
-Typing.propTypes = {
+SubTitle.propTypes = {
usersTyping: PropTypes.array,
- theme: PropTypes.string
+ theme: PropTypes.string,
+ subtitle: PropTypes.string
};
const HeaderTitle = React.memo(({
@@ -108,54 +125,45 @@ HeaderTitle.propTypes = {
};
const Header = React.memo(({
- title, type, status, usersTyping, width, height, prid, tmid, widthOffset, connecting, goRoomActionsView, theme
+ title, subtitle, type, status, usersTyping, width, height, prid, tmid, widthOffset, connecting, goRoomActionsView, theme
}) => {
const portrait = height > width;
let scale = 1;
if (!portrait && !tmid) {
- if (usersTyping.length > 0) {
+ if (usersTyping.length > 0 || subtitle) {
scale = 0.8;
}
}
- const onPress = () => {
- if (!tmid) {
- goRoomActionsView();
- }
- };
+ const onPress = () => goRoomActionsView();
return (
-
-
-
-
+
+
- {type === 'thread' ? null : }
+ {tmid ? null : }
);
});
Header.propTypes = {
title: PropTypes.string.isRequired,
+ subtitle: PropTypes.string,
type: PropTypes.string.isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
diff --git a/app/views/RoomView/Header/Icon.js b/app/views/RoomView/Header/Icon.js
index de490b7d4..bf4b5cd4a 100644
--- a/app/views/RoomView/Header/Icon.js
+++ b/app/views/RoomView/Header/Icon.js
@@ -13,11 +13,11 @@ const styles = StyleSheet.create({
type: {
width: ICON_SIZE,
height: ICON_SIZE,
- marginRight: 8
+ marginRight: 4,
+ marginLeft: -4
},
status: {
- marginLeft: 4,
- marginRight: 12
+ marginRight: 8
}
});
diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js
index 145e22792..a6497a074 100644
--- a/app/views/RoomView/Header/index.js
+++ b/app/views/RoomView/Header/index.js
@@ -13,12 +13,14 @@ import { getUserSelector } from '../../../selectors/login';
class RoomHeaderView extends Component {
static propTypes = {
title: PropTypes.string,
+ subtitle: PropTypes.string,
type: PropTypes.string,
prid: PropTypes.string,
tmid: PropTypes.string,
usersTyping: PropTypes.string,
window: PropTypes.object,
status: PropTypes.string,
+ statusText: PropTypes.string,
connecting: PropTypes.bool,
theme: PropTypes.string,
widthOffset: PropTypes.number,
@@ -27,7 +29,7 @@ class RoomHeaderView extends Component {
shouldComponentUpdate(nextProps) {
const {
- type, title, status, window, connecting, goRoomActionsView, usersTyping, theme
+ type, title, subtitle, status, statusText, window, connecting, goRoomActionsView, usersTyping, theme
} = this.props;
if (nextProps.theme !== theme) {
return true;
@@ -38,9 +40,15 @@ class RoomHeaderView extends Component {
if (nextProps.title !== title) {
return true;
}
+ if (nextProps.subtitle !== subtitle) {
+ return true;
+ }
if (nextProps.status !== status) {
return true;
}
+ if (nextProps.statusText !== statusText) {
+ return true;
+ }
if (nextProps.connecting !== connecting) {
return true;
}
@@ -61,7 +69,7 @@ class RoomHeaderView extends Component {
render() {
const {
- window, title, type, prid, tmid, widthOffset, status = 'offline', connecting, usersTyping, goRoomActionsView, theme
+ window, title, subtitle, type, prid, tmid, widthOffset, status = 'offline', statusText, connecting, usersTyping, goRoomActionsView, theme
} = this.props;
return (
@@ -69,6 +77,7 @@ class RoomHeaderView extends Component {
prid={prid}
tmid={tmid}
title={title}
+ subtitle={type === 'd' ? statusText : subtitle}
type={type}
status={status}
width={window.width}
@@ -85,19 +94,23 @@ class RoomHeaderView extends Component {
const mapStateToProps = (state, ownProps) => {
let status;
+ let statusText;
const { rid, type } = ownProps;
if (type === 'd') {
const user = getUserSelector(state);
if (user.id) {
const userId = rid.replace(user.id, '').trim();
- status = state.activeUsers[userId];
+ if (state.activeUsers[userId]) {
+ ({ status, statusText } = state.activeUsers[userId]);
+ }
}
}
return {
connecting: state.meteor.connecting,
usersTyping: state.usersTyping,
- status
+ status,
+ statusText
};
};
diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js
index c450a866e..d43d4c914 100644
--- a/app/views/RoomView/index.js
+++ b/app/views/RoomView/index.js
@@ -1,10 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text, View, InteractionManager } from 'react-native';
-import { ScrollView, BorderlessButton } from 'react-native-gesture-handler';
import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation';
-import Modal from 'react-native-modal';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import moment from 'moment';
@@ -53,7 +51,7 @@ import { Review } from '../../utils/review';
import RoomClass from '../../lib/methods/subscriptions/room';
import { getUserSelector } from '../../selectors/login';
import { CONTAINER_TYPES } from '../../lib/methods/actions';
-import Markdown from '../../containers/markdown';
+import Banner from './Banner';
import Navigation from '../../lib/Navigation';
const stateAttrsUpdate = [
@@ -67,15 +65,16 @@ const stateAttrsUpdate = [
'editing',
'replying',
'reacting',
- 'showAnnouncementModal'
+ 'member'
];
-const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'muted', 'jitsiTimeout', 'announcement', 'sysMes'];
+const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'muted', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname'];
class RoomView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => {
const rid = navigation.getParam('rid', null);
const prid = navigation.getParam('prid');
const title = navigation.getParam('name');
+ const subtitle = navigation.getParam('subtitle');
const t = navigation.getParam('t');
const tmid = navigation.getParam('tmid');
const baseUrl = navigation.getParam('baseUrl');
@@ -98,6 +97,7 @@ class RoomView extends React.Component {
prid={prid}
tmid={tmid}
title={title}
+ subtitle={subtitle}
type={t}
widthOffset={tmid ? 95 : 130}
goRoomActionsView={goRoomActionsView}
@@ -168,6 +168,7 @@ class RoomView extends React.Component {
rid: this.rid, t: this.t, name, fname
},
roomUpdate: {},
+ member: {},
lastOpen: null,
reactionsModalVisible: false,
selectedMessage: selectedMessage || {},
@@ -179,7 +180,6 @@ class RoomView extends React.Component {
replying: !!selectedMessage,
replyWithMention: false,
reacting: false,
- showAnnouncementModal: false,
announcement: null
};
@@ -207,6 +207,7 @@ class RoomView extends React.Component {
if ((room.id || room.rid) && !this.tmid) {
navigation.setParams({
name: this.getRoomTitle(room),
+ subtitle: room.topic,
avatar: room.name,
t: room.t,
token: user.token,
@@ -236,7 +237,7 @@ class RoomView extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
const { state } = this;
- const { roomUpdate } = state;
+ const { roomUpdate, member } = state;
const { appState, theme } = this.props;
if (theme !== nextProps.theme) {
return true;
@@ -244,6 +245,9 @@ class RoomView extends React.Component {
if (appState !== nextProps.appState) {
return true;
}
+ if (member.statusText !== nextState.member.statusText) {
+ return true;
+ }
const stateUpdated = stateAttrsUpdate.some(key => nextState[key] !== state[key]);
if (stateUpdated) {
return true;
@@ -251,8 +255,9 @@ class RoomView extends React.Component {
return roomAttrsUpdate.some(key => !isEqual(nextState.roomUpdate[key], roomUpdate[key]));
}
- componentDidUpdate(prevProps) {
- const { appState } = this.props;
+ componentDidUpdate(prevProps, prevState) {
+ const { roomUpdate, room } = this.state;
+ const { appState, navigation } = this.props;
if (appState === 'foreground' && appState !== prevProps.appState && this.rid) {
this.onForegroundInteraction = InteractionManager.runAfterInteractions(() => {
@@ -262,6 +267,15 @@ class RoomView extends React.Component {
}
});
}
+ // If it's not direct message
+ if (this.t !== 'd') {
+ if (roomUpdate.topic !== prevState.roomUpdate.topic) {
+ navigation.setParams({ subtitle: roomUpdate.topic });
+ }
+ }
+ if (((roomUpdate.fname !== prevState.roomUpdate.fname) || (roomUpdate.name !== prevState.roomUpdate.name)) && !this.tmid) {
+ navigation.setParams({ name: this.getRoomTitle(room) });
+ }
}
async componentWillUnmount() {
@@ -319,9 +333,11 @@ class RoomView extends React.Component {
// eslint-disable-next-line react/sort-comp
goRoomActionsView = () => {
- const { room } = this.state;
+ const { room, member } = this.state;
const { navigation } = this.props;
- navigation.navigate('RoomActionsView', { rid: this.rid, t: this.t, room });
+ navigation.navigate('RoomActionsView', {
+ rid: this.rid, t: this.t, room, member
+ });
}
init = async() => {
@@ -349,7 +365,10 @@ class RoomView extends React.Component {
// We run `canAutoTranslate` again in order to refetch auto translate permission
// in case of a missing connection or poor connection on room open
const canAutoTranslate = await RocketChat.canAutoTranslate();
- this.setState({ canAutoTranslate, loading: false });
+
+ const member = await this.getRoomMember();
+
+ this.setState({ canAutoTranslate, member, loading: false });
} catch (e) {
this.setState({ loading: false });
this.retryInit = this.retryInit + 1 || 1;
@@ -361,6 +380,27 @@ class RoomView extends React.Component {
}
}
+ getRoomMember = async() => {
+ const { room } = this.state;
+ const { rid, t } = room;
+
+ if (t === 'd') {
+ const { user } = this.props;
+
+ try {
+ const roomUserId = RocketChat.getRoomMemberId(rid, user.id);
+ const result = await RocketChat.getUserInfo(roomUserId);
+ if (result.success) {
+ return result.user;
+ }
+ } catch (e) {
+ log(e);
+ }
+ }
+
+ return {};
+ }
+
findAndObserveRoom = async(rid) => {
try {
const db = database.active;
@@ -371,6 +411,7 @@ class RoomView extends React.Component {
if (!this.tmid) {
navigation.setParams({
name: this.getRoomTitle(room),
+ subtitle: room.topic,
avatar: room.name,
t: room.t
});
@@ -805,54 +846,6 @@ class RoomView extends React.Component {
return message;
}
- toggleAnnouncementModal = (showModal) => {
- this.setState({ showAnnouncementModal: showModal });
- }
-
- renderAnnouncement = () => {
- const { theme } = this.props;
- const { room } = this.state;
- if (room.announcement) {
- return (
- this.toggleAnnouncementModal(true)}>
-
-
- );
- } else {
- return null;
- }
- }
-
- renderAnnouncementModal = () => {
- const { room, showAnnouncementModal } = this.state;
- const { theme } = this.props;
- return (
- this.toggleAnnouncementModal(false)}
- onBackButtonPress={() => this.toggleAnnouncementModal(false)}
- useNativeDriver
- isVisible={showAnnouncementModal}
- animationIn='fadeIn'
- animationOut='fadeOut'
- >
-
- {I18n.t('Announcement')}
-
-
-
-
-
- );
- }
-
renderFooter = () => {
const {
joined, room, selectedMessage, editing, replying, replyWithMention
@@ -973,7 +966,12 @@ class RoomView extends React.Component {
forceInset={{ vertical: 'never' }}
>
- {this.renderAnnouncement()}
+
- {this.renderAnnouncementModal()}
{this.renderFooter()}
{this.renderActions()}
diff --git a/app/views/ShareListView/index.js b/app/views/ShareListView/index.js
index d16b9c8fe..55d00d71d 100644
--- a/app/views/ShareListView/index.js
+++ b/app/views/ShareListView/index.js
@@ -20,7 +20,7 @@ import log from '../../utils/log';
import { canUploadFile } from '../../utils/media';
import DirectoryItem, { ROW_HEIGHT } from '../../presentation/DirectoryItem';
import ServerItem from '../../presentation/ServerItem';
-import { CloseShareExtensionButton, CustomHeaderButtons, Item } from '../../containers/HeaderButton';
+import { CancelModalButton, CustomHeaderButtons, Item } from '../../containers/HeaderButton';
import ShareListHeader from './Header';
import ActivityIndicator from '../../containers/ActivityIndicator';
@@ -66,7 +66,7 @@ class ShareListView extends React.Component {
)
: (
-
diff --git a/app/views/SidebarView/SidebarItem.js b/app/views/SidebarView/SidebarItem.js
index c0eadea63..0b2b8d2bc 100644
--- a/app/views/SidebarView/SidebarItem.js
+++ b/app/views/SidebarView/SidebarItem.js
@@ -8,7 +8,7 @@ import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
const Item = React.memo(({
- left, text, onPress, testID, current, theme
+ left, right, text, onPress, testID, current, theme
}) => (
-
+
{left}
@@ -25,11 +25,15 @@ const Item = React.memo(({
{text}
+
+ {right}
+
));
Item.propTypes = {
left: PropTypes.element,
+ right: PropTypes.element,
text: PropTypes.string,
current: PropTypes.bool,
onPress: PropTypes.func,
diff --git a/app/views/SidebarView/index.js b/app/views/SidebarView/index.js
index 65ef944fc..cad318486 100644
--- a/app/views/SidebarView/index.js
+++ b/app/views/SidebarView/index.js
@@ -1,16 +1,13 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
- ScrollView, Text, View, FlatList, SafeAreaView
+ ScrollView, Text, View, SafeAreaView
} from 'react-native';
import { connect } from 'react-redux';
-import equal from 'deep-equal';
import { Q } from '@nozbe/watermelondb';
-import Touch from '../../utils/touch';
import Avatar from '../../containers/Avatar';
import Status from '../../containers/Status/Status';
-import RocketChat from '../../lib/rocketchat';
import log from '../../utils/log';
import I18n from '../../i18n';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
@@ -19,12 +16,10 @@ import styles from './styles';
import SidebarItem from './SidebarItem';
import { themes } from '../../constants/colors';
import database from '../../lib/database';
-import { animateNextTransition } from '../../utils/layoutAnimation';
import { withTheme } from '../../theme';
import { withSplit } from '../../split';
import { getUserSelector } from '../../selectors/login';
-
-const keyExtractor = item => item.id;
+import Navigation from '../../lib/Navigation';
const Separator = React.memo(({ theme }) => );
Separator.propTypes = {
@@ -48,6 +43,7 @@ class Sidebar extends Component {
theme: PropTypes.string,
loadingServer: PropTypes.bool,
useRealName: PropTypes.bool,
+ allowStatusMessage: PropTypes.bool,
split: PropTypes.bool
}
@@ -55,28 +51,23 @@ class Sidebar extends Component {
super(props);
this.state = {
showStatus: false,
- isAdmin: false,
- status: []
+ isAdmin: false
};
}
componentDidMount() {
- this.setStatus();
this.setIsAdmin();
}
componentWillReceiveProps(nextProps) {
- const { user, loadingServer } = this.props;
- if (nextProps.user && user && user.language !== nextProps.user.language) {
- this.setStatus();
- }
+ const { loadingServer } = this.props;
if (loadingServer && nextProps.loadingServer !== loadingServer) {
this.setIsAdmin();
}
}
shouldComponentUpdate(nextProps, nextState) {
- const { status, showStatus, isAdmin } = this.state;
+ const { showStatus, isAdmin } = this.state;
const {
Site_Name, user, baseUrl, activeItemKey, split, useRealName, theme
} = this.props;
@@ -108,6 +99,9 @@ class Sidebar extends Component {
if (nextProps.user.username !== user.username) {
return true;
}
+ if (nextProps.user.statusText !== user.statusText) {
+ return true;
+ }
}
if (nextProps.split !== split) {
return true;
@@ -115,33 +109,12 @@ class Sidebar extends Component {
if (nextProps.useRealName !== useRealName) {
return true;
}
- if (!equal(nextState.status, status)) {
- return true;
- }
if (nextState.isAdmin !== isAdmin) {
return true;
}
return false;
}
- setStatus = () => {
- this.setState({
- status: [{
- id: 'online',
- name: I18n.t('Online')
- }, {
- id: 'busy',
- name: I18n.t('Busy')
- }, {
- id: 'away',
- name: I18n.t('Away')
- }, {
- id: 'offline',
- name: I18n.t('Invisible')
- }]
- });
- }
-
async setIsAdmin() {
const db = database.active;
const { user } = this.props;
@@ -165,32 +138,6 @@ class Sidebar extends Component {
navigation.navigate(route);
}
- toggleStatus = () => {
- animateNextTransition();
- this.setState(prevState => ({ showStatus: !prevState.showStatus }));
- }
-
- renderStatusItem = ({ item }) => {
- const { user } = this.props;
- return (
- }
- current={user.status === item.id}
- onPress={() => {
- this.toggleStatus();
- if (user.status !== item.id) {
- try {
- RocketChat.setUserPresenceDefaultStatus(item.id);
- } catch (e) {
- log(e);
- }
- }
- }}
- />
- );
- }
-
renderNavigation = () => {
const { isAdmin } = this.state;
const { activeItemKey, theme } = this.props;
@@ -230,23 +177,22 @@ class Sidebar extends Component {
);
}
- renderStatus = () => {
- const { status } = this.state;
- const { user } = this.props;
+ renderCustomStatus = () => {
+ const { user, theme } = this.props;
return (
- }
+ right={}
+ onPress={() => Navigation.navigate('StatusView')}
+ testID='sidebar-custom-status'
/>
);
}
render() {
- const { showStatus } = this.state;
const {
- user, Site_Name, baseUrl, useRealName, split, theme
+ user, Site_Name, baseUrl, useRealName, allowStatusMessage, split, theme
} = this.props;
if (!user) {
@@ -265,12 +211,7 @@ class Sidebar extends Component {
]}
{...scrollPersistTaps}
>
-
+
-
{useRealName ? user.name : user.username}
{Site_Name}
-
-
+
- {!split ? : null}
+
- {!showStatus && !split ? this.renderNavigation() : null}
- {showStatus ? this.renderStatus() : null}
- {!split ? : null}
+ {allowStatusMessage ? this.renderCustomStatus() : null}
+ {!split ? (
+ <>
+
+ {this.renderNavigation()}
+
+ >
+ ) : null}
);
@@ -305,7 +249,8 @@ const mapStateToProps = state => ({
user: getUserSelector(state),
baseUrl: state.server.server,
loadingServer: state.server.loading,
- useRealName: state.settings.UI_Use_Real_Name
+ useRealName: state.settings.UI_Use_Real_Name,
+ allowStatusMessage: state.settings.Accounts_AllowUserStatusMessageChange
});
export default connect(mapStateToProps)(withTheme(withSplit(Sidebar)));
diff --git a/app/views/SidebarView/styles.js b/app/views/SidebarView/styles.js
index b3b07f787..0075c098c 100644
--- a/app/views/SidebarView/styles.js
+++ b/app/views/SidebarView/styles.js
@@ -13,7 +13,7 @@ export default StyleSheet.create({
itemCurrent: {
backgroundColor: '#E1E5E8'
},
- itemLeft: {
+ itemHorizontal: {
marginHorizontal: 10,
width: 30,
alignItems: 'center'
@@ -48,9 +48,6 @@ export default StyleSheet.create({
fontSize: 14,
...sharedStyles.textMedium
},
- headerIcon: {
- paddingHorizontal: 10
- },
avatar: {
marginHorizontal: 10
},
diff --git a/app/views/StatusView.js b/app/views/StatusView.js
new file mode 100644
index 000000000..1f142794c
--- /dev/null
+++ b/app/views/StatusView.js
@@ -0,0 +1,218 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { FlatList, StyleSheet } from 'react-native';
+import { SafeAreaView } from 'react-navigation';
+import { connect } from 'react-redux';
+
+import I18n from '../i18n';
+import Separator from '../containers/Separator';
+import ListItem from '../containers/ListItem';
+import Status from '../containers/Status/Status';
+import TextInput from '../containers/TextInput';
+import EventEmitter from '../utils/events';
+import Loading from '../containers/Loading';
+import RocketChat from '../lib/rocketchat';
+import log from '../utils/log';
+
+import { LISTENER } from '../containers/Toast';
+import { themes } from '../constants/colors';
+import { withTheme } from '../theme';
+import { withSplit } from '../split';
+import { themedHeader } from '../utils/navigation';
+import { getUserSelector } from '../selectors/login';
+import { CustomHeaderButtons, Item, CancelModalButton } from '../containers/HeaderButton';
+
+const STATUS = [{
+ id: 'online',
+ name: I18n.t('Online')
+}, {
+ id: 'busy',
+ name: I18n.t('Busy')
+}, {
+ id: 'away',
+ name: I18n.t('Away')
+}, {
+ id: 'offline',
+ name: I18n.t('Invisible')
+}];
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1
+ },
+ status: {
+ marginRight: 16
+ },
+ inputContainer: {
+ marginTop: 32,
+ marginBottom: 32
+ },
+ inputLeft: {
+ position: 'absolute',
+ top: 18,
+ left: 14
+ },
+ inputStyle: {
+ paddingLeft: 40
+ }
+});
+
+class StatusView extends React.Component {
+ static navigationOptions = ({ navigation, screenProps }) => ({
+ title: I18n.t('Edit_Status'),
+ headerLeft: {})} />,
+ headerRight: (
+
+ - {})}
+ testID='status-view-submit'
+ />
+
+ ),
+ ...themedHeader(screenProps.theme)
+ })
+
+ static propTypes = {
+ user: PropTypes.shape({
+ status: PropTypes.string,
+ statusText: PropTypes.string
+ }),
+ theme: PropTypes.string,
+ split: PropTypes.bool,
+ navigation: PropTypes.object
+ }
+
+ constructor(props) {
+ super(props);
+
+ const { statusText } = props.user;
+ this.state = { statusText, loading: false };
+
+ props.navigation.setParams({ submit: this.submit, close: this.close });
+ }
+
+ submit = async() => {
+ const { statusText } = this.state;
+ const { user } = this.props;
+ if (statusText !== user.statusText) {
+ await this.setCustomStatus();
+ }
+ this.close();
+ }
+
+ close = () => {
+ const { navigation, split } = this.props;
+ if (split) {
+ navigation.goBack();
+ } else {
+ navigation.pop();
+ }
+ }
+
+ setCustomStatus = async() => {
+ const { statusText } = this.state;
+
+ this.setState({ loading: true });
+
+ try {
+ const result = await RocketChat.setUserStatus(statusText);
+ if (result.success) {
+ EventEmitter.emit(LISTENER, { message: I18n.t('Status_saved_successfully') });
+ } else {
+ EventEmitter.emit(LISTENER, { message: I18n.t('error-could-not-change-status') });
+ }
+ } catch {
+ EventEmitter.emit(LISTENER, { message: I18n.t('error-could-not-change-status') });
+ }
+
+ this.setState({ loading: false });
+ }
+
+ renderSeparator = () => {
+ const { theme } = this.props;
+ return ;
+ }
+
+ renderHeader = () => {
+ const { statusText } = this.state;
+ const { user, theme } = this.props;
+
+ return (
+ <>
+ this.setState({ statusText: text })}
+ left={(
+
+ )}
+ inputStyle={styles.inputStyle}
+ placeholder={I18n.t('What_are_you_doing_right_now')}
+ testID='status-view-input'
+ />
+
+ >
+ );
+ }
+
+ renderItem = ({ item }) => {
+ const { theme, user } = this.props;
+ const { id, name } = item;
+ return (
+ {
+ if (user.status !== item.id) {
+ try {
+ await RocketChat.setUserPresenceDefaultStatus(item.id);
+ } catch (e) {
+ log(e);
+ }
+ }
+ }}
+ testID={`status-view-${ id }`}
+ left={() => }
+ theme={theme}
+ />
+ );
+ }
+
+ render() {
+ const { loading } = this.state;
+ const { theme } = this.props;
+ return (
+
+ item.id}
+ contentContainerStyle={{ borderColor: themes[theme].separatorColor }}
+ renderItem={this.renderItem}
+ ListHeaderComponent={this.renderHeader}
+ ListFooterComponent={() => }
+ ItemSeparatorComponent={this.renderSeparator}
+ />
+
+
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ user: getUserSelector(state)
+});
+
+export default connect(mapStateToProps)(withSplit(withTheme(StatusView)));
diff --git a/app/views/WithoutServersView.js b/app/views/WithoutServersView.js
index 75b7968ed..566f6e1bc 100644
--- a/app/views/WithoutServersView.js
+++ b/app/views/WithoutServersView.js
@@ -5,7 +5,7 @@ import {
import PropTypes from 'prop-types';
import ShareExtension from 'rn-extensions-share';
-import { CloseShareExtensionButton } from '../containers/HeaderButton';
+import { CancelModalButton } from '../containers/HeaderButton';
import sharedStyles from './Styles';
import I18n from '../i18n';
import { themes } from '../constants/colors';
@@ -34,7 +34,7 @@ class WithoutServerView extends React.Component {
static navigationOptions = ({ screenProps }) => ({
...themedHeader(screenProps.theme),
headerLeft: (
-
diff --git a/e2e/13-profile.spec.js b/e2e/13-profile.spec.js
index 1896ca704..2447aa57f 100644
--- a/e2e/13-profile.spec.js
+++ b/e2e/13-profile.spec.js
@@ -34,6 +34,10 @@ describe('Profile screen', () => {
await expect(element(by.id('profile-view-avatar')).atIndex(0)).toExist();
});
+ it('should have custom status', async() => {
+ await expect(element(by.id('profile-view-custom-status'))).toExist();
+ });
+
it('should have name', async() => {
await expect(element(by.id('profile-view-name'))).toExist();
});
@@ -76,6 +80,16 @@ describe('Profile screen', () => {
});
describe('Usage', async() => {
+ it('should change custom status', async() => {
+ await element(by.type('UIScrollView')).atIndex(1).swipe('down');
+ await element(by.id('profile-view-custom-status')).replaceText(`${ data.user }new`);
+ await sleep(1000);
+ await element(by.type('UIScrollView')).atIndex(1).swipe('up');
+ await sleep(1000);
+ await element(by.id('profile-view-submit')).tap();
+ await waitForToast();
+ });
+
it('should change name and username', async() => {
await element(by.type('UIScrollView')).atIndex(1).swipe('down');
await element(by.id('profile-view-name')).replaceText(`${ data.user }new`);
diff --git a/e2e/16-status.spec.js b/e2e/16-status.spec.js
new file mode 100644
index 000000000..5b9e6b2d1
--- /dev/null
+++ b/e2e/16-status.spec.js
@@ -0,0 +1,44 @@
+const {
+ expect, element, by, waitFor
+} = require('detox');
+const { sleep } = require('./helpers/app');
+
+async function waitForToast() {
+ await sleep(5000);
+}
+
+describe('Status screen', () => {
+ before(async() => {
+ await element(by.id('rooms-list-view-sidebar')).tap();
+ await waitFor(element(by.id('sidebar-view'))).toBeVisible().withTimeout(2000);
+ await waitFor(element(by.id('sidebar-custom-status'))).toBeVisible().withTimeout(2000);
+
+ await element(by.id('sidebar-custom-status')).tap();
+ await waitFor(element(by.id('status-view'))).toBeVisible().withTimeout(2000);
+ });
+
+ describe('Render', async() => {
+ it('should have status input', async() => {
+ await expect(element(by.id('status-view-input'))).toBeVisible();
+ await expect(element(by.id('status-view-online'))).toExist();
+ await expect(element(by.id('status-view-busy'))).toExist();
+ await expect(element(by.id('status-view-away'))).toExist();
+ await expect(element(by.id('status-view-offline'))).toExist();
+ });
+ });
+
+ describe('Usage', async() => {
+ it('should change status', async() => {
+ await element(by.id('status-view-busy')).tap();
+ sleep(1000);
+ await expect(element(by.id('status-view-current-busy'))).toExist();
+ });
+
+ it('should change status text', async() => {
+ await element(by.id('status-view-input')).replaceText('status-text-new');
+ await sleep(1000);
+ await element(by.id('status-view-submit')).tap();
+ await waitForToast();
+ });
+ });
+});
diff --git a/storybook/stories/index.js b/storybook/stories/index.js
index 6e3a8114b..0d0cf5b2a 100644
--- a/storybook/stories/index.js
+++ b/storybook/stories/index.js
@@ -22,7 +22,7 @@ const reducers = combineReducers({
}
}),
meteor: () => ({ connected: true }),
- activeUsers: () => ({ abc: 'online' })
+ activeUsers: () => ({ abc: { status: 'online', statusText: 'dog' } })
});
const store = createStore(reducers);