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:
@@ -3,6 +3,7 @@ import type { SaveStatus } from '../../hooks/useAutoSave'
|
||||
import { useCompanion, type Mood } from './useCompanion'
|
||||
import { LottiePlayer } from './LottiePlayer'
|
||||
import { COMPANIONS, DEFAULT_COMPANION } from './companions'
|
||||
import { onPrefsScopeChange, readPref, writePref } from '../../lib/prefs'
|
||||
|
||||
interface Props {
|
||||
wordCount: number
|
||||
@@ -38,13 +39,22 @@ export function PetalCompanion({ wordCount, saveStatus, llmDown, editTick, accep
|
||||
text,
|
||||
})
|
||||
|
||||
const [companionId, setCompanionId] = useState<string>(() => {
|
||||
try {
|
||||
return localStorage.getItem(STORAGE_KEY) || DEFAULT_COMPANION
|
||||
} catch {
|
||||
return DEFAULT_COMPANION
|
||||
}
|
||||
})
|
||||
const [companionId, setCompanionId] = useState<string>(
|
||||
() => readPref(STORAGE_KEY) || DEFAULT_COMPANION,
|
||||
)
|
||||
|
||||
// The mascot belongs to the writer, not the browser. That first read happens
|
||||
// before /api/me answers, so pick the choice up again once the account is
|
||||
// known — unless she's already swapped companions in the meantime.
|
||||
const touched = useRef(false)
|
||||
useEffect(
|
||||
() =>
|
||||
onPrefsScopeChange(() => {
|
||||
if (touched.current) return
|
||||
setCompanionId(readPref(STORAGE_KEY) || DEFAULT_COMPANION)
|
||||
}),
|
||||
[],
|
||||
)
|
||||
const companion = COMPANIONS.find((c) => c.id === companionId) ?? COMPANIONS[0]
|
||||
const [pickerOpen, setPickerOpen] = useState(false)
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
@@ -69,11 +79,8 @@ export function PetalCompanion({ wordCount, saveStatus, llmDown, editTick, accep
|
||||
|
||||
function choose(id: string) {
|
||||
setCompanionId(id)
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, id)
|
||||
} catch {
|
||||
/* private mode / storage disabled — selection just won't persist */
|
||||
}
|
||||
touched.current = true
|
||||
writePref(STORAGE_KEY, id)
|
||||
setPickerOpen(false)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user