From 92bf933c105f58297b69a444e837c538b74175da Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 24 Feb 2025 17:13:46 +0100 Subject: [PATCH 1/2] feat(frontend): link custom preferences in profile editor --- Foxnouns.Frontend/src/lib/components/Error.svelte | 2 ++ .../lib/components/editor/CustomPreferencesNotice.svelte | 8 ++++++++ Foxnouns.Frontend/src/lib/i18n/locales/en.json | 7 +++++-- .../src/routes/settings/members/[id]/fields/+page.svelte | 5 +++++ .../settings/members/[id]/names-pronouns/+page.svelte | 2 ++ .../src/routes/settings/profile/fields/+page.svelte | 5 +++++ .../routes/settings/profile/names-pronouns/+page.svelte | 2 ++ 7 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 Foxnouns.Frontend/src/lib/components/editor/CustomPreferencesNotice.svelte diff --git a/Foxnouns.Frontend/src/lib/components/Error.svelte b/Foxnouns.Frontend/src/lib/components/Error.svelte index 7817dfa..c9e2c0e 100644 --- a/Foxnouns.Frontend/src/lib/components/Error.svelte +++ b/Foxnouns.Frontend/src/lib/components/Error.svelte @@ -12,6 +12,8 @@ {#if error.code === ErrorCode.BadRequest} {$t("error.bad-request-header")} + {:else if error.status === 404} + {$t("error.not-found-header")} {:else} {$t("error.generic-header")} {/if} diff --git a/Foxnouns.Frontend/src/lib/components/editor/CustomPreferencesNotice.svelte b/Foxnouns.Frontend/src/lib/components/editor/CustomPreferencesNotice.svelte new file mode 100644 index 0000000..319c1da --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/editor/CustomPreferencesNotice.svelte @@ -0,0 +1,8 @@ + + +
+ {$t("editor.custom-preference-notice")} + {$t("editor.custom-preference-notice-link")} +
diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index 0c2f958..1c15a83 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -121,7 +121,8 @@ "500-description": "Something went wrong on the server. Please try again later.", "unknown-status-description": "Something went wrong, but we're not sure what. Please try again.", "error-id": "If you report this error to the developers, please give them this ID:", - "page-not-found": "No page exists at this URL." + "page-not-found": "No page exists at this URL.", + "not-found-header": "That page could not be found" }, "settings": { "general-information-tab": "General information", @@ -288,7 +289,9 @@ "custom-preference-size-small": "Small", "custom-preference-size": "Text size", "custom-preference-muted": "Show as muted text", - "custom-preference-favourite": "Treat like favourite" + "custom-preference-favourite": "Treat like favourite", + "custom-preference-notice": "Want to edit your custom preferences?", + "custom-preference-notice-link": "Go to settings" }, "cancel": "Cancel", "report": { diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/fields/+page.svelte b/Foxnouns.Frontend/src/routes/settings/members/[id]/fields/+page.svelte index 491a45f..470f4f9 100644 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/fields/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/fields/+page.svelte @@ -2,7 +2,9 @@ import { apiRequest } from "$api"; import ApiError, { type RawApiError } from "$api/error"; import { mergePreferences, type User } from "$api/models/user"; + import CustomPreferencesNotice from "$components/editor/CustomPreferencesNotice.svelte"; import FieldsEditor from "$components/editor/FieldsEditor.svelte"; + import NoscriptWarning from "$components/editor/NoscriptWarning.svelte"; import log from "$lib/log"; import type { PageData } from "./$types"; @@ -29,4 +31,7 @@ }; + + + diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/names-pronouns/+page.svelte b/Foxnouns.Frontend/src/routes/settings/members/[id]/names-pronouns/+page.svelte index 19ed7e5..918381d 100644 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/names-pronouns/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/names-pronouns/+page.svelte @@ -3,6 +3,7 @@ import ApiError, { type RawApiError } from "$api/error"; import type { Member } from "$api/models"; import { mergePreferences } from "$api/models/user"; + import CustomPreferencesNotice from "$components/editor/CustomPreferencesNotice.svelte"; import FieldEditor from "$components/editor/FieldEditor.svelte"; import FormStatusMarker from "$components/editor/FormStatusMarker.svelte"; import NoscriptWarning from "$components/editor/NoscriptWarning.svelte"; @@ -40,6 +41,7 @@ +
diff --git a/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte index 4c61a58..44b0cf7 100644 --- a/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte @@ -2,7 +2,9 @@ import { apiRequest } from "$api"; import ApiError, { type RawApiError } from "$api/error"; import { mergePreferences, type User } from "$api/models/user"; + import CustomPreferencesNotice from "$components/editor/CustomPreferencesNotice.svelte"; import FieldsEditor from "$components/editor/FieldsEditor.svelte"; + import NoscriptWarning from "$components/editor/NoscriptWarning.svelte"; import log from "$lib/log"; import type { PageData } from "./$types"; @@ -29,4 +31,7 @@ }; + + + diff --git a/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte index e22c5d5..b787096 100644 --- a/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte @@ -2,6 +2,7 @@ import { apiRequest } from "$api"; import ApiError, { type RawApiError } from "$api/error"; import { mergePreferences, type User } from "$api/models/user"; + import CustomPreferencesNotice from "$components/editor/CustomPreferencesNotice.svelte"; import FieldEditor from "$components/editor/FieldEditor.svelte"; import FormStatusMarker from "$components/editor/FormStatusMarker.svelte"; import NoscriptWarning from "$components/editor/NoscriptWarning.svelte"; @@ -37,6 +38,7 @@ +
From d1faf1ddee74b4a95cc32e12aaa194645d58ca31 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 24 Feb 2025 17:37:49 +0100 Subject: [PATCH 2/2] feat(frontend): store pending profile changes in sessionStorage --- .../lib/components/editor/LinksEditor.svelte | 10 ++++- .../editor/ProfileFlagsEditor.svelte | 10 ++++- Foxnouns.Frontend/src/lib/state.svelte.ts | 37 +++++++++++++++++++ .../routes/settings/members/[id]/+page.svelte | 7 ++++ .../settings/members/[id]/fields/+page.svelte | 7 ++++ .../members/[id]/flags-links/+page.svelte | 8 +++- .../members/[id]/names-pronouns/+page.svelte | 10 +++++ .../routes/settings/profile/bio/+page.svelte | 7 ++++ .../settings/profile/fields/+page.svelte | 7 ++++ .../settings/profile/flags-links/+page.svelte | 8 +++- .../profile/names-pronouns/+page.svelte | 10 +++++ 11 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 Foxnouns.Frontend/src/lib/state.svelte.ts diff --git a/Foxnouns.Frontend/src/lib/components/editor/LinksEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/LinksEditor.svelte index f908e0e..4047880 100644 --- a/Foxnouns.Frontend/src/lib/components/editor/LinksEditor.svelte +++ b/Foxnouns.Frontend/src/lib/components/editor/LinksEditor.svelte @@ -2,14 +2,16 @@ import type { RawApiError } from "$api/error"; import IconButton from "$components/IconButton.svelte"; import { t } from "$lib/i18n"; + import ephemeralState from "$lib/state.svelte"; import FormStatusMarker from "./FormStatusMarker.svelte"; type Props = { + stateKey: string; currentLinks: string[]; save(links: string[]): Promise; form: { ok: boolean; error: RawApiError | null } | null; }; - let { currentLinks, save, form }: Props = $props(); + let { stateKey, currentLinks, save, form }: Props = $props(); let links = $state(currentLinks); let newEntry = $state(""); @@ -37,6 +39,12 @@ links = [...links, newEntry]; newEntry = ""; }; + + ephemeralState( + stateKey, + () => links, + (data) => (links = data), + );

