"use client"; import { useEffect, useState } from "react"; import { useTranslations, useFormatter } from "next-intl"; import { Loader2 } from "lucide-react"; import { useRouter } from "@/i18n/navigation"; import { useAuthStore } from "@/stores/auth-store"; import { useEmailStore } from "@/stores/email-store"; import { useCalendarStore } from "@/stores/calendar-store"; import { Avatar } from "@/components/ui/avatar"; import { Section } from "./contact-detail"; import type { ContactCard, Email, CalendarEvent } from "@/lib/jmap/types"; const EMAIL_LIMIT = 5; const EVENT_LIMIT = 5; const EVENT_LOOKAHEAD_DAYS = 365; interface ContactActivityProps { contact: ContactCard; } function getContactEmails(contact: ContactCard): string[] { if (!contact.emails) return []; const seen = new Set(); const result: string[] = []; for (const e of Object.values(contact.emails)) { const addr = e.address?.trim().toLowerCase(); if (addr && !seen.has(addr)) { seen.add(addr); result.push(addr); } } return result; } function buildEmailFilter(addresses: string[]): Record { const conditions: Record[] = []; for (const addr of addresses) { conditions.push({ from: addr }); conditions.push({ to: addr }); } if (conditions.length === 1) return conditions[0]; return { operator: "OR", conditions }; } function eventInvolvesContact(event: CalendarEvent, addresses: Set): boolean { if (event.participants) { for (const p of Object.values(event.participants)) { const email = p.email?.trim().toLowerCase(); if (email && addresses.has(email)) return true; if (p.sendTo) { for (const target of Object.values(p.sendTo)) { const m = typeof target === "string" ? target.match(/mailto:(.+)/i) : null; if (m && addresses.has(m[1].trim().toLowerCase())) return true; } } } } if (event.organizerCalendarAddress) { const m = event.organizerCalendarAddress.match(/mailto:(.+)/i); const org = m ? m[1].trim().toLowerCase() : event.organizerCalendarAddress.trim().toLowerCase(); if (addresses.has(org)) return true; } return false; } function getEmailSender(email: Email): { name: string; address: string } { const from = email.from?.[0]; return { name: from?.name?.trim() || from?.email || "", address: from?.email || "", }; } function groupEventsByDate(events: CalendarEvent[]): Map { const groups = new Map(); for (const e of events) { const d = new Date(e.start); const key = isNaN(d.getTime()) ? e.start : d.toISOString().slice(0, 10); const arr = groups.get(key) || []; arr.push(e); groups.set(key, arr); } return groups; } export function ContactActivity({ contact }: ContactActivityProps) { const t = useTranslations("contacts.activity"); const format = useFormatter(); const router = useRouter(); const client = useAuthStore((s) => s.client); const selectEmail = useEmailStore((s) => s.selectEmail); const setSelectedEventId = useCalendarStore((s) => s.setSelectedEventId); const setSelectedDate = useCalendarStore((s) => s.setSelectedDate); const [emails, setEmails] = useState(null); const [events, setEvents] = useState(null); const [emailsLoading, setEmailsLoading] = useState(false); const [eventsLoading, setEventsLoading] = useState(false); const [emailsError, setEmailsError] = useState(false); const [eventsError, setEventsError] = useState(false); const addresses = getContactEmails(contact); const addressKey = addresses.join(","); useEffect(() => { if (!client || addresses.length === 0) { setEmails([]); return; } let cancelled = false; setEmailsLoading(true); setEmailsError(false); client .advancedSearchEmails(buildEmailFilter(addresses), undefined, EMAIL_LIMIT, 0) .then((res) => { if (cancelled) return; setEmails(res.emails); }) .catch(() => { if (cancelled) return; setEmailsError(true); setEmails([]); }) .finally(() => { if (!cancelled) setEmailsLoading(false); }); return () => { cancelled = true; }; }, [client, addressKey]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (!client || addresses.length === 0) { setEvents([]); return; } let cancelled = false; setEventsLoading(true); setEventsError(false); const now = new Date(); const before = new Date(now.getTime() + EVENT_LOOKAHEAD_DAYS * 24 * 60 * 60 * 1000); const addrSet = new Set(addresses); client .queryAllCalendarEvents( { after: now.toISOString(), before: before.toISOString() }, [{ property: "start", isAscending: true }], 500, ) .then((all) => { if (cancelled) return; const matching = all .filter((e) => eventInvolvesContact(e, addrSet)) .sort((a, b) => a.start.localeCompare(b.start)) .slice(0, EVENT_LIMIT); setEvents(matching); }) .catch(() => { if (cancelled) return; setEventsError(true); setEvents([]); }) .finally(() => { if (!cancelled) setEventsLoading(false); }); return () => { cancelled = true; }; }, [client, addressKey]); // eslint-disable-line react-hooks/exhaustive-deps if (addresses.length === 0) return null; const handleOpenEmail = (email: Email) => { selectEmail(email); router.push("/mail"); }; const handleOpenEvent = (event: CalendarEvent) => { const start = new Date(event.start); if (!isNaN(start.getTime())) setSelectedDate(start); setSelectedEventId(event.id); router.push("/calendar"); }; const formatEmailDate = (dateStr: string) => { const d = new Date(dateStr); if (isNaN(d.getTime())) return dateStr; const now = new Date(); const sameYear = d.getFullYear() === now.getFullYear(); return format.dateTime(d, sameYear ? { month: "short", day: "numeric" } : { year: "numeric", month: "short", day: "numeric" }); }; const formatEventTime = (event: CalendarEvent) => { if (event.showWithoutTime) return t("all_day"); const d = new Date(event.start); if (isNaN(d.getTime())) return ""; return format.dateTime(d, { hour: "numeric", minute: "2-digit" }); }; const formatEventDateHeader = (isoDate: string) => { const d = new Date(isoDate); if (isNaN(d.getTime())) return isoDate; const now = new Date(); const sameYear = d.getFullYear() === now.getFullYear(); return format.dateTime(d, sameYear ? { weekday: "long", month: "long", day: "numeric" } : { weekday: "long", year: "numeric", month: "long", day: "numeric" }); }; const eventGroups = events ? Array.from(groupEventsByDate(events).entries()) : []; return (
{emailsLoading ? ( ) : emailsError ? (

{t("load_failed")}

) : !emails || emails.length === 0 ? (

{t("no_emails")}

) : (
{emails.map((email) => { const sender = getEmailSender(email); return ( ); })}
)}
{eventsLoading ? ( ) : eventsError ? (

{t("load_failed")}

) : !events || events.length === 0 ? (

{t("no_events")}

) : (
{eventGroups.map(([dateKey, group]) => (
{formatEventDateHeader(dateKey)}
{group.map((event) => ( ))}
))}
)}
); } function LoadingRow() { return (
); }