mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
136 lines
5.4 KiB
TypeScript
136 lines
5.4 KiB
TypeScript
'use client'
|
|
|
|
import { useMemo } from 'react'
|
|
import { useAtom } from 'jotai'
|
|
import { coinsAtom, habitsAtom, wishlistAtom, usersAtom } from '@/lib/atoms'
|
|
import { Bell } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useTranslations } from 'next-intl';
|
|
import NotificationDropdown from './NotificationDropdown';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu'
|
|
import { updateLastNotificationReadTimestamp } from '@/app/actions/data';
|
|
import { d2t, getNow, t2d } from '@/lib/utils';
|
|
import { useHelpers } from '@/lib/client-helpers';
|
|
import { User, CoinTransaction } from '@/lib/types';
|
|
|
|
export default function NotificationBell() {
|
|
const t = useTranslations('NotificationBell');
|
|
const { currentUser } = useHelpers();
|
|
const [coinsData] = useAtom(coinsAtom)
|
|
const [habitsData] = useAtom(habitsAtom)
|
|
const [wishlistData] = useAtom(wishlistAtom)
|
|
const [usersData] = useAtom(usersAtom);
|
|
|
|
// --- Calculate Unread and Read Notifications ---
|
|
const { unreadNotifications, displayedReadNotifications } = useMemo(() => {
|
|
const unread: CoinTransaction[] = [];
|
|
const read: CoinTransaction[] = [];
|
|
const MAX_READ_NOTIFICATIONS = 10; // Limit the number of past notifications shown
|
|
|
|
if (!currentUser || !currentUser.id) {
|
|
return { unreadNotifications: [], displayedReadNotifications: [] };
|
|
}
|
|
|
|
const lastReadTimestamp = currentUser.lastNotificationReadTimestamp
|
|
? t2d({ timestamp: currentUser.lastNotificationReadTimestamp, timezone: 'UTC' })
|
|
: null;
|
|
|
|
// Iterate through transactions (assuming they are sorted newest first)
|
|
for (const tx of coinsData.transactions) {
|
|
// Stop processing if we have enough read notifications
|
|
if (read.length >= MAX_READ_NOTIFICATIONS && (!lastReadTimestamp || t2d({ timestamp: tx.timestamp, timezone: 'UTC' }) <= lastReadTimestamp)) {
|
|
break; // Optimization: stop early if we have enough read and are past the unread ones
|
|
}
|
|
|
|
// Basic checks: must have a related item and be triggered by someone else
|
|
if (!tx.relatedItemId || tx.userId === currentUser.id) {
|
|
continue;
|
|
}
|
|
|
|
// Check if the transaction type indicates a notification-worthy event
|
|
const isRelevantType = tx.type === 'HABIT_COMPLETION' || tx.type === 'TASK_COMPLETION' || tx.type === 'WISH_REDEMPTION';
|
|
if (!isRelevantType) {
|
|
continue;
|
|
}
|
|
|
|
// Check if the related item is shared with the current user
|
|
let isShared = false;
|
|
const isHabitCompletion = tx.type === 'HABIT_COMPLETION' || tx.type === 'TASK_COMPLETION';
|
|
const isWishRedemption = tx.type === 'WISH_REDEMPTION';
|
|
|
|
if (isHabitCompletion) {
|
|
const habit = habitsData.habits.find(h => h.id === tx.relatedItemId);
|
|
if (habit?.userIds?.includes(currentUser.id) && tx.userId && habit.userIds.includes(tx.userId)) {
|
|
isShared = true;
|
|
}
|
|
} else if (isWishRedemption) {
|
|
const wish = wishlistData.items.find(w => w.id === tx.relatedItemId);
|
|
if (wish?.userIds?.includes(currentUser.id) && tx.userId && wish.userIds.includes(tx.userId)) {
|
|
isShared = true;
|
|
}
|
|
}
|
|
|
|
if (!isShared) {
|
|
continue; // Skip if not shared
|
|
}
|
|
|
|
// Transaction is relevant, determine if read or unread
|
|
const txTimestamp = t2d({ timestamp: tx.timestamp, timezone: 'UTC' });
|
|
if (!lastReadTimestamp || txTimestamp > lastReadTimestamp) {
|
|
unread.push(tx);
|
|
} else if (read.length < MAX_READ_NOTIFICATIONS) {
|
|
// Only add to read if we haven't hit the limit
|
|
read.push(tx);
|
|
}
|
|
}
|
|
|
|
// Transactions are assumed to be sorted newest first from the source
|
|
return { unreadNotifications: unread, displayedReadNotifications: read };
|
|
}, [coinsData.transactions, habitsData.habits, wishlistData.items, currentUser]);
|
|
// --- End Calculate Notifications ---
|
|
|
|
const unreadCount = unreadNotifications.length;
|
|
|
|
const handleNotificationClick = async () => {
|
|
if (!currentUser || !currentUser.id || unreadCount === 0) return; // Only update if there are unread notifications
|
|
try {
|
|
const nowTimestamp = d2t({ dateTime: getNow({}) });
|
|
await updateLastNotificationReadTimestamp(currentUser.id, nowTimestamp);
|
|
} catch (error) {
|
|
console.error(t('errorUpdateTimestamp'), error);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<DropdownMenu onOpenChange={(open) => {
|
|
// Update timestamp only when opening the dropdown and there are unread notifications
|
|
if (open && unreadCount > 0) {
|
|
handleNotificationClick();
|
|
}
|
|
}}>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" aria-label="Notifications" className="relative">
|
|
<Bell className="h-5 w-5" />
|
|
{unreadCount > 0 && (
|
|
<span className="absolute top-1 right-1 block h-2 w-2 rounded-full bg-red-500 ring-1 ring-white dark:ring-gray-800" />
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="p-0 w-80 md:w-96">
|
|
<NotificationDropdown
|
|
currentUser={currentUser as User | null} // Cast needed as useHelpers can return undefined initially
|
|
unreadNotifications={unreadNotifications}
|
|
displayedReadNotifications={displayedReadNotifications}
|
|
habitsData={habitsData} // Pass necessary data down
|
|
wishlistData={wishlistData}
|
|
usersData={usersData}
|
|
/>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|