Skip to content

feat(dm): explain the mutual-follow rule when the recipient list is empty (#86) - #95

Merged
Adron merged 1 commit into
parity/queuefrom
issue/86-dm-recipient-rule
Sep 16, 2026
Merged

Adron merged 1 commit into
parity/queuefrom
issue/86-dm-recipient-rule

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

Closes #86. Part of epic #84.

What changed

The DM recipient picker collapsed loading, empty and error into one blank list, so a new account's
normal state read as a broken screen. It now distinguishes four surfaces via a derived
NewMessageContent selector (the old ad-hoc isEmpty flag is gone, so there is one source of
truth):

State Renders
Loading spinner
Error message + Retry
NoRecipients the mutual-follow rule + Find people to follow
NoMatches "No one matches that search."
Recipients the list

The search field only appears when there is something to search.

The rule is quoted from the help page, not invented

https://interlinedlist.com/help/direct-messages → Who you can message: "You can only message
people you follow each other: you follow them and they follow you back (both follow
requests approved). You also cannot message someone if either of you has blocked the other… Once
you both follow each other, they'll appear in your recipient list." It also says of the picker:
"If the list is empty, it means no one who follows you also follows you back yet."

The shipped copy states the mutual-follow rule and is phrased so it stays true whichever reason
produced the empty set. The blocking caveat is deliberately left out of the empty state to keep it
short — for a new account the cause is virtually always "no mutuals yet", and the sentence is true
either way. Copy lives in RecipientRuleCopy.kt with the help-page citation in its KDoc.

Route out of the dead end

directMessagesGraph gained an onFindPeople hook; the NavHost wires it to the existing
Routes.USER_SEARCH destination the Account hub already uses. No new destination, no new screen —
4 lines in InterlinedListNavHost.

Verification

./gradlew :app:assembleDebug testDebugUnitTest → BUILD SUCCESSFUL, 723 tests, 0 failures
(aggregated from the JUnit XML). New JVM tests include content is NoRecipients when the server returns an empty set, content is Error on failure, never the empty explanation, a query that matches no one is NoMatches, not the rule explanation, and retry reloads and clears the previous error. :feature:directmessages:compileDebugAndroidTestKotlin succeeds.

Reviewer notes

  1. NoMatches is a small addition beyond the literal issue text. It prevents a real bug: without
    it, typing a non-matching query against a non-empty recipient list would show the
    mutual-follow explanation, which would be false.
  2. The Compose test compiles but was not executed — no emulator available to the agent, and this
    repo has no Robolectric anywhere, so it follows the module's existing convention of Compose tests
    living in androidTest (InboxScreenTest, ThreadScreenTest). The state selection driving
    those renderings is fully covered by JVM tests that did run. Making the render assertions run in
    CI means adding Robolectric — a new convention for the repo, so not done unilaterally.
  3. Known rough edge: the recipient list is loaded once in init, so following someone and
    navigating straight back still shows the empty state until the screen is recreated. Left as-is
    because the mutual-follow rule means a newly-followed user would not appear until they follow
    back anyway; refresh-on-resume is a reasonable follow-up.

`GET /api/dm/recipients` returns only people the account may DM — mutual
follows, per /help/direct-messages → "Who you can message". For a new
account that set is empty and the picker rendered an empty list, which
read as a broken screen rather than a rule being enforced.

The picker now tells four surfaces apart via `NewMessageUiState.content`:
loading (progress), error (message + Retry), loaded-but-empty (the rule,
in the app's voice, plus a "Find people to follow" button), and a plain
"no matches" note when a search filters every recipient out — so the rule
copy is never shown for a query miss. The call to action routes to the
existing user search destination (`Routes.USER_SEARCH`) through a new
`onFindPeople` hook on `directMessagesGraph`; no new destination added.

Empty-state wording is pinned in `RecipientRuleCopy` and asserted by
tests so it cannot drift from the rule the server enforces.

Tests: NewMessageViewModelTest covers the loading / loaded-empty / error /
no-match / retry states; RecipientRuleCopyTest guards the copy;
NewMessageScreenTest (Compose) asserts the empty state renders the
explanation and the call to action, a non-empty list still renders rows,
and an error renders the retryable error rather than the empty-state copy.

Closes #86
@Adron
Adron merged commit 5064a27 into parity/queue Sep 16, 2026
1 check passed
@Adron Adron mentioned this pull request 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