import { useMemo, useState } from "react"; import { useLocation } from "wouter"; import { ChevronLeft, ChevronRight, MoreVertical, Pencil, Plus, Share2, Trash2, Eye, EyeOff, Star, UserMinus, X } from "lucide-react"; import { useCalendar } from "@/store/calendar"; import { dateTimeKey, useSettings } from "@/store/settings"; import { addMonths, isSameDay, isToday, monthGrid, startOfDay, toLocalDateOnly } from "@/lib/dates"; import { formatMonthYear } from "@/lib/format"; import { formatWeekday } from "@/lib/datetime"; import { MenuItem, MenuSep, Popover, useMenu } from "@/ui/popover"; import { confirmDialog } from "@/ui/dialog"; import { toast } from "@/ui/toast"; import type { Calendar } from "@/jmap/types"; import { CalendarDialog } from "./CalendarDialog"; import { ShareDialog } from "../settings/ShareDialog"; export function CalendarSidebar() { const [location, navigate] = useLocation(); const cal = useCalendar(); const weekStart = useSettings((s) => s.settings.weekStart); const locale = useSettings((s) => dateTimeKey(s.settings)); const parts = location.split("/"); const view = parts[2] || "week"; const dateStr = parts[3]; const selected = useMemo(() => (dateStr ? new Date(`${dateStr}T00:00:00`) : new Date()), [dateStr]); const [anchor, setAnchor] = useState(() => startOfDay(selected)); const grid = useMemo(() => monthGrid(anchor, weekStart), [anchor, weekStart]); const menu = useMenu(); /* Added if the server says so or the reader's settings do; Stalwart will not always take the flag, so the settings carry it where it refuses. */ const addedShares = new Set(useSettings((s) => s.settings).addedShares); const isAdded = (c: { accountId: string; calendar: { id: string; isSubscribed?: boolean } }) => Boolean(c.calendar.isSubscribed) || addedShares.has(`${c.accountId}:${c.calendar.id}`); const sharedSubscribed = cal.sharedCalendars.filter(isAdded); const sharedAvailable = cal.sharedCalendars.filter((c) => !isAdded(c)); const [menuCal, setMenuCal] = useState(null); const [editCal, setEditCal] = useState | null>(null); const [share, setShare] = useState(null); const instances = cal.instancesIn(grid[0]!, new Date(grid[41]!.getTime() + 86400000)); const dow = useMemo(() => grid.slice(0, 7).map((d) => formatWeekday(d, "narrow")), [grid, locale]); if (!cal.available) return null; const calendars = Object.values(cal.calendars).sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)); return (
{formatMonthYear(anchor)}
{dow.map((d, i) =>
{d}
)} {grid.map((d) => (
i.start < new Date(d.getTime() + 86400000) && i.end > d) ? "has-events" : ""}`} onClick={() => navigate(`/calendar/${view === "month" ? "day" : view}/${toLocalDateOnly(d)}`)}> {d.getDate()}
))}
My calendars
{calendars.map((c) => (
cal.toggleHidden(c.id)} onContextMenu={(e) => { e.preventDefault(); setMenuCal(c); menu.openAt(e.clientX, e.clientY); }}> {c.name} {Object.keys(c.shareWith ?? {}).length > 0 && } {c.isDefault && }
))} {/* Calendars other people shared, split by whether the reader has added them. Stalwart returns every calendar in a reachable account with full rights, so "shared with me" and "there is an account here at all" look identical -- `isSubscribed` is the only thing that tells them apart, and adding one is a deliberate act rather than a guess on our part. */} {sharedSubscribed.length > 0 && ( <>
Shared with me
{sharedSubscribed.map(({ accountId, accountName, calendar: c }) => { const key = `${accountId}:${c.id}`; return (
cal.toggleHidden(key)} title={`${c.name} — shared by ${accountName}`}> {c.name}
); })} )} {sharedAvailable.length > 0 && ( <>
Available to add
{sharedAvailable.map(({ accountId, accountName, calendar: c }) => (
{c.name}
))} )} {menuCal && ( <> : } label={cal.hidden[menuCal.id] ? "Show" : "Hide"} onClick={() => cal.toggleHidden(menuCal.id)} /> } label="Edit" onClick={() => setEditCal(menuCal)} /> } label="Share…" onClick={() => setShare(menuCal)} disabled={!menuCal.myRights.mayShare} /> {/* Revoking every share at once, without walking the dialog and removing people one at a time. Only offered when there is something to revoke. */} {Object.keys(menuCal.shareWith ?? {}).length > 0 && ( } label="Stop sharing" disabled={!menuCal.myRights.mayShare} onClick={async () => { const who = Object.keys(menuCal.shareWith ?? {}).length; if (!(await confirmDialog({ title: `Stop sharing “${menuCal.name}”?`, message: `${who === 1 ? "One person" : `${who} people`} will lose access. Events in it are not affected.`, confirmLabel: "Stop sharing", danger: true, }))) return; try { await cal.updateCalendar(menuCal.id, { shareWith: null }); toast.success("No longer shared"); } catch (err) { toast.error((err as Error).message); } }} /> )} } label="Make default" disabled={menuCal.isDefault} onClick={() => void cal.updateCalendar(menuCal.id, { isDefault: true } as Partial).catch((err) => toast.error((err as Error).message))} /> } label="Delete" disabled={!menuCal.myRights.mayDelete} onClick={async () => { if (await confirmDialog({ title: `Delete “${menuCal.name}”?`, message: "All events in this calendar will be deleted.", confirmLabel: "Delete", danger: true })) void cal.destroyCalendar(menuCal.id).catch((err) => toast.error((err as Error).message)); }} /> )} {editCal && setEditCal(null)} />} {share && setShare(null)} />}
); }