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:
Reinaldo Neto 2023-08-01 14:34:05 -03:00 committed by GitHub
parent 5c5ff2d51f
commit a203f67a4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 132 additions and 39 deletions

View File

@ -22,6 +22,8 @@ export interface ILoggedUser {
isFromWebView?: boolean;
enableMessageParserEarlyAdoption: boolean;
alsoSendThreadToChannel: 'default' | 'always' | 'never';
bio?: string;
nickname?: string;
}
export interface ILoggedUserResultFromServer

View File

@ -7,6 +7,8 @@ export interface IProfileParams {
email: string | null;
newPassword: string;
currentPassword: string;
bio?: string;
nickname?: string;
}
export interface IAvatarButton {

View File

@ -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",

View File

@ -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",

View File

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

View File

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

View File

@ -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({

View File

@ -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') {

View File

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

View File

@ -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 {

View File

@ -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)

View File

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

View File

@ -22,5 +22,9 @@ export default StyleSheet.create({
marginRight: 15,
marginBottom: 15,
borderRadius: 4
},
inputBio: {
height: 100,
textAlignVertical: 'top'
}
});

View File

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