Skip to content

feat(sharing): manage public profiles and share links from the phone - #78

Merged
fullynocturnal merged 4 commits into
mainfrom
feat/sharing-owner-controls
Sep 19, 2026
Merged

fullynocturnal merged 4 commits into
mainfrom
feat/sharing-owner-controls

Conversation

@fullynocturnal

Copy link
Copy Markdown
Collaborator

Stacked on #77, which adds the profile_visibility category and the pending-exposure models this builds on. Merge that first; this PR's diff is against it.

The owner side of public profiles, ported from web. There is no native viewer: a shared link already renders in any mobile browser, so the app covers what the browser page cannot, which is managing what you share.

The Sharing screen

Settings > Safety > Sharing (the follow-up PR moves it into System). It lists:

  • Share views, each with whether it is live, how many members it holds, and whether a change is staged.
  • Who can see what, from /sharing/audit: one row per live grant with its view, link or public profile, expiry and note, with Rotate link and Revoke behind a menu. The curated member count and the served one are kept apart ("3 of 5 members shown"), and a roster that is off reads as off rather than as zero.
  • Not currently serving: grants the audit does not describe, pending or expired, so revoke stays reachable for them too.

When nothing is served at all, profile_suppressed says why (system not public, publishing blocked, account state).

The entry is shown whatever the instance switch says. An owner must always be able to reach revoke, including after an operator turns publishing off; the screen explains that nothing new can be published instead of hiding the list.

Creating a link shows its token once, since nothing can read it back. Expiry is chosen at creation, from presets or a date picker, and a date means the end of that day on the user's own calendar. The API cannot move an expiry later, and the dialog says so. A date on or before the day a staged grant goes live is refused: that link would expire without ever having been readable.

A single view

  • The six exposure flags show their staged twins ("staged to turn on") while the live value stays the truth. Member permalinks sit outside the gate in both directions, since they expose nobody the roster does not already show.
  • Members show the server's own served / not_served_reason (never shareable, archived, queued for deletion, private) instead of a client-side guess.
  • Adding a group reports who it left out and why, and detaching one asks whether its members go too.
  • Preview as visitor renders /share-views/{id}/preview, the same projection the public router uses, so it cannot show a page that would not really be served. A section that is not published reads as not published, which is different from served and empty.

Step-up and staging

The client mirrors visibility_step_up_required so the credential sheet can open before the round trip, and a server bounce still wins and reopens it, since a mirror can drift. The error detail separates the cases that need different answers, because re-auth only clears one of them:

Response Meaning What the app does
400 Password required / TOTP code required step-up needed opens the sheet
403 wrong credentials rejected keeps the sheet open with the reason
403 publishing off on the instance operator switch plain error, no sheet
403 18+ not declared first publish asks for the declaration, then carries on
409 account pending deletion plain error

Ceilings, where the data is edited

The same gate now runs in the member editor (privacy, never shareable, keep fronting private), system privacy, per-edge relationship visibility (new PATCH endpoints; group edges are never gated, matching the server), and custom field and group privacy. A raise the grace window staged is shown as staged rather than as done.

Worth knowing

  • The member PATCH body serializes nulls, and the server rejects an explicit null on never_shareable and fronting_private, so both are always sent with their real value.
  • Error bodies are parsed with Moshi, not org.json. org.json is a stub off-device, which would have left the error classifier untested.
  • No "change expiry": moving an expiry would need a new endpoint. Rotating or revoking are the ways to change your mind today.

Checked

  • Unit tests: the error classifier across every row of the table above, share model parsing (staged twins null vs false, curated vs served counts, a preview's unpublished vs empty sections, the one-time token), and expiry (end of day in the local zone, the grace-window guard at its boundary, dormant-grant wording).
  • Full unit suite and assembleOpenDebug on this branch alone.

The owner side of public profiles, ported from web. There is no native
viewer: a shared link already renders in any mobile browser, so the app
covers what the browser page cannot, which is managing what you share.

**Sharing screen** (Settings > Safety > Sharing). Share views, "who can see
what" from /sharing/audit, and a list of grants that are not serving right
now (pending, expired). The entry is shown whatever the instance switch says,
because an owner must always be able to reach revoke, including after an
operator turns publishing off. The audit keeps the curated member count and
the served one apart, and says when nothing is served at all and why.

Creating a link shows its token once, since nothing can read it back. Expiry
is picked at creation: presets or a date, meaning the end of that day on the
user's own calendar. The API has no way to move it later, and the dialog says
so. A date on or before the day a staged grant goes live is refused, because
that link would expire without ever having been readable.

**A view.** The six exposure flags show their staged twins, and member
permalinks sit outside the gate in both directions since they expose nobody
new. Members carry the server's own served/not-served reason rather than a
client guess. Fields and groups can be added and removed, and a group reports
who it left out and asks whether detaching it takes its members too. Preview
renders /share-views/{id}/preview, the same projection the public router
uses, so it cannot show a page that would not really be served.

**Step-up.** The client mirrors visibility_step_up_required to open the
credential sheet before the round trip, and a server bounce still wins and
reopens it. The error detail separates the cases that need different
answers: a missing credential (400), a wrong one, publishing switched off on
the instance, the missing 18+ declaration (all 403), and a pending account
deletion (409). Re-auth only clears one of them. The 18+ declaration is asked
for the first time someone publishes and then resumes what they were doing.

**Ceilings where the data is edited.** The same gate now runs in the member
editor (privacy, never shareable, keep fronting private), system privacy, the
per-edge relationship visibility (new PATCH endpoints; group edges are never
gated, matching the server), and custom field and group privacy. A raise the
grace window has staged is shown as staged instead of as done.

The member PATCH body serializes nulls and the server rejects an explicit
null on never_shareable and fronting_private, so both are always sent with
their real value. Error bodies are parsed with Moshi rather than org.json,
which is only a stub off-device and would leave the classifier untested.
Safety was a top-level category holding three rows: System Safety, Sharing
and Revision retention. All three describe the system rather than the app or
the account, so they now sit in System after tags, custom fields and archived
members, and the separate category is gone.

The System row's subtitle names safety and sharing so they can still be found
from the top level. What the top level loses is the auth tier summary the
Safety row used to show; it is still on the System Safety row inside.

Nothing else routed to the Safety category. The Home banners for pending
deletions and pending exposures link straight to System Safety and are
unaffected.
@fullynocturnal
fullynocturnal merged commit d64891e into main Sep 19, 2026
1 check passed
@fullynocturnal
fullynocturnal deleted the feat/sharing-owner-controls branch September 19, 2026 04:55
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