Skip to content

feat(messages): Push (repost) and Quote (#20) - #110

Merged
Adron merged 1 commit into
parity/queuefrom
issue/20-push-quote
Sep 16, 2026
Merged

Adron merged 1 commit into
parity/queuefrom
issue/20-push-quote

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

Closes #20. Part of epic #17.

What changed

  • Push posts immediately from the card's action bar — pushedMessageId with no content field
    at all
    . That is the docs' one case where content may be absent ("required unless pushing with no
    comment"); every other post still sends its content verbatim, so a media-only post is unaffected.
  • Quote opens the existing composer with the original attached, sending both content and
    pushedMessageId.
  • Feed rendering comes from the server's nested pushedMessage — no second fetch — modelled
    through DTO → Room entity → domain. A push draws a "<author> pushed" header with the original in
    place of a body; a quote draws the author's note with the original inset below. Tapping the inset
    opens the original, wired in both feed and detail.

Always-public is consumed, not restated

#18 landed MessageVisibility.PUSH_OR_QUOTE as the single home for this rule. It is applied in
DefaultMessagesRepository.createMessage whenever pushedMessageId is non-null, so it overrides
the caller's selection
— the invariant cannot be bypassed by a call site. The composer reads the
same rule to hide the Private chip, lock Public, and show "Pushes and quotes are always public."

Edge cases live in one documented predicate

Message.canBePushed = not mine (help centre: re-shares "someone else's" message) and
publicly visible (API: "repost this public message ID") and not itself a re-share — no page
defines a push-of-a-push, so nesting was not invented. The card withholds both actions in those
cases, showing only the count when there is one, and the ViewModel guards again. A quote is never
scheduled, since scheduledAt and pushedMessageId are mutually exclusive. Server rejections
(403/409/400) surface the server's own wording. Repeat taps cannot double-post the same repost (an
in-flight id set).

Verification

./gradlew :app:assembleDebug testDebugUnitTest → BUILD SUCCESSFUL; :feature:messages 178 tests,
0 failures
. New: MessagePushTest (10), MessageConvertersTest (3), plus additions to
MessageDtoMapperTest, DefaultMessagesRepositoryTest (wire-body assertions via MockWebServer),
MessagesFeedViewModelTest and MessagesFeedScreenTest.

Mutation-checked the two load-bearing repository rules: reverting "omit content on a push" and
"force public" makes exactly those two tests fail.

MessagesDatabase v3 → v4 with the existing destructive-migration fallback.

Reviewer notes

  • Instrumented render tests compile but were not executed (no emulator); the same behaviour is
    covered on the JVM via the domain predicates and the DTO→cache→domain path.
  • Pre-existing test-helper bug, not touched: setFeed in MessagesFeedScreenTest declares
    mutableStateOf without remember inside setContent, so state mutations there may not survive
    recomposition. The new composer tests seed state directly rather than depend on it.
  • pushCount is not optimistically bumped after a push — the server's count arrives on the next
    refresh, which keeps push and quote consistent with each other.

Adds the two ways to re-share a message that the web has and Android
lacked, both posting `pushedMessageId` on POST /api/messages:

- Push (repost) posts immediately with no content at all — the one case
  the API allows an absent `content` ("required unless pushing with no
  comment").
- Quote reuses the normal composer with the original attached and sends
  both the note and `pushedMessageId`.

The feed renders a push and a quote from the nested `pushedMessage` the
server already embeds, so the original needs no second fetch: it is
modelled on the DTO, the Room entity (JSON-converted, db v4) and the
domain type. A push shows a "<author> pushed" header with the original
in place of a body; a quote shows the author's note with the original
inset beneath it. Tapping the inset opens the original's own page.

The always-public rule is consumed from #18's
MessageVisibility.PUSH_OR_QUOTE rather than restated: the repository
posts it instead of the caller's selection whenever a `pushedMessageId`
is present, and the composer both hides the Private chip and shows the
"pushes and quotes are always public" banner before sending.

Push and Quote are withheld where the docs say a message cannot be
re-shared — your own message, a private one, or a re-share of a
re-share (a quote is also never scheduled, since the API rejects
`scheduledAt` with `pushedMessageId`) — and a server rejection is
surfaced with the server's own wording.

Tests: a push sends `pushedMessageId` with no content; a quote sends
both; a push is public even when private was asked for; the feed caches
and renders the embedded original for each; the visibility control
cannot select Private for a quote; a rejected push surfaces the server's
message.

Closes #20
@Adron
Adron merged commit 05a923e into parity/queue Sep 16, 2026
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