Fixed notification composition

This commit is contained in:
2026-10-22 23:36:03 +02:00
parent 46c6ddcc80
commit 5050c901bc
9 changed files with 2949 additions and 358 deletions
@@ -39,70 +39,73 @@ export function useInAppNotifications() {
const trashStorageKey = `pfm_mailbox_trash_${userId || 'default'}`;
const sentStorageKey = `pfm_mailbox_sent_${userId || 'default'}`;
// Starred messages set
const [starredIds, setStarredIds] = useState<string[]>(() => {
const readStorageList = useCallback(<T,>(key: string, fallbackKey?: string): T[] => {
try {
const stored = localStorage.getItem(starredStorageKey);
return stored ? JSON.parse(stored) : [];
const stored = localStorage.getItem(key);
const parsed: T[] = stored ? JSON.parse(stored) : [];
if (fallbackKey && fallbackKey !== key) {
const fallbackStored = localStorage.getItem(fallbackKey);
const fallbackParsed: T[] = fallbackStored ? JSON.parse(fallbackStored) : [];
if (fallbackParsed.length > 0) {
const merged = [...parsed];
for (const item of fallbackParsed) {
const itemId = (item as any)?.id ?? item;
if (!merged.some(m => ((m as any)?.id ?? m) === itemId)) {
merged.push(item);
}
}
localStorage.setItem(key, JSON.stringify(merged));
localStorage.removeItem(fallbackKey);
return merged;
}
}
return parsed;
} catch {
return [];
}
});
}, []);
// Starred messages set
const [starredIds, setStarredIds] = useState<string[]>(() =>
readStorageList<string>(starredStorageKey, 'pfm_mailbox_starred_default')
);
// Trashed messages set
const [trashedIds, setTrashedIds] = useState<string[]>(() => {
try {
const stored = localStorage.getItem(trashStorageKey);
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
});
const [trashedIds, setTrashedIds] = useState<string[]>(() =>
readStorageList<string>(trashStorageKey, 'pfm_mailbox_trash_default')
);
// Local drafts list
const [drafts, setDrafts] = useState<DraftMessage[]>(() => {
try {
const stored = localStorage.getItem(draftsStorageKey);
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
});
const [drafts, setDrafts] = useState<DraftMessage[]>(() =>
readStorageList<DraftMessage>(draftsStorageKey, 'pfm_mailbox_drafts_default')
);
// Local sent messages history
const [localSent, setLocalSent] = useState<Notification[]>(() => {
try {
const stored = localStorage.getItem(sentStorageKey);
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
});
const [localSent, setLocalSent] = useState<Notification[]>(() =>
readStorageList<Notification>(sentStorageKey, 'pfm_mailbox_sent_default')
);
// Sync state to localStorage
useEffect(() => {
try {
localStorage.setItem(starredStorageKey, JSON.stringify(starredIds));
} catch {}
}, [starredIds, starredStorageKey]);
// Synchronize across hook instances and userId changes
const syncFromStorage = useCallback(() => {
setStarredIds(readStorageList<string>(starredStorageKey, 'pfm_mailbox_starred_default'));
setTrashedIds(readStorageList<string>(trashStorageKey, 'pfm_mailbox_trash_default'));
setDrafts(readStorageList<DraftMessage>(draftsStorageKey, 'pfm_mailbox_drafts_default'));
setLocalSent(readStorageList<Notification>(sentStorageKey, 'pfm_mailbox_sent_default'));
}, [readStorageList, starredStorageKey, trashStorageKey, draftsStorageKey, sentStorageKey]);
useEffect(() => {
try {
localStorage.setItem(trashStorageKey, JSON.stringify(trashedIds));
} catch {}
}, [trashedIds, trashStorageKey]);
syncFromStorage();
}, [syncFromStorage]);
useEffect(() => {
try {
localStorage.setItem(draftsStorageKey, JSON.stringify(drafts));
} catch {}
}, [drafts, draftsStorageKey]);
useEffect(() => {
try {
localStorage.setItem(sentStorageKey, JSON.stringify(localSent));
} catch {}
}, [localSent, sentStorageKey]);
const handleSync = () => syncFromStorage();
window.addEventListener('pfm-mailbox-sync', handleSync);
window.addEventListener('storage', handleSync);
return () => {
window.removeEventListener('pfm-mailbox-sync', handleSync);
window.removeEventListener('storage', handleSync);
};
}, [syncFromStorage]);
// Fetch notifications from backend
const { data: rawNotifications = [], isLoading, refetch } = useQuery<Notification[]>({
@@ -131,23 +134,16 @@ export function useInAppNotifications() {
);
}, [rawNotifications]);
// Active inbox notifications (InApp + Email for mailbox, excluding trashed and drafts)
const allMailboxNotifications = useMemo(() => {
return rawNotifications.filter(
n =>
(Number(n.platform) === NotificationPlatform.InApp || Number(n.platform) === NotificationPlatform.Email) &&
!trashedIds.includes(String(n.id)) &&
Number(n.status) !== NotificationStatus.Draft
);
}, [rawNotifications, trashedIds]);
// Active in-app notifications in inbox
// Active in-app notifications in inbox (excluding trashed and drafts)
const inboxNotifications = useMemo(() => {
return inAppNotifications.filter(
n => !trashedIds.includes(String(n.id)) && Number(n.status) !== NotificationStatus.Draft
);
}, [inAppNotifications, trashedIds]);
// Active mailbox notifications (always In-App notifications)
const allMailboxNotifications = inboxNotifications;
// Unread InApp notifications: status !== Read && status !== Draft && status !== Cancelled && status !== Failed
const unreadNotifications = useMemo(() => {
return inboxNotifications.filter(
@@ -161,20 +157,19 @@ export function useInAppNotifications() {
const unreadCount = unreadNotifications.length;
const top5Unread = useMemo(() => unreadNotifications.slice(0, 5), [unreadNotifications]);
// Starred notifications
// Starred notifications (In-App only)
const starredNotifications = useMemo(() => {
return rawNotifications.filter(
return inAppNotifications.filter(
n =>
starredIds.includes(String(n.id)) &&
!trashedIds.includes(String(n.id)) &&
(Number(n.platform) === NotificationPlatform.InApp || Number(n.platform) === NotificationPlatform.Email)
!trashedIds.includes(String(n.id))
);
}, [rawNotifications, starredIds, trashedIds]);
}, [inAppNotifications, starredIds, trashedIds]);
// Trashed notifications
// Trashed notifications (In-App only)
const trashedNotifications = useMemo(() => {
return rawNotifications.filter(n => trashedIds.includes(String(n.id)));
}, [rawNotifications, trashedIds]);
return inAppNotifications.filter(n => trashedIds.includes(String(n.id)));
}, [inAppNotifications, trashedIds]);
// Mutation to mark notification as read
const markAsReadMutation = useMutation({
@@ -206,27 +201,61 @@ export function useInAppNotifications() {
// Toggle star
const toggleStar = useCallback((id: string | number) => {
const strId = String(id);
setStarredIds(prev => (prev.includes(strId) ? prev.filter(x => x !== strId) : [...prev, strId]));
}, []);
setStarredIds(prev => {
const next = prev.includes(strId) ? prev.filter(x => x !== strId) : [...prev, strId];
try {
localStorage.setItem(starredStorageKey, JSON.stringify(next));
window.dispatchEvent(new Event('pfm-mailbox-sync'));
} catch {}
return next;
});
}, [starredStorageKey]);
// Delete message (move to trash)
const deleteMessage = useCallback((id: string | number) => {
const strId = String(id);
setTrashedIds(prev => (prev.includes(strId) ? prev : [...prev, strId]));
}, []);
setTrashedIds(prev => {
const next = prev.includes(strId) ? prev : [...prev, strId];
try {
localStorage.setItem(trashStorageKey, JSON.stringify(next));
window.dispatchEvent(new Event('pfm-mailbox-sync'));
} catch {}
return next;
});
}, [trashStorageKey]);
// Restore from trash
const restoreMessage = useCallback((id: string | number) => {
const strId = String(id);
setTrashedIds(prev => prev.filter(x => x !== strId));
}, []);
setTrashedIds(prev => {
const next = prev.filter(x => x !== strId);
try {
localStorage.setItem(trashStorageKey, JSON.stringify(next));
window.dispatchEvent(new Event('pfm-mailbox-sync'));
} catch {}
return next;
});
}, [trashStorageKey]);
// Permanent delete
const permanentDelete = useCallback((id: string | number) => {
const strId = String(id);
setTrashedIds(prev => prev.filter(x => x !== strId));
setLocalSent(prev => prev.filter(n => String(n.id) !== strId));
}, []);
setTrashedIds(prev => {
const next = prev.filter(x => x !== strId);
try {
localStorage.setItem(trashStorageKey, JSON.stringify(next));
} catch {}
return next;
});
setLocalSent(prev => {
const next = prev.filter(n => String(n.id) !== strId);
try {
localStorage.setItem(sentStorageKey, JSON.stringify(next));
window.dispatchEvent(new Event('pfm-mailbox-sync'));
} catch {}
return next;
});
}, [trashStorageKey, sentStorageKey]);
// Draft management
const saveDraft = useCallback((draft: Omit<DraftMessage, 'id' | 'updatedAt'> & { id?: string }) => {
@@ -237,15 +266,31 @@ export function useInAppNotifications() {
updatedAt: new Date().toISOString(),
};
setDrafts(prev => {
const filtered = prev.filter(d => d.id !== draftId);
return [newDraft, ...filtered];
const current = readStorageList<DraftMessage>(draftsStorageKey, 'pfm_mailbox_drafts_default');
const base = current.length >= prev.length ? current : prev;
const filtered = base.filter(d => d.id !== draftId);
const next = [newDraft, ...filtered];
try {
localStorage.setItem(draftsStorageKey, JSON.stringify(next));
window.dispatchEvent(new Event('pfm-mailbox-sync'));
} catch {}
return next;
});
return draftId;
}, []);
}, [draftsStorageKey, readStorageList]);
const deleteDraft = useCallback((id: string) => {
setDrafts(prev => prev.filter(d => d.id !== id));
}, []);
setDrafts(prev => {
const current = readStorageList<DraftMessage>(draftsStorageKey, 'pfm_mailbox_drafts_default');
const base = current.length >= prev.length ? current : prev;
const next = base.filter(d => d.id !== id);
try {
localStorage.setItem(draftsStorageKey, JSON.stringify(next));
window.dispatchEvent(new Event('pfm-mailbox-sync'));
} catch {}
return next;
});
}, [draftsStorageKey, readStorageList]);
// Send new notification (InApp or Email)
const sendMessageMutation = useMutation({
@@ -288,7 +333,14 @@ export function useInAppNotifications() {
sentAt: new Date().toISOString(),
};
setLocalSent(prev => [sentItem, ...prev]);
setLocalSent(prev => {
const next = [sentItem, ...prev];
try {
localStorage.setItem(sentStorageKey, JSON.stringify(next));
window.dispatchEvent(new Event('pfm-mailbox-sync'));
} catch {}
return next;
});
// If sending a draft, remove the draft
if (payload.draftId) {