Skip to content

feat(messages): tag input with prefix autocomplete, and tags on the card (#28) - #116

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

Adron merged 1 commit into
parity/queuefrom
issue/28-composer-tags

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

Closes #28. Part of epic #27.

What changed

CreateMessageRequest gained tags: List<String>? (omitted entirely when empty), plumbed through
the repository, and the composer sends committed chips plus anything still uncommitted in the field.

Tags are free-form labels, not hashtags. Only surrounding whitespace is trimmed — nothing is
split on spaces or rewritten. That is not a hypothetical: real tags on the live account include
"life is short, o brave girl" and "light rail". Committing is deliberate (Add button, keyboard
Done, or tapping a suggestion).

Autocomplete

Wired with q, not prefix — sending prefix returns
400 {"error":"Query parameter 'q' is required"}. Suggestions are shown in the server's order and
never re-filtered client-side: the server's case-insensitive literal prefix is the only matcher,
so the UI cannot show a suggestion the server would not have returned.

One tagSuggestionJob is held; each keystroke cancels it, covering both the pending 300 ms debounce
and a request already in flight. After a call returns, its result is applied only while
tagQuery still matches the query it answered — so an overtaken response cannot overwrite newer
suggestions.

Verification

./gradlew :app:assembleDebug testDebugUnitTest → BUILD SUCCESSFUL, 1156 tests repo-wide, 0
failures
(:feature:messages alone: 204).

Mutation-checked rather than trusting green tests: deleting the stale-query guard fails a late response for an older prefix never overwrites a newer one; deleting tagSuggestionJob?.cancel()
fails three tests (single-request debounce, per-word requests, in-flight cancellation). Both
reverted.

The stale-response test is genuinely adversarial: the fake's gate awaits under NonCancellable, so
the overtaken response really does arrive after its coroutine was cancelled (asserted via
completedAutocompleteQueries), and the test proves the guard discards it with expectNoEvents().

Messages DB 4 → 5 for the cached tags column, destructive as the module already does.

Reviewer notes

  • Tag rendering is intentionally inert. Tags: tag-filtered feed (GET /api/messages?tag=) #29 (tag-filtered feed) can hang a click handler on
    MessageCardTags.tagTag(tag) without touching the layout. Tags: trending tags surface (GET /api/tags/trending) #30 (trending) is likewise untouched.
  • Composer-interaction and card-render tests live in androidTest per module convention (no
    Robolectric here). They compile but were not executed — no emulator.
  • Replies still send no tags. The help centre documents tags on create generally, but the reply
    path has no tag UI and adding one was out of scope.

Adds a tag input to the composer, sends `tags[]` on `POST /api/messages`, and
renders a message's tags on its card from the `tags[]` the feed already returns
(no extra fetch).

Tags are free-form labels, not hashtag tokens: the live API returns tags
containing spaces and punctuation (e.g. "life is short, o brave girl"), so the
input never splits what is typed and the card never prettifies it. A tag is
committed deliberately — Add, the keyboard's Done action, or tapping a
suggestion.

Suggestions come from `GET /api/tags/autocomplete`, whose query parameter is
`q` (`prefix` is a 400). Matching is the server's case-insensitive literal
prefix and the results are shown in its order, unfiltered: re-matching client
side would misrepresent the endpoint's semantics.

The lookup is debounced by 300 ms and every keystroke supersedes the last,
cancelling both the pending delay and any request already in flight, so a fast
typist issues one request rather than a pile of concurrent ones. A response is
applied only while it still answers the current text, so an overtaken reply for
an older prefix can never overwrite newer suggestions.

Cached messages gain a tags column (messages db 4 -> 5, destructive as before).

Tests: the create request carries `tags` (and omits the field when empty); a tag
with spaces and a comma round-trips out and back unchanged; autocomplete asks
with `q` and keeps the server's order; debounce, per-keystroke cancellation, and
a late response for an older prefix being discarded; the card renders tags.

Closes #28
@Adron
Adron merged commit a7ab691 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