diff --git a/apps/mana/apps/web/src/lib/modules/picture/ListView.svelte b/apps/mana/apps/web/src/lib/modules/picture/ListView.svelte index 9ec1b5d1f..e3053c11f 100644 --- a/apps/mana/apps/web/src/lib/modules/picture/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/picture/ListView.svelte @@ -194,7 +194,7 @@ fileSize: uf.file.size, width: dims?.width ?? null, height: dims?.height ?? null, - isPublic: false, + visibility: 'private', isFavorite: false, downloadCount: 0, createdAt: nowIso, diff --git a/apps/mana/apps/web/src/lib/modules/picture/collections.ts b/apps/mana/apps/web/src/lib/modules/picture/collections.ts index 0552884d4..fc862833b 100644 --- a/apps/mana/apps/web/src/lib/modules/picture/collections.ts +++ b/apps/mana/apps/web/src/lib/modules/picture/collections.ts @@ -27,7 +27,7 @@ export const PICTURE_GUEST_SEED = { canvasWidth: 2000, canvasHeight: 1500, backgroundColor: '#1e1e2e', - isPublic: false, + visibility: 'private', }, ] satisfies LocalBoard[], boardItems: [ diff --git a/apps/mana/apps/web/src/lib/modules/picture/queries.ts b/apps/mana/apps/web/src/lib/modules/picture/queries.ts index f2b57ed4b..c6d027e2f 100644 --- a/apps/mana/apps/web/src/lib/modules/picture/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/picture/queries.ts @@ -38,7 +38,9 @@ export function toImage(local: LocalImage): Image { height: local.height ?? undefined, fileSize: local.fileSize ?? undefined, blurhash: local.blurhash ?? undefined, - isPublic: local.isPublic, + // Soft-migration fallback: rows written before M3 only have the + // legacy `isPublic` flag; map it to the nearest visibility level. + visibility: local.visibility ?? (local.isPublic === true ? 'public' : 'private'), isFavorite: local.isFavorite, downloadCount: local.downloadCount, rating: local.rating ?? undefined, @@ -62,7 +64,7 @@ export function toBoard(local: LocalBoard): Board { canvasWidth: local.canvasWidth, canvasHeight: local.canvasHeight, backgroundColor: local.backgroundColor, - isPublic: local.isPublic, + visibility: local.visibility ?? (local.isPublic === true ? 'public' : 'private'), createdAt: local.createdAt ?? new Date().toISOString(), updatedAt: local.updatedAt ?? new Date().toISOString(), }; diff --git a/apps/mana/apps/web/src/lib/modules/picture/stores/boards.svelte.ts b/apps/mana/apps/web/src/lib/modules/picture/stores/boards.svelte.ts index 4d56fca8a..365ff25bb 100644 --- a/apps/mana/apps/web/src/lib/modules/picture/stores/boards.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/picture/stores/boards.svelte.ts @@ -8,6 +8,14 @@ import { db } from '$lib/data/database'; import { encryptRecord, decryptRecord } from '$lib/data/crypto'; +import { emitDomainEvent } from '$lib/data/events'; +import { getActiveSpace } from '$lib/data/scope'; +import { getEffectiveUserId } from '$lib/data/current-user'; +import { + defaultVisibilityFor, + generateUnlistedToken, + type VisibilityLevel, +} from '@mana/shared-privacy'; import type { LocalBoard, LocalBoardItem } from '../types'; import { toBoard } from '../queries'; @@ -39,7 +47,7 @@ export const boardsStore = { canvasWidth: 2000, canvasHeight: 1500, backgroundColor: input.backgroundColor || '#ffffff', - isPublic: false, + visibility: defaultVisibilityFor(getActiveSpace()?.type), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; @@ -125,6 +133,9 @@ export const boardsStore = { const newId = crypto.randomUUID(); const now = new Date().toISOString(); + // Duplicate inherits the same default-for-space visibility, not + // the original's. A copy of a public board should NOT auto- + // publish — the user has to explicitly flip visibility again. const duplicated: LocalBoard = { id: newId, name: `${original.name} (Kopie)`, @@ -132,7 +143,7 @@ export const boardsStore = { canvasWidth: original.canvasWidth, canvasHeight: original.canvasHeight, backgroundColor: original.backgroundColor, - isPublic: false, + visibility: defaultVisibilityFor(getActiveSpace()?.type), createdAt: now, updatedAt: now, }; @@ -168,4 +179,45 @@ export const boardsStore = { return { success: false, error }; } }, + + /** + * Flip the board's visibility. Mints/clears an unlisted token on the + * transition boundary and emits the cross-module VisibilityChanged + * event. Caller passes the raw level; no-op if it already matches. + */ + async setVisibility(id: string, next: VisibilityLevel) { + error = null; + try { + const existing = await db.table('boards').get(id); + if (!existing) return { success: false, error: 'Board not found' }; + const before: VisibilityLevel = + existing.visibility ?? (existing.isPublic === true ? 'public' : 'private'); + if (before === next) return { success: true }; + + const now = new Date().toISOString(); + const patch: Partial = { + visibility: next, + visibilityChangedAt: now, + visibilityChangedBy: getEffectiveUserId(), + updatedAt: now, + }; + if (next === 'unlisted' && !existing.unlistedToken) { + patch.unlistedToken = generateUnlistedToken(); + } else if (next !== 'unlisted' && existing.unlistedToken) { + patch.unlistedToken = undefined; + } + await db.table('boards').update(id, patch); + + emitDomainEvent('VisibilityChanged', 'picture', 'boards', id, { + recordId: id, + collection: 'boards', + before, + after: next, + }); + return { success: true }; + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to set visibility'; + return { success: false, error }; + } + }, }; diff --git a/apps/mana/apps/web/src/lib/modules/picture/types.ts b/apps/mana/apps/web/src/lib/modules/picture/types.ts index b096651a5..324e06be4 100644 --- a/apps/mana/apps/web/src/lib/modules/picture/types.ts +++ b/apps/mana/apps/web/src/lib/modules/picture/types.ts @@ -3,6 +3,7 @@ */ import type { BaseRecord } from '@mana/local-store'; +import type { VisibilityLevel } from '@mana/shared-privacy'; /** * How the image was created. 'text' is the classic prompt-only @@ -25,7 +26,16 @@ export interface LocalImage extends BaseRecord { height?: number | null; fileSize?: number | null; blurhash?: string | null; - isPublic: boolean; + /** + * @deprecated Use `visibility` instead. Kept for the soft-migration + * window — will be dropped in the hard follow-up once no reader + * references it. See docs/plans/visibility-system.md §M3. + */ + isPublic?: boolean; + visibility?: VisibilityLevel; + visibilityChangedAt?: string; + visibilityChangedBy?: string; + unlistedToken?: string; isFavorite: boolean; downloadCount: number; rating?: number | null; @@ -51,7 +61,15 @@ export interface LocalBoard extends BaseRecord { canvasWidth: number; canvasHeight: number; backgroundColor: string; - isPublic: boolean; + /** + * @deprecated Use `visibility` instead. Kept during the M3 soft + * migration — dropped in the hard follow-up. + */ + isPublic?: boolean; + visibility?: VisibilityLevel; + visibilityChangedAt?: string; + visibilityChangedBy?: string; + unlistedToken?: string; } export interface LocalBoardItem extends BaseRecord { @@ -94,7 +112,7 @@ export interface Image { height?: number; fileSize?: number; blurhash?: string; - isPublic: boolean; + visibility: VisibilityLevel; isFavorite: boolean; downloadCount: number; rating?: number; @@ -116,7 +134,7 @@ export interface Board { canvasWidth: number; canvasHeight: number; backgroundColor: string; - isPublic: boolean; + visibility: VisibilityLevel; createdAt: string; updatedAt: string; } diff --git a/apps/mana/apps/web/src/lib/modules/wardrobe/api/try-on.ts b/apps/mana/apps/web/src/lib/modules/wardrobe/api/try-on.ts index 123284d3b..a5b7b6673 100644 --- a/apps/mana/apps/web/src/lib/modules/wardrobe/api/try-on.ts +++ b/apps/mana/apps/web/src/lib/modules/wardrobe/api/try-on.ts @@ -188,7 +188,7 @@ export async function runOutfitTryOn(params: RunOutfitTryOnParams): Promise { if (!props.sourceId) { @@ -72,8 +74,10 @@ async function resolvePictureBoard(props: ModuleEmbedProps): Promise @@ -74,6 +80,7 @@
+