diff --git a/frontend/src/components/ActivityForm.svelte b/frontend/src/components/ActivityForm.svelte index 40ec2c3..ef0ad95 100644 --- a/frontend/src/components/ActivityForm.svelte +++ b/frontend/src/components/ActivityForm.svelte @@ -45,6 +45,14 @@ : '', ); // svelte-ignore state_referenced_locally + let description = $state( + existing + ? (existing.visibility === 'private' + ? initialPriv?.description ?? '' + : existing.description ?? '') + : '', + ); + // svelte-ignore state_referenced_locally let tags: string[] = $state( existing ? (existing.visibility === 'private' ? initialPriv?.tags ?? [] : [...existing.tags]) @@ -103,6 +111,7 @@ const payload: PrivatePayload = { title: title.trim(), tags, + description: description.trim() || undefined, loc_label: locLabel || undefined, scheduled_at: scheduledEpoch() ?? undefined, }; @@ -116,6 +125,7 @@ return { visibility, title: title.trim(), + description: description.trim() || null, tags, loc_label: locLabel || null, scheduled_at: scheduledEpoch(), @@ -195,6 +205,10 @@ + + +
Etiketter
(tags = t)} /> diff --git a/frontend/src/components/ActivityRow.svelte b/frontend/src/components/ActivityRow.svelte index 81673fe..740fd3e 100644 --- a/frontend/src/components/ActivityRow.svelte +++ b/frontend/src/components/ActivityRow.svelte @@ -181,6 +181,9 @@ Privat + {#if decrypted.description} +

{decrypted.description}

+ {/if} {#if decrypted.tags.length}
{#each decrypted.tags as t}{t}{/each} @@ -202,6 +205,9 @@ {activity.visibility === 'semi' ? 'Anonym' : 'Offentlig'} + {#if activity.description} +

{activity.description}

+ {/if} {#if activity.tags.length}
{#each activity.tags as t}{t}{/each} diff --git a/frontend/src/components/Home.svelte b/frontend/src/components/Home.svelte index 7d0b4e2..870e3de 100644 --- a/frontend/src/components/Home.svelte +++ b/frontend/src/components/Home.svelte @@ -84,14 +84,15 @@ if (a.visibility === 'private') { const p = privateCleartext.get(a.id); if (!p) return false; - return [p.title, p.loc_label ?? '', ...p.tags] + return [p.title, p.description ?? '', p.loc_label ?? '', ...p.tags] .some((s) => s.toLowerCase().includes(needle)); } return [ a.title, + a.description ?? '', a.loc_label ?? '', ...a.tags, - a.visibility === 'public' ? a.owner_display : '', + a.visibility === 'public' ? a.owner_display ?? '' : '', ].some((s) => s.toLowerCase().includes(needle)); } diff --git a/server/activities.ts b/server/activities.ts index 2dc5a3b..754e254 100644 --- a/server/activities.ts +++ b/server/activities.ts @@ -29,6 +29,7 @@ interface ActivityRow { ciphertext: Uint8Array | null; nonce: Uint8Array | null; title: string | null; + description: string | null; scheduled_at: number | null; loc_label: string | null; loc_lat: number | null; @@ -128,6 +129,7 @@ function serialize(row: ActivityRow, viewerId: string | null): Activity { id: row.id, visibility: 'semi', title: row.title ?? '', + description: row.description, tags, loc_label: row.loc_label, loc_lat: row.loc_lat, @@ -149,6 +151,7 @@ function serialize(row: ActivityRow, viewerId: string | null): Activity { owner_display: attrib.display, owner_username: attrib.username, title: row.title ?? '', + description: row.description, tags, loc_label: row.loc_label, loc_lat: row.loc_lat, @@ -237,11 +240,13 @@ activitiesRoutes.post('/', requireAuth, async (c) => { } else { db.prepare(` INSERT INTO activities - (id, owner_id, visibility, title, scheduled_at, loc_label, loc_lat, loc_lng, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + (id, owner_id, visibility, title, description, scheduled_at, + loc_label, loc_lat, loc_lng, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( id, userId, body.visibility, body.title!.trim(), + body.description?.trim() || null, body.scheduled_at ?? null, body.loc_label ?? null, body.loc_lat ?? null, @@ -282,6 +287,7 @@ activitiesRoutes.patch('/:id', requireAuth, async (c) => { ciphertext = ?, nonce = ?, title = NULL, + description = NULL, scheduled_at = NULL, loc_label = NULL, loc_lat = NULL, @@ -296,6 +302,7 @@ activitiesRoutes.patch('/:id', requireAuth, async (c) => { UPDATE activities SET visibility = ?, title = ?, + description = ?, scheduled_at = ?, loc_label = ?, loc_lat = ?, @@ -307,6 +314,7 @@ activitiesRoutes.patch('/:id', requireAuth, async (c) => { `).run( body.visibility, body.title!.trim(), + body.description?.trim() || null, body.scheduled_at ?? null, body.loc_label ?? null, body.loc_lat ?? null, diff --git a/server/db.ts b/server/db.ts index 776a072..5df5f61 100644 --- a/server/db.ts +++ b/server/db.ts @@ -58,6 +58,10 @@ const SCHEMA_STATEMENTS: readonly string[] = [ ciphertext BLOB, nonce BLOB, title TEXT, + -- Optional free-text body. Plain text for now (markdown is a possible + -- future polish). For private rows this column stays NULL — the + -- description lives inside the encrypted payload alongside the title. + description TEXT, scheduled_at INTEGER, loc_label TEXT, loc_lat REAL, @@ -184,6 +188,7 @@ export function getDb(): Database { // Feedback triage columns (added after the feedback feature shipped). ensureColumn(db, 'feedback', 'done_at', 'INTEGER'); ensureColumn(db, 'feedback', 'done_by', 'TEXT'); + ensureColumn(db, 'activities', 'description', 'TEXT'); ensureColumn(db, 'users', 'username', 'TEXT'); ensureColumn(db, 'users', 'public_list_enabled', 'INTEGER NOT NULL DEFAULT 0'); // UNIQUE index on username via separate CREATE INDEX so the ALTER TABLE diff --git a/server/users.ts b/server/users.ts index 0e31cf0..cecd5a6 100644 --- a/server/users.ts +++ b/server/users.ts @@ -15,6 +15,7 @@ interface ActivityRow { id: string; owner_id: string; title: string | null; + description: string | null; scheduled_at: number | null; loc_label: string | null; loc_lat: number | null; @@ -41,8 +42,8 @@ usersRoutes.get('/:username/list', (c) => { const rows = db .prepare(` - SELECT id, owner_id, title, scheduled_at, loc_label, loc_lat, loc_lng, - created_at, updated_at + SELECT id, owner_id, title, description, scheduled_at, loc_label, + loc_lat, loc_lng, created_at, updated_at FROM activities WHERE owner_id = ? AND visibility = 'public' ORDER BY created_at DESC @@ -64,6 +65,7 @@ usersRoutes.get('/:username/list', (c) => { // Surfacing it on each row keeps ActivityRow's rendering uniform. owner_username: username, title: r.title ?? '', + description: r.description, tags: tagsFor(r.id), loc_label: r.loc_label, loc_lat: r.loc_lat, diff --git a/shared/crypto.ts b/shared/crypto.ts index d4c3e12..e5199c5 100644 --- a/shared/crypto.ts +++ b/shared/crypto.ts @@ -182,6 +182,8 @@ export function unwrapDek(sealed: Sealed, kek: Bytes): Bytes { export interface PrivatePayload { title: string; tags: string[]; + /** Optional free-text body. Plain text (markdown rendering is a future polish). */ + description?: string; loc_label?: string; loc_lat?: number; loc_lng?: number; diff --git a/shared/types.ts b/shared/types.ts index 546b371..ae75f7d 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -184,6 +184,8 @@ export interface ActivityPublic { // client renders the owner attribution as a link to //list. owner_username: string | null; title: string; + /** Optional free-text body. Plain text. Empty string and null treated the same client-side. */ + description: string | null; tags: string[]; loc_label: string | null; loc_lat: number | null; @@ -205,6 +207,7 @@ export interface ActivitySemi { // anyone else. Stripped server-side for any other viewer; see SECURITY.md. owner_id?: string; title: string; + description: string | null; tags: string[]; loc_label: string | null; loc_lat: number | null; @@ -237,6 +240,7 @@ export interface CreateActivityRequest { visibility: Visibility; // For semi/public: title?: string; + description?: string | null; tags?: string[]; loc_label?: string | null; loc_lat?: number | null;