diff --git a/Foxnouns.Frontend/src/lib/components/editor/ProfileFlagsEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/ProfileFlagsEditor.svelte index 5bd62fd..304ae88 100644 --- a/Foxnouns.Frontend/src/lib/components/editor/ProfileFlagsEditor.svelte +++ b/Foxnouns.Frontend/src/lib/components/editor/ProfileFlagsEditor.svelte @@ -4,16 +4,18 @@ import FlagSearch from "$components/editor/FlagSearch.svelte"; import IconButton from "$components/IconButton.svelte"; import { t } from "$lib/i18n"; + import ephemeralState from "$lib/state.svelte"; import FlagButton from "./FlagButton.svelte"; import FormStatusMarker from "./FormStatusMarker.svelte"; type Props = { + stateKey: string; profileFlags: PrideFlag[]; allFlags: PrideFlag[]; save(flags: string[]): Promise; form: { ok: boolean; error: RawApiError | null } | null; }; - let { profileFlags, allFlags, save, form }: Props = $props(); + let { stateKey, profileFlags, allFlags, save, form }: Props = $props(); let flags = $state(profileFlags); @@ -40,6 +42,12 @@ }; const saveChanges = () => save(flags.map((f) => f.id)); + + ephemeralState( + stateKey, + () => flags, + (data) => (flags = data), + );
diff --git a/Foxnouns.Frontend/src/lib/state.svelte.ts b/Foxnouns.Frontend/src/lib/state.svelte.ts new file mode 100644 index 0000000..8358bf3 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/state.svelte.ts @@ -0,0 +1,37 @@ +import { onMount, onDestroy } from "svelte"; +import { browser } from "$app/environment"; +import log from "./log"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { Snapshot } from "@sveltejs/kit"; + +/** + * Store ephemeral state in sessionStorage to persist between navigations. + * Similar to {@link Snapshot}, but doesn't attach it to a history entry. + * @param key Unique key to use for this state. + * @param capture Function that returns the state to store. + * @param restore Function that takes the state that was stored previously and assigns it back to component variables. + */ +export default function ephemeralState( + key: string, + capture: () => T, + restore: (data: T) => void, +): void { + if (!browser) return; + + onMount(() => { + if (!("sessionStorage" in window)) return; + const rawData = sessionStorage.getItem("ephemeral-" + key); + if (!rawData) return; + + log.debug("Restoring data %s from session storage", key); + const data = JSON.parse(rawData) as T; + restore(data); + }); + + onDestroy(() => { + if (!("sessionStorage" in window)) return; + + log.debug("Saving data %s to session storage", key); + sessionStorage.setItem("ephemeral-" + key, JSON.stringify(capture())); + }); +} diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte b/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte index 64be86e..0e34638 100644 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte @@ -15,6 +15,7 @@ import BioEditor from "$components/editor/BioEditor.svelte"; import { PUBLIC_BASE_URL } from "$env/static/public"; import { enhance } from "$app/forms"; + import ephemeralState from "$lib/state.svelte"; type Props = { data: PageData; form: ActionData }; let { data, form }: Props = $props(); @@ -61,6 +62,12 @@ // Bio is stored in a $state() so we have a markdown preview let bio = $state(data.member.bio || ""); + + ephemeralState( + "member-" + data.member.id + "-bio", + () => bio, + (data) => (bio = data), + ); {#if error} diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/fields/+page.svelte b/Foxnouns.Frontend/src/routes/settings/members/[id]/fields/+page.svelte index 470f4f9..7e86851 100644 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/fields/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/fields/+page.svelte @@ -6,6 +6,7 @@ import FieldsEditor from "$components/editor/FieldsEditor.svelte"; import NoscriptWarning from "$components/editor/NoscriptWarning.svelte"; import log from "$lib/log"; + import ephemeralState from "$lib/state.svelte"; import type { PageData } from "./$types"; type Props = { data: PageData }; @@ -29,6 +30,12 @@ if (e instanceof ApiError) ok.error = e.obj; } }; + + ephemeralState( + "member-" + data.member.id + "-fields", + () => fields, + (data) => (fields = data), + ); diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/flags-links/+page.svelte b/Foxnouns.Frontend/src/routes/settings/members/[id]/flags-links/+page.svelte index b6aaadb..e9e1c2d 100644 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/flags-links/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/flags-links/+page.svelte @@ -41,10 +41,16 @@ - + diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/names-pronouns/+page.svelte b/Foxnouns.Frontend/src/routes/settings/members/[id]/names-pronouns/+page.svelte index 918381d..9aa19bb 100644 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/names-pronouns/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/names-pronouns/+page.svelte @@ -10,6 +10,7 @@ import PronounsEditor from "$components/editor/PronounsEditor.svelte"; import { t } from "$lib/i18n"; import log from "$lib/log"; + import ephemeralState from "$lib/state.svelte"; import type { PageData } from "./$types"; type Props = { data: PageData }; @@ -22,6 +23,15 @@ let allPreferences = $derived(mergePreferences(data.user.custom_preferences)); + ephemeralState( + "member-" + data.member.id + "-names-pronouns", + () => ({ names, pronouns }), + (data) => { + names = data.names; + pronouns = data.pronouns; + }, + ); + const update = async () => { try { const resp = await apiRequest("PATCH", `/users/@me/members/${data.member.id}`, { diff --git a/Foxnouns.Frontend/src/routes/settings/profile/bio/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/bio/+page.svelte index 19e04fb..91e452b 100644 --- a/Foxnouns.Frontend/src/routes/settings/profile/bio/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/profile/bio/+page.svelte @@ -3,11 +3,18 @@ import type { ActionData, PageData } from "./$types"; import BioEditor from "$components/editor/BioEditor.svelte"; import { t } from "$lib/i18n"; + import ephemeralState from "$lib/state.svelte"; type Props = { data: PageData; form: ActionData }; let { data, form }: Props = $props(); let bio = $state(data.user.bio || ""); + + ephemeralState( + "user-bio", + () => bio, + (data) => (bio = data), + );

