From d5b24837b12135d5e4675200691e06432fd27aa4 Mon Sep 17 00:00:00 2001 From: Tim Disney Date: Sat, 7 Mar 2026 15:36:07 -0800 Subject: [PATCH] Add archive/leave boards from dashboard - Add `archived` field to Board type (local-only, not synced to PDS) - Add Dexie v11 with archived index on boards table - Create board-actions.ts with leaveBoard(), archiveBoard(), unarchiveBoard() - Leave board: revokes trust from PDS, removes all local board data - Archive board: hides from dashboard list (local-only soft hide) - Add action menu (ellipsis) on BoardCard with Archive/Leave options - Leave shows confirmation dialog before proceeding - Dashboard filters active vs archived boards - Collapsible "Archived Boards" section shows archived boards with unarchive - Discovery respects archived boards (won't re-fetch and auto-unarchive) Co-Authored-By: Claude Opus 4.6 --- src/lib/appview.ts | 2 +- src/lib/board-actions.ts | 69 ++++++++ src/lib/components/BoardCard.svelte | 255 +++++++++++++++++++++++++++- src/lib/db.ts | 5 + src/lib/types.ts | 1 + src/routes/+page.svelte | 83 ++++++++- 6 files changed, 407 insertions(+), 8 deletions(-) create mode 100644 src/lib/board-actions.ts diff --git a/src/lib/appview.ts b/src/lib/appview.ts index 43dcb15..17ff9ae 100644 --- a/src/lib/appview.ts +++ b/src/lib/appview.ts @@ -332,7 +332,7 @@ export async function discoverMyBoards( const knownBoardUris = new Set(); let newCount = 0; - // Collect boards already in Dexie so we don't re-fetch them + // Collect boards already in Dexie (including archived) so we don't re-fetch them const existingBoards = await db.boards.toArray(); for (const b of existingBoards) { knownBoardUris.add(buildAtUri(b.did, BOARD_COLLECTION, b.rkey)); diff --git a/src/lib/board-actions.ts b/src/lib/board-actions.ts new file mode 100644 index 0000000..0455489 --- /dev/null +++ b/src/lib/board-actions.ts @@ -0,0 +1,69 @@ +import { db } from "./db.js"; +import { revokeTrust } from "./trust.js"; +import { buildAtUri, BOARD_COLLECTION } from "./tid.js"; +import type { Board } from "./types.js"; + +/** + * Leave a joined board: revoke trust records from PDS and remove all local data. + * The board won't reappear in discovery since the trust record is deleted. + */ +export async function leaveBoard(board: Board, userDid: string): Promise { + const boardUri = buildAtUri(board.did, BOARD_COLLECTION, board.rkey); + + // Revoke all trust records the current user has for this board + const userTrusts = await db.trusts + .where("boardUri") + .equals(boardUri) + .filter((t) => t.did === userDid) + .toArray(); + + for (const trust of userTrusts) { + await revokeTrust(userDid, trust.trustedDid, boardUri); + } + + // Delete all local data for this board + await db.transaction( + "rw", + [ + db.boards, + db.tasks, + db.ops, + db.trusts, + db.comments, + db.approvals, + db.reactions, + db.notifications, + db.filterViews, + ], + async () => { + await db.tasks.where("boardUri").equals(boardUri).delete(); + await db.ops.where("boardUri").equals(boardUri).delete(); + await db.trusts.where("boardUri").equals(boardUri).delete(); + await db.comments.where("boardUri").equals(boardUri).delete(); + await db.approvals.where("boardUri").equals(boardUri).delete(); + await db.reactions.where("boardUri").equals(boardUri).delete(); + await db.notifications.where("boardUri").equals(boardUri).delete(); + await db.filterViews.where("boardUri").equals(boardUri).delete(); + if (board.id) { + await db.boards.delete(board.id); + } + }, + ); +} + +/** + * Archive an owned board — hides it from the dashboard locally. + * The board record stays in PDS (local-only operation). + */ +export async function archiveBoard(board: Board): Promise { + if (!board.id) return; + await db.boards.update(board.id, { archived: true }); +} + +/** + * Unarchive a board — restores it to the main dashboard list. + */ +export async function unarchiveBoard(board: Board): Promise { + if (!board.id) return; + await db.boards.update(board.id, { archived: false }); +} diff --git a/src/lib/components/BoardCard.svelte b/src/lib/components/BoardCard.svelte index e53330d..3deb143 100644 --- a/src/lib/components/BoardCard.svelte +++ b/src/lib/components/BoardCard.svelte @@ -1,10 +1,122 @@ + { + showMenu = false; + }} +/> + + {#if board.archived} +
+ Archived + +
+ {:else if userDid} + + {#if showMenu} + + +
e.stopPropagation()}> + {#if isOwner} + + {:else} + + {/if} +
+ {/if} + {/if} + + {#if confirmingLeave} + + +
{ + e.preventDefault(); + e.stopPropagation(); + }} + > +

Leave {board.name}?

+

You'll need to rejoin to access it again.

+
+ + +
+
+ {/if} +

{board.name}

{#if board.description}

{board.description}

@@ -36,6 +148,7 @@ diff --git a/src/lib/db.ts b/src/lib/db.ts index c1d0c29..ddf68bd 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -168,6 +168,11 @@ function createDb(name: string): SkyboardDb { knownParticipants: null, }); + // Add archived index to boards for archive/leave feature + d.version(11).stores({ + boards: "++id, rkey, did, syncStatus, archived", + }); + return d; } diff --git a/src/lib/types.ts b/src/lib/types.ts index 462e7d9..3ce2974 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -22,6 +22,7 @@ export interface Board { columns: Column[]; labels?: Label[]; open?: boolean; + archived?: boolean; createdAt: string; syncStatus: SyncStatus; } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 3ad5a6c..b1b12eb 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -8,14 +8,27 @@ import { loadBoardFromAppview } from "$lib/appview.js"; import { grantTrust } from "$lib/trust.js"; import { getDiscoveryState } from "$lib/discovery.svelte.js"; + import { + archiveBoard, + unarchiveBoard, + leaveBoard, + } from "$lib/board-actions.js"; import BoardCard from "$lib/components/BoardCard.svelte"; const auth = getAuth(); const discovery = getDiscoveryState(); // Show all boards — both owned and joined - const boards = useLiveQuery(() => db.boards.toArray()); + const allBoards = useLiveQuery(() => db.boards.toArray()); + const activeBoards = $derived( + allBoards.current?.filter((b) => !b.archived) ?? [], + ); + const archivedBoards = $derived( + allBoards.current?.filter((b) => b.archived) ?? [], + ); + + let showArchived = $state(false); let newBoardName = $state(""); let newBoardDescription = $state(""); let creating = $state(false); @@ -125,6 +138,19 @@ joining = false; } } + + async function handleArchive(board: Board) { + await archiveBoard(board); + } + + async function handleUnarchive(board: Board) { + await unarchiveBoard(board); + } + + async function handleLeave(board: Board) { + if (!auth.did) return; + await leaveBoard(board, auth.did); + }
@@ -169,21 +195,48 @@ {/if}
- {#if boards.current && boards.current.length > 0} - {#each boards.current as board (board.id)} - + {#if activeBoards.length > 0} + {#each activeBoards as board (board.id)} + {/each} - {:else if boards.current && discovery.isDiscovering} + {:else if allBoards.current && discovery.isDiscovering}

Discovering your boards...

- {:else if boards.current} + {:else if allBoards.current}

No boards yet. Create one above or paste a board link to join.

{/if}
+ + {#if archivedBoards.length > 0} +
+ + {#if showArchived} +
+ {#each archivedBoards as board (board.id)} + + {/each} +
+ {/if} +
+ {/if}