Skip to content

feat(messages): public/private visibility toggle in the composer (#18) - #91

Merged
Adron merged 1 commit into
parity/queuefrom
issue/18-composer-visibility
Sep 16, 2026
Merged

Adron merged 1 commit into
parity/queuefrom
issue/18-composer-visibility

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

Closes #18. Part of epic #17.

What changed

The composer now owns the public/private decision instead of letting the server default silently
decide. publiclyVisible is sent explicitly on every create, seeded from the account's
defaultPubliclyVisible preference, overridable per message, and the resulting visibility is
rendered on the author's own cards.

  • MessageVisibility(PUBLIC/PRIVATE) is the domain vocabulary for the wire boolean, so
    ViewModel/UI call sites read unambiguously.
  • The push/quote-is-always-public invariant lives in one place as
    MessageVisibility.PUSH_OR_QUOTE = PUBLIC. No push/quote UI or request plumbing was built —
    that is Messages: Push (repost) and Quote (pushedMessageId) #20; this just gives that work a single rule to consume.
  • The preference is read from the already-modelled current user (InterlinedListApi.getCurrentUser()),
    adding defaultPubliclyVisible to the existing UserDto/User rather than creating a new fetch
    path. Reading it is best-effort: a failure leaves the composer on public and does not raise a
    feed-level error.
  • Default vs. override is visibilityOverride ?: defaultVisibility, so a late-arriving preference
    still applies and an explicit user choice always wins. The override is dropped on dismiss and
    after a successful post.
  • Private marker predicate is mine && !publiclyVisible — public messages are never badged, and
    another user's message is never labelled.

Verification

./gradlew :app:assembleDebug testDebugUnitTest → BUILD SUCCESSFUL, 705 unit tests repo-wide, 0
failures
(113 in :feature:messages). Tests were written first and confirmed red before
implementing. :feature:messages:compileDebugAndroidTestKotlin compiles clean.

New/extended tests: MessageVisibilityTest (wire mapping both ways, the push/quote invariant,
showsPrivateBadge for own-private / own-public / another user's message); repository tests
asserting the body carries "publiclyVisible":true and false, that getDefaultVisibility reads
the preference and falls back to public when absent, and that visibility round-trips through the
Room cache; ViewModel tests for default-from-preference, error-free fallback, override wins, and
override cleared on dismiss/post; DTO mapper tests. Compose UI tests for the badge and composer
chips were added but not run (no emulator available to the agent).

Reviewer notes

  • The preference is fetched once per MessagesFeedViewModel creation, mirroring the existing
    loadLinkedNetworks() pattern. It is not persisted, so an offline compose opens on public.
    Persisting it belongs with the Settings epic (Settings & preferences parity — most of PATCH /api/user/update is unreachable from Android #31), which owns defaultPubliclyVisible writes.
  • postReply deliberately still omits publiclyVisible, preserving existing behaviour — the field
    is nullable on CreateMessageRequest for exactly that reason.
  • MessagesDatabase v2 → v3 with the existing destructive-migration fallback, so the local message
    cache is dropped once on upgrade.

Android never sent `publiclyVisible` on `POST /api/messages`, so the server
default silently decided whether a post was public or private and the user had
no control and no indication of what they had posted.

- Add `MessageVisibility` (PUBLIC/PRIVATE) as the domain vocabulary for the
  wire boolean, including the documented `PUSH_OR_QUOTE` invariant: a push or
  quote amplifies someone else's message and is therefore always public. The
  push/quote UI itself is out of scope (#20) — this is the single place that
  rule lives, for that work to consume.
- Send `publiclyVisible` explicitly on every create, plumbed through
  `CreateMessageRequest` -> `MessagesRepository.createMessage` -> the composer.
- Seed the composer from the account preference `defaultPubliclyVisible` on
  `GET /api/user`, read via the existing shared current-user endpoint; the
  field is added to the shared `UserDto`/`User` rather than a new fetch path.
  Reading it is best-effort: a failure leaves the composer on public.
- Let the user override the default per message (Public / Private chips plus a
  hint spelling out what private means). The override is dropped when the sheet
  is dismissed and after a successful post.
- Model `publiclyVisible` on the message DTO, entity (Room v2 -> v3, destructive
  fallback already configured) and domain model, and render an unmistakable
  lock + "Private" badge on the author's own private cards. Public messages are
  never badged, and another user's message is never labelled.

Tests: the create request carries the flag both ways; the default comes from
the account preference; a per-message override wins and is reset on
dismiss/post; the DTO/entity round-trips the flag; `showsPrivateBadge` is true
only for the author's own private message. Compose assertions for the card
badge and the composer chips are added to the (emulator-only) androidTest suite.

Verified with `./gradlew :app:assembleDebug testDebugUnitTest`: BUILD
SUCCESSFUL, 705 unit tests, 0 failures.

Closes #18
@Adron
Adron merged commit f382542 into parity/queue Sep 16, 2026
1 check passed
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