Phase 18: settings that belong to the writer, not the browser
The mute toggle, the falling-petals toggle and the chosen companion lived in localStorage, which is a property of the machine. Now that two people can sign in to one Petal, sharing a laptop would have meant sharing a mascot and one person's silence muting the other. Each key is namespaced by user id. The awkward part is timing: sounds.ts and petals.ts read their value the moment they are imported, long before /api/me can have answered. Rather than block startup on the network for a mute flag, a read before the answer arrives sees the old un-namespaced key -- on a single-writer browser, exactly the right value -- and setPrefsScope then adopts it into that account's namespace and tells every reader to look again. Adoption moves rather than copies, so the first account inherits what was set before accounts existed and the second starts from Petal's defaults. The personal spelling dictionary moves further than that: onto the server. It is built from her own writing, so it should not be readable by whoever sits down at the same browser next -- but merely namespacing it would have split the list she already has between her laptop and her tablet, which is worse than where we started. A table keyed (user_id, lang, word) follows her instead. The lang is the dictionary's, not hers: an English exception must not silence a pt-PT flag once the second pair ships. Adding a word takes effect in the editor immediately and persists in the background, so the underline goes away the instant she asks. A browser still holding the old list hands it over on first load, and only lets go once the server has taken it. Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
This commit is contained in:
+16
-10
@@ -13,7 +13,10 @@ import blockUrl from '../assets/sounds/block.mp3'
|
||||
import baodingUrl from '../assets/sounds/baoding.mp3'
|
||||
import milestoneUrl from '../assets/sounds/milestone.mp3'
|
||||
import errorUrl from '../assets/sounds/error.mp3'
|
||||
import { onPrefsScopeChange, readPref, writePref } from '../lib/prefs'
|
||||
|
||||
// The mute choice belongs to the account, not the browser — one person's
|
||||
// silence must not mute the next writer to sign in here.
|
||||
const STORAGE_KEY = 'petal.sound'
|
||||
|
||||
// Master volume — deliberately gentle. These are background delights, not alerts.
|
||||
@@ -53,24 +56,27 @@ let enabled = readEnabled()
|
||||
const listeners = new Set<(on: boolean) => void>()
|
||||
|
||||
function readEnabled(): boolean {
|
||||
try {
|
||||
return localStorage.getItem(STORAGE_KEY) !== 'off'
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
return readPref(STORAGE_KEY) !== 'off'
|
||||
}
|
||||
|
||||
// This module reads its value at import time, before /api/me has answered.
|
||||
// Re-read once the account is known, in case this writer's choice differs from
|
||||
// whatever the browser was holding.
|
||||
onPrefsScopeChange(() => {
|
||||
const next = readEnabled()
|
||||
if (next === enabled) return
|
||||
enabled = next
|
||||
listeners.forEach((fn) => fn(next))
|
||||
if (next) void ensureContext()
|
||||
})
|
||||
|
||||
export function isSoundEnabled(): boolean {
|
||||
return enabled
|
||||
}
|
||||
|
||||
export function setSoundEnabled(on: boolean): void {
|
||||
enabled = on
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, on ? 'on' : 'off')
|
||||
} catch {
|
||||
/* private mode — choice just won't persist */
|
||||
}
|
||||
writePref(STORAGE_KEY, on ? 'on' : 'off')
|
||||
listeners.forEach((fn) => fn(on))
|
||||
// Touching the context on enable doubles as a user-gesture unlock + warm-up.
|
||||
if (on) void ensureContext()
|
||||
|
||||
Reference in New Issue
Block a user