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:
@@ -150,6 +150,13 @@ export interface MechanicsFinding {
|
||||
explanation: string
|
||||
}
|
||||
|
||||
// One dictionary's worth of personal words — the ones she's excused from
|
||||
// spell-check. Keyed by the dictionary's language, not the writer's.
|
||||
export interface PersonalWords {
|
||||
lang: string
|
||||
words: string[]
|
||||
}
|
||||
|
||||
// Who's writing. Mirrors the backend db.User.
|
||||
export interface Me {
|
||||
id: string
|
||||
@@ -333,6 +340,24 @@ export const api = {
|
||||
req<VocabWord>(`/vocab/${id}/review`, { method: 'POST', body: JSON.stringify({ grade }) }),
|
||||
deleteVocab: (id: string) => req<void>(`/vocab/${id}`, { method: 'DELETE' }),
|
||||
|
||||
// The personal spelling dictionary — words she's told Petal to stop flagging.
|
||||
// Server-side, so it belongs to her account and follows her between devices.
|
||||
// `lang` is the *dictionary's* language: an English exception must not silence
|
||||
// a pt-PT flag. Every call answers with the full resulting list, so the client
|
||||
// never has to merge two views of the same set.
|
||||
listPersonalWords: (lang: string) =>
|
||||
req<PersonalWords>(`/spell/words?lang=${encodeURIComponent(lang)}`),
|
||||
addPersonalWords: (lang: string, words: string[]) =>
|
||||
req<PersonalWords>('/spell/words', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ lang, words }),
|
||||
}),
|
||||
removePersonalWord: (lang: string, word: string) =>
|
||||
req<PersonalWords>(
|
||||
`/spell/words?lang=${encodeURIComponent(lang)}&word=${encodeURIComponent(word)}`,
|
||||
{ method: 'DELETE' },
|
||||
),
|
||||
|
||||
// Current deployed build id — changes whenever a new frontend ships. The
|
||||
// app polls this to offer a refresh. Bypasses any cache so the answer is live.
|
||||
version: () => req<{ version: string }>('/version', { cache: 'no-store' }),
|
||||
|
||||
Reference in New Issue
Block a user