Skip to content

feat: display message reactions#70

Open
guitaripod wants to merge 1 commit intolinuxmobile:mainfrom
guitaripod:feat/reactions
Open

feat: display message reactions#70
guitaripod wants to merge 1 commit intolinuxmobile:mainfrom
guitaripod:feat/reactions

Conversation

@guitaripod
Copy link
Copy Markdown

@guitaripod guitaripod commented Apr 21, 2026

What

Closes #2 (reactions portion) / revives intent of #27.

Reactions were already parsed from the REST message payload (parse_reactions in client.rs) and the gateway codec already decoded MESSAGE_REACTION_ADD, MESSAGE_REACTION_REMOVE, and MESSAGE_REACTION_REMOVE_ALL. The app subscribed to the reaction intent but the three dispatch events only emitted debug! logs, and the message pane never drew the reactions that did arrive via REST. This PR closes that gap.

Changes

  • domain/entities/message.rsMessage::add_reaction / remove_reaction / clear_reactions encapsulate list mutation (increment/decrement with me tracking, drop entries at zero). ReactionEmoji::display() renders unicode emoji verbatim and custom guild emoji as :name:.
  • presentation/widgets/message_pane.rs — new format_reaction_lines builds indented chip rows ({emoji} {count}) that wrap on content_width, UiMessage caches them, layout_message and calculate_message_height fold them into the existing O(1) geometry, and render_ui_message paints them after embeds. MessagePaneStyle gains reaction_me_style (theme accent + bold) and reaction_other_style (dimmed). MessagePaneData::apply_reaction_{add,remove,remove_all} mutate the cached Arc<Message> via Arc::make_mut and invalidate layout for that row only.
  • presentation/ui/chat_screen.rs / app.rs — wires the three gateway dispatch events through to the pane, resolving is_me against current_user_id.

No new dependencies. No new allocations on the hot render path beyond the cached chip lines. The reaction intent was already enabled via GatewayIntents::with_reactions().

Visual proof

Screenshot_20260421_223213

Snapshot test that renders the reactions through a real ratatui::buffer::Buffer (reaction_lines_buffer_snapshot, run with -- --nocapture):

--- reactions (width 40) ---
      ❤ 3  👍  1  :wave: 2               
--- end ---
--- reactions (width 16, wrapped) ---
      ❤ 3  👍  1 
      :wave: 2  
--- end ---

The first snapshot shows unicode emoji (, 👍) and a custom guild emoji (:wave:) rendered inline with their counts at the CONTENT_INDENT column. The second shows the same reactions forced to wrap at width 16 — chips never split mid-chip, and each wrapped row re-applies the indent so the block stays visually aligned with the message body. In a real terminal session, ❤ 3 would be drawn in the theme accent + bold (it has me: true); 👍 1 and :wave: 2 in dim gray.

Tests

10 new unit tests, all green:

domain::entities::message::reaction_tests::add_reaction_inserts_new_entry ... ok
domain::entities::message::reaction_tests::add_reaction_increments_existing_and_sets_me ... ok
domain::entities::message::reaction_tests::remove_reaction_decrements_and_drops_at_zero ... ok
domain::entities::message::reaction_tests::clear_reactions_empties_list ... ok
domain::entities::message::reaction_tests::display_formats_custom_and_unicode ... ok
presentation::widgets::message_pane::tests::reaction_lines_empty_when_no_reactions ... ok
presentation::widgets::message_pane::tests::reaction_lines_wrap_when_exceeding_width ... ok
presentation::widgets::message_pane::tests::reaction_lines_apply_me_style_only_to_self_chip ... ok
presentation::widgets::message_pane::tests::reaction_lines_buffer_snapshot ... ok
infrastructure::discord::client::tests::test_message_response_parsing_with_reactions ... ok

Full suite: test result: ok. 275 passed; 0 failed; 1 ignored. cargo clippy --all-targets introduces no new warnings.

Not in scope

  • Adding/removing one's own reactions (write path, reaction picker UI, keybind). Happy to do this as a follow-up; this PR is strictly the read/display side so the feedback loop stays short.
  • Emoji image rendering for custom guild emoji. The terminal can't draw those images, so :name: is the canonical representation.

Reactions are parsed from REST history and gateway events are dispatched,
but nothing was rendered. Wire through display and mutation so the UI
reflects reaction state in real time.

Message domain grows add/remove/clear helpers, used both by the REST
seed path and by the three gateway events (MESSAGE_REACTION_ADD,
MESSAGE_REACTION_REMOVE, MESSAGE_REACTION_REMOVE_ALL). Chips render as
`{emoji} {count}` below content/attachments/embeds, wrapping across
lines when they exceed message width; the current user's own reactions
use the theme accent + bold, others are dimmed.
@guitaripod guitaripod marked this pull request as draft April 21, 2026 19:26
@guitaripod guitaripod marked this pull request as ready for review April 21, 2026 19:33
@linuxmobile
Copy link
Copy Markdown
Owner

I don't have enough time to review the PR right now, but I'll take a look as soon as I have a chance. I have a lot of work to do with the repository (issues and such).

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.

Feedback / Missing Features

2 participants