{$t("edit-profile.bio-tab")}

diff --git a/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte index 44b0cf7..3f47f74 100644 --- a/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte @@ -6,6 +6,7 @@ import FieldsEditor from "$components/editor/FieldsEditor.svelte"; import NoscriptWarning from "$components/editor/NoscriptWarning.svelte"; import log from "$lib/log"; + import ephemeralState from "$lib/state.svelte"; import type { PageData } from "./$types"; type Props = { data: PageData }; @@ -29,6 +30,12 @@ if (e instanceof ApiError) ok.error = e.obj; } }; + + ephemeralState( + "user-fields", + () => fields, + (data) => (fields = data), + ); diff --git a/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.svelte index 4b2b165..b56d0c2 100644 --- a/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.svelte @@ -41,10 +41,16 @@ - + diff --git a/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte index b787096..2703748 100644 --- a/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte @@ -9,6 +9,7 @@ import PronounsEditor from "$components/editor/PronounsEditor.svelte"; import { t } from "$lib/i18n"; import log from "$lib/log"; + import ephemeralState from "$lib/state.svelte"; import type { PageData } from "./$types"; type Props = { data: PageData }; @@ -19,6 +20,15 @@ let ok: { ok: boolean; error: RawApiError | null } | null = $state(null); let allPreferences = $derived(mergePreferences(data.user.custom_preferences)); + ephemeralState( + "user-names-pronouns", + () => ({ names, pronouns }), + (data) => { + names = data.names; + pronouns = data.pronouns; + }, + ); + const update = async () => { try { const resp = await apiRequest("PATCH", "/users/@me", {