diff --git a/app/constants/settings.js b/app/constants/settings.js index 2b26af32..f01c79ae 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 f696f2af..7cb5375f 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 dcdaa658..faccf4c0 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 e6443dc0..87734785 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 669b8102..5f053591 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 b6fec37b..431762b6 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 6915dbb6..dc071971 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 e2d04714..c767abaa 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 806cac9d..ce010f49 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 b18fba46..9efe0e60 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 7745eb5b..c17b18b2 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 eea82afd..5535ef44 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 00000000..d11b7643 --- /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 33af6c1d..ec4980d1 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 e4a87856..2d8fe592 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 1bb3791f..15c5f904 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 9993220d..2db35a8f 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 a5bb8736..1eb50472 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 076b2f1c..9d4c04b1 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 e530c74b..89dc9ab7 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 7f60e6e6..ba184f1d 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 7b0d1436..610d49f6 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 00000000..a023fa1c --- /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 bc681858..f8594665 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 de490b7d..bf4b5cd4 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 145e2279..a6497a07 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 c450a866..d43d4c91 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 d16b9c8f..55d00d71 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 c0eadea6..0b2b8d2b 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 65ef944f..cad31848 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 b3b07f78..0075c098 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 00000000..1f142794 --- /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 75b7968e..566f6e1b 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 1896ca70..2447aa57 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 00000000..5b9e6b2d --- /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 6e3a8114..0d0cf5b2 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);