From 226936310d090daedf845c90b3b3e753c1110ca9 Mon Sep 17 00:00:00 2001 From: ayn2op Date: Thu, 30 Jul 2026 02:22:21 -0400 Subject: [PATCH] feat(chat): render message reactions (#819) --- internal/config/config.toml | 2 ++ internal/config/theme.go | 2 ++ internal/ui/chat/messages_list.go | 25 +++++++++++++++++++++++++ internal/ui/chat/model.go | 8 ++++++++ internal/ui/chat/state.go | 15 +++++++++++++++ 5 files changed, 52 insertions(+) diff --git a/internal/config/config.toml b/internal/config/config.toml index 0b837c6..7ea2110 100644 --- a/internal/config/config.toml +++ b/internal/config/config.toml @@ -239,6 +239,8 @@ mention_style = { foreground = "blue", attributes = "bold" } emoji_style = { foreground = "green" } url_style = { foreground = "blue" } attachment_style = { foreground = "yellow" } +reaction_style = { attributes = "dim" } +own_reaction_style = { attributes = "bold" } message_style = {} selected_message_style = { attributes = "reverse" } diff --git a/internal/config/theme.go b/internal/config/theme.go index b36966e..6017396 100644 --- a/internal/config/theme.go +++ b/internal/config/theme.go @@ -223,6 +223,8 @@ type ( EmojiStyle StyleWrapper `toml:"emoji_style"` URLStyle StyleWrapper `toml:"url_style"` AttachmentStyle StyleWrapper `toml:"attachment_style"` + ReactionStyle StyleWrapper `toml:"reaction_style"` + OwnReactionStyle StyleWrapper `toml:"own_reaction_style"` MessageStyle StyleWrapper `toml:"message_style"` SelectedMessageStyle StyleWrapper `toml:"selected_message_style"` diff --git a/internal/ui/chat/messages_list.go b/internal/ui/chat/messages_list.go index 8efcc38..8df438c 100644 --- a/internal/ui/chat/messages_list.go +++ b/internal/ui/chat/messages_list.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "slices" + "strconv" "strings" "time" "unicode/utf8" @@ -357,6 +358,30 @@ func (ml *messagesList) writeMessage(builder *tview.LineBuilder, message discord ml.drawTimestamps(builder, message.Timestamp, baseStyle) ml.drawAuthor(builder, message, baseStyle) } + ml.drawReactions(builder, message.Reactions, baseStyle) +} + +func (ml *messagesList) drawReactions(builder *tview.LineBuilder, reactions []discord.Reaction, baseStyle tcell.Style) { + if len(reactions) == 0 { + return + } + + builder.NewLine() + for i, reaction := range reactions { + if i > 0 { + builder.Write(" ", baseStyle) + } + + name := reaction.Emoji.Name + if reaction.Emoji.IsCustom() { + name = ":" + name + ":" + } + style := ml.cfg.Theme.MessagesList.ReactionStyle.Style + if reaction.Me { + style = ml.cfg.Theme.MessagesList.OwnReactionStyle.Style + } + builder.Write(name+" "+strconv.Itoa(reaction.Count), tview.MergeStyle(baseStyle, style)) + } } func (ml *messagesList) formatTimestamp(ts discord.Timestamp) string { diff --git a/internal/ui/chat/model.go b/internal/ui/chat/model.go index 1343ea0..8845eda 100644 --- a/internal/ui/chat/model.go +++ b/internal/ui/chat/model.go @@ -268,6 +268,14 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd { m.onPresenceUpdate(eventMsg) case *gateway.MessageDeleteEvent: m.onMessageDelete(eventMsg) + case *gateway.MessageReactionAddEvent: + m.onMessageReaction(eventMsg.ChannelID, eventMsg.MessageID) + case *gateway.MessageReactionRemoveEvent: + m.onMessageReaction(eventMsg.ChannelID, eventMsg.MessageID) + case *gateway.MessageReactionRemoveAllEvent: + m.onMessageReaction(eventMsg.ChannelID, eventMsg.MessageID) + case *gateway.MessageReactionRemoveEmojiEvent: + m.onMessageReaction(eventMsg.ChannelID, eventMsg.MessageID) case *gateway.GuildMembersChunkEvent: return tview.Batch(m.onGuildMembersChunk(eventMsg), m.listen()) diff --git a/internal/ui/chat/state.go b/internal/ui/chat/state.go index 264688a..b1c98ad 100644 --- a/internal/ui/chat/state.go +++ b/internal/ui/chat/state.go @@ -157,6 +157,21 @@ func (m *Model) onMessageDelete(message *gateway.MessageDeleteEvent) { } } +func (m *Model) onMessageReaction(channelID discord.ChannelID, messageID discord.MessageID) { + selectedChannel, ok := m.SelectedChannel() + if !ok || selectedChannel.ID != channelID { + return + } + + index := slices.IndexFunc(m.messagesList.messages, func(message discord.Message) bool { + return message.ID == messageID + }) + message, err := m.state.Cabinet.Message(channelID, messageID) + if index >= 0 && err == nil { + m.messagesList.setMessage(index, *message) + } +} + func cursorAfterDelete(prevCursor, deletedIndex, remaining int) int { switch { case prevCursor > deletedIndex: