import { useThreadOverlayNavigation } from "$/components/posts/hooks/useThreadOverlayNavigation"; import { useAppSession } from "$/contexts/app-session"; import { addColumn, getColumns, removeColumn, reorderColumns, updateColumn } from "$/lib/api/columns"; import { FeedController } from "$/lib/api/feeds"; import type { Column, ColumnKind, ColumnWidth } from "$/lib/api/types/columns"; import { getFeedName } from "$/lib/feeds"; import type { FeedGeneratorView } from "$/lib/types"; import * as logger from "@tauri-apps/plugin-log"; import { createEffect, For, onCleanup, onMount, Show } from "solid-js"; import { createStore, produce } from "solid-js/store"; import { Motion } from "solid-motionone"; import { ActionIcon, Icon, LoadingIcon } from "../shared/Icon"; import { AddColumnPanel } from "./AddColumnPanel"; import { DeckColumn } from "./DeckColumn"; import { parseFeedConfig, resolveFeedColumn } from "./helpers"; import { type ResolvedFeedColumn } from "./types"; type DeckState = { addPanelOpen: boolean; columns: Column[]; dragOverId: string | null; error: string | null; feedColumns: Record; loading: boolean; }; function DeckToolbar(props: { columnCount: number; onAdd: () => void }) { return (

Deck

{props.columnCount === 0 ? "No columns" : `${props.columnCount} column${props.columnCount === 1 ? "" : "s"}`}

); } function EmptyDeck(props: { onAdd: () => void }) { return (

No columns yet

Add a feed, explorer, or diagnostics column to get started.

); } function ColumnList( props: { columns: Column[]; dragOverId: string | null; feedColumns: Record; onClose: (id: string) => void; onDragEnd: () => void; onDragOver: (id: string) => void; onDragStart: (id: string) => void; onDrop: (targetId: string) => void; onMoveLeft: (id: string) => void; onMoveRight: (id: string) => void; onOpenThread: (uri: string) => void; onWidthChange: (id: string, width: ColumnWidth) => void; }, ) { return (
{(column) => ( )}
); } function createDeckKeyboardHandler(onAddColumn: () => void, onCloseLastColumn: () => void) { return (e: KeyboardEvent) => { if (!e.ctrlKey && !e.metaKey) return; if (!e.shiftKey) return; if (e.key === "N" || e.key === "n") { e.preventDefault(); onAddColumn(); } else if (e.key === "W" || e.key === "w") { e.preventDefault(); onCloseLastColumn(); } }; } export function DeckWorkspace() { const session = useAppSession(); const threadOverlay = useThreadOverlayNavigation(); let feedColumnRequest = 0; let draggingColumnId: string | null = null; const [state, setState] = createStore({ addPanelOpen: false, columns: [], dragOverId: null, error: null, feedColumns: {}, loading: true, }); const activeDid = () => session.activeDid; async function loadColumns() { const did = activeDid(); if (!did) return; try { const cols = await getColumns(did); setState("columns", cols); setState("error", null); void hydrateFeedColumns(cols); } catch (err) { const message = err instanceof Error ? err.message : String(err); logger.error(`Failed to load deck columns: ${message}`); setState("error", message); } finally { setState("loading", false); } } async function hydrateFeedColumns(columns: Column[]) { const currentRequest = ++feedColumnRequest; const parsedFeedColumns = columns.flatMap((column) => { if (column.kind !== "feed") { return []; } const config = parseFeedConfig(column.config); return config ? [{ columnId: column.id, config }] : []; }); if (parsedFeedColumns.length === 0) { setState("feedColumns", {}); return; } setState( "feedColumns", Object.fromEntries(parsedFeedColumns.map(({ columnId, config }) => [columnId, resolveFeedColumn(config)])), ); try { const preferences = await FeedController.getPreferences(); const savedFeedTitles = Object.fromEntries( preferences.savedFeeds.map((feed) => [feed.value, getFeedName(feed, void 0)]), ); const generatorUris = [ ...new Set( parsedFeedColumns.filter(({ config }) => config.feedType === "feed").map(({ config }) => config.feedUri ), ), ]; let generators: Record = {}; if (generatorUris.length > 0) { const hydrated = await FeedController.getFeedGenerators(generatorUris); generators = Object.fromEntries(hydrated.feeds.map((generator) => [generator.uri, generator])); } const nextFeedColumns = Object.fromEntries( parsedFeedColumns.map(( { columnId, config }, ) => [ columnId, resolveFeedColumn(config, { generator: generators[config.feedUri], savedFeedTitle: savedFeedTitles[config.feedUri], }), ]), ); if (currentRequest !== feedColumnRequest) { return; } setState("feedColumns", nextFeedColumns); } catch (err) { logger.warn(`Failed to hydrate deck feed columns: ${String(err)}`); } } async function handleAdd(kind: ColumnKind, config: string) { const did = activeDid(); if (!did) return; try { const col = await addColumn(did, kind, config); const nextColumns = [...state.columns, col]; setState("columns", nextColumns); setState("addPanelOpen", false); if (kind === "feed") { void hydrateFeedColumns(nextColumns); } } catch (err) { logger.error(`Failed to add column: ${String(err)}`); } } async function handleClose(id: string) { try { await removeColumn(id); const nextColumns = state.columns.filter((column) => column.id !== id); setState("columns", nextColumns); void hydrateFeedColumns(nextColumns); } catch (err) { logger.error(`Failed to remove column: ${String(err)}`); } } async function handleWidthChange(id: string, width: ColumnWidth) { try { const updated = await updateColumn(id, { width }); setState("columns", (prev) => prev.map((c) => (c.id === id ? updated : c))); } catch (err) { logger.error(`Failed to update column width: ${String(err)}`); } } async function handleMoveLeft(id: string) { const cols = state.columns; const idx = cols.findIndex((c) => c.id === id); if (idx === -1 || idx === 0) return; const newOrder = cols.map((c) => c.id); newOrder.splice(idx, 1); newOrder.splice(idx - 1, 0, id); try { await reorderColumns(newOrder); setState( "columns", produce((draft) => { const item = draft.splice(idx, 1)[0]; if (item) draft.splice(idx - 1, 0, item); }), ); } catch (err) { logger.error(`Failed to reorder columns: ${String(err)}`); } } async function handleMoveRight(id: string) { const cols = state.columns; const idx = cols.findIndex((c) => c.id === id); if (idx === -1 || idx >= cols.length - 1) return; const newOrder = cols.map((c) => c.id); newOrder.splice(idx, 1); newOrder.splice(idx + 1, 0, id); try { await reorderColumns(newOrder); setState( "columns", produce((draft) => { const item = draft.splice(idx, 1)[0]; if (item) draft.splice(idx + 1, 0, item); }), ); } catch (err) { logger.error(`Failed to reorder columns: ${String(err)}`); } } function handleDragStart(id: string) { draggingColumnId = id; } function handleDragEnd() { draggingColumnId = null; setState("dragOverId", null); } function handleDragOver(id: string) { if (draggingColumnId && draggingColumnId !== id) { setState("dragOverId", id); } } async function handleDrop(targetId: string) { const sourceId = draggingColumnId; draggingColumnId = null; setState("dragOverId", null); if (!sourceId || sourceId === targetId) return; const cols = state.columns; const fromIdx = cols.findIndex((c) => c.id === sourceId); const toIdx = cols.findIndex((c) => c.id === targetId); if (fromIdx === -1 || toIdx === -1) return; const newOrder = cols.map((c) => c.id); newOrder.splice(fromIdx, 1); newOrder.splice(toIdx, 0, sourceId); try { await reorderColumns(newOrder); setState( "columns", produce((draft) => { const item = draft.splice(fromIdx, 1)[0]; if (item) draft.splice(toIdx, 0, item); }), ); } catch (err) { logger.error(`Failed to reorder columns via drag: ${String(err)}`); } } function handleOpenThread(uri: string) { void threadOverlay.openThread(uri); } createEffect(() => { const handler = createDeckKeyboardHandler(() => setState("addPanelOpen", true), () => { const last = state.columns.at(-1); if (last) void handleClose(last.id); }); globalThis.addEventListener("keydown", handler); onCleanup(() => globalThis.removeEventListener("keydown", handler)); }); onMount(() => { void loadColumns(); }); return (
setState("addPanelOpen", true)} />
{state.error}
setState("addPanelOpen", true)} /> 0}>
setState("addPanelOpen", false)} />
); }