Fixed notifications vanishing when app wasn't ready and sometimes showing up twice
This commit is contained in:
1 parent
ed7c6d630c
commit
78769604a5
6 files changed
+2651
-6775
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import * as Notifications from "expo-notifications";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { AppState } from "react-native";
|
||||
import { Category, categoryKeys } from "../types/Category";
|
||||
import { Item } from "../types/Item";
|
||||
|
||||
@@ -8,107 +9,62 @@ export const STORAGE_KEY = "notifications-data";
|
||||
const MAX_STORED_NOTIFICATIONS = 100;
|
||||
const MAX_AGE_DAYS = 60;
|
||||
|
||||
// Simple queue to prevent race conditions
|
||||
class StorageQueue {
|
||||
private queue: Array<() => Promise<void>> = [];
|
||||
private processing = false;
|
||||
|
||||
async add(task: () => Promise<void>) {
|
||||
this.queue.push(task);
|
||||
if (!this.processing) {
|
||||
await this.process();
|
||||
}
|
||||
}
|
||||
|
||||
private async process() {
|
||||
this.processing = true;
|
||||
while (this.queue.length > 0) {
|
||||
const task = this.queue.shift();
|
||||
if (task) {
|
||||
try {
|
||||
await task();
|
||||
} catch (e) {
|
||||
console.error("Storage task failed:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.processing = false;
|
||||
}
|
||||
let chain: Promise<void> = Promise.resolve();
|
||||
function enqueue(task: () => Promise<void>) {
|
||||
chain = chain.then(task).catch((e) => console.error("Storage task failed:", e));
|
||||
return chain;
|
||||
}
|
||||
|
||||
const storageQueue = new StorageQueue();
|
||||
function mergeItems(newItems: Item[], emit: (items: Item[]) => void) {
|
||||
return enqueue(async () => {
|
||||
const saved = await AsyncStorage.getItem(STORAGE_KEY);
|
||||
const prev: Item[] = saved ? JSON.parse(saved) : [];
|
||||
const merged = cleanOldItems(
|
||||
deduplicateItems([...newItems, ...prev].map((i) => ({ ...i, timestamp: Number(i.timestamp) })))
|
||||
);
|
||||
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(merged));
|
||||
emit(merged);
|
||||
});
|
||||
}
|
||||
|
||||
export function useNotificationListener(onItems: (items: Item[]) => void) {
|
||||
const onItemsRef = useRef(onItems);
|
||||
const mountedRef = useRef(true);
|
||||
|
||||
// Keep ref updated but don't trigger effect re-runs
|
||||
useEffect(() => {
|
||||
onItemsRef.current = onItems;
|
||||
}, [onItems]);
|
||||
useEffect(() => { onItemsRef.current = onItems; }, [onItems]);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
let active = true;
|
||||
const emit = (items: Item[]) => { if (active) onItemsRef.current(items); };
|
||||
const add = (n: Notifications.Notification) => {
|
||||
const item = mapNotificationToItem(n);
|
||||
if (item) mergeItems([item], emit);
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const saved = await AsyncStorage.getItem(STORAGE_KEY);
|
||||
let existing: Item[] = saved ? JSON.parse(saved) : [];
|
||||
const sync = async () => {
|
||||
const incoming: Item[] = [];
|
||||
const delivered = await Notifications.getPresentedNotificationsAsync();
|
||||
incoming.push(...delivered.map(mapNotificationToItem).filter((i): i is Item => !!i));
|
||||
|
||||
// Parse timestamp correctly
|
||||
existing = existing.map((i) => ({
|
||||
...i,
|
||||
timestamp: Number(i.timestamp),
|
||||
}));
|
||||
const last = await Notifications.getLastNotificationResponseAsync();
|
||||
const lastItem = last && mapNotificationToItem(last.notification);
|
||||
if (lastItem) incoming.push(lastItem);
|
||||
|
||||
// Load delivered notifications from the system
|
||||
const delivered = await Notifications.getPresentedNotificationsAsync?.();
|
||||
const deliveredItems: Item[] = delivered
|
||||
?.map(mapNotificationToItem)
|
||||
.filter((i): i is Item => i !== null) ?? [];
|
||||
await mergeItems(incoming, emit);
|
||||
};
|
||||
sync().catch(console.error);
|
||||
|
||||
// Combine, deduplicate, and clean
|
||||
const combined = deduplicateItems([...deliveredItems, ...existing]);
|
||||
const cleaned = cleanOldItems(combined);
|
||||
|
||||
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(cleaned));
|
||||
|
||||
if (mountedRef.current) {
|
||||
onItemsRef.current(cleaned);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load notifications:", e);
|
||||
}
|
||||
})();
|
||||
|
||||
const receivedSub = Notifications.addNotificationReceivedListener(async (notification) => {
|
||||
const item = mapNotificationToItem(notification);
|
||||
if (item && mountedRef.current) {
|
||||
await addItem(item, (items) => {
|
||||
if (mountedRef.current) {
|
||||
onItemsRef.current(items);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const responseSub = Notifications.addNotificationResponseReceivedListener(async (response) => {
|
||||
const item = mapNotificationToItem(response.notification);
|
||||
if (item && mountedRef.current) {
|
||||
await addItem(item, (items) => {
|
||||
if (mountedRef.current) {
|
||||
onItemsRef.current(items);
|
||||
}
|
||||
});
|
||||
}
|
||||
const receivedSub = Notifications.addNotificationReceivedListener(add);
|
||||
const responseSub = Notifications.addNotificationResponseReceivedListener((r) => add(r.notification));
|
||||
const appStateSub = AppState.addEventListener("change", (s) => {
|
||||
if (s === "active") sync().catch(console.error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
receivedSub.remove();
|
||||
responseSub.remove();
|
||||
active = false;
|
||||
receivedSub.remove();
|
||||
responseSub.remove();
|
||||
appStateSub.remove();
|
||||
};
|
||||
}, []); // Empty deps - only run once
|
||||
}, []);
|
||||
}
|
||||
|
||||
function normalizeTimestamp(timestamp: any): number {
|
||||
@@ -126,11 +82,6 @@ function normalizeTimestamp(timestamp: any): number {
|
||||
return num;
|
||||
}
|
||||
|
||||
function generateItemId(item: Item): string {
|
||||
// Create a unique ID based on timestamp, title, and info
|
||||
return `${item.timestamp}-${item.title}-${item.info}`.substring(0, 100);
|
||||
}
|
||||
|
||||
function deduplicateItems(items: Item[]): Item[] {
|
||||
const seen = new Set<string>();
|
||||
const unique: Item[] = [];
|
||||
@@ -164,11 +115,10 @@ export function mapNotificationToItem(notification: Notifications.Notification):
|
||||
? (rawCategory as Category)
|
||||
: "utility";
|
||||
|
||||
const timestamp = data.timestamp
|
||||
? normalizeTimestamp(data.timestamp)
|
||||
: Date.now();
|
||||
const timestamp = normalizeTimestamp(data.timestamp ?? notification.date);
|
||||
|
||||
return {
|
||||
id: notification.request.identifier,
|
||||
timestamp,
|
||||
category,
|
||||
title: content.title ?? "No title",
|
||||
@@ -181,27 +131,12 @@ export function mapNotificationToItem(notification: Notifications.Notification):
|
||||
}
|
||||
}
|
||||
|
||||
async function addItem(item: Item, onItems: (items: Item[]) => void) {
|
||||
await storageQueue.add(async () => {
|
||||
try {
|
||||
const saved = await AsyncStorage.getItem(STORAGE_KEY);
|
||||
const prev: Item[] = saved ? JSON.parse(saved) : [];
|
||||
export function clearStoredNotifications() {
|
||||
return enqueue(async () => { await AsyncStorage.removeItem(STORAGE_KEY); });
|
||||
}
|
||||
|
||||
const combined = [item, ...prev]
|
||||
.map((i) => ({ ...i, timestamp: Number(i.timestamp) }));
|
||||
|
||||
// Deduplicate and clean
|
||||
const deduplicated = deduplicateItems(combined);
|
||||
const cleaned = cleanOldItems(deduplicated);
|
||||
|
||||
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(cleaned));
|
||||
onItems(cleaned);
|
||||
} catch (e) {
|
||||
console.error("Failed to persist notification:", e);
|
||||
// Still call onItems with the new item so UI updates even if persistence fails
|
||||
onItems([item]);
|
||||
}
|
||||
});
|
||||
function generateItemId(item: Item): string {
|
||||
return item.id ?? `${item.timestamp}-${item.title}-${item.info}`.substring(0, 100);
|
||||
}
|
||||
|
||||
export default null;
|
||||
Reference in new issue
Block a user