[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:
parent
3437b9039f
commit
d8c8817f04
|
@ -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'
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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}}!',
|
||||||
|
|
13
app/index.js
13
app/index.js
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
|
@ -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 }
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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() => {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()}
|
||||||
|
|
|
@ -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;
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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'
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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'
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)));
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)));
|
|
@ -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'
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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`);
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue