Skip to content

Messages: tags — compose, chips, filter, autocomplete, trending - #155

Merged
Adron merged 2 commits into
mainfrom
issue-32-tags
Sep 24, 2026
Merged

Adron merged 2 commits into
mainfrom
issue-32-tags

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

Stacked PR — base is issue-31-push-quote (#152), not main

Second of three stacked PRs (#31 → #32 → #33). All three touch
FeedView.xaml / FeedViewModel.cs / MessageItemViewModel.cs, so they are
sequential rather than parallel. This one builds on #152's
Models/NewMessage.cs (it adds Tags to that record) and on the same
issue-127-wire-types base underneath, which is where Message.Tags lives.

Merge order: #131 → #152 → this.

What this does

Message already carried Tags; nothing composed, rendered or filtered on
them.

Service — new Services/InterlinedApiClient.Tags.cs:

  • GetTrendingTagsAsync / AutocompleteTagsAsync. Both wrap rows under
    { "tags": [ … ] } and both honor ?limit=.
  • AutocompleteTagsAsync short-circuits a blank query. An empty q is a
    400 {"error":"Query parameter 'q' is required"}, so there is no point
    round-tripping into an error the caller would have to swallow.
  • Models/TrendingTag.cs. Worth knowing: the two endpoints' Count values
    are not the same number
    — trending counts uses within its trailing window,
    autocomplete counts all-time (live: thoughts was 1 from trending and 5
    from autocomplete). The doc comment says so, so nobody treats one as a cache
    of the other.

GetFeedPageAsync(limit, offset, tag) went into the Messages partial rather
than becoming an overload of GetMessagesAsync: that method is in
InterlinedApiClient.cs, which is off-limits (open PR #130), and its optional
parameters would have made an added tag overload ambiguous at the call site.

ViewModel — ComposeTags chips, ActiveTag + FilterByTag /
ClearTagFilter, TrendingTags, and a debounced (250 ms) prefix autocomplete.
The autocomplete is fire-and-forget by design — it must not block the
dispatcher, and a keystroke that supersedes an in-flight request cancels it
rather than awaiting it.

View — a tag chip style, the composer's chip list + input (Enter adds, commas
split) + suggestion chips, clickable chips on every feed card, and a trending
bar.

The trending panel is inside FeedView, not the shell's right rail, because
MainWindow.xaml is in open PR #149 and editing it would have guaranteed a
conflict. It reads fine as a horizontal chip bar between the composer and the
list, and the feed is the only place tags mean anything anyway.

Tags are not hashtags

Tag input splits on commas only, never whitespace. Live tags contain spaces
— orbit culture, light rail, life is short, o brave girl — so a
whitespace delimiter would shred them. Same reason the ?tag= filter
URL-encodes.

Live verification (read-only, 2026-09-16)

  • GET /api/tags/trending → 20 rows with {tag, count, lastUsedAt}; ?limit=3
    honored.
  • GET /api/tags/autocomplete?q= is a case-insensitive literal prefix match:
    q=le → Lego, learning; q=L → lists, llms, launch, Lego, …;
    q=ego → nothing, even though Lego contains it. Empty q → 400.
    ?limit=3 honored.
  • GET /api/messages?tag= is case-insensitive (tag=terror matched), pages with
    offset/limit exactly like the unfiltered feed, and handles a
    space-containing tag (tag=orbit%20culture → 2 rows) once encoded.
  • 57 of 80 sampled messages carry tags, up to 6 on a single message.

Left unexercised, on purpose

The compose write path — tags: string[] on POST /api/messages — is built and
not live-verified. Posting from this account publishes publicly and
syndicates to Mastodon/Bluesky. All the read paths above were exercised freely.

Also not built here: the AI Suggest tags action the issue mentions as a
pairing. UseTagSuggestionCommand is the seam it would call into — an accepted
suggestion is just another AddTagValue.

Notes for other open branches

MainWindow.xaml (PR #149) untouched, deliberately. The card-body TextBlock
that PR #141's MentionText will claim is unchanged by this PR (#152 is where
that reconcile lands).

Builds clean in both -c Debug and -c Release.

Closes #32

🤖 Generated with Claude Code

Tags were already on the Message wire type but nothing composed, rendered or
filtered on them. Adds the two tag endpoints, the compose input, clickable
chips, and a trending panel.

- Services/InterlinedApiClient.Tags.cs — GetTrendingTagsAsync and
  AutocompleteTagsAsync. Both wrap their rows under { "tags": [...] } and both
  honor ?limit= (live-verified). Autocomplete short-circuits a blank query: an
  empty q is a 400 ("Query parameter 'q' is required"), so there is no point
  round-tripping into an error the caller has to swallow.
- Models/TrendingTag.cs — Tag/Count/LastUsedAt. The two endpoints' Count values
  are NOT the same number (trending counts a trailing window, autocomplete
  counts all-time: "thoughts" was 1 vs 5 live), which the doc comment records so
  nobody treats one as a cache of the other.
- GetFeedPageAsync(limit, offset, tag) in the Messages partial — a distinct name
  rather than an overload, because GetMessagesAsync in InterlinedApiClient.cs
  can't be edited (open PR #130) and its optional parameters would have made an
  added tag overload ambiguous.
- NewMessage gains Tags, posted as tags: string[].
- FeedViewModel: ComposeTags chips, a debounced (250ms) prefix autocomplete
  whose superseded requests are cancelled rather than awaited, ActiveTag +
  FilterByTag/ClearTagFilter, and TrendingTags.
- FeedView.xaml: a tag chip style, the composer's chip list + input (Enter adds,
  commas split) + suggestion chips, clickable chips on every card, and a
  trending bar. The trending panel is inside FeedView, not the shell's right
  rail, because MainWindow.xaml is in open PR #149.

Tag input splits on commas only, never whitespace: live tags contain spaces
("orbit culture", "life is short, o brave girl"), so they are not
hashtag-shaped single words.

Live-verified read-only 2026-09-16: autocomplete is a case-insensitive literal
prefix match (q=le -> Lego, learning; q=ego -> nothing, though Lego contains
it); ?tag= is case-insensitive, pages with offset/limit, and handles
space-containing tags once URL-encoded; 57 of 80 sampled messages carry tags,
up to 6 each.

The compose write path (tags on POST /api/messages) is built and deliberately
unexercised — posting from this account publishes publicly and syndicates on.

Closes #32

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Adron
Adron merged commit 5a3affd into main Sep 24, 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.

Messages: tags — compose, display, filter, trending and autocomplete

1 participant