[NEW] Custom Status (#1811)

* [NEW] Custom Status

* [FIX] Subscribe to changes

* [FIX] Improve code using Banner component

* [IMPROVEMENT] Toggle modal

* [NEW] Edit custom status from Sidebar

* [FIX] Modal when tablet

* [FIX] Styles

* [FIX] Switch to react-native-promp-android

* [FIX] Custom Status UI

* [TESTS] E2E Custom Status

* Fix banner

* Fix banner

* Fix subtitle

* status text

* Fix topic header

* Fix RoomActionsView topic

* Fix header alignment on Android

* [FIX] RoomInfo crashes when without statusText

* [FIX] Use users.setStatus

* [FIX] Remove customStatus of ProfileView

* [FIX] Room View Thread Header

Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Djorkaeff Alexandre 2020-03-30 17:19:01 -03:00 committed by GitHub
parent 3437b9039f
commit d8c8817f04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 634 additions and 250 deletions

View File

@ -14,6 +14,9 @@ export default {
Accounts_AllowUserProfileChange: { Accounts_AllowUserProfileChange: {
type: 'valueAsBoolean' type: 'valueAsBoolean'
}, },
Accounts_AllowUserStatusMessageChange: {
type: 'valueAsBoolean'
},
Accounts_AllowUsernameChange: { Accounts_AllowUsernameChange: {
type: 'valueAsBoolean' type: 'valueAsBoolean'
}, },

View File

@ -42,7 +42,7 @@ export const CloseModalButton = React.memo(({ navigation, testID, onPress = () =
</CustomHeaderButtons> </CustomHeaderButtons>
)); ));
export const CloseShareExtensionButton = React.memo(({ onPress, testID }) => ( export const CancelModalButton = React.memo(({ onPress, testID }) => (
<CustomHeaderButtons left> <CustomHeaderButtons left>
{isIOS {isIOS
? <Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} /> ? <Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} />
@ -79,7 +79,7 @@ CloseModalButton.propTypes = {
testID: PropTypes.string.isRequired, testID: PropTypes.string.isRequired,
onPress: PropTypes.func onPress: PropTypes.func
}; };
CloseShareExtensionButton.propTypes = { CancelModalButton.propTypes = {
onPress: PropTypes.func.isRequired, onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired testID: PropTypes.string.isRequired
}; };

View File

@ -33,9 +33,10 @@ const styles = StyleSheet.create({
}); });
const Content = React.memo(({ const Content = React.memo(({
title, subtitle, disabled, testID, right, color, theme title, subtitle, disabled, testID, left, right, color, theme
}) => ( }) => (
<View style={[styles.container, disabled && styles.disabled]} testID={testID}> <View style={[styles.container, disabled && styles.disabled]} testID={testID}>
{left ? left() : null}
<View style={styles.textContainer}> <View style={styles.textContainer}>
<Text style={[styles.title, { color: color || themes[theme].titleText }]}>{title}</Text> <Text style={[styles.title, { color: color || themes[theme].titleText }]}>{title}</Text>
{subtitle {subtitle
@ -79,6 +80,7 @@ Item.propTypes = {
Content.propTypes = { Content.propTypes = {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
subtitle: PropTypes.string, subtitle: PropTypes.string,
left: PropTypes.func,
right: PropTypes.func, right: PropTypes.func,
disabled: PropTypes.bool, disabled: PropTypes.bool,
testID: PropTypes.string, testID: PropTypes.string,

View File

@ -225,7 +225,7 @@ class UploadModal extends Component {
hideModalContentWhileAnimating hideModalContentWhileAnimating
avoidKeyboard avoidKeyboard
> >
<View style={[styles.container, { width: width - 32, backgroundColor: themes[theme].chatComponentBackground }, split && sharedStyles.modal]}> <View style={[styles.container, { width: width - 32, backgroundColor: themes[theme].chatComponentBackground }, split && [sharedStyles.modal, sharedStyles.modalFormSheet]]}>
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('Upload_file_question_mark')}</Text> <Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('Upload_file_question_mark')}</Text>
</View> </View>

View File

@ -4,7 +4,7 @@ import { View } from 'react-native';
import { STATUS_COLORS, themes } from '../../constants/colors'; import { STATUS_COLORS, themes } from '../../constants/colors';
const Status = React.memo(({ const Status = React.memo(({
status, size, style, theme status, size, style, theme, ...props
}) => ( }) => (
<View <View
style={ style={
@ -18,6 +18,7 @@ const Status = React.memo(({
borderColor: themes[theme].backgroundColor borderColor: themes[theme].backgroundColor
} }
]} ]}
{...props}
/> />
)); ));
Status.propTypes = { Status.propTypes = {

View File

@ -26,7 +26,7 @@ class StatusContainer extends React.PureComponent {
} }
const mapStateToProps = (state, ownProps) => ({ 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)); export default connect(mapStateToProps)(withTheme(StatusContainer));

View File

@ -65,6 +65,7 @@ export default class RCTextInput extends React.PureComponent {
testID: PropTypes.string, testID: PropTypes.string,
iconLeft: PropTypes.string, iconLeft: PropTypes.string,
placeholder: PropTypes.string, placeholder: PropTypes.string,
left: PropTypes.element,
theme: PropTypes.string theme: PropTypes.string
} }
@ -116,7 +117,7 @@ export default class RCTextInput extends React.PureComponent {
render() { render() {
const { showPassword } = this.state; const { showPassword } = this.state;
const { 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; } = this.props;
const { dangerColor } = themes[theme]; const { dangerColor } = themes[theme];
return ( return (
@ -166,6 +167,7 @@ export default class RCTextInput extends React.PureComponent {
{iconLeft ? this.iconLeft : null} {iconLeft ? this.iconLeft : null}
{secureTextEntry ? this.iconPassword : null} {secureTextEntry ? this.iconPassword : null}
{loading ? this.loading : null} {loading ? this.loading : null}
{left}
</View> </View>
{error && error.reason ? <Text style={[styles.error, { color: dangerColor }]}>{error.reason}</Text> : null} {error && error.reason ? <Text style={[styles.error, { color: dangerColor }]}>{error.reason}</Text> : null}
</View> </View>

View File

@ -10,6 +10,7 @@ export default {
'error-could-not-change-email': 'Could not change email', 'error-could-not-change-email': 'Could not change email',
'error-could-not-change-name': 'Could not change name', 'error-could-not-change-name': 'Could not change name',
'error-could-not-change-username': 'Could not change username', '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-delete-protected-role': 'Cannot delete a protected role',
'error-department-not-found': 'Department not found', 'error-department-not-found': 'Department not found',
'error-direct-message-file-upload-not-allowed': 'File sharing not allowed in direct messages', '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', Created_snippet: 'Created a snippet',
Create_a_new_workspace: 'Create a new workspace', Create_a_new_workspace: 'Create a new workspace',
Create: 'Create', Create: 'Create',
Custom_Status: 'Custom Status',
Dark: 'Dark', Dark: 'Dark',
Dark_level: 'Dark Level', Dark_level: 'Dark Level',
Default: 'Default', Default: 'Default',
@ -177,6 +179,7 @@ export default {
Discussions: 'Discussions', 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_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', Discussion_name: 'Discussion name',
Done: 'Done',
Dont_Have_An_Account: 'Don\'t you have an account?', Dont_Have_An_Account: 'Don\'t you have an account?',
Do_you_have_an_account: 'Do you have an account?', Do_you_have_an_account: 'Do you have an account?',
Do_you_have_a_certificate: 'Do you have a certificate?', Do_you_have_a_certificate: 'Do you have a certificate?',
@ -184,6 +187,7 @@ export default {
edit: 'edit', edit: 'edit',
edited: 'edited', edited: 'edited',
Edit: 'Edit', Edit: 'Edit',
Edit_Status: 'Edit Status',
Edit_Invite: 'Edit Invite', Edit_Invite: 'Edit Invite',
Email_or_password_field_is_empty: 'Email or password field is empty', Email_or_password_field_is_empty: 'Email or password field is empty',
Email: 'Email', Email: 'Email',
@ -416,6 +420,9 @@ export default {
Servers: 'Servers', Servers: 'Servers',
Server_version: 'Server version: {{version}}', Server_version: 'Server version: {{version}}',
Set_username_subtitle: 'The username is used to allow others to mention you in messages', 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: 'Settings',
Settings_succesfully_changed: 'Settings succesfully changed!', Settings_succesfully_changed: 'Settings succesfully changed!',
Share: 'Share', Share: 'Share',
@ -497,6 +504,7 @@ export default {
Voice_call: 'Voice call', Voice_call: 'Voice call',
Websocket_disabled: 'Websocket is disabled for this server.\n{{contact}}', Websocket_disabled: 'Websocket is disabled for this server.\n{{contact}}',
Welcome: 'Welcome', Welcome: 'Welcome',
What_are_you_doing_right_now: 'What are you doing right now?',
Whats_your_2fa: 'What\'s your 2FA code?', Whats_your_2fa: 'What\'s your 2FA code?',
Without_Servers: 'Without Servers', Without_Servers: 'Without Servers',
Workspaces: 'Workspaces', Workspaces: 'Workspaces',

View File

@ -173,6 +173,7 @@ export default {
Discussions: 'Discussões', 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_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', Discussion_name: 'Nome da discussão',
Done: 'Pronto',
Dont_Have_An_Account: 'Não tem uma conta?', Dont_Have_An_Account: 'Não tem uma conta?',
Do_you_have_an_account: 'Você 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?', Do_you_really_want_to_key_this_room_question_mark: 'Você quer realmente {{key}} esta sala?',
@ -180,6 +181,7 @@ export default {
edited: 'editado', edited: 'editado',
Edit: 'Editar', Edit: 'Editar',
Edit_Invite: 'Editar convite', Edit_Invite: 'Editar convite',
Edit_Status: 'Editar Status',
Email_or_password_field_is_empty: 'Email ou senha estão vazios', Email_or_password_field_is_empty: 'Email ou senha estão vazios',
Email: 'Email', Email: 'Email',
email: 'e-mail', email: 'e-mail',
@ -449,6 +451,7 @@ export default {
Websocket_disabled: 'Websocket está desativado para esse servidor.\n{{contact}}', Websocket_disabled: 'Websocket está desativado para esse servidor.\n{{contact}}',
Welcome: 'Bem vindo', Welcome: 'Bem vindo',
Whats_your_2fa: 'Qual seu código de autenticação?', 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', Without_Servers: 'Sem Servidores',
Workspaces: 'Workspaces', Workspaces: 'Workspaces',
Yes_action_it: 'Sim, {{action}}!', Yes_action_it: 'Sim, {{action}}!',

View File

@ -298,11 +298,21 @@ const CreateDiscussionStack = createStackNavigator({
cardStyle cardStyle
}); });
const StatusStack = createStackNavigator({
StatusView: {
getScreen: () => require('./views/StatusView').default
}
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
const InsideStackModal = createStackNavigator({ const InsideStackModal = createStackNavigator({
Main: ChatsDrawer, Main: ChatsDrawer,
NewMessageStack, NewMessageStack,
AttachmentStack, AttachmentStack,
ModalBlockStack, ModalBlockStack,
StatusStack,
CreateDiscussionStack, CreateDiscussionStack,
JitsiMeetView: { JitsiMeetView: {
getScreen: () => require('./views/JitsiMeetView').default getScreen: () => require('./views/JitsiMeetView').default
@ -395,6 +405,9 @@ const SidebarStack = createStackNavigator({
}, },
AdminPanelView: { AdminPanelView: {
getScreen: () => require('./views/AdminPanelView').default getScreen: () => require('./views/AdminPanelView').default
},
StatusView: {
getScreen: () => require('./views/StatusView').default
} }
}, { }, {
defaultNavigationOptions: defaultHeader, defaultNavigationOptions: defaultHeader,

View File

@ -23,6 +23,8 @@ import appSchema from './schema/app';
import migrations from './model/migrations'; import migrations from './model/migrations';
import serversMigrations from './model/serversMigrations';
import { isIOS } from '../../utils/deviceInfo'; import { isIOS } from '../../utils/deviceInfo';
const appGroupPath = isIOS ? `${ RNFetchBlob.fs.syncPathAppGroup('group.ios.chat.rocket') }/` : ''; const appGroupPath = isIOS ? `${ RNFetchBlob.fs.syncPathAppGroup('group.ios.chat.rocket') }/` : '';
@ -36,7 +38,8 @@ class DB {
serversDB: new Database({ serversDB: new Database({
adapter: new SQLiteAdapter({ adapter: new SQLiteAdapter({
dbName: `${ appGroupPath }default.db`, dbName: `${ appGroupPath }default.db`,
schema: serversSchema schema: serversSchema,
migrations: serversMigrations
}), }),
modelClasses: [Server, User], modelClasses: [Server, User],
actionsEnabled: true actionsEnabled: true

View File

@ -16,5 +16,7 @@ export default class User extends Model {
@field('status') status; @field('status') status;
@field('statusText') statusText;
@json('roles', sanitizer) roles; @json('roles', sanitizer) roles;
} }

View File

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

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb'; import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({ export default appSchema({
version: 2, version: 3,
tables: [ tables: [
tableSchema({ tableSchema({
name: 'users', name: 'users',
@ -11,6 +11,7 @@ export default appSchema({
{ name: 'name', type: 'string', isOptional: true }, { name: 'name', type: 'string', isOptional: true },
{ name: 'language', type: 'string', isOptional: true }, { name: 'language', type: 'string', isOptional: true },
{ name: 'status', type: 'string', isOptional: true }, { name: 'status', type: 'string', isOptional: true },
{ name: 'statusText', type: 'string', isOptional: true },
{ name: 'roles', type: 'string', isOptional: true } { name: 'roles', type: 'string', isOptional: true }
] ]
}), }),

View File

@ -45,7 +45,10 @@ export default async function getUsersPresence() {
const result = await this.sdk.get('users.presence', params); const result = await this.sdk.get('users.presence', params);
if (result.success) { if (result.success) {
const activeUsers = result.users.reduce((ret, item) => { const activeUsers = result.users.reduce((ret, item) => {
ret[item._id] = item.status; ret[item._id] = {
status: item.status,
statusText: item.statusText
};
return ret; return ret;
}, {}); }, {});
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(() => {

View File

@ -241,12 +241,12 @@ const RocketChat = {
}, 10000); }, 10000);
} }
const userStatus = ddpMessage.fields.args[0]; const userStatus = ddpMessage.fields.args[0];
const [id,, status] = userStatus; const [id,, status, statusText] = userStatus;
this.activeUsers[id] = STATUSES[status]; this.activeUsers[id] = { status: STATUSES[status], statusText };
const { user: loggedUser } = reduxStore.getState().login; const { user: loggedUser } = reduxStore.getState().login;
if (loggedUser && loggedUser.id === id) { 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, name: result.me.name,
language: result.me.language, language: result.me.language,
status: result.me.status, status: result.me.status,
statusText: result.me.statusText,
customFields: result.me.customFields, customFields: result.me.customFields,
emails: result.me.emails, emails: result.me.emails,
roles: result.me.roles roles: result.me.roles
@ -741,6 +742,10 @@ const RocketChat = {
setUserPresenceDefaultStatus(status) { setUserPresenceDefaultStatus(status) {
return this.sdk.methodCall('UserPresence:setDefaultStatus', status); return this.sdk.methodCall('UserPresence:setDefaultStatus', status);
}, },
setUserStatus(message) {
// RC 1.2.0
return this.sdk.post('users.setStatus', { message });
},
setReaction(emoji, messageId) { setReaction(emoji, messageId) {
// RC 0.62.2 // RC 0.62.2
return this.sdk.post('chat.react', { emoji, messageId }); return this.sdk.post('chat.react', { emoji, messageId });
@ -1056,7 +1061,7 @@ const RocketChat = {
} }
if (ddpMessage.cleared && user && user.id === ddpMessage.id) { if (ddpMessage.cleared && user && user.id === ddpMessage.id) {
reduxStore.dispatch(setUser({ status: 'offline' })); reduxStore.dispatch(setUser({ status: { status: 'offline' } }));
} }
if (!this._setUserTimer) { if (!this._setUserTimer) {
@ -1071,9 +1076,9 @@ const RocketChat = {
} }
if (!ddpMessage.fields) { if (!ddpMessage.fields) {
this.activeUsers[ddpMessage.id] = 'offline'; this.activeUsers[ddpMessage.id] = { status: 'offline' };
} else if (ddpMessage.fields.status) { } else if (ddpMessage.fields.status) {
this.activeUsers[ddpMessage.id] = ddpMessage.fields.status; this.activeUsers[ddpMessage.id] = { status: ddpMessage.fields.status };
} }
}, },
getUsersPresence, getUsersPresence,

View File

@ -210,7 +210,7 @@ RoomItem.defaultProps = {
const mapStateToProps = (state, ownProps) => ({ const mapStateToProps = (state, ownProps) => ({
status: status:
state.meteor.connected && ownProps.type === 'd' state.meteor.connected && ownProps.type === 'd'
? state.activeUsers[ownProps.id] ? state.activeUsers[ownProps.id] && state.activeUsers[ownProps.id].status
: 'offline' : 'offline'
}); });

View File

@ -101,6 +101,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
name: user.name, name: user.name,
language: user.language, language: user.language,
status: user.status, status: user.status,
statusText: user.statusText,
roles: user.roles roles: user.roles
}; };
yield serversDB.action(async() => { yield serversDB.action(async() => {

View File

@ -78,6 +78,7 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
name: userRecord.name, name: userRecord.name,
language: userRecord.language, language: userRecord.language,
status: userRecord.status, status: userRecord.status,
statusText: userRecord.statusText,
roles: userRecord.roles roles: userRecord.roles
}; };
} catch (e) { } catch (e) {

View File

@ -112,17 +112,11 @@ export const initTabletNav = (setState) => {
KeyCommands.deleteKeyCommands([...defaultCommands, ...keyCommands]); KeyCommands.deleteKeyCommands([...defaultCommands, ...keyCommands]);
setState({ inside: false, showModal: false }); setState({ inside: false, showModal: false });
} }
if (routeName === 'ModalBlockView') { if (routeName === 'ModalBlockView' || routeName === 'StatusView' || routeName === 'CreateDiscussionView') {
modalRef.dispatch(NavigationActions.navigate({ routeName, params })); modalRef.dispatch(NavigationActions.navigate({ routeName, params }));
setState({ showModal: true }); setState({ showModal: true });
return null; return null;
} }
if (routeName === 'CreateDiscussionView') {
modalRef.dispatch(NavigationActions.navigate({ routeName, params }));
setState({ showModal: true });
return null;
}
if (routeName === 'RoomView') { if (routeName === 'RoomView') {
const resetAction = StackActions.reset({ const resetAction = StackActions.reset({
index: 0, index: 0,

View File

@ -5,6 +5,7 @@ import {
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView } from 'react-navigation';
import _ from 'lodash';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import { leaveRoom as leaveRoomAction } from '../../actions/room'; import { leaveRoom as leaveRoomAction } from '../../actions/room';
@ -25,6 +26,7 @@ import { withTheme } from '../../theme';
import { themedHeader } from '../../utils/navigation'; import { themedHeader } from '../../utils/navigation';
import { CloseModalButton } from '../../containers/HeaderButton'; import { CloseModalButton } from '../../containers/HeaderButton';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import Markdown from '../../containers/markdown';
class RoomActionsView extends React.Component { class RoomActionsView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => { static navigationOptions = ({ navigation, screenProps }) => {
@ -54,12 +56,13 @@ class RoomActionsView extends React.Component {
super(props); super(props);
this.mounted = false; this.mounted = false;
const room = props.navigation.getParam('room'); const room = props.navigation.getParam('room');
const member = props.navigation.getParam('member');
this.rid = props.navigation.getParam('rid'); this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t'); this.t = props.navigation.getParam('t');
this.state = { this.state = {
room: room || { rid: this.rid, t: this.t }, room: room || { rid: this.rid, t: this.t },
membersCount: 0, membersCount: 0,
member: {}, member: member || {},
joined: !!room, joined: !!room,
canViewMembers: false, canViewMembers: false,
canAutoTranslate: false, canAutoTranslate: false,
@ -81,7 +84,7 @@ class RoomActionsView extends React.Component {
async componentDidMount() { async componentDidMount() {
this.mounted = true; this.mounted = true;
const { room } = this.state; const { room, member } = this.state;
if (!room.id) { if (!room.id) {
try { try {
const result = await RocketChat.getChannelInfo(room.rid); const result = await RocketChat.getChannelInfo(room.rid);
@ -102,7 +105,7 @@ class RoomActionsView extends React.Component {
} catch (e) { } catch (e) {
log(e); log(e);
} }
} else if (room.t === 'd') { } else if (room.t === 'd' && _.isEmpty(member)) {
this.updateRoomMember(); this.updateRoomMember();
} }
@ -181,7 +184,7 @@ class RoomActionsView extends React.Component {
get sections() { get sections() {
const { const {
room, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate room, member, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate
} = this.state; } = this.state;
const { jitsiEnabled } = this.props; const { jitsiEnabled } = this.props;
const { const {
@ -217,7 +220,9 @@ class RoomActionsView extends React.Component {
name: I18n.t('Room_Info'), name: I18n.t('Room_Info'),
route: 'RoomInfoView', route: 'RoomInfoView',
// forward room only if room isn't joined // forward room only if room isn't joined
params: { rid, t, room }, params: {
rid, t, room, member
},
testID: 'room-actions-info' testID: 'room-actions-info'
}], }],
renderItem: this.renderRoomInfo renderItem: this.renderRoomInfo
@ -451,7 +456,14 @@ class RoomActionsView extends React.Component {
</View> </View>
) )
} }
<Text style={[styles.roomDescription, { color: themes[theme].auxiliaryText }]} ellipsizeMode='tail' numberOfLines={1}>{t === 'd' ? `@${ name }` : topic}</Text> <Markdown
preview
msg={t === 'd' ? `@${ name }` : topic}
style={[styles.roomDescription, { color: themes[theme].auxiliaryText }]}
numberOfLines={1}
theme={theme}
/>
{room.t === 'd' && <Markdown msg={member.statusText} style={[styles.roomDescription, { color: themes[theme].auxiliaryText }]} preview theme={theme} />}
</View>, </View>,
<DisclosureIndicator theme={theme} key='disclosure-indicator' /> <DisclosureIndicator theme={theme} key='disclosure-indicator' />
], item) ], item)

View File

@ -4,6 +4,7 @@ import { View, Text, ScrollView } from 'react-native';
import { BorderlessButton } from 'react-native-gesture-handler'; import { BorderlessButton } from 'react-native-gesture-handler';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import moment from 'moment'; import moment from 'moment';
import _ from 'lodash';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView } from 'react-navigation';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import Status from '../../containers/Status'; import Status from '../../containers/Status';
@ -24,11 +25,12 @@ import { getUserSelector } from '../../selectors/login';
import Markdown from '../../containers/markdown'; import Markdown from '../../containers/markdown';
const PERMISSION_EDIT_ROOM = 'edit-room'; 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'
? ( ? (
<> <>
<Text testID='room-info-view-name' style={[styles.roomTitle, { color: themes[theme].titleText }]}>{ name }</Text> <Text testID='room-info-view-name' style={[styles.roomTitle, { color: themes[theme].titleText }]}>{ name }</Text>
{username && <Text testID='room-info-view-username' style={[styles.roomUsername, { color: themes[theme].auxiliaryText }]}>{`@${ username }`}</Text>} {username && <Text testID='room-info-view-username' style={[styles.roomUsername, { color: themes[theme].auxiliaryText }]}>{`@${ username }`}</Text>}
{!!statusText && <View testID='room-info-view-custom-status'><Markdown msg={statusText} style={[styles.roomUsername, { color: themes[theme].auxiliaryText }]} preview theme={theme} /></View>}
</> </>
) )
: ( : (
@ -71,16 +73,22 @@ class RoomInfoView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const room = props.navigation.getParam('room'); const room = props.navigation.getParam('room');
const roomUser = props.navigation.getParam('member');
this.rid = props.navigation.getParam('rid'); this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t'); this.t = props.navigation.getParam('t');
this.state = { this.state = {
room: room || {}, room: room || {},
roomUser: {}, roomUser: roomUser || {},
parsedRoles: [] parsedRoles: []
}; };
} }
async componentDidMount() { async componentDidMount() {
const { roomUser } = this.state;
if (this.t === 'd' && !_.isEmpty(roomUser)) {
return;
}
if (this.t === 'd') { if (this.t === 'd') {
const { user } = this.props; const { user } = this.props;
const roomUserId = RocketChat.getRoomMemberId(this.rid, user.id); const roomUserId = RocketChat.getRoomMemberId(this.rid, user.id);
@ -342,7 +350,7 @@ class RoomInfoView extends React.Component {
> >
<View style={[styles.avatarContainer, isDirect && styles.avatarContainerDirectRoom, { backgroundColor: themes[theme].auxiliaryBackground }]}> <View style={[styles.avatarContainer, isDirect && styles.avatarContainerDirectRoom, { backgroundColor: themes[theme].auxiliaryBackground }]}>
{this.renderAvatar(room, roomUser)} {this.renderAvatar(room, roomUser)}
<View style={styles.roomTitleContainer}>{ getRoomTitle(room, this.t, roomUser && roomUser.name, roomUser && roomUser.username, theme) }</View> <View style={styles.roomTitleContainer}>{ getRoomTitle(room, this.t, roomUser && roomUser.name, roomUser && roomUser.username, roomUser && roomUser.statusText, theme) }</View>
{isDirect ? this.renderButtons() : null} {isDirect ? this.renderButtons() : null}
</View> </View>
{isDirect ? this.renderDirect() : this.renderChannel()} {isDirect ? this.renderDirect() : this.renderChannel()}

View File

@ -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 (
<>
<BorderlessButton
style={[styles.bannerContainer, { backgroundColor: themes[theme].bannerBackground }]}
testID='room-view-banner'
onPress={toggleModal}
>
<Markdown
msg={text}
theme={theme}
numberOfLines={1}
preview
/>
</BorderlessButton>
<Modal
onBackdropPress={toggleModal}
onBackButtonPress={toggleModal}
useNativeDriver
isVisible={showModal}
animationIn='fadeIn'
animationOut='fadeOut'
>
<View style={[styles.modalView, { backgroundColor: themes[theme].bannerBackground }]}>
<Text style={[styles.bannerModalTitle, { color: themes[theme].auxiliaryText }]}>{title}</Text>
<ScrollView style={styles.modalScrollView}>
<Markdown
msg={text}
theme={theme}
/>
</ScrollView>
</View>
</Modal>
</>
);
}
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;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
View, Text, StyleSheet, ScrollView, TouchableOpacity View, Text, StyleSheet, TouchableOpacity
} from 'react-native'; } from 'react-native';
import I18n from '../../../i18n'; import I18n from '../../../i18n';
@ -11,18 +11,17 @@ import Icon from './Icon';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import Markdown from '../../../containers/markdown'; import Markdown from '../../../containers/markdown';
const androidMarginLeft = isTablet ? 0 : 10; const androidMarginLeft = isTablet ? 0 : 4;
const TITLE_SIZE = 16; const TITLE_SIZE = 16;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
height: '100%',
marginRight: isAndroid ? 15 : 5, marginRight: isAndroid ? 15 : 5,
marginLeft: isAndroid ? androidMarginLeft : -12 marginLeft: isAndroid ? androidMarginLeft : -10
}, },
titleContainer: { titleContainer: {
flex: 6, alignItems: 'center',
flexDirection: 'row' flexDirection: 'row'
}, },
threadContainer: { threadContainer: {
@ -35,36 +34,54 @@ const styles = StyleSheet.create({
scroll: { scroll: {
alignItems: 'center' alignItems: 'center'
}, },
typing: { subtitle: {
...sharedStyles.textRegular, ...sharedStyles.textRegular,
fontSize: 12, fontSize: 12
flex: 4
}, },
typingUsers: { typingUsers: {
...sharedStyles.textSemibold ...sharedStyles.textSemibold
} }
}); });
const Typing = React.memo(({ usersTyping, theme }) => { const SubTitle = React.memo(({ usersTyping, subtitle, theme }) => {
let usersText; if (!subtitle && !usersTyping.length) {
if (!usersTyping.length) {
return null; return null;
} else if (usersTyping.length === 2) {
usersText = usersTyping.join(` ${ I18n.t('and') } `);
} else {
usersText = usersTyping.join(', ');
} }
return (
<Text style={[styles.typing, { color: themes[theme].headerTitleColor }]} numberOfLines={1}> // typing
<Text style={styles.typingUsers}>{usersText} </Text> if (usersTyping.length) {
{ usersTyping.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }... let usersText;
</Text> if (usersTyping.length === 2) {
); usersText = usersTyping.join(` ${ I18n.t('and') } `);
} else {
usersText = usersTyping.join(', ');
}
return (
<Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
<Text style={styles.typingUsers}>{usersText} </Text>
{ usersTyping.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }...
</Text>
);
}
// subtitle
if (subtitle) {
return (
<Markdown
preview
msg={subtitle}
style={[styles.subtitle, { color: themes[theme].auxiliaryText }]}
numberOfLines={1}
theme={theme}
/>
);
}
}); });
Typing.propTypes = { SubTitle.propTypes = {
usersTyping: PropTypes.array, usersTyping: PropTypes.array,
theme: PropTypes.string theme: PropTypes.string,
subtitle: PropTypes.string
}; };
const HeaderTitle = React.memo(({ const HeaderTitle = React.memo(({
@ -108,54 +125,45 @@ HeaderTitle.propTypes = {
}; };
const Header = React.memo(({ 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; const portrait = height > width;
let scale = 1; let scale = 1;
if (!portrait && !tmid) { if (!portrait && !tmid) {
if (usersTyping.length > 0) { if (usersTyping.length > 0 || subtitle) {
scale = 0.8; scale = 0.8;
} }
} }
const onPress = () => { const onPress = () => goRoomActionsView();
if (!tmid) {
goRoomActionsView();
}
};
return ( return (
<TouchableOpacity <TouchableOpacity
testID='room-view-header-actions' testID='room-view-header-actions'
onPress={onPress} onPress={onPress}
style={[styles.container, { width: width - widthOffset }]} style={[styles.container, { width: width - widthOffset }]}
disabled={tmid}
> >
<View style={[styles.titleContainer, tmid && styles.threadContainer]}> <View style={[styles.titleContainer, tmid && styles.threadContainer]}>
<ScrollView <Icon type={prid ? 'discussion' : type} status={status} theme={theme} />
showsHorizontalScrollIndicator={false} <HeaderTitle
horizontal title={title}
bounces={false} tmid={tmid}
contentContainerStyle={styles.scroll} prid={prid}
> scale={scale}
<Icon type={prid ? 'discussion' : type} status={status} theme={theme} /> connecting={connecting}
<HeaderTitle theme={theme}
title={title} />
tmid={tmid}
prid={prid}
scale={scale}
connecting={connecting}
theme={theme}
/>
</ScrollView>
</View> </View>
{type === 'thread' ? null : <Typing usersTyping={usersTyping} theme={theme} />} {tmid ? null : <SubTitle usersTyping={usersTyping} subtitle={subtitle} theme={theme} />}
</TouchableOpacity> </TouchableOpacity>
); );
}); });
Header.propTypes = { Header.propTypes = {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
subtitle: PropTypes.string,
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
width: PropTypes.number.isRequired, width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired, height: PropTypes.number.isRequired,

View File

@ -13,11 +13,11 @@ const styles = StyleSheet.create({
type: { type: {
width: ICON_SIZE, width: ICON_SIZE,
height: ICON_SIZE, height: ICON_SIZE,
marginRight: 8 marginRight: 4,
marginLeft: -4
}, },
status: { status: {
marginLeft: 4, marginRight: 8
marginRight: 12
} }
}); });

View File

@ -13,12 +13,14 @@ import { getUserSelector } from '../../../selectors/login';
class RoomHeaderView extends Component { class RoomHeaderView extends Component {
static propTypes = { static propTypes = {
title: PropTypes.string, title: PropTypes.string,
subtitle: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
prid: PropTypes.string, prid: PropTypes.string,
tmid: PropTypes.string, tmid: PropTypes.string,
usersTyping: PropTypes.string, usersTyping: PropTypes.string,
window: PropTypes.object, window: PropTypes.object,
status: PropTypes.string, status: PropTypes.string,
statusText: PropTypes.string,
connecting: PropTypes.bool, connecting: PropTypes.bool,
theme: PropTypes.string, theme: PropTypes.string,
widthOffset: PropTypes.number, widthOffset: PropTypes.number,
@ -27,7 +29,7 @@ class RoomHeaderView extends Component {
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
const { const {
type, title, status, window, connecting, goRoomActionsView, usersTyping, theme type, title, subtitle, status, statusText, window, connecting, goRoomActionsView, usersTyping, theme
} = this.props; } = this.props;
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
return true; return true;
@ -38,9 +40,15 @@ class RoomHeaderView extends Component {
if (nextProps.title !== title) { if (nextProps.title !== title) {
return true; return true;
} }
if (nextProps.subtitle !== subtitle) {
return true;
}
if (nextProps.status !== status) { if (nextProps.status !== status) {
return true; return true;
} }
if (nextProps.statusText !== statusText) {
return true;
}
if (nextProps.connecting !== connecting) { if (nextProps.connecting !== connecting) {
return true; return true;
} }
@ -61,7 +69,7 @@ class RoomHeaderView extends Component {
render() { render() {
const { 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; } = this.props;
return ( return (
@ -69,6 +77,7 @@ class RoomHeaderView extends Component {
prid={prid} prid={prid}
tmid={tmid} tmid={tmid}
title={title} title={title}
subtitle={type === 'd' ? statusText : subtitle}
type={type} type={type}
status={status} status={status}
width={window.width} width={window.width}
@ -85,19 +94,23 @@ class RoomHeaderView extends Component {
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
let status; let status;
let statusText;
const { rid, type } = ownProps; const { rid, type } = ownProps;
if (type === 'd') { if (type === 'd') {
const user = getUserSelector(state); const user = getUserSelector(state);
if (user.id) { if (user.id) {
const userId = rid.replace(user.id, '').trim(); const userId = rid.replace(user.id, '').trim();
status = state.activeUsers[userId]; if (state.activeUsers[userId]) {
({ status, statusText } = state.activeUsers[userId]);
}
} }
} }
return { return {
connecting: state.meteor.connecting, connecting: state.meteor.connecting,
usersTyping: state.usersTyping, usersTyping: state.usersTyping,
status status,
statusText
}; };
}; };

View File

@ -1,10 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Text, View, InteractionManager } from 'react-native'; import { Text, View, InteractionManager } from 'react-native';
import { ScrollView, BorderlessButton } from 'react-native-gesture-handler';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView } from 'react-navigation';
import Modal from 'react-native-modal';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import moment from 'moment'; import moment from 'moment';
@ -53,7 +51,7 @@ import { Review } from '../../utils/review';
import RoomClass from '../../lib/methods/subscriptions/room'; import RoomClass from '../../lib/methods/subscriptions/room';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import { CONTAINER_TYPES } from '../../lib/methods/actions'; import { CONTAINER_TYPES } from '../../lib/methods/actions';
import Markdown from '../../containers/markdown'; import Banner from './Banner';
import Navigation from '../../lib/Navigation'; import Navigation from '../../lib/Navigation';
const stateAttrsUpdate = [ const stateAttrsUpdate = [
@ -67,15 +65,16 @@ const stateAttrsUpdate = [
'editing', 'editing',
'replying', 'replying',
'reacting', '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 { class RoomView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => { static navigationOptions = ({ navigation, screenProps }) => {
const rid = navigation.getParam('rid', null); const rid = navigation.getParam('rid', null);
const prid = navigation.getParam('prid'); const prid = navigation.getParam('prid');
const title = navigation.getParam('name'); const title = navigation.getParam('name');
const subtitle = navigation.getParam('subtitle');
const t = navigation.getParam('t'); const t = navigation.getParam('t');
const tmid = navigation.getParam('tmid'); const tmid = navigation.getParam('tmid');
const baseUrl = navigation.getParam('baseUrl'); const baseUrl = navigation.getParam('baseUrl');
@ -98,6 +97,7 @@ class RoomView extends React.Component {
prid={prid} prid={prid}
tmid={tmid} tmid={tmid}
title={title} title={title}
subtitle={subtitle}
type={t} type={t}
widthOffset={tmid ? 95 : 130} widthOffset={tmid ? 95 : 130}
goRoomActionsView={goRoomActionsView} goRoomActionsView={goRoomActionsView}
@ -168,6 +168,7 @@ class RoomView extends React.Component {
rid: this.rid, t: this.t, name, fname rid: this.rid, t: this.t, name, fname
}, },
roomUpdate: {}, roomUpdate: {},
member: {},
lastOpen: null, lastOpen: null,
reactionsModalVisible: false, reactionsModalVisible: false,
selectedMessage: selectedMessage || {}, selectedMessage: selectedMessage || {},
@ -179,7 +180,6 @@ class RoomView extends React.Component {
replying: !!selectedMessage, replying: !!selectedMessage,
replyWithMention: false, replyWithMention: false,
reacting: false, reacting: false,
showAnnouncementModal: false,
announcement: null announcement: null
}; };
@ -207,6 +207,7 @@ class RoomView extends React.Component {
if ((room.id || room.rid) && !this.tmid) { if ((room.id || room.rid) && !this.tmid) {
navigation.setParams({ navigation.setParams({
name: this.getRoomTitle(room), name: this.getRoomTitle(room),
subtitle: room.topic,
avatar: room.name, avatar: room.name,
t: room.t, t: room.t,
token: user.token, token: user.token,
@ -236,7 +237,7 @@ class RoomView extends React.Component {
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { state } = this; const { state } = this;
const { roomUpdate } = state; const { roomUpdate, member } = state;
const { appState, theme } = this.props; const { appState, theme } = this.props;
if (theme !== nextProps.theme) { if (theme !== nextProps.theme) {
return true; return true;
@ -244,6 +245,9 @@ class RoomView extends React.Component {
if (appState !== nextProps.appState) { if (appState !== nextProps.appState) {
return true; return true;
} }
if (member.statusText !== nextState.member.statusText) {
return true;
}
const stateUpdated = stateAttrsUpdate.some(key => nextState[key] !== state[key]); const stateUpdated = stateAttrsUpdate.some(key => nextState[key] !== state[key]);
if (stateUpdated) { if (stateUpdated) {
return true; return true;
@ -251,8 +255,9 @@ class RoomView extends React.Component {
return roomAttrsUpdate.some(key => !isEqual(nextState.roomUpdate[key], roomUpdate[key])); return roomAttrsUpdate.some(key => !isEqual(nextState.roomUpdate[key], roomUpdate[key]));
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps, prevState) {
const { appState } = this.props; const { roomUpdate, room } = this.state;
const { appState, navigation } = this.props;
if (appState === 'foreground' && appState !== prevProps.appState && this.rid) { if (appState === 'foreground' && appState !== prevProps.appState && this.rid) {
this.onForegroundInteraction = InteractionManager.runAfterInteractions(() => { 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() { async componentWillUnmount() {
@ -319,9 +333,11 @@ class RoomView extends React.Component {
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
goRoomActionsView = () => { goRoomActionsView = () => {
const { room } = this.state; const { room, member } = this.state;
const { navigation } = this.props; 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() => { init = async() => {
@ -349,7 +365,10 @@ class RoomView extends React.Component {
// We run `canAutoTranslate` again in order to refetch auto translate permission // We run `canAutoTranslate` again in order to refetch auto translate permission
// in case of a missing connection or poor connection on room open // in case of a missing connection or poor connection on room open
const canAutoTranslate = await RocketChat.canAutoTranslate(); const canAutoTranslate = await RocketChat.canAutoTranslate();
this.setState({ canAutoTranslate, loading: false });
const member = await this.getRoomMember();
this.setState({ canAutoTranslate, member, loading: false });
} catch (e) { } catch (e) {
this.setState({ loading: false }); this.setState({ loading: false });
this.retryInit = this.retryInit + 1 || 1; 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) => { findAndObserveRoom = async(rid) => {
try { try {
const db = database.active; const db = database.active;
@ -371,6 +411,7 @@ class RoomView extends React.Component {
if (!this.tmid) { if (!this.tmid) {
navigation.setParams({ navigation.setParams({
name: this.getRoomTitle(room), name: this.getRoomTitle(room),
subtitle: room.topic,
avatar: room.name, avatar: room.name,
t: room.t t: room.t
}); });
@ -805,54 +846,6 @@ class RoomView extends React.Component {
return message; return message;
} }
toggleAnnouncementModal = (showModal) => {
this.setState({ showAnnouncementModal: showModal });
}
renderAnnouncement = () => {
const { theme } = this.props;
const { room } = this.state;
if (room.announcement) {
return (
<BorderlessButton style={[styles.announcementTextContainer, { backgroundColor: themes[theme].bannerBackground }]} key='room-user-status' testID='room-user-status' onPress={() => this.toggleAnnouncementModal(true)}>
<Markdown
msg={room.announcement}
theme={theme}
numberOfLines={1}
preview
/>
</BorderlessButton>
);
} else {
return null;
}
}
renderAnnouncementModal = () => {
const { room, showAnnouncementModal } = this.state;
const { theme } = this.props;
return (
<Modal
onBackdropPress={() => this.toggleAnnouncementModal(false)}
onBackButtonPress={() => this.toggleAnnouncementModal(false)}
useNativeDriver
isVisible={showAnnouncementModal}
animationIn='fadeIn'
animationOut='fadeOut'
>
<View style={[styles.modalView, { backgroundColor: themes[theme].bannerBackground }]}>
<Text style={[styles.announcementTitle, { color: themes[theme].auxiliaryText }]}>{I18n.t('Announcement')}</Text>
<ScrollView style={styles.modalScrollView}>
<Markdown
msg={room.announcement}
theme={theme}
/>
</ScrollView>
</View>
</Modal>
);
}
renderFooter = () => { renderFooter = () => {
const { const {
joined, room, selectedMessage, editing, replying, replyWithMention joined, room, selectedMessage, editing, replying, replyWithMention
@ -973,7 +966,12 @@ class RoomView extends React.Component {
forceInset={{ vertical: 'never' }} forceInset={{ vertical: 'never' }}
> >
<StatusBar theme={theme} /> <StatusBar theme={theme} />
{this.renderAnnouncement()} <Banner
rid={rid}
title={I18n.t('Announcement')}
text={room.announcement}
theme={theme}
/>
<List <List
ref={this.list} ref={this.list}
listRef={this.setListRef} listRef={this.setListRef}
@ -987,7 +985,6 @@ class RoomView extends React.Component {
navigation={navigation} navigation={navigation}
hideSystemMessages={sysMes || Hide_System_Messages} hideSystemMessages={sysMes || Hide_System_Messages}
/> />
{this.renderAnnouncementModal()}
{this.renderFooter()} {this.renderFooter()}
{this.renderActions()} {this.renderActions()}
<ReactionPicker <ReactionPicker

View File

@ -25,12 +25,12 @@ export default StyleSheet.create({
flexDirection: 'column', flexDirection: 'column',
overflow: 'hidden' overflow: 'hidden'
}, },
announcementTextContainer: { bannerContainer: {
paddingVertical: 12, paddingVertical: 12,
paddingHorizontal: 15, paddingHorizontal: 15,
alignItems: 'center' alignItems: 'center'
}, },
announcementTitle: { bannerModalTitle: {
fontSize: 16, fontSize: 16,
...sharedStyles.textMedium ...sharedStyles.textMedium
}, },

View File

@ -4,7 +4,7 @@ import { Keyboard, View, StyleSheet } from 'react-native';
import ShareExtension from 'rn-extensions-share'; import ShareExtension from 'rn-extensions-share';
import SearchBox from '../../../containers/SearchBox'; import SearchBox from '../../../containers/SearchBox';
import { CloseShareExtensionButton } from '../../../containers/HeaderButton'; import { CancelModalButton } from '../../../containers/HeaderButton';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import sharedStyles from '../../Styles'; import sharedStyles from '../../Styles';
@ -52,7 +52,7 @@ const Header = React.memo(({
{ {
!searching !searching
? ( ? (
<CloseShareExtensionButton <CancelModalButton
onPress={ShareExtension.close} onPress={ShareExtension.close}
testID='share-extension-close' testID='share-extension-close'
/> />

View File

@ -20,7 +20,7 @@ import log from '../../utils/log';
import { canUploadFile } from '../../utils/media'; import { canUploadFile } from '../../utils/media';
import DirectoryItem, { ROW_HEIGHT } from '../../presentation/DirectoryItem'; import DirectoryItem, { ROW_HEIGHT } from '../../presentation/DirectoryItem';
import ServerItem from '../../presentation/ServerItem'; import ServerItem from '../../presentation/ServerItem';
import { CloseShareExtensionButton, CustomHeaderButtons, Item } from '../../containers/HeaderButton'; import { CancelModalButton, CustomHeaderButtons, Item } from '../../containers/HeaderButton';
import ShareListHeader from './Header'; import ShareListHeader from './Header';
import ActivityIndicator from '../../containers/ActivityIndicator'; import ActivityIndicator from '../../containers/ActivityIndicator';
@ -66,7 +66,7 @@ class ShareListView extends React.Component {
</CustomHeaderButtons> </CustomHeaderButtons>
) )
: ( : (
<CloseShareExtensionButton <CancelModalButton
onPress={ShareExtension.close} onPress={ShareExtension.close}
testID='share-extension-close' testID='share-extension-close'
/> />

View File

@ -8,7 +8,7 @@ import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
const Item = React.memo(({ const Item = React.memo(({
left, text, onPress, testID, current, theme left, right, text, onPress, testID, current, theme
}) => ( }) => (
<Touch <Touch
key={testID} key={testID}
@ -17,7 +17,7 @@ const Item = React.memo(({
theme={theme} theme={theme}
style={[styles.item, current && { backgroundColor: themes[theme].borderColor }]} style={[styles.item, current && { backgroundColor: themes[theme].borderColor }]}
> >
<View style={styles.itemLeft}> <View style={styles.itemHorizontal}>
{left} {left}
</View> </View>
<View style={styles.itemCenter}> <View style={styles.itemCenter}>
@ -25,11 +25,15 @@ const Item = React.memo(({
{text} {text}
</Text> </Text>
</View> </View>
<View style={styles.itemHorizontal}>
{right}
</View>
</Touch> </Touch>
)); ));
Item.propTypes = { Item.propTypes = {
left: PropTypes.element, left: PropTypes.element,
right: PropTypes.element,
text: PropTypes.string, text: PropTypes.string,
current: PropTypes.bool, current: PropTypes.bool,
onPress: PropTypes.func, onPress: PropTypes.func,

View File

@ -1,16 +1,13 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
ScrollView, Text, View, FlatList, SafeAreaView ScrollView, Text, View, SafeAreaView
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import equal from 'deep-equal';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import Touch from '../../utils/touch';
import Avatar from '../../containers/Avatar'; import Avatar from '../../containers/Avatar';
import Status from '../../containers/Status/Status'; import Status from '../../containers/Status/Status';
import RocketChat from '../../lib/rocketchat';
import log from '../../utils/log'; import log from '../../utils/log';
import I18n from '../../i18n'; import I18n from '../../i18n';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
@ -19,12 +16,10 @@ import styles from './styles';
import SidebarItem from './SidebarItem'; import SidebarItem from './SidebarItem';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import database from '../../lib/database'; import database from '../../lib/database';
import { animateNextTransition } from '../../utils/layoutAnimation';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { withSplit } from '../../split'; import { withSplit } from '../../split';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import Navigation from '../../lib/Navigation';
const keyExtractor = item => item.id;
const Separator = React.memo(({ theme }) => <View style={[styles.separator, { borderColor: themes[theme].separatorColor }]} />); const Separator = React.memo(({ theme }) => <View style={[styles.separator, { borderColor: themes[theme].separatorColor }]} />);
Separator.propTypes = { Separator.propTypes = {
@ -48,6 +43,7 @@ class Sidebar extends Component {
theme: PropTypes.string, theme: PropTypes.string,
loadingServer: PropTypes.bool, loadingServer: PropTypes.bool,
useRealName: PropTypes.bool, useRealName: PropTypes.bool,
allowStatusMessage: PropTypes.bool,
split: PropTypes.bool split: PropTypes.bool
} }
@ -55,28 +51,23 @@ class Sidebar extends Component {
super(props); super(props);
this.state = { this.state = {
showStatus: false, showStatus: false,
isAdmin: false, isAdmin: false
status: []
}; };
} }
componentDidMount() { componentDidMount() {
this.setStatus();
this.setIsAdmin(); this.setIsAdmin();
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
const { user, loadingServer } = this.props; const { loadingServer } = this.props;
if (nextProps.user && user && user.language !== nextProps.user.language) {
this.setStatus();
}
if (loadingServer && nextProps.loadingServer !== loadingServer) { if (loadingServer && nextProps.loadingServer !== loadingServer) {
this.setIsAdmin(); this.setIsAdmin();
} }
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { status, showStatus, isAdmin } = this.state; const { showStatus, isAdmin } = this.state;
const { const {
Site_Name, user, baseUrl, activeItemKey, split, useRealName, theme Site_Name, user, baseUrl, activeItemKey, split, useRealName, theme
} = this.props; } = this.props;
@ -108,6 +99,9 @@ class Sidebar extends Component {
if (nextProps.user.username !== user.username) { if (nextProps.user.username !== user.username) {
return true; return true;
} }
if (nextProps.user.statusText !== user.statusText) {
return true;
}
} }
if (nextProps.split !== split) { if (nextProps.split !== split) {
return true; return true;
@ -115,33 +109,12 @@ class Sidebar extends Component {
if (nextProps.useRealName !== useRealName) { if (nextProps.useRealName !== useRealName) {
return true; return true;
} }
if (!equal(nextState.status, status)) {
return true;
}
if (nextState.isAdmin !== isAdmin) { if (nextState.isAdmin !== isAdmin) {
return true; return true;
} }
return false; 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() { async setIsAdmin() {
const db = database.active; const db = database.active;
const { user } = this.props; const { user } = this.props;
@ -165,32 +138,6 @@ class Sidebar extends Component {
navigation.navigate(route); navigation.navigate(route);
} }
toggleStatus = () => {
animateNextTransition();
this.setState(prevState => ({ showStatus: !prevState.showStatus }));
}
renderStatusItem = ({ item }) => {
const { user } = this.props;
return (
<SidebarItem
text={item.name}
left={<Status style={styles.status} size={12} status={item.id} />}
current={user.status === item.id}
onPress={() => {
this.toggleStatus();
if (user.status !== item.id) {
try {
RocketChat.setUserPresenceDefaultStatus(item.id);
} catch (e) {
log(e);
}
}
}}
/>
);
}
renderNavigation = () => { renderNavigation = () => {
const { isAdmin } = this.state; const { isAdmin } = this.state;
const { activeItemKey, theme } = this.props; const { activeItemKey, theme } = this.props;
@ -230,23 +177,22 @@ class Sidebar extends Component {
); );
} }
renderStatus = () => { renderCustomStatus = () => {
const { status } = this.state; const { user, theme } = this.props;
const { user } = this.props;
return ( return (
<FlatList <SidebarItem
data={status} text={user.statusText || I18n.t('Edit_Status')}
extraData={user} left={<Status style={styles.status} size={12} status={user && user.status} />}
renderItem={this.renderStatusItem} right={<CustomIcon name='edit' size={20} color={themes[theme].titleText} />}
keyExtractor={keyExtractor} onPress={() => Navigation.navigate('StatusView')}
testID='sidebar-custom-status'
/> />
); );
} }
render() { render() {
const { showStatus } = this.state;
const { const {
user, Site_Name, baseUrl, useRealName, split, theme user, Site_Name, baseUrl, useRealName, allowStatusMessage, split, theme
} = this.props; } = this.props;
if (!user) { if (!user) {
@ -265,12 +211,7 @@ class Sidebar extends Component {
]} ]}
{...scrollPersistTaps} {...scrollPersistTaps}
> >
<Touch <View style={styles.header} theme={theme}>
onPress={this.toggleStatus}
testID='sidebar-toggle-status'
style={styles.header}
theme={theme}
>
<Avatar <Avatar
text={user.username} text={user.username}
size={30} size={30}
@ -281,19 +222,22 @@ class Sidebar extends Component {
/> />
<View style={styles.headerTextContainer}> <View style={styles.headerTextContainer}>
<View style={styles.headerUsername}> <View style={styles.headerUsername}>
<Status style={styles.status} size={12} status={user && user.status} theme={theme} />
<Text numberOfLines={1} style={[styles.username, { color: themes[theme].titleText }]}>{useRealName ? user.name : user.username}</Text> <Text numberOfLines={1} style={[styles.username, { color: themes[theme].titleText }]}>{useRealName ? user.name : user.username}</Text>
</View> </View>
<Text style={[styles.currentServerText, { color: themes[theme].titleText }]} numberOfLines={1}>{Site_Name}</Text> <Text style={[styles.currentServerText, { color: themes[theme].titleText }]} numberOfLines={1}>{Site_Name}</Text>
</View> </View>
<CustomIcon name='arrow-down' size={20} style={[styles.headerIcon, showStatus && styles.inverted, { color: themes[theme].titleText }]} /> </View>
</Touch>
{!split ? <Separator theme={theme} /> : null} <Separator theme={theme} />
{!showStatus && !split ? this.renderNavigation() : null} {allowStatusMessage ? this.renderCustomStatus() : null}
{showStatus ? this.renderStatus() : null} {!split ? (
{!split ? <Separator theme={theme} /> : null} <>
<Separator theme={theme} />
{this.renderNavigation()}
<Separator theme={theme} />
</>
) : null}
</ScrollView> </ScrollView>
</SafeAreaView> </SafeAreaView>
); );
@ -305,7 +249,8 @@ const mapStateToProps = state => ({
user: getUserSelector(state), user: getUserSelector(state),
baseUrl: state.server.server, baseUrl: state.server.server,
loadingServer: state.server.loading, 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))); export default connect(mapStateToProps)(withTheme(withSplit(Sidebar)));

View File

@ -13,7 +13,7 @@ export default StyleSheet.create({
itemCurrent: { itemCurrent: {
backgroundColor: '#E1E5E8' backgroundColor: '#E1E5E8'
}, },
itemLeft: { itemHorizontal: {
marginHorizontal: 10, marginHorizontal: 10,
width: 30, width: 30,
alignItems: 'center' alignItems: 'center'
@ -48,9 +48,6 @@ export default StyleSheet.create({
fontSize: 14, fontSize: 14,
...sharedStyles.textMedium ...sharedStyles.textMedium
}, },
headerIcon: {
paddingHorizontal: 10
},
avatar: { avatar: {
marginHorizontal: 10 marginHorizontal: 10
}, },

218
app/views/StatusView.js Normal file
View File

@ -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: <CancelModalButton onPress={navigation.getParam('close', () => {})} />,
headerRight: (
<CustomHeaderButtons>
<Item
title={I18n.t('Done')}
onPress={navigation.getParam('submit', () => {})}
testID='status-view-submit'
/>
</CustomHeaderButtons>
),
...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 <Separator theme={theme} />;
}
renderHeader = () => {
const { statusText } = this.state;
const { user, theme } = this.props;
return (
<>
<TextInput
theme={theme}
value={statusText}
containerStyle={styles.inputContainer}
onChangeText={text => this.setState({ statusText: text })}
left={(
<Status
testID={`status-view-current-${ user.status }`}
style={styles.inputLeft}
status={user.status}
size={12}
/>
)}
inputStyle={styles.inputStyle}
placeholder={I18n.t('What_are_you_doing_right_now')}
testID='status-view-input'
/>
<Separator theme={theme} />
</>
);
}
renderItem = ({ item }) => {
const { theme, user } = this.props;
const { id, name } = item;
return (
<ListItem
title={name}
onPress={async() => {
if (user.status !== item.id) {
try {
await RocketChat.setUserPresenceDefaultStatus(item.id);
} catch (e) {
log(e);
}
}
}}
testID={`status-view-${ id }`}
left={() => <Status style={styles.status} size={12} status={item.id} />}
theme={theme}
/>
);
}
render() {
const { loading } = this.state;
const { theme } = this.props;
return (
<SafeAreaView
style={[
styles.container,
{ backgroundColor: themes[theme].auxiliaryBackground }
]}
forceInset={{ vertical: 'never' }}
testID='status-view'
>
<FlatList
data={STATUS}
keyExtractor={item => item.id}
contentContainerStyle={{ borderColor: themes[theme].separatorColor }}
renderItem={this.renderItem}
ListHeaderComponent={this.renderHeader}
ListFooterComponent={() => <Separator theme={theme} />}
ItemSeparatorComponent={this.renderSeparator}
/>
<Loading visible={loading} />
</SafeAreaView>
);
}
}
const mapStateToProps = state => ({
user: getUserSelector(state)
});
export default connect(mapStateToProps)(withSplit(withTheme(StatusView)));

View File

@ -5,7 +5,7 @@ import {
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ShareExtension from 'rn-extensions-share'; import ShareExtension from 'rn-extensions-share';
import { CloseShareExtensionButton } from '../containers/HeaderButton'; import { CancelModalButton } from '../containers/HeaderButton';
import sharedStyles from './Styles'; import sharedStyles from './Styles';
import I18n from '../i18n'; import I18n from '../i18n';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
@ -34,7 +34,7 @@ class WithoutServerView extends React.Component {
static navigationOptions = ({ screenProps }) => ({ static navigationOptions = ({ screenProps }) => ({
...themedHeader(screenProps.theme), ...themedHeader(screenProps.theme),
headerLeft: ( headerLeft: (
<CloseShareExtensionButton <CancelModalButton
onPress={ShareExtension.close} onPress={ShareExtension.close}
testID='share-extension-close' testID='share-extension-close'
/> />

View File

@ -34,6 +34,10 @@ describe('Profile screen', () => {
await expect(element(by.id('profile-view-avatar')).atIndex(0)).toExist(); 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() => { it('should have name', async() => {
await expect(element(by.id('profile-view-name'))).toExist(); await expect(element(by.id('profile-view-name'))).toExist();
}); });
@ -76,6 +80,16 @@ describe('Profile screen', () => {
}); });
describe('Usage', async() => { 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() => { it('should change name and username', async() => {
await element(by.type('UIScrollView')).atIndex(1).swipe('down'); await element(by.type('UIScrollView')).atIndex(1).swipe('down');
await element(by.id('profile-view-name')).replaceText(`${ data.user }new`); await element(by.id('profile-view-name')).replaceText(`${ data.user }new`);

44
e2e/16-status.spec.js Normal file
View File

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

View File

@ -22,7 +22,7 @@ const reducers = combineReducers({
} }
}), }),
meteor: () => ({ connected: true }), meteor: () => ({ connected: true }),
activeUsers: () => ({ abc: 'online' }) activeUsers: () => ({ abc: { status: 'online', statusText: 'dog' } })
}); });
const store = createStore(reducers); const store = createStore(reducers);