mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
fix: multiple notifications on android app #5367
This commit is contained in:
parent
42a66e7f14
commit
cb4fa5d8ae
3 changed files with 106 additions and 2 deletions
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
24
src/app/features/android/android-notification-id.util.ts
Normal file
24
src/app/features/android/android-notification-id.util.ts
Normal 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;
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue