-
Notifications
You must be signed in to change notification settings - Fork 1.5k
fix(notifications): deduplicate stale video conference notifications on cold boot #7440
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
63a00c1
d3f11f8
193cf24
7e85caf
47aa70a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import UserPreferences from '../methods/userPreferences'; | ||
|
|
||
| const NOTIFICATION_DEDUPLICATOR_KEY = 'processedNotificationIds'; | ||
| const MAX_STORED_IDS = 100; | ||
|
|
||
| export const isNotificationProcessed = (id?: string | null): boolean => { | ||
| if (!id) { | ||
| return false; | ||
| } | ||
| try { | ||
| const storedJson = UserPreferences.getString(NOTIFICATION_DEDUPLICATOR_KEY); | ||
| if (!storedJson) { | ||
| return false; | ||
| } | ||
| const processedIds = JSON.parse(storedJson); | ||
| if (Array.isArray(processedIds)) { | ||
| return processedIds.includes(id); | ||
| } | ||
| UserPreferences.removeItem(NOTIFICATION_DEDUPLICATOR_KEY); | ||
| return false; | ||
| } catch (e) { | ||
| console.warn('[NotificationDeduplicator] Error reading processed notification IDs:', e); | ||
| UserPreferences.removeItem(NOTIFICATION_DEDUPLICATOR_KEY); | ||
| return false; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| }; | ||
|
|
||
| export const markNotificationAsProcessed = (id?: string | null): void => { | ||
| if (!id) { | ||
| return; | ||
| } | ||
| try { | ||
| const storedJson = UserPreferences.getString(NOTIFICATION_DEDUPLICATOR_KEY); | ||
| let processedIds: string[] = []; | ||
| if (storedJson) { | ||
| const parsed = JSON.parse(storedJson); | ||
| if (Array.isArray(parsed)) { | ||
| processedIds = parsed; | ||
| } else { | ||
| UserPreferences.removeItem(NOTIFICATION_DEDUPLICATOR_KEY); | ||
| } | ||
| } | ||
|
|
||
| // Avoid duplicates in the array | ||
| if (!processedIds.includes(id)) { | ||
| processedIds.push(id); | ||
| // Limit size to MAX_STORED_IDS | ||
| if (processedIds.length > MAX_STORED_IDS) { | ||
| processedIds = processedIds.slice(processedIds.length - MAX_STORED_IDS); | ||
| } | ||
| UserPreferences.setString(NOTIFICATION_DEDUPLICATOR_KEY, JSON.stringify(processedIds)); | ||
| } | ||
| } catch (e) { | ||
| console.warn('[NotificationDeduplicator] Error writing processed notification IDs:', e); | ||
| UserPreferences.removeItem(NOTIFICATION_DEDUPLICATOR_KEY); | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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(); | ||||||||||||||||||||||||||||||||||
|
|
@@ -67,6 +76,7 @@ const ProfileView = ({ navigation }: IProfileViewProps): ReactElement => { | |||||||||||||||||||||||||||||||||
| Accounts_AllowUserAvatarChange, | ||||||||||||||||||||||||||||||||||
| Accounts_AllowUsernameChange, | ||||||||||||||||||||||||||||||||||
| Accounts_CustomFields, | ||||||||||||||||||||||||||||||||||
| UTF8_User_Names_Validation, | ||||||||||||||||||||||||||||||||||
| serverVersion, | ||||||||||||||||||||||||||||||||||
| user | ||||||||||||||||||||||||||||||||||
| } = useAppSelector(state => ({ | ||||||||||||||||||||||||||||||||||
|
|
@@ -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
+95
to
+101
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Only validate usernames that the user is actually changing. Line 101 re-checks the current username against the latest server regex on every submit. If a workspace tightens 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 => {
+ if ((value ?? '') === (user?.username ?? '')) {
+ return true;
+ }
+ return isValidUsername(value ?? '', UTF8_User_Names_Validation);
+ })
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| const isMasterDetail = useMasterDetail(); | ||||||||||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||||||||||
| control, | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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 the error text match the configurable validator.
Username_invalidnow backs a server-defined regex, but this copy hardcodes the fallback character set. On workspaces with a customUTF8_User_Names_Validation, users will get the wrong guidance for a rejection that came from a different pattern.Suggested copy
📝 Committable suggestion
🤖 Prompt for AI Agents