Skip to content

Bring across five recent web changes: system banner, custom-field privacy, and three unavailable-option fixes - #96

Merged
SiteRelEnby merged 5 commits into
mainfrom
feat/system-banner-and-field-privacy
Sep 30, 2026
Merged

SiteRelEnby merged 5 commits into
mainfrom
feat/system-banner-and-field-privacy

Conversation

@SiteRelEnby

Copy link
Copy Markdown
Collaborator

Five UI changes the web client picked up recently, brought across.

A banner for the system

System.banner_url landed server-side as the exact twin of Member.banner_url, and Android already had member banners end to end. Settings > Profile gains the control above the avatar, in the same place the member editor puts it, using the same 3:1 cropper and the same upload endpoint and purpose tag.

The control moved into a shared BannerPicker rather than being written a second time. banner_url is registered as clearing in SystemUpdateJsonAdapter, which exists precisely because Moshi drops nulls and there would otherwise be no way to remove one.

Needs a server running 1.6.0 or later; older ones simply never send the field.

Custom fields say who can see them

The member editor listed custom fields as a label and an input, so "wait, is that one public?" meant leaving the member, opening Settings, finding the field and coming back - which is a poor thing to have to do before typing something sensitive into a box. The level lives on the definition and applies to every member, which is exactly why the editor could not answer it on its own.

The tag now appears under each input in the editor, beside each value on the member profile, and on the field list in Settings, from one component so the three cannot disagree. Display only: changing a level stays in Settings, where the raise runs the step-up and grace-period flow.

Where a raise is waiting out a grace period, all three surfaces say what it will become and when. That needed pending_privacy and privacy_activates_at adding to CustomFieldRead - the server sends both and the app was dropping them, so without it that half would have silently rendered nothing.

Rendered as a tinted pill rather than web's tinted word. Bare coloured text has to stay legible across seventeen palettes in light and dark, and the pill is what this app already uses for the pending-delete badge; Public borrows that badge's warning colours, the existing "visible, or about to be" tone. Same vocabulary, same order. The Settings list said "Friends" before and now says "Friends only" like everywhere else.

Public is no longer offered where the instance cannot publish

An instance with public profiles switched off refuses a raise with a 403. All five privacy selects went on offering it, so choosing Public was a guaranteed dead end surfacing as a bare permissions error. Members, groups, custom fields, the system profile and relationship edges now render it disabled with one line underneath, which opens with the same clause as the sharing screen's card rather than inventing a sixth description of one state.

Disabled rather than removed: dropping the option would make the feature look absent, which is a different untruth from the one being fixed.

Two exceptions, both load-bearing:

  • A record already stored as public keeps the option. That is how its own value displays and, above all, how somebody lowers it again. Nothing may stand between a user and reducing their own exposure.
  • Group relationship edges are not gated at all. The public projection never queries them, so the server stores public there as asked, and disabling it would invent a restriction that does not exist.

Both are pinned by tests, since they are the cases a mechanical sweep gets wrong.

A channel that stopped sending says so

A channel whose destination keeps failing is switched off by the server after a day. Nothing said so anywhere the owner looks: notifications simply stopped, which is indistinguishable from nothing having happened, and is a bad failure mode for the feature whose job is telling you things. Worse, the recipient-facing label read "Unsubscribed" - telling an owner someone opted out when in fact their endpoint died.

disabled_reason now reaches both sides: a banner naming the stopped channels and what to check, and a "Stopped: deliveries failing" row state instead of "Disabled", which reads as a choice somebody made. Only server-stopped channels warn; one the owner paused needs no warning.

Mobile push is no longer offered where it cannot work

The picker offered it on every instance. On a self-hosted one without credentials you filled in the whole form, pressed Create, and got back a message that read like a setting somebody forgot. It is not one: a push credential is paired to an app build rather than to a server, so no amount of configuring reaches the store builds.

The option is disabled where the instance cannot offer it, with the reason folded behind a question, and the form moves off it so you cannot submit into a refusal. The reason comes from GET /v1/notifications/server-config, which exists so every client explains this in the same words.

Notes

Both notification changes need server 1.6.0 and default to today's behaviour on anything older.

WarningCard moved to the components package now that a second screen needs it.

Two things for a reviewer rather than decisions taken here:

  • The push reason text ends by pointing at ntfy and Pushover, but this screen only offers Mobile push and Web push. The text is the server's canonical wording and is accurate about the API; that Android cannot create those types is a separate gap worth logging.
  • The system banner is not on the Settings header card. That card is account identity, not a profile display, and web only added the control plus public-profile rendering, so putting one there would be an Android-only design call.

