feat: add bio and nickname to profile view (#5060)
* feat: add bio and nickname to profile view * add the text input in profileview * fix the bio layout and add to translation * fix e2e tests * add max length to nickname and bio * refactor a bit the inputRef * fix the text align vertical of multiline
This commit is contained in:
parent
5c5ff2d51f
commit
a203f67a4a
|
@ -22,6 +22,8 @@ export interface ILoggedUser {
|
|||
isFromWebView?: boolean;
|
||||
enableMessageParserEarlyAdoption: boolean;
|
||||
alsoSendThreadToChannel: 'default' | 'always' | 'never';
|
||||
bio?: string;
|
||||
nickname?: string;
|
||||
}
|
||||
|
||||
export interface ILoggedUserResultFromServer
|
||||
|
|
|
@ -7,6 +7,8 @@ export interface IProfileParams {
|
|||
email: string | null;
|
||||
newPassword: string;
|
||||
currentPassword: string;
|
||||
bio?: string;
|
||||
nickname?: string;
|
||||
}
|
||||
|
||||
export interface IAvatarButton {
|
||||
|
|
|
@ -726,6 +726,8 @@
|
|||
"Presence_Cap_Warning_Description": "Active connections have reached the limit for the workspace, thus the service that handles user status is disabled. It can be re-enabled manually in workspace settings.",
|
||||
"Learn_more": "Learn more",
|
||||
"and_N_more": "and {{count}} more",
|
||||
"Nickname": "Nickname",
|
||||
"Bio":"Bio",
|
||||
"decline": "Decline",
|
||||
"accept": "Accept",
|
||||
"Incoming_call_from": "Incoming call from",
|
||||
|
|
|
@ -711,6 +711,8 @@
|
|||
"Discard_changes_description": "Todas as alterações serão perdidas, se você sair sem salvar.",
|
||||
"Presence_Cap_Warning_Title": "Status do usuário desabilitado temporariamente",
|
||||
"Presence_Cap_Warning_Description": "O limite de conexões ativas para a workspace foi atingido, por isso o serviço responsável pela presença dos usuários está temporariamente desabilitado. Ele pode ser reabilitado manualmente nas configurações da workspace.",
|
||||
"Nickname": "Apelido",
|
||||
"Bio": "Biografia",
|
||||
"decline": "Recusar",
|
||||
"accept": "Aceitar",
|
||||
"Incoming_call_from": "Chamada recebida de",
|
||||
|
|
|
@ -29,4 +29,8 @@ export default class User extends Model {
|
|||
@field('is_from_webview') isFromWebView;
|
||||
|
||||
@field('enable_message_parser_early_adoption') enableMessageParserEarlyAdoption;
|
||||
|
||||
@field('nickname') nickname;
|
||||
|
||||
@field('bio') bio;
|
||||
}
|
||||
|
|
|
@ -103,6 +103,18 @@ export default schemaMigrations({
|
|||
columns: [{ name: 'enable_message_parser_early_adoption', type: 'boolean', isOptional: true }]
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
toVersion: 13,
|
||||
steps: [
|
||||
addColumns({
|
||||
table: 'users',
|
||||
columns: [
|
||||
{ name: 'nickname', type: 'string', isOptional: true },
|
||||
{ name: 'bio', type: 'string', isOptional: true }
|
||||
]
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { appSchema, tableSchema } from '@nozbe/watermelondb';
|
||||
|
||||
export default appSchema({
|
||||
version: 12,
|
||||
version: 13,
|
||||
tables: [
|
||||
tableSchema({
|
||||
name: 'users',
|
||||
|
@ -17,7 +17,9 @@ export default appSchema({
|
|||
{ name: 'show_message_in_main_thread', type: 'boolean', isOptional: true },
|
||||
{ name: 'avatar_etag', type: 'string', isOptional: true },
|
||||
{ name: 'is_from_webview', type: 'boolean', isOptional: true },
|
||||
{ name: 'enable_message_parser_early_adoption', type: 'boolean', isOptional: true }
|
||||
{ name: 'enable_message_parser_early_adoption', type: 'boolean', isOptional: true },
|
||||
{ name: 'nickname', type: 'string', isOptional: true },
|
||||
{ name: 'bio', type: 'string', isOptional: true }
|
||||
]
|
||||
}),
|
||||
tableSchema({
|
||||
|
|
|
@ -309,6 +309,12 @@ export default function subscribeRooms() {
|
|||
if (unset?.avatarETag) {
|
||||
store.dispatch(setUser({ avatarETag: '' }));
|
||||
}
|
||||
if (diff?.bio) {
|
||||
store.dispatch(setUser({ bio: diff.bio }));
|
||||
}
|
||||
if (diff?.nickname) {
|
||||
store.dispatch(setUser({ nickname: diff.nickname }));
|
||||
}
|
||||
}
|
||||
if (/subscriptions/.test(ev)) {
|
||||
if (type === 'removed') {
|
||||
|
|
|
@ -304,7 +304,9 @@ async function login(credentials: ICredentials, isFromWebView = false): Promise<
|
|||
isFromWebView,
|
||||
showMessageInMainThread,
|
||||
enableMessageParserEarlyAdoption,
|
||||
alsoSendThreadToChannel: result.me.settings?.preferences?.alsoSendThreadToChannel
|
||||
alsoSendThreadToChannel: result.me.settings?.preferences?.alsoSendThreadToChannel,
|
||||
bio: result.me.bio,
|
||||
nickname: result.me.nickname
|
||||
};
|
||||
return user;
|
||||
}
|
||||
|
|
|
@ -171,7 +171,9 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
|
|||
roles: user.roles,
|
||||
isFromWebView: user.isFromWebView,
|
||||
showMessageInMainThread: user.showMessageInMainThread,
|
||||
avatarETag: user.avatarETag
|
||||
avatarETag: user.avatarETag,
|
||||
bio: user.bio,
|
||||
nickname: user.nickname
|
||||
};
|
||||
yield serversDB.action(async () => {
|
||||
try {
|
||||
|
|
|
@ -94,7 +94,9 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
|
|||
status: userRecord.status,
|
||||
statusText: userRecord.statusText,
|
||||
roles: userRecord.roles,
|
||||
avatarETag: userRecord.avatarETag
|
||||
avatarETag: userRecord.avatarETag,
|
||||
bio: userRecord.bio,
|
||||
nickname: userRecord.nickname
|
||||
};
|
||||
} catch {
|
||||
// search credentials on shared credentials (Experimental/Official)
|
||||
|
|
|
@ -11,7 +11,7 @@ import Touch from '../../containers/Touch';
|
|||
import KeyboardView from '../../containers/KeyboardView';
|
||||
import sharedStyles from '../Styles';
|
||||
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
|
||||
import { showErrorAlert, showConfirmationAlert } from '../../lib/methods/helpers';
|
||||
import { showErrorAlert, showConfirmationAlert, compareServerVersion } from '../../lib/methods/helpers';
|
||||
import { LISTENER } from '../../containers/Toast';
|
||||
import EventEmitter from '../../lib/methods/helpers/events';
|
||||
import { FormTextInput } from '../../containers/TextInput';
|
||||
|
@ -36,6 +36,10 @@ import { withActionSheet, IActionSheetProvider } from '../../containers/ActionSh
|
|||
import { DeleteAccountActionSheetContent } from './components/DeleteAccountActionSheetContent';
|
||||
import ActionSheetContentWithInputAndSubmit from '../../containers/ActionSheet/ActionSheetContentWithInputAndSubmit';
|
||||
|
||||
// https://github.com/RocketChat/Rocket.Chat/blob/174c28d40b3d5a52023ee2dca2e81dd77ff33fa5/apps/meteor/app/lib/server/functions/saveUser.js#L24-L25
|
||||
const MAX_BIO_LENGTH = 260;
|
||||
const MAX_NICKNAME_LENGTH = 120;
|
||||
|
||||
interface IProfileViewProps extends IActionSheetProvider, IBaseScreen<ProfileStackParamList, 'ProfileView'> {
|
||||
user: IUser;
|
||||
baseUrl: string;
|
||||
|
@ -48,6 +52,7 @@ interface IProfileViewProps extends IActionSheetProvider, IBaseScreen<ProfileSta
|
|||
theme: TSupportedThemes;
|
||||
Accounts_AllowDeleteOwnAccount: boolean;
|
||||
isMasterDetail: boolean;
|
||||
serverVersion: string;
|
||||
}
|
||||
|
||||
interface IProfileViewState {
|
||||
|
@ -55,6 +60,8 @@ interface IProfileViewState {
|
|||
name: string;
|
||||
username: string;
|
||||
email: string | null;
|
||||
bio?: string;
|
||||
nickname?: string;
|
||||
newPassword: string | null;
|
||||
currentPassword: string | null;
|
||||
customFields: {
|
||||
|
@ -69,9 +76,11 @@ interface IProfileViewState {
|
|||
class ProfileView extends React.Component<IProfileViewProps, IProfileViewState> {
|
||||
private name?: TextInput | null;
|
||||
private username?: TextInput | null;
|
||||
private email?: TextInput;
|
||||
private avatarUrl?: TextInput;
|
||||
private newPassword?: TextInput;
|
||||
private email?: TextInput | null;
|
||||
private avatarUrl?: TextInput | null;
|
||||
private newPassword?: TextInput | null;
|
||||
private nickname?: TextInput | null;
|
||||
private bio?: TextInput | null;
|
||||
|
||||
setHeader = () => {
|
||||
const { navigation, isMasterDetail } = this.props;
|
||||
|
@ -98,6 +107,8 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
name: '',
|
||||
username: '',
|
||||
email: '',
|
||||
bio: '',
|
||||
nickname: '',
|
||||
newPassword: '',
|
||||
currentPassword: '',
|
||||
customFields: {},
|
||||
|
@ -123,7 +134,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
|
||||
init = (user?: IUser) => {
|
||||
const { user: userProps } = this.props;
|
||||
const { name, username, emails, customFields } = user || userProps;
|
||||
const { name, username, emails, customFields, bio, nickname } = user || userProps;
|
||||
|
||||
this.setState({
|
||||
name: name as string,
|
||||
|
@ -131,12 +142,14 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
email: emails ? emails[0].address : null,
|
||||
newPassword: null,
|
||||
currentPassword: null,
|
||||
customFields: customFields || {}
|
||||
customFields: customFields || {},
|
||||
bio,
|
||||
nickname
|
||||
});
|
||||
};
|
||||
|
||||
formIsChanged = () => {
|
||||
const { name, username, email, newPassword, customFields } = this.state;
|
||||
const { name, username, email, newPassword, customFields, bio, nickname } = this.state;
|
||||
const { user } = this.props;
|
||||
let customFieldsChanged = false;
|
||||
|
||||
|
@ -152,6 +165,8 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
return !(
|
||||
user.name === name &&
|
||||
user.username === username &&
|
||||
user.bio === bio &&
|
||||
user.nickname === nickname &&
|
||||
!newPassword &&
|
||||
user.emails &&
|
||||
user.emails[0].address === email &&
|
||||
|
@ -168,7 +183,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
|
||||
this.setState({ saving: true });
|
||||
|
||||
const { name, username, email, newPassword, currentPassword, customFields, twoFactorCode } = this.state;
|
||||
const { name, username, email, newPassword, currentPassword, customFields, twoFactorCode, bio, nickname } = this.state;
|
||||
const { user, dispatch } = this.props;
|
||||
const params = {} as IProfileParams;
|
||||
|
||||
|
@ -187,6 +202,14 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
params.email = email;
|
||||
}
|
||||
|
||||
if (user.bio !== bio) {
|
||||
params.bio = bio;
|
||||
}
|
||||
|
||||
if (user.nickname !== nickname) {
|
||||
params.nickname = nickname;
|
||||
}
|
||||
|
||||
// newPassword
|
||||
if (newPassword) {
|
||||
params.newPassword = newPassword;
|
||||
|
@ -400,7 +423,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
};
|
||||
|
||||
render() {
|
||||
const { name, username, email, newPassword, customFields, saving } = this.state;
|
||||
const { name, username, email, newPassword, customFields, saving, nickname, bio } = this.state;
|
||||
const {
|
||||
user,
|
||||
theme,
|
||||
|
@ -410,7 +433,8 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
Accounts_AllowUserAvatarChange,
|
||||
Accounts_AllowUsernameChange,
|
||||
Accounts_CustomFields,
|
||||
Accounts_AllowDeleteOwnAccount
|
||||
Accounts_AllowDeleteOwnAccount,
|
||||
serverVersion
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
@ -431,9 +455,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
<FormTextInput
|
||||
editable={Accounts_AllowRealNameChange}
|
||||
inputStyle={[!Accounts_AllowRealNameChange && styles.disabled]}
|
||||
inputRef={e => {
|
||||
this.name = e;
|
||||
}}
|
||||
inputRef={e => (this.name = e)}
|
||||
label={I18n.t('Name')}
|
||||
placeholder={I18n.t('Name')}
|
||||
value={name}
|
||||
|
@ -446,9 +468,7 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
<FormTextInput
|
||||
editable={Accounts_AllowUsernameChange}
|
||||
inputStyle={[!Accounts_AllowUsernameChange && styles.disabled]}
|
||||
inputRef={e => {
|
||||
this.username = e;
|
||||
}}
|
||||
inputRef={e => (this.username = e)}
|
||||
label={I18n.t('Username')}
|
||||
placeholder={I18n.t('Username')}
|
||||
value={username}
|
||||
|
@ -461,28 +481,48 @@ class ProfileView extends React.Component<IProfileViewProps, IProfileViewState>
|
|||
<FormTextInput
|
||||
editable={Accounts_AllowEmailChange}
|
||||
inputStyle={[!Accounts_AllowEmailChange && styles.disabled]}
|
||||
inputRef={e => {
|
||||
if (e) {
|
||||
this.email = e;
|
||||
}
|
||||
}}
|
||||
inputRef={e => (this.email = e)}
|
||||
label={I18n.t('Email')}
|
||||
placeholder={I18n.t('Email')}
|
||||
value={email || undefined}
|
||||
onChangeText={value => this.setState({ email: value })}
|
||||
onSubmitEditing={() => {
|
||||
this.newPassword?.focus();
|
||||
this.nickname?.focus();
|
||||
}}
|
||||
testID='profile-view-email'
|
||||
/>
|
||||
{compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '3.5.0') ? (
|
||||
<FormTextInput
|
||||
inputRef={e => (this.nickname = e)}
|
||||
label={I18n.t('Nickname')}
|
||||
value={nickname}
|
||||
onChangeText={value => this.setState({ nickname: value })}
|
||||
onSubmitEditing={() => {
|
||||
this.bio?.focus();
|
||||
}}
|
||||
testID='profile-view-nickname'
|
||||
maxLength={MAX_NICKNAME_LENGTH}
|
||||
/>
|
||||
) : null}
|
||||
{compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '3.1.0') ? (
|
||||
<FormTextInput
|
||||
inputRef={e => (this.bio = e)}
|
||||
label={I18n.t('Bio')}
|
||||
inputStyle={styles.inputBio}
|
||||
multiline
|
||||
maxLength={MAX_BIO_LENGTH}
|
||||
value={bio}
|
||||
onChangeText={value => this.setState({ bio: value })}
|
||||
onSubmitEditing={() => {
|
||||
this.newPassword?.focus();
|
||||
}}
|
||||
testID='profile-view-bio'
|
||||
/>
|
||||
) : null}
|
||||
<FormTextInput
|
||||
editable={Accounts_AllowPasswordChange}
|
||||
inputStyle={[!Accounts_AllowPasswordChange && styles.disabled]}
|
||||
inputRef={e => {
|
||||
if (e) {
|
||||
this.newPassword = e;
|
||||
}
|
||||
}}
|
||||
inputRef={e => (this.newPassword = e)}
|
||||
label={I18n.t('New_Password')}
|
||||
placeholder={I18n.t('New_Password')}
|
||||
value={newPassword || undefined}
|
||||
|
@ -539,6 +579,7 @@ const mapStateToProps = (state: IApplicationState) => ({
|
|||
Accounts_AllowUsernameChange: state.settings.Accounts_AllowUsernameChange as boolean,
|
||||
Accounts_CustomFields: state.settings.Accounts_CustomFields as string,
|
||||
baseUrl: state.server.server,
|
||||
serverVersion: state.server.version,
|
||||
Accounts_AllowDeleteOwnAccount: state.settings.Accounts_AllowDeleteOwnAccount as boolean
|
||||
});
|
||||
|
||||
|
|
|
@ -22,5 +22,9 @@ export default StyleSheet.create({
|
|||
marginRight: 15,
|
||||
marginBottom: 15,
|
||||
borderRadius: 4
|
||||
},
|
||||
inputBio: {
|
||||
height: 100,
|
||||
textAlignVertical: 'top'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -10,6 +10,12 @@ async function waitForToast() {
|
|||
await sleep(600);
|
||||
}
|
||||
|
||||
async function dismissKeyboardAndScrollUp() {
|
||||
await element(by.id('profile-view-list')).swipe('down');
|
||||
await sleep(300);
|
||||
await element(by.id('profile-view-list')).swipe('up');
|
||||
}
|
||||
|
||||
describe('Profile screen', () => {
|
||||
let textMatcher: TTextMatcher;
|
||||
let user: ITestUser;
|
||||
|
@ -74,23 +80,27 @@ describe('Profile screen', () => {
|
|||
it('should change name and username', async () => {
|
||||
await element(by.id('profile-view-name')).replaceText(`${user.username}new`);
|
||||
await element(by.id('profile-view-username')).replaceText(`${user.username}new`);
|
||||
// dismiss keyboard
|
||||
await element(by.id('profile-view-list')).swipe('down');
|
||||
await dismissKeyboardAndScrollUp();
|
||||
await element(by.id('profile-view-submit')).tap();
|
||||
await waitForToast();
|
||||
});
|
||||
|
||||
it('should change nickname and bio', async () => {
|
||||
await element(by.id('profile-view-nickname')).replaceText(`nickname-${user.username}`);
|
||||
await element(by.id('profile-view-bio')).replaceText(`bio-${user.username}`);
|
||||
await dismissKeyboardAndScrollUp();
|
||||
await element(by.id('profile-view-submit')).tap();
|
||||
await waitForToast();
|
||||
});
|
||||
|
||||
it('should change email and password', async () => {
|
||||
await element(by.id('profile-view-list')).swipe('up');
|
||||
await element(by.id('profile-view-list')).swipe('down');
|
||||
await waitFor(element(by.id('profile-view-email')))
|
||||
.toBeVisible()
|
||||
.withTimeout(2000);
|
||||
await element(by.id('profile-view-email')).replaceText(`mobile+profileChangesNew${random()}@rocket.chat`);
|
||||
// dismiss keyboard
|
||||
await element(by.id('profile-view-list')).swipe('down');
|
||||
await dismissKeyboardAndScrollUp();
|
||||
await element(by.id('profile-view-new-password')).replaceText(`${user.password}new`);
|
||||
// dismiss keyboard
|
||||
await element(by.id('profile-view-list')).swipe('down');
|
||||
await waitFor(element(by.id('profile-view-submit')))
|
||||
.toExist()
|
||||
.withTimeout(2000);
|
||||
|
|
Loading…
Reference in New Issue