Unverified Commit d8c8817f authored by Djorkaeff Alexandre's avatar Djorkaeff Alexandre Committed by GitHub
Browse files

[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: default avatarDiego Mello <diegolmello@gmail.com>
parent 3437b903
......@@ -14,6 +14,9 @@ export default {
Accounts_AllowUserProfileChange: {
type: 'valueAsBoolean'
},
Accounts_AllowUserStatusMessageChange: {
type: 'valueAsBoolean'
},
Accounts_AllowUsernameChange: {
type: 'valueAsBoolean'
},
......
......@@ -42,7 +42,7 @@ export const CloseModalButton = React.memo(({ navigation, testID, onPress = () =
</CustomHeaderButtons>
));
export const CloseShareExtensionButton = React.memo(({ onPress, testID }) => (
export const CancelModalButton = React.memo(({ onPress, testID }) => (
<CustomHeaderButtons left>
{isIOS
? <Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} />
......@@ -79,7 +79,7 @@ CloseModalButton.propTypes = {
testID: PropTypes.string.isRequired,
onPress: PropTypes.func
};
CloseShareExtensionButton.propTypes = {
CancelModalButton.propTypes = {
onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired
};
......
......@@ -33,9 +33,10 @@ const styles = StyleSheet.create({
});
const Content = React.memo(({
title, subtitle, disabled, testID, right, color, theme
title, subtitle, disabled, testID, left, right, color, theme
}) => (
<View style={[styles.container, disabled && styles.disabled]} testID={testID}>
{left ? left() : null}
<View style={styles.textContainer}>
<Text style={[styles.title, { color: color || themes[theme].titleText }]}>{title}</Text>
{subtitle
......@@ -79,6 +80,7 @@ Item.propTypes = {
Content.propTypes = {
title: PropTypes.string.isRequired,
subtitle: PropTypes.string,
left: PropTypes.func,
right: PropTypes.func,
disabled: PropTypes.bool,
testID: PropTypes.string,
......
......@@ -225,7 +225,7 @@ class UploadModal extends Component {
hideModalContentWhileAnimating
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}>
<Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('Upload_file_question_mark')}</Text>
</View>
......
......@@ -4,7 +4,7 @@ import { View } from 'react-native';
import { STATUS_COLORS, themes } from '../../constants/colors';
const Status = React.memo(({
status, size, style, theme
status, size, style, theme, ...props
}) => (
<View
style={
......@@ -18,6 +18,7 @@ const Status = React.memo(({
borderColor: themes[theme].backgroundColor
}
]}
{...props}
/>
));
Status.propTypes = {
......
......@@ -26,7 +26,7 @@ class StatusContainer extends React.PureComponent {
}
const mapStateToProps = (state, ownProps) => ({
status: state.meteor.connected ? state.activeUsers[ownProps.id] : 'offline'
status: state.meteor.connected ? (state.activeUsers[ownProps.id] && state.activeUsers[ownProps.id].status) : 'offline'
});
export default connect(mapStateToProps)(withTheme(StatusContainer));
......@@ -65,6 +65,7 @@ export default class RCTextInput extends React.PureComponent {
testID: PropTypes.string,
iconLeft: PropTypes.string,
placeholder: PropTypes.string,
left: PropTypes.element,
theme: PropTypes.string
}
......@@ -116,7 +117,7 @@ export default class RCTextInput extends React.PureComponent {
render() {
const { showPassword } = this.state;
const {
label, error, loading, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, testID, placeholder, theme, ...inputProps
label, left, error, loading, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, testID, placeholder, theme, ...inputProps
} = this.props;
const { dangerColor } = themes[theme];
return (
......@@ -166,6 +167,7 @@ export default class RCTextInput extends React.PureComponent {
{iconLeft ? this.iconLeft : null}
{secureTextEntry ? this.iconPassword : null}
{loading ? this.loading : null}
{left}
</View>
{error && error.reason ? <Text style={[styles.error, { color: dangerColor }]}>{error.reason}</Text> : null}
</View>
......
......@@ -10,6 +10,7 @@ export default {
'error-could-not-change-email': 'Could not change email',
'error-could-not-change-name': 'Could not change name',
'error-could-not-change-username': 'Could not change username',
'error-could-not-change-status': 'Could not change status',
'error-delete-protected-role': 'Cannot delete a protected role',
'error-department-not-found': 'Department not found',
'error-direct-message-file-upload-not-allowed': 'File sharing not allowed in direct messages',
......@@ -159,6 +160,7 @@ export default {
Created_snippet: 'Created a snippet',
Create_a_new_workspace: 'Create a new workspace',
Create: 'Create',
Custom_Status: 'Custom Status',
Dark: 'Dark',
Dark_level: 'Dark Level',
Default: 'Default',
......@@ -177,6 +179,7 @@ export default {
Discussions: 'Discussions',
Discussion_Desc: 'Help keeping an overview about what\'s going on! By creating a discussion, a sub-channel of the one you selected is created and both are linked.',
Discussion_name: 'Discussion name',
Done: 'Done',
Dont_Have_An_Account: 'Don\'t you have an account?',
Do_you_have_an_account: 'Do you have an account?',
Do_you_have_a_certificate: 'Do you have a certificate?',
......@@ -184,6 +187,7 @@ export default {
edit: 'edit',
edited: 'edited',
Edit: 'Edit',
Edit_Status: 'Edit Status',
Edit_Invite: 'Edit Invite',
Email_or_password_field_is_empty: 'Email or password field is empty',
Email: 'Email',
......@@ -416,6 +420,9 @@ export default {
Servers: 'Servers',
Server_version: 'Server version: {{version}}',
Set_username_subtitle: 'The username is used to allow others to mention you in messages',
Set_custom_status: 'Set custom status',
Set_status: 'Set status',
Status_saved_successfully: 'Status saved successfully!',
Settings: 'Settings',
Settings_succesfully_changed: 'Settings succesfully changed!',
Share: 'Share',
......@@ -497,6 +504,7 @@ export default {
Voice_call: 'Voice call',
Websocket_disabled: 'Websocket is disabled for this server.\n{{contact}}',
Welcome: 'Welcome',
What_are_you_doing_right_now: 'What are you doing right now?',
Whats_your_2fa: 'What\'s your 2FA code?',
Without_Servers: 'Without Servers',
Workspaces: 'Workspaces',
......
......@@ -173,6 +173,7 @@ export default {
Discussions: 'Discussões',
Discussion_Desc: 'Ajude a manter uma visão geral sobre o que está acontecendo! Ao criar uma discussão, um sub-canal do que você selecionou é criado e os dois são vinculados.',
Discussion_name: 'Nome da discussão',
Done: 'Pronto',
Dont_Have_An_Account: 'Não tem uma conta?',
Do_you_have_an_account: 'Você tem uma conta?',
Do_you_really_want_to_key_this_room_question_mark: 'Você quer realmente {{key}} esta sala?',
......@@ -180,6 +181,7 @@ export default {
edited: 'editado',
Edit: 'Editar',
Edit_Invite: 'Editar convite',
Edit_Status: 'Editar Status',
Email_or_password_field_is_empty: 'Email ou senha estão vazios',
Email: 'Email',
email: 'e-mail',
......@@ -449,6 +451,7 @@ export default {
Websocket_disabled: 'Websocket está desativado para esse servidor.\n{{contact}}',
Welcome: 'Bem vindo',
Whats_your_2fa: 'Qual seu código de autenticação?',
What_are_you_doing_right_now: 'O que você está fazendo agora?',
Without_Servers: 'Sem Servidores',
Workspaces: 'Workspaces',
Yes_action_it: 'Sim, {{action}}!',
......
......@@ -298,11 +298,21 @@ const CreateDiscussionStack = createStackNavigator({
cardStyle
});
const StatusStack = createStackNavigator({
StatusView: {
getScreen: () => require('./views/StatusView').default
}
}, {
defaultNavigationOptions: defaultHeader,
cardStyle
});
const InsideStackModal = createStackNavigator({
Main: ChatsDrawer,
NewMessageStack,
AttachmentStack,
ModalBlockStack,
StatusStack,
CreateDiscussionStack,
JitsiMeetView: {
getScreen: () => require('./views/JitsiMeetView').default
......@@ -395,6 +405,9 @@ const SidebarStack = createStackNavigator({
},
AdminPanelView: {
getScreen: () => require('./views/AdminPanelView').default
},
StatusView: {
getScreen: () => require('./views/StatusView').default
}
}, {
defaultNavigationOptions: defaultHeader,
......
......@@ -23,6 +23,8 @@ import appSchema from './schema/app';
import migrations from './model/migrations';
import serversMigrations from './model/serversMigrations';
import { isIOS } from '../../utils/deviceInfo';
const appGroupPath = isIOS ? `${ RNFetchBlob.fs.syncPathAppGroup('group.ios.chat.rocket') }/` : '';
......@@ -36,7 +38,8 @@ class DB {
serversDB: new Database({
adapter: new SQLiteAdapter({
dbName: `${ appGroupPath }default.db`,
schema: serversSchema
schema: serversSchema,
migrations: serversMigrations
}),
modelClasses: [Server, User],
actionsEnabled: true
......
......@@ -16,5 +16,7 @@ export default class User extends Model {
@field('status') status;
@field('statusText') statusText;
@json('roles', sanitizer) roles;
}
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 }
]
})
]
}
]
});
import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({
version: 2,
version: 3,
tables: [
tableSchema({
name: 'users',
......@@ -11,6 +11,7 @@ export default appSchema({
{ name: 'name', type: 'string', isOptional: true },
{ name: 'language', type: 'string', isOptional: true },
{ name: 'status', type: 'string', isOptional: true },
{ name: 'statusText', type: 'string', isOptional: true },
{ name: 'roles', type: 'string', isOptional: true }
]
}),
......
......@@ -45,7 +45,10 @@ export default async function getUsersPresence() {
const result = await this.sdk.get('users.presence', params);
if (result.success) {
const activeUsers = result.users.reduce((ret, item) => {
ret[item._id] = item.status;
ret[item._id] = {
status: item.status,
statusText: item.statusText
};
return ret;
}, {});
InteractionManager.runAfterInteractions(() => {
......
......@@ -241,12 +241,12 @@ const RocketChat = {
}, 10000);
}
const userStatus = ddpMessage.fields.args[0];
const [id,, status] = userStatus;
this.activeUsers[id] = STATUSES[status];
const [id,, status, statusText] = userStatus;
this.activeUsers[id] = { status: STATUSES[status], statusText };
const { user: loggedUser } = reduxStore.getState().login;
if (loggedUser && loggedUser.id === id) {
reduxStore.dispatch(setUser({ status: STATUSES[status] }));
reduxStore.dispatch(setUser({ status: STATUSES[status], statusText }));
}
}
}));
......@@ -378,6 +378,7 @@ const RocketChat = {
name: result.me.name,
language: result.me.language,
status: result.me.status,
statusText: result.me.statusText,
customFields: result.me.customFields,
emails: result.me.emails,
roles: result.me.roles
......@@ -741,6 +742,10 @@ const RocketChat = {
setUserPresenceDefaultStatus(status) {
return this.sdk.methodCall('UserPresence:setDefaultStatus', status);
},
setUserStatus(message) {
// RC 1.2.0
return this.sdk.post('users.setStatus', { message });
},
setReaction(emoji, messageId) {
// RC 0.62.2
return this.sdk.post('chat.react', { emoji, messageId });
......@@ -1056,7 +1061,7 @@ const RocketChat = {
}
if (ddpMessage.cleared && user && user.id === ddpMessage.id) {
reduxStore.dispatch(setUser({ status: 'offline' }));
reduxStore.dispatch(setUser({ status: { status: 'offline' } }));
}
if (!this._setUserTimer) {
......@@ -1071,9 +1076,9 @@ const RocketChat = {
}
if (!ddpMessage.fields) {
this.activeUsers[ddpMessage.id] = 'offline';
this.activeUsers[ddpMessage.id] = { status: 'offline' };
} else if (ddpMessage.fields.status) {
this.activeUsers[ddpMessage.id] = ddpMessage.fields.status;
this.activeUsers[ddpMessage.id] = { status: ddpMessage.fields.status };
}
},
getUsersPresence,
......
......@@ -210,7 +210,7 @@ RoomItem.defaultProps = {
const mapStateToProps = (state, ownProps) => ({
status:
state.meteor.connected && ownProps.type === 'd'
? state.activeUsers[ownProps.id]
? state.activeUsers[ownProps.id] && state.activeUsers[ownProps.id].status
: 'offline'
});
......
......@@ -101,6 +101,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
name: user.name,
language: user.language,
status: user.status,
statusText: user.statusText,
roles: user.roles
};
yield serversDB.action(async() => {
......
......@@ -78,6 +78,7 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
name: userRecord.name,
language: userRecord.language,
status: userRecord.status,
statusText: userRecord.statusText,
roles: userRecord.roles
};
} catch (e) {
......
......@@ -112,17 +112,11 @@ export const initTabletNav = (setState) => {
KeyCommands.deleteKeyCommands([...defaultCommands, ...keyCommands]);
setState({ inside: false, showModal: false });
}
if (routeName === 'ModalBlockView') {
if (routeName === 'ModalBlockView' || routeName === 'StatusView' || routeName === 'CreateDiscussionView') {
modalRef.dispatch(NavigationActions.navigate({ routeName, params }));
setState({ showModal: true });
return null;
}
if (routeName === 'CreateDiscussionView') {
modalRef.dispatch(NavigationActions.navigate({ routeName, params }));
setState({ showModal: true });
return null;
}
if (routeName === 'RoomView') {
const resetAction = StackActions.reset({
index: 0,
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment