diff --git a/frontend/src/views/BoardView.vue b/frontend/src/views/BoardView.vue index 1d56e9c..cff184b 100644 --- a/frontend/src/views/BoardView.vue +++ b/frontend/src/views/BoardView.vue @@ -11,6 +11,7 @@ import EmptyState from "../components/EmptyState.vue"; import FilterBar from "../components/FilterBar.vue"; import NoteCard from "../components/NoteCard.vue"; import NoteEditor from "../components/NoteEditor.vue"; +import { isDesktop, sync as syncBridge } from "../desktop/bridge"; const notes = useNotesStore(); const config = useConfigStore(); @@ -157,12 +158,45 @@ const retentionNotice = computed(() => { : `Notes here are permanently deleted ${days} days after you trash them. Restore one to keep it.`; }); +/** + * Whether this desktop install has been linked to a server. Only consulted for the + * first-run copy below — offering to connect a server to someone who already did + * would be noise. Never set on the web build, which IS a server's UI. + */ +const linkedToServer = ref(false); + +/** + * The desktop's start state: a fresh install, nothing written yet. + * + * There is no login and no onboarding gate — the app is usable the instant it + * opens, and interrupting that with a welcome modal would tax the one thing the + * product is for (dumping a thought in a second). So the empty board carries the + * message instead, and retires itself the moment a first note exists. That also + * means no "seen it" flag to persist, migrate, or get out of step with reality. + */ +const startState = computed( + () => + isDesktop() && + isMainBoard.value && + !filtered.value && + !linkedToServer.value && + notes.items.length === 0, +); + const emptyState = computed(() => { if (filtered.value) return { title: "No notes match these filters", subtitle: "Try clearing or loosening a facet." }; if (currentView.value === "trash") return { title: "Trash is empty", subtitle: "Notes you delete land here first." }; if (currentView.value === "archived") return { title: "Nothing archived", subtitle: "Archived notes are tucked away here." }; if (currentLabel.value) return { title: "No notes with this label", subtitle: "Tag a note to see it here." }; + // Says what "no account" actually means for the notes about to be written here. + // A new user otherwise has no way to tell whether this thing is storing their + // thoughts locally, silently waiting for a login, or quietly sending them off. + if (startState.value) + return { + title: "Your notes live on this device", + subtitle: "ThoughtSync works fully offline — no account, nothing to sign in to.", + }; return { title: "No notes yet", subtitle: "Your captured thoughts will show up here." }; }); @@ -178,6 +212,17 @@ async function reload() { onMounted(() => { void reload(); window.addEventListener("keydown", onBoardKey); + // Best-effort and never awaited: the board must render at full speed whether or + // not this answers. A failure just leaves the start-state copy showing, which is + // the correct reading of "we don't know of a server". + if (isDesktop()) { + void syncBridge + .status() + .then((s) => { + linkedToServer.value = s.linked; + }) + .catch(() => {}); + } }); onBeforeUnmount(() => { window.removeEventListener("keydown", onBoardKey); @@ -234,7 +279,21 @@ async function onDrop(target: Note) { error-title="Couldn't load your notes" @retry="reload" > - + + + +