diff --git a/apps/desktop/src/components/main/sidebar/timeline/index.tsx b/apps/desktop/src/components/main/sidebar/timeline/index.tsx index c242d1a267..8ca72cc42a 100644 --- a/apps/desktop/src/components/main/sidebar/timeline/index.tsx +++ b/apps/desktop/src/components/main/sidebar/timeline/index.tsx @@ -1,10 +1,11 @@ import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; -import { type ReactNode, useMemo } from "react"; +import { type ReactNode, useCallback, useMemo, useState } from "react"; import { Button } from "@hypr/ui/components/ui/button"; import { cn, startOfDay } from "@hypr/utils"; import { useConfigValue } from "../../../../config/use-config"; +import { useNativeContextMenu } from "../../../../hooks/useNativeContextMenu"; import * as main from "../../../../store/tinybase/store/main"; import { useTabs } from "../../../../store/zustand/tabs"; import { @@ -24,8 +25,24 @@ import { } from "./realtime"; export function TimelineView() { - const buckets = useTimelineData(); + const allBuckets = useTimelineData(); const timezone = useConfigValue("timezone") || undefined; + const [showIgnored, setShowIgnored] = useState(false); + + const buckets = useMemo(() => { + if (showIgnored) { + return allBuckets; + } + return allBuckets + .map((bucket) => ({ + ...bucket, + items: bucket.items.filter( + (item) => item.type !== "event" || !item.data.ignored, + ), + })) + .filter((bucket) => bucket.items.length > 0); + }, [allBuckets, showIgnored]); + const hasToday = useMemo( () => buckets.some((bucket) => bucket.label === "Today"), [buckets], @@ -85,10 +102,28 @@ export function TimelineView() { ); }, [buckets, hasToday, todayTimestamp]); + const toggleShowIgnored = useCallback(() => { + setShowIgnored((prev) => !prev); + }, []); + + const contextMenuItems = useMemo( + () => [ + { + id: "toggle-ignored", + text: showIgnored ? "Hide Ignored Events" : "Show Ignored Events", + action: toggleShowIgnored, + }, + ], + [showIgnored, toggleShowIgnored], + ); + + const showContextMenu = useNativeContextMenu(contextMenuItems); + return (
void; onCmdClick: () => void; contextMenu: Array<{ id: string; text: string; action: () => void }>; @@ -91,6 +93,7 @@ function ItemBase({ "cursor-pointer w-full text-left px-3 py-2 rounded-lg", selected && "bg-neutral-200", !selected && "hover:bg-neutral-100", + ignored && "opacity-40", ])} >
@@ -100,7 +103,14 @@ function ItemBase({
)}
-
{title}
+
+ {title} +
{displayTime && (
{displayTime}
)} @@ -133,6 +143,7 @@ const EventItem = memo( const title = item.data.title || "Untitled"; const calendarId = item.data.calendar_id ?? null; const recurrenceSeriesId = item.data.recurrence_series_id; + const ignored = !!item.data.ignored; const displayTime = useMemo( () => formatDisplayTime(item.data.started_at, precision, timezone), [item.data.started_at, precision, timezone], @@ -161,6 +172,34 @@ const EventItem = memo( store.setPartialRow("events", eventId, { ignored: true }); }, [store, eventId, invalidateResource, indexes]); + const handleUnignore = useCallback(() => { + if (!store) { + return; + } + store.setPartialRow("events", eventId, { ignored: false }); + }, [store, eventId]); + + const handleUnignoreSeries = useCallback(() => { + if (!store || !recurrenceSeriesId) { + return; + } + store.transaction(() => { + store.forEachRow("events", (rowId, _forEachCell) => { + const event = store.getRow("events", rowId); + if (event?.recurrence_series_id === recurrenceSeriesId) { + store.setPartialRow("events", rowId, { ignored: false }); + } + }); + + const currentIgnored = store.getValue("ignored_recurring_series"); + const ignoredList: string[] = currentIgnored + ? JSON.parse(String(currentIgnored)) + : []; + const filtered = ignoredList.filter((id) => id !== recurrenceSeriesId); + store.setValue("ignored_recurring_series", JSON.stringify(filtered)); + }); + }, [store, recurrenceSeriesId]); + const handleIgnoreSeries = useCallback(() => { if (!store || !recurrenceSeriesId) { return; @@ -188,6 +227,25 @@ const EventItem = memo( }, [store, recurrenceSeriesId]); const contextMenu = useMemo(() => { + if (ignored) { + if (recurrenceSeriesId) { + return [ + { + id: "unignore", + text: "Unignore Only This Event", + action: handleUnignore, + }, + { + id: "unignore-series", + text: "Unignore All Recurring Events", + action: handleUnignoreSeries, + }, + ]; + } + return [ + { id: "unignore", text: "Unignore Event", action: handleUnignore }, + ]; + } const menu = [ { id: "ignore", text: "Ignore Event", action: handleIgnore }, ]; @@ -199,7 +257,14 @@ const EventItem = memo( }); } return menu; - }, [handleCmdClick, handleIgnore, handleIgnoreSeries, recurrenceSeriesId]); + }, [ + ignored, + handleIgnore, + handleUnignore, + handleUnignoreSeries, + handleIgnoreSeries, + recurrenceSeriesId, + ]); return ( { e.preventDefault(); + e.stopPropagation(); const menuItems = await Promise.all( items.map((item) => diff --git a/apps/desktop/src/store/tinybase/store/main.ts b/apps/desktop/src/store/tinybase/store/main.ts index 92097b1036..cf35cf8953 100644 --- a/apps/desktop/src/store/tinybase/store/main.ts +++ b/apps/desktop/src/store/tinybase/store/main.ts @@ -97,18 +97,14 @@ export const StoreComponent = () => { store, (store) => createQueries(store) - .setQueryDefinition( - QUERIES.timelineEvents, - "events", - ({ select, where }) => { - select("title"); - select("started_at"); - select("ended_at"); - select("calendar_id"); - select("recurrence_series_id"); - where((getTableCell) => !getTableCell("events", "ignored")); - }, - ) + .setQueryDefinition(QUERIES.timelineEvents, "events", ({ select }) => { + select("title"); + select("started_at"); + select("ended_at"); + select("calendar_id"); + select("recurrence_series_id"); + select("ignored"); + }) .setQueryDefinition( QUERIES.timelineSessions, "sessions", diff --git a/apps/desktop/src/utils/timeline.ts b/apps/desktop/src/utils/timeline.ts index 843b5bb351..94bd1ac9a8 100644 --- a/apps/desktop/src/utils/timeline.ts +++ b/apps/desktop/src/utils/timeline.ts @@ -59,6 +59,7 @@ export type TimelineEventRow = { ended_at?: string | null; calendar_id?: string | null; recurrence_series_id?: string | null; + ignored?: boolean | null; }; // comes from QUERIES.timelineSessions