Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions app/containers/markdown/Markdown.textStyle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Markdown
msg='hello [my link](https://rocket.chat) @rocket.cat #general'
textStyle={textStyle}
onLinkPress={onLinkPress}
mentions={[{ _id: 'u1', username: 'rocket.cat', name: 'Rocket Cat', type: 'user' }]}
username='another.user'
channels={[{ _id: 'r1', name: 'general' }]}
/>
);

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' })]));
Comment on lines +64 to +67

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor

Assert the resolved color priority, not just presence in the style array.

The current arrayContaining check only proves that a red color object appears somewhere in props.style. It does not verify that the passed textStyle takes visual precedence over the internal theme color. If the order were regressed (e.g., textStyle applied before the theme), the test would still pass while the component renders the wrong color. Refine the assertions to verify the effective resolved color or the specific position of the override object to ensure the fix is actually protected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/containers/markdown/Markdown.textStyle.test.tsx` around lines 64 - 67,
The assertions in Markdown.textStyle.test.tsx only check that a red style exists
somewhere in the style array, so they do not protect the override order in the
Markdown rendering path. Update the expectations around plainTextNode, linkNode,
mentionNode, and hashtagNode to verify the effective resolved color or that the
textStyle override is applied in the correct precedence relative to the internal
theme style, using the relevant props.style on the rendered nodes.

});
});
2 changes: 1 addition & 1 deletion app/containers/markdown/components/inline/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const Link = ({ value }: ILinkProps) => {

return (
<Text
style={[styles.link, ...(textStyle ? [textStyle] : []), { color: themes[theme].fontInfo }]}
style={[styles.link, { color: themes[theme].fontInfo }, ...(textStyle ? [textStyle] : [])]}
onPress={handlePress}
onLongPress={onLongPress}>
{(block => {
Expand Down
8 changes: 4 additions & 4 deletions app/containers/markdown/components/mentions/AtMention.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ const AtMention = memo(({ mention, mentions, username, navToRoomInfo, useRealNam
<Text
style={[
styles.mention,
...(textStyle ? [textStyle] : []),
{
color: themes[theme].statusFontService
}
},
...(textStyle ? [textStyle] : [])
]}>
{preffix}
{mention}
Expand Down Expand Up @@ -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
<Text
style={[styles.mention, ...(textStyle ? [textStyle] : []), mentionStyle]}
style={[styles.mention, mentionStyle, ...(textStyle ? [textStyle] : [])]}
onPress={atMentioned?.type === 'team' ? undefined : handlePress}>
{preffix}
{text}
Expand All @@ -85,7 +85,7 @@ const AtMention = memo(({ mention, mentions, username, navToRoomInfo, useRealNam
}

return (
<Text style={[styles.text, ...(textStyle ? [textStyle] : []), { color: themes[theme].fontDefault }]}>{`@${mention}`}</Text>
<Text style={[styles.text, { color: themes[theme].fontDefault }, ...(textStyle ? [textStyle] : [])]}>{`@${mention}`}</Text>
);
});

Expand Down
6 changes: 3 additions & 3 deletions app/containers/markdown/components/mentions/Hashtag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,18 @@ const Hashtag = memo(({ hashtag, channels, navToRoomInfo }: IHashtag) => {
<Text
style={[
styles.mention,
...(textStyle ? [textStyle] : []),
{
color: themes[theme].fontInfo
}
},
...(textStyle ? [textStyle] : [])
]}
onPress={handlePress}>
{`${preffix}${hashtag}`}
</Text>
);
}
return (
<Text style={[styles.text, ...(textStyle ? [textStyle] : []), { color: themes[theme].fontDefault }]}>{`#${hashtag}`}</Text>
<Text style={[styles.text, { color: themes[theme].fontDefault }, ...(textStyle ? [textStyle] : [])]}>{`#${hashtag}`}</Text>
);
});

Expand Down
1 change: 1 addition & 0 deletions app/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make this error message generic.

Validation now follows the server-configured UTF8_User_Names_Validation, so this copy is only correct for the default fallback pattern. On workspaces with a custom username regex, the app will show misleading guidance.

Suggested fix
-  "Username_invalid": "Use only letters, numbers, dots, hyphens and underscores",
+  "Username_invalid": "Enter a valid username",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"Username_invalid": "Use only letters, numbers, dots, hyphens and underscores",
"Username_invalid": "Enter a valid username",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/i18n/locales/en.json` at line 1005, The Username_invalid locale copy is
too specific and can be misleading when the server uses a custom
UTF8_User_Names_Validation pattern. Update the en.json entry for
Username_invalid to a generic validation message that does not list character
rules, so it applies to both the fallback and server-configured username regex.
Keep the key unchanged and only revise the wording in this locale string.

"Username_is_already_in_use": "Username is already in use.",
"Username_not_available": "Username not available",
"Username_or_email": "Username or email",
Expand Down
25 changes: 24 additions & 1 deletion app/views/ProfileView/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -69,7 +70,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-_.]+'
}
});

Expand Down Expand Up @@ -110,6 +112,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();

Expand Down
32 changes: 26 additions & 6 deletions app/views/ProfileView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,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): boolean => {
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<ProfileStackParamList, 'ProfileView'>;
}
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();
Expand All @@ -67,6 +76,7 @@ const ProfileView = ({ navigation }: IProfileViewProps): ReactElement => {
Accounts_AllowUserAvatarChange,
Accounts_AllowUsernameChange,
Accounts_CustomFields,
UTF8_User_Names_Validation,
serverVersion,
user
} = useAppSelector(state => ({
Expand All @@ -77,9 +87,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))
Comment on lines +98 to +101

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Skip regex validation for unchanged or locked usernames.

This test now runs on every submit, so a legacy username that no longer matches UTF8_User_Names_Validation will block saving unrelated profile changes. That can happen even when Accounts_AllowUsernameChange is false, because validateFormInfo() validates the whole schema before submit.

Suggested fix
 	username: yup
 		.string()
 		.required(I18n.t('Username_required'))
-		.test('valid-username', I18n.t('Username_invalid'), value => isValidUsername(value ?? '', UTF8_User_Names_Validation))
+		.test('valid-username', I18n.t('Username_invalid'), value => {
+			const nextUsername = value ?? '';
+			if (!Accounts_AllowUsernameChange || nextUsername === (user?.username ?? '')) {
+				return true;
+			}
+			return isValidUsername(nextUsername, UTF8_User_Names_Validation);
+		})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
username: yup
.string()
.required(I18n.t('Username_required'))
.test('valid-username', I18n.t('Username_invalid'), value => isValidUsername(value ?? '', UTF8_User_Names_Validation))
username: yup
.string()
.required(I18n.t('Username_required'))
.test('valid-username', I18n.t('Username_invalid'), value => {
const nextUsername = value ?? '';
if (!Accounts_AllowUsernameChange || nextUsername === (user?.username ?? '')) {
return true;
}
return isValidUsername(nextUsername, UTF8_User_Names_Validation);
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/views/ProfileView/index.tsx` around lines 98 - 101, The username schema
in ProfileView’s validation currently always runs the UTF8_User_Names_Validation
test, which blocks saving unrelated profile changes for legacy or locked
usernames. Update the yup rule in the ProfileView form schema so the
`valid-username` test is skipped when the username is unchanged or when
`Accounts_AllowUsernameChange` is false, and keep the check only for editable
username changes during submit validation.

});
const isMasterDetail = useMasterDetail();
const {
control,
Expand Down