@@ -381,69 +399,82 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
// ─── Save ─────────────────────────────────────────────────────────────────────
- const save = async () => {
+ const save = () => {
+ if (savingRef.current) return
+ savingRef.current = true
setSaving(true)
- setHasChanged(false)
- try {
- // Strip empty/incomplete rows, trim whitespace.
- const validTags = profileTags
- .filter((t) => {
- if (!Array.isArray(t) || !(t[0] ?? '').trim()) return false
- const name = (t[0] ?? '').trim()
- if (name === 'bot') return true
- return t.length >= 2 && (t[1] ?? '').trim()
- })
- .map((t) => {
- const name = (t[0] ?? '').trim()
- const v1 = (t[1] ?? '').trim()
- if (name === 'bot') {
- if (t.length === 1 || !v1) return ['bot']
- const low = v1.toLowerCase()
- if (low === 'false') return ['bot', 'false']
- if (low === 'true') return ['bot', 'true']
- return ['bot', v1]
- }
- return [name, v1, ...t.slice(2)]
- })
-
- // Sort alphabetically by tag name (stable: same-name tags keep their relative order).
- const sortedTags = [...validTags]
- .sort((a, b) => a[0].localeCompare(b[0]))
- // Enforce at-most-one uniqueness: keep only the first occurrence.
- .filter((() => {
- const seen = new Set()
- return (t: string[]) => {
- if (!AT_MOST_ONE_NAMES.includes(t[0])) return true
- if (seen.has(t[0])) return false
- seen.add(t[0])
- return true
+
+ const savePromise = (async () => {
+ try {
+ // Strip empty/incomplete rows, trim whitespace.
+ const validTags = profileTags
+ .filter((t) => {
+ if (!Array.isArray(t) || !(t[0] ?? '').trim()) return false
+ const name = (t[0] ?? '').trim()
+ if (name === 'bot') return true
+ return t.length >= 2 && (t[1] ?? '').trim()
+ })
+ .map((t) => {
+ const name = (t[0] ?? '').trim()
+ const v1 = (t[1] ?? '').trim()
+ if (name === 'bot') {
+ if (t.length === 1 || !v1) return ['bot']
+ const low = v1.toLowerCase()
+ if (low === 'false') return ['bot', 'false']
+ if (low === 'true') return ['bot', 'true']
+ return ['bot', v1]
+ }
+ return [name, v1, ...t.slice(2)]
+ })
+
+ // Sort alphabetically by tag name (stable: same-name tags keep their relative order).
+ const sortedTags = [...validTags]
+ .sort((a, b) => a[0].localeCompare(b[0]))
+ // Enforce at-most-one uniqueness: keep only the first occurrence.
+ .filter((() => {
+ const seen = new Set()
+ return (t: string[]) => {
+ if (!AT_MOST_ONE_NAMES.includes(t[0])) return true
+ if (seen.has(t[0])) return false
+ seen.add(t[0])
+ return true
+ }
+ })())
+
+ // Derive content JSON: first occurrence of each known field.
+ const content: Record = {}
+ const seenContent = new Set()
+ for (const tag of sortedTags) {
+ const name = tag[0]
+ if (name === 'bot') continue
+ if (DISPLAY_ORDER.includes(name) && !seenContent.has(name)) {
+ content[name] = tag[1]
+ seenContent.add(name)
}
- })())
-
- // Derive content JSON: first occurrence of each known field.
- const content: Record = {}
- const seenContent = new Set()
- for (const tag of sortedTags) {
- const name = tag[0]
- if (name === 'bot') continue
- if (DISPLAY_ORDER.includes(name) && !seenContent.has(name)) {
- content[name] = tag[1]
- seenContent.add(name)
}
+ // Keep displayName alias for backward compatibility.
+ if (content['display_name']) content['displayName'] = content['display_name']
+
+ const draft = createProfileDraftEvent(JSON.stringify(content), sortedTags)
+ const published = await publish(draft)
+ await updateProfileEvent(published)
+ if (!mountedRef.current) return
+ setHasChanged(false)
+ pop()
+ } catch {
+ if (mountedRef.current) setHasChanged(true)
+ throw new Error(t('Failed to publish profile'))
+ } finally {
+ savingRef.current = false
+ if (mountedRef.current) setSaving(false)
}
- // Keep displayName alias for backward compatibility.
- if (content['display_name']) content['displayName'] = content['display_name']
+ })()
- const draft = createProfileDraftEvent(JSON.stringify(content), sortedTags)
- const published = await publish(draft)
- await updateProfileEvent(published)
- pop()
- } catch {
- toast.error(t('Failed to publish profile'))
- setHasChanged(true)
- } finally {
- setSaving(false)
- }
+ toastPublishPromise(savePromise, {
+ loading: t('Saving…'),
+ success: t('Profile updated'),
+ error: () => t('Failed to publish profile')
+ })
}
const saveFullProfile = async () => {
@@ -477,38 +508,10 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
}
}
- // ─── Controls ─────────────────────────────────────────────────────────────────
-
- const controls = (
-
-
-
-
- )
-
// ─── Render ───────────────────────────────────────────────────────────────────
return (
-
+
{/* Banner & avatar uploaders */}
{/* Banner under avatar in stacking order; fetchPriority still loads the pic first. */}
@@ -577,7 +580,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
-
+
{/* ── Unified tag list ── */}
-
@@ -703,6 +706,16 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
+
+
+
+
{/* ── Full profile event JSON (collapsible) ── */}
{profileEvent && (
-