fix: multiple notifications on android app #5367

This commit is contained in:
Johannes Millan 2025-10-30 16:07:10 +01:00
parent 42a66e7f14
commit cb4fa5d8ae
3 changed files with 106 additions and 2 deletions

View file

@ -0,0 +1,79 @@
import { generateNotificationId } from './android-notification-id.util';
describe('generateNotificationId', () => {
it('should generate the same ID for the same input', () => {
const reminderId = 'test-reminder-id-123';
const id1 = generateNotificationId(reminderId);
const id2 = generateNotificationId(reminderId);
expect(id1).toBe(id2);
});
it('should generate different IDs for different inputs', () => {
const id1 = generateNotificationId('reminder-abc-123');
const id2 = generateNotificationId('reminder-xyz-456');
expect(id1).not.toBe(id2);
});
it('should always return a positive integer', () => {
const testIds = [
'short',
'a-very-long-reminder-id-with-many-characters-123456789',
'special-chars-!@#$%',
'nanoid-V1StGXR8_Z5jdHi6B-myT',
];
testIds.forEach((reminderId) => {
const id = generateNotificationId(reminderId);
expect(id).toBeGreaterThan(0);
expect(Number.isInteger(id)).toBe(true);
});
});
it('should return ID within safe Android range', () => {
const testIds = [
'test-1',
'test-2',
'very-long-id-that-might-cause-overflow-123456789',
];
testIds.forEach((reminderId) => {
const id = generateNotificationId(reminderId);
expect(id).toBeLessThan(2147483647); // Max 32-bit signed integer
expect(id).toBeGreaterThan(0);
});
});
it('should throw error for invalid input', () => {
expect(() => generateNotificationId('')).toThrow();
expect(() => generateNotificationId(null as any)).toThrow();
expect(() => generateNotificationId(undefined as any)).toThrow();
expect(() => generateNotificationId(123 as any)).toThrow();
});
it('should handle typical nanoid format', () => {
// Typical nanoid format used in the app
const nanoidExamples = [
'V1StGXR8_Z5jdHi6B-myT',
'xQY6fK9kL3mN5pR2sT7vW',
'aB1cD2eF3gH4iJ5kL6mN7',
];
nanoidExamples.forEach((reminderId) => {
const id = generateNotificationId(reminderId);
expect(id).toBeGreaterThan(0);
expect(Number.isInteger(id)).toBe(true);
});
});
it('should be deterministic across multiple calls', () => {
const reminderId = 'consistent-test-id';
const ids = Array.from({ length: 100 }, () => generateNotificationId(reminderId));
const firstId = ids[0];
ids.forEach((id) => {
expect(id).toBe(firstId);
});
});
});

View file

@ -0,0 +1,24 @@
/**
* Generates a deterministic numeric notification ID from a string reminder ID.
* This ensures the same reminder always gets the same notification ID,
* preventing duplicate notifications on Android.
*
* @param reminderId - The reminder's relatedId (task/note ID)
* @returns A positive integer suitable for Android notification ID
*/
export const generateNotificationId = (reminderId: string): number => {
if (!reminderId || typeof reminderId !== 'string') {
throw new Error('Invalid reminderId: must be a non-empty string');
}
// Simple hash function to convert string to positive integer
let hash = 0;
for (let i = 0; i < reminderId.length; i++) {
const char = reminderId.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
// Ensure positive integer and within safe range for Android
return Math.abs(hash) % 2147483647;
};

View file

@ -9,6 +9,7 @@ import { SnackService } from '../../../core/snack/snack.service';
import { IS_ANDROID_WEB_VIEW } from '../../../util/is-android-web-view';
import { LocalNotificationSchema } from '@capacitor/local-notifications/dist/esm/definitions';
import { DroidLog } from '../../../core/log';
import { generateNotificationId } from '../android-notification-id.util';
// TODO send message to electron when current task changes here
@ -93,8 +94,8 @@ export class AndroidEffects {
// Re-schedule the full set so the native alarm manager is always in sync.
await LocalNotifications.schedule({
notifications: reminders.map((reminder) => {
// since the ids are temporary we can use just Math.random()
const id = Math.round(Math.random() * 10000000);
// Use deterministic ID based on reminder's relatedId to prevent duplicate notifications
const id = generateNotificationId(reminder.relatedId);
const now = Date.now();
const scheduleAt =
reminder.remindAt <= now ? now + 1000 : reminder.remindAt; // push overdue reminders into the immediate future