diff --git a/README.md b/README.md index b1012cb..c856c5a 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,9 @@ GitHub Pages — with no server rewrite rules. plspace registers itself as an OAuth app on your server the first time you sign in there, then redirects you to that server's own consent screen. Your password -is never entered into plspace; it only ever receives an access token, which is -stored in `localStorage` and used directly from your browser. +is never entered into plspace; it only ever receives OAuth tokens, which are +stored in `localStorage` and used directly from your browser. Short-lived access +tokens are renewed with a server-issued refresh token when available. PKCE is used where the server supports it, with an automatic fallback for servers that don't. @@ -113,9 +114,14 @@ rewritten to in-app routes; every other link gets `target="_blank"` with ## Compatibility -Written against the standard `/api/v1` REST API and tested against live servers. +Written against the standard `/api/v1` REST API and the local Egregoros +implementation, as well as live servers. Anything implementations differ on degrades rather than fails: `/api/v2/instance` falls back to v1, `/api/v1/accounts/lookup` falls back to search, pagination falls back to the last item's id when a server drops the `Link` header, and -per-account privacy flags are read in both their spellings. Other software -speaking the same API therefore works, but Pleroma is what this targets. +per-account privacy flags are read in both their spellings. Egregoros' +hour-long access tokens and rotating refresh tokens are supported; notification +folders are filtered defensively because its current API ignores `types[]`. +Profile controls are capability-aware, so unsupported Egregoros fields are not +shown as though they could be saved. Other software speaking the same API +therefore works, but Pleroma is what this targets. diff --git a/src/components/profile/PublicProfileEditor.svelte b/src/components/profile/PublicProfileEditor.svelte index c3663e5..9b1f320 100644 --- a/src/components/profile/PublicProfileEditor.svelte +++ b/src/components/profile/PublicProfileEditor.svelte @@ -5,6 +5,7 @@ */ import { untrack } from 'svelte' import type { CredentialAccount } from '$lib/api/types' + import { isEgregoros, publicProfileCapabilities } from '$lib/api/capabilities' import { useAppServices } from '$lib/app-services' import { profileFieldLimits } from '$lib/stores/theme.svelte' import { toPlainText } from '$lib/util/html' @@ -65,16 +66,21 @@ let error = $state(null) const isPleroma = $derived(Boolean(session.instance?.pleroma)) + const isEgregorosServer = $derived(isEgregoros(session.instance)) + const capabilities = $derived(publicProfileCapabilities(session.instance)) const limits = $derived(profileFieldLimits(session.instance)) const reservedFields = $derived(internalFields(session.me).length) - const availablePublicFields = $derived(Math.max(0, limits.maxFields - reservedFields)) + const availablePublicFields = $derived( + capabilities.fields ? Math.max(0, limits.maxFields - reservedFields) : 0, + ) const canAddField = $derived(fields.length < availablePublicFields) const fieldsValid = $derived( - fields.filter((field) => field.name.trim()).length <= availablePublicFields && - fields.every( - (field) => - field.name.length <= limits.nameLength && field.value.length <= limits.valueLength, - ), + !capabilities.fields || + (fields.filter((field) => field.name.trim()).length <= availablePublicFields && + fields.every( + (field) => + field.name.length <= limits.nameLength && field.value.length <= limits.valueLength, + )), ) const canSave = $derived(Boolean(displayName.trim()) && fieldsValid && !busy) @@ -140,23 +146,25 @@ try { // Read these at save time so a CSS edit made while this form is open is // never overwritten by an older copy of the hidden storage fields. - const hidden = internalFields(session.me) - const visible = fields - .filter((field) => field.name.trim()) - .map(({ name, value }) => ({ name: name.trim(), value })) + const hidden = capabilities.fields ? internalFields(session.me) : [] + const visible = capabilities.fields + ? fields + .filter((field) => field.name.trim()) + .map(({ name, value }) => ({ name: name.trim(), value })) + : [] const updated = await endpoints.updatePublicProfile(session.api, { displayName: displayName.trim(), note, - fields: [...visible, ...hidden], - avatar: imageValue(avatarMode, avatarFile), - header: imageValue(headerMode, headerFile), + fields: capabilities.fields ? [...visible, ...hidden] : undefined, + avatar: capabilities.avatar ? imageValue(avatarMode, avatarFile) : undefined, + header: capabilities.header ? imageValue(headerMode, headerFile) : undefined, background: isPleroma ? imageValue(backgroundMode, backgroundFile) : undefined, avatarDescription: isPleroma ? avatarDescription : undefined, headerDescription: isPleroma ? headerDescription : undefined, - bot: isPleroma ? undefined : actorType === 'Service', - actorType: isPleroma ? actorType : undefined, - birthday: isPleroma ? birthday : undefined, - showBirthday: isPleroma ? showBirthday : undefined, + bot: capabilities.identity && !isPleroma ? actorType === 'Service' : undefined, + actorType: capabilities.identity && isPleroma ? actorType : undefined, + birthday: capabilities.identity && isPleroma ? birthday : undefined, + showBirthday: capabilities.identity && isPleroma ? showBirthday : undefined, }) session.me = updated resetFrom(updated) @@ -176,6 +184,12 @@

Everything in this form is public-facing and may be federated to other servers.

+ {#if isEgregorosServer} +

+ Egregoros currently exposes display name, bio, and profile photo through its Mastodon API. + plspace hides the other profile controls because that server would silently ignore them. +

+ {/if} {#if error}{/if} {#if saved}

Your public profile was updated.

{/if} @@ -210,41 +224,45 @@ aria-label="Choose a new profile photo" onchange={(event) => chooseImage('avatar', event)} /> - + {#if capabilities.removeAvatar} + + {/if} {#if avatarMode === 'replace' && avatarFile}{avatarFile.name}{/if} {#if avatarMode === 'remove'}Will be removed when saved.{/if} -
- Current banner -
- Profile banner - chooseImage('header', event)} - /> - - {#if headerMode === 'replace' && headerFile}{headerFile.name}{/if} - {#if headerMode === 'remove'}Will be removed when saved.{/if} + {#if capabilities.header} +
+ Current banner +
+ Profile banner + chooseImage('header', event)} + /> + + {#if headerMode === 'replace' && headerFile}{headerFile.name}{/if} + {#if headerMode === 'remove'}Will be removed when saved.{/if} +
-
+ {/if} {#if isPleroma}
@@ -287,78 +305,82 @@ {/if} -
- Profile details -

- These become the Interests and Details rows on your profile. Hidden - plspace: storage fields are preserved automatically. -

- {#each fields as field (field.id)} -
- - - -
- {/each} - - {fields.length} of {availablePublicFields} public fields used. -
+ {#if capabilities.fields} +
+ Profile details +

+ These become the Interests and Details rows on your profile. Hidden + plspace: storage fields are preserved automatically. +

+ {#each fields as field (field.id)} +
+ + + +
+ {/each} + + {fields.length} of {availablePublicFields} public fields used. +
+ {/if} -
- Public identity - {#if isPleroma} - -
-
- {:else} - - {/if} -
+ {/if} + + {/if}