From 63a00c1cf6a397e5fa013f47bc55480c30001783 Mon Sep 17 00:00:00 2001 From: Shevilll Date: Mon, 22 Jun 2026 16:21:40 +0530 Subject: [PATCH 1/3] fix: validate username characters in profile before saving Validate the username field against the server's UTF8_User_Names_Validation setting (with the default pattern as a fallback) before submitting the profile. This prevents invalid characters such as spaces from reaching the server, which previously surfaced only as a generic save error, and shows an inline message explaining the allowed characters. Closes #1682 --- app/i18n/locales/en.json | 1 + app/views/ProfileView/index.test.tsx | 25 +++++++++++++++++++++- app/views/ProfileView/index.tsx | 32 ++++++++++++++++++++++------ 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index ac7f240d4d9..24b689ce196 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -994,6 +994,7 @@ "User_not_found_or": "User not found or incorrect password", "User_sent_an_attachment": "{{user}} sent an attachment", "Username": "Username", + "Username_invalid": "Use only letters, numbers, dots, hyphens and underscores", "Username_is_already_in_use": "Username is already in use.", "Username_not_available": "Username not available", "Username_or_email": "Username or email", diff --git a/app/views/ProfileView/index.test.tsx b/app/views/ProfileView/index.test.tsx index 455c467a31d..c37decd216c 100644 --- a/app/views/ProfileView/index.test.tsx +++ b/app/views/ProfileView/index.test.tsx @@ -9,6 +9,7 @@ import { twoFactor } from '../../lib/services/twoFactor'; import handleSaveUserProfileError from '../../lib/methods/helpers/handleSaveUserProfileError'; import EventEmitter from '../../lib/methods/helpers/events'; import { setUser } from '../../actions/login'; +import I18n from '../../i18n'; jest.mock('react-redux', () => ({ useDispatch: jest.fn() @@ -70,7 +71,8 @@ const buildState = () => ({ Accounts_AllowUserAvatarChange: true, Accounts_AllowUsernameChange: true, Accounts_CustomFields: '', - Accounts_AllowDeleteOwnAccount: true + Accounts_AllowDeleteOwnAccount: true, + UTF8_User_Names_Validation: '[0-9a-zA-Z-_.]+' } }); @@ -111,6 +113,27 @@ describe('ProfileView submit', () => { expect(handleSaveUserProfileError).not.toHaveBeenCalled(); }); + it('does not save when the username contains invalid characters', async () => { + const { getByTestId, findByText } = renderProfile(); + + fireEvent.changeText(getByTestId('profile-view-username'), 'cygnus b'); + fireEvent.press(getByTestId('profile-view-submit')); + + await findByText(I18n.t('Username_invalid')); + expect(saveUserProfile).not.toHaveBeenCalled(); + }); + + it('saves when the username is corrected to a valid value', async () => { + (saveUserProfile as jest.Mock).mockResolvedValue(true); + + const { getByTestId } = renderProfile(); + + fireEvent.changeText(getByTestId('profile-view-username'), 'cygnus.b'); + fireEvent.press(getByTestId('profile-view-submit')); + + await waitFor(() => expect(saveUserProfile).toHaveBeenCalledWith({ username: 'cygnus.b' }, {})); + }); + it('asks for the current password before changing the email', async () => { const { getByTestId } = renderProfile(); diff --git a/app/views/ProfileView/index.tsx b/app/views/ProfileView/index.tsx index 36e9ed8bf77..0b5c33580b2 100644 --- a/app/views/ProfileView/index.tsx +++ b/app/views/ProfileView/index.tsx @@ -45,16 +45,25 @@ import ConfirmEmailChangeActionSheetContent from './components/ConfirmEmailChang const MAX_BIO_LENGTH = 260; const MAX_NICKNAME_LENGTH = 120; +// Mirror the server's username validation, which matches the value against the +// UTF8_User_Names_Validation setting anchored as a full match (`^pattern$`), +// falling back to the default pattern when the setting is empty or invalid. +// https://github.com/RocketChat/Rocket.Chat/blob/develop/apps/meteor/app/lib/server/functions/validateUsername.ts +const DEFAULT_USERNAME_VALIDATION = '[0-9a-zA-Z-_.]+'; + +const isValidUsername = (username: string, pattern: string) => { + try { + return new RegExp(`^(${pattern || DEFAULT_USERNAME_VALIDATION})$`).test(username); + } catch { + // Fall back to the default pattern if the server provides an invalid regex. + return new RegExp(`^(${DEFAULT_USERNAME_VALIDATION})$`).test(username); + } +}; + interface IProfileViewProps { navigation: NativeStackNavigationProp; } const ProfileView = ({ navigation }: IProfileViewProps): ReactElement => { - const validationSchema = yup.object().shape({ - name: yup.string().required(I18n.t('Name_required')), - email: yup.string().email(I18n.t('Email_must_be_a_valid_email')).required(I18n.t('Email_required')), - username: yup.string().required(I18n.t('Username_required')) - }); - const { showActionSheet, hideActionSheet } = useActionSheet(); const { colors } = useTheme(); const dispatch = useDispatch(); @@ -66,6 +75,7 @@ const ProfileView = ({ navigation }: IProfileViewProps): ReactElement => { Accounts_AllowUserAvatarChange, Accounts_AllowUsernameChange, Accounts_CustomFields, + UTF8_User_Names_Validation, isMasterDetail, serverVersion, user @@ -78,9 +88,19 @@ const ProfileView = ({ navigation }: IProfileViewProps): ReactElement => { Accounts_AllowUserAvatarChange: state.settings.Accounts_AllowUserAvatarChange as boolean, Accounts_AllowUsernameChange: state.settings.Accounts_AllowUsernameChange as boolean, Accounts_CustomFields: state.settings.Accounts_CustomFields as string, + UTF8_User_Names_Validation: state.settings.UTF8_User_Names_Validation as string, serverVersion: state.server.version, Accounts_AllowDeleteOwnAccount: state.settings.Accounts_AllowDeleteOwnAccount as boolean })); + + const validationSchema = yup.object().shape({ + name: yup.string().required(I18n.t('Name_required')), + email: yup.string().email(I18n.t('Email_must_be_a_valid_email')).required(I18n.t('Email_required')), + username: yup + .string() + .required(I18n.t('Username_required')) + .test('valid-username', I18n.t('Username_invalid'), value => isValidUsername(value ?? '', UTF8_User_Names_Validation)) + }); const { control, handleSubmit, From 193cf2473b60ce8981dbdacdc7b4304b615c489b Mon Sep 17 00:00:00 2001 From: Shevilll Date: Wed, 24 Jun 2026 16:52:24 +0530 Subject: [PATCH 2/3] fix(profile): remove duplicate isMasterDetail binding and type guard Drop the stale isMasterDetail destructured from useAppSelector (the selector no longer returns it) so only the useMasterDetail() hook declares it, fixing the no-redeclare lint/build failure introduced by merging develop. Also annotate isValidUsername with an explicit boolean return type per the TS guideline. --- app/views/ProfileView/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/ProfileView/index.tsx b/app/views/ProfileView/index.tsx index fee2232ae29..34496096703 100644 --- a/app/views/ProfileView/index.tsx +++ b/app/views/ProfileView/index.tsx @@ -52,7 +52,7 @@ const MAX_NICKNAME_LENGTH = 120; // https://github.com/RocketChat/Rocket.Chat/blob/develop/apps/meteor/app/lib/server/functions/validateUsername.ts const DEFAULT_USERNAME_VALIDATION = '[0-9a-zA-Z-_.]+'; -const isValidUsername = (username: string, pattern: string) => { +const isValidUsername = (username: string, pattern: string): boolean => { try { return new RegExp(`^(${pattern || DEFAULT_USERNAME_VALIDATION})$`).test(username); } catch { @@ -77,7 +77,6 @@ const ProfileView = ({ navigation }: IProfileViewProps): ReactElement => { Accounts_AllowUsernameChange, Accounts_CustomFields, UTF8_User_Names_Validation, - isMasterDetail, serverVersion, user } = useAppSelector(state => ({ From 40fbb6db4a791be45a18cfbbd49e63cfe94381b7 Mon Sep 17 00:00:00 2001 From: Shevilll Date: Fri, 26 Jun 2026 00:01:28 +0530 Subject: [PATCH 3/3] fix(markdown): ensure custom style overrides precede theme colors --- .../markdown/Markdown.textStyle.test.tsx | 26 +++++++++++++++++++ .../markdown/components/inline/Link.tsx | 2 +- .../components/mentions/AtMention.tsx | 8 +++--- .../markdown/components/mentions/Hashtag.tsx | 6 ++--- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/app/containers/markdown/Markdown.textStyle.test.tsx b/app/containers/markdown/Markdown.textStyle.test.tsx index ae7b1b4af12..c25835e5435 100644 --- a/app/containers/markdown/Markdown.textStyle.test.tsx +++ b/app/containers/markdown/Markdown.textStyle.test.tsx @@ -40,4 +40,30 @@ describe('Markdown textStyle integration', () => { expect(onLinkPress).toHaveBeenCalledWith('https://rocket.chat'); }); + + it('propagates custom color in textStyle to link, mention, hashtag and plain text', () => { + const onLinkPress = jest.fn(); + const textStyle = { color: 'red' }; + + const { getByLabelText, getByText } = render( + + ); + + const plainTextNode = getByLabelText('hello '); + const linkNode = getByText('my link'); + const mentionNode = getByText('@rocket.cat'); + const hashtagNode = getByText('#general'); + + expect(plainTextNode.props.style).toEqual(expect.arrayContaining([expect.objectContaining({ color: 'red' })])); + expect(linkNode.props.style).toEqual(expect.arrayContaining([expect.objectContaining({ color: 'red' })])); + expect(mentionNode.props.style).toEqual(expect.arrayContaining([expect.objectContaining({ color: 'red' })])); + expect(hashtagNode.props.style).toEqual(expect.arrayContaining([expect.objectContaining({ color: 'red' })])); + }); }); diff --git a/app/containers/markdown/components/inline/Link.tsx b/app/containers/markdown/components/inline/Link.tsx index 8bc9ea9d527..4da2c3124d8 100644 --- a/app/containers/markdown/components/inline/Link.tsx +++ b/app/containers/markdown/components/inline/Link.tsx @@ -49,7 +49,7 @@ const Link = ({ value }: ILinkProps) => { return ( {(block => { diff --git a/app/containers/markdown/components/mentions/AtMention.tsx b/app/containers/markdown/components/mentions/AtMention.tsx index bb454b7bbd2..ae3fcfdfed0 100644 --- a/app/containers/markdown/components/mentions/AtMention.tsx +++ b/app/containers/markdown/components/mentions/AtMention.tsx @@ -28,10 +28,10 @@ const AtMention = memo(({ mention, mentions, username, navToRoomInfo, useRealNam {preffix} {mention} @@ -76,7 +76,7 @@ const AtMention = memo(({ mention, mentions, username, navToRoomInfo, useRealNam return ( // not enough information on mentions to navigate to team info, so we don't handle onPress {preffix} {text} @@ -85,7 +85,7 @@ const AtMention = memo(({ mention, mentions, username, navToRoomInfo, useRealNam } return ( - {`@${mention}`} + {`@${mention}`} ); }); diff --git a/app/containers/markdown/components/mentions/Hashtag.tsx b/app/containers/markdown/components/mentions/Hashtag.tsx index 3336998369c..425779204e7 100644 --- a/app/containers/markdown/components/mentions/Hashtag.tsx +++ b/app/containers/markdown/components/mentions/Hashtag.tsx @@ -57,10 +57,10 @@ const Hashtag = memo(({ hashtag, channels, navToRoomInfo }: IHashtag) => { {`${preffix}${hashtag}`} @@ -68,7 +68,7 @@ const Hashtag = memo(({ hashtag, channels, navToRoomInfo }: IHashtag) => { ); } return ( - {`#${hashtag}`} + {`#${hashtag}`} ); });