Touches CustomFieldsScreen.kt, which #94 also changes; expect a small conflict there, resolved by keeping the tag in #94's supporting-content position.

Checked against web and found already done, so not included: group description editing, markdown formatting help (Android had both first), explicit null on custom-field clear, relationship types and per-relationship privacy, journal pinning, BerryTree import, the share-view public rule, staged member privacy raises, link preview modes and member created date.

Two additions the web client picked up this week.

The system profile gains a banner, the exact twin of a member's: same
3:1 shape, same cropper, same upload endpoint and purpose tag, sitting
above the avatar in the same place the member editor puts it. The
control itself moves into a shared BannerPicker rather than being
written twice, since two copies of a thing this fiddly drift.

Custom fields now say who can see them where their values are edited
and shown. The level lives on the definition and applies to every
member, which is exactly why the member editor could not answer "wait,
is that one public?" on its own: you had to leave the member, open
Settings, find the field and come back, which is a poor thing to have
to do before typing something sensitive into a box. The tag appears
under each input in the editor, beside each value on the member
profile, and on the field list in Settings, from one component so the
three cannot disagree.

Display only, as on web: changing a level stays in Settings, where the
raise runs the step-up and grace-period flow. Where a raise is staged,
all three surfaces now say what it will become and when, which needed
pending_privacy and privacy_activates_at adding to the field model.

Rendered as a tinted pill rather than web's tinted word: bare coloured
text has to stay legible across seventeen palettes in light and dark,
and the pill is what this app already uses for the pending-delete
badge. Public borrows that badge's warning colours, the app's existing
"visible, or about to be" tone.
An instance with public profiles switched off cannot publish anything,
and the API refuses a raise to public with a 403 saying so. All five
privacy selects went on offering it anyway, so choosing Public was a
guaranteed dead end that surfaced as a bare permissions error with no
hint as to why. Members, groups, custom fields, the system profile and
relationship edges now render it disabled with one line underneath.

Disabled rather than removed: dropping the option would make the
feature look absent, which is a different untruth from the one being
fixed.

Two cases keep it offered, and both matter more than the tidiness of
the rule. A record already stored as public keeps it, because that is
how its own value displays and, above all, how somebody lowers it again
- nothing may stand between a user and reducing their own exposure.
And relationship edges between groups are not gated at all, because the
public projection never queries them, so the server stores public there
as asked and disabling it would invent a restriction that does not
exist.

The predicate and the wording live next to the existing raise gate, so
the five surfaces cannot drift, and the note opens with the same clause
as the sharing screen's card rather than inventing a sixth description
of one state.
…nnot work

Two states the app had no way to show, both landing in server 1.6.0.

A channel whose destination kept failing is switched off by the server
after a day. Nothing said so: notifications simply stopped, which is
indistinguishable from nothing having happened, and is a bad failure
mode for the feature whose job is telling you things. Worse, the
recipient-facing label read "Unsubscribed", which tells an owner their
recipient opted out when in fact their endpoint died.

disabled_reason now reaches both sides. The owner's channel list carries
a banner naming the stopped channels and what to check, and the row
says "Stopped: deliveries failing" rather than "Disabled", which would
read as a choice somebody made. The Receiving tab gets the same
distinction. Only server-stopped channels warn: one the owner paused
needs no warning, they know, they did it.

The channel-type picker offered Mobile push on every instance. On a
self-hosted one without credentials you filled the form, pressed
Create, and got back a message that read like a setting someone forgot.
It is not one: a push credential is paired to an app build rather than
to a server, so no amount of configuring reaches the store builds. The
option is now disabled where the instance cannot offer it, with the
reason folded away behind a question, and the form moves off it so you
cannot submit into a refusal.

The reason is the server's own words from /v1/notifications/server-config,
which exists so every client explains this identically rather than each
inventing its own wording. Both features default to today's behaviour
on a server too old to be asked.

WarningCard moves to the components package, since a second screen now
needs it and the warning tone should mean one thing across the app.
…d-field-privacy

# Conflicts:
#	CHANGELOG.md
#	sheaf/app/src/main/java/systems/lupine/sheaf/ui/fields/CustomFieldsScreen.kt
@SiteRelEnby
SiteRelEnby merged commit 70de16d into main Sep 30, 2026
1 check passed
@SiteRelEnby
SiteRelEnby deleted the feat/system-banner-and-field-privacy branch September 30, 2026 08:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant