|
@@ -3,6 +3,7 @@ package chat
|
|
|
import (
|
|
import (
|
|
|
"context"
|
|
"context"
|
|
|
"errors"
|
|
"errors"
|
|
|
|
|
+ "fmt"
|
|
|
"log/slog"
|
|
"log/slog"
|
|
|
"slices"
|
|
"slices"
|
|
|
"strings"
|
|
"strings"
|
|
@@ -184,10 +185,26 @@ func (ml *messagesList) buildItem(index int, cursor int) list.Item {
|
|
|
SetWordWrap(true).
|
|
SetWordWrap(true).
|
|
|
SetLines(ml.renderMessage(message, ml.cfg.Theme.MessagesList.MessageStyle.Style))
|
|
SetLines(ml.renderMessage(message, ml.cfg.Theme.MessagesList.MessageStyle.Style))
|
|
|
ml.itemByID[message.ID] = item
|
|
ml.itemByID[message.ID] = item
|
|
|
|
|
+ // Evict stale cache entries when the map grows too large.
|
|
|
|
|
+ if len(ml.itemByID) > 500 {
|
|
|
|
|
+ ml.evictStaleCache()
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
return item
|
|
return item
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+func (ml *messagesList) evictStaleCache() {
|
|
|
|
|
+ active := make(map[discord.MessageID]struct{}, len(ml.messages))
|
|
|
|
|
+ for _, msg := range ml.messages {
|
|
|
|
|
+ active[msg.ID] = struct{}{}
|
|
|
|
|
+ }
|
|
|
|
|
+ for id := range ml.itemByID {
|
|
|
|
|
+ if _, ok := active[id]; !ok {
|
|
|
|
|
+ delete(ml.itemByID, id)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
func (ml *messagesList) renderMessage(message discord.Message, baseStyle tcell.Style) []tview.Line {
|
|
func (ml *messagesList) renderMessage(message discord.Message, baseStyle tcell.Style) []tview.Line {
|
|
|
builder := tview.NewLineBuilder()
|
|
builder := tview.NewLineBuilder()
|
|
|
ml.writeMessage(builder, message, baseStyle)
|
|
ml.writeMessage(builder, message, baseStyle)
|
|
@@ -492,6 +509,43 @@ func (ml *messagesList) drawDefaultMessage(builder *tview.LineBuilder, message d
|
|
|
builder.Write(a.Filename, attachmentStyle)
|
|
builder.Write(a.Filename, attachmentStyle)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // Thread indicator
|
|
|
|
|
+ if message.Thread != nil {
|
|
|
|
|
+ thread := *message.Thread
|
|
|
|
|
+ builder.NewLine()
|
|
|
|
|
+ dimStyle := baseStyle.Dim(true)
|
|
|
|
|
+ threadLabel := "Thread: " + thread.Name
|
|
|
|
|
+ builder.Write(threadLabel, dimStyle)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ml.drawReactions(builder, message, baseStyle)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (ml *messagesList) drawReactions(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style) {
|
|
|
|
|
+ if len(message.Reactions) == 0 {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ builder.NewLine()
|
|
|
|
|
+ dimStyle := baseStyle.Dim(true)
|
|
|
|
|
+ boldStyle := baseStyle.Bold(true)
|
|
|
|
|
+ for i, r := range message.Reactions {
|
|
|
|
|
+ if i > 0 {
|
|
|
|
|
+ builder.Write(" ", dimStyle)
|
|
|
|
|
+ }
|
|
|
|
|
+ name := r.Emoji.Name
|
|
|
|
|
+ if !r.Emoji.ID.IsValid() {
|
|
|
|
|
+ // Unicode emoji — use name directly
|
|
|
|
|
+ } else if name != "" {
|
|
|
|
|
+ name = ":" + name + ":"
|
|
|
|
|
+ }
|
|
|
|
|
+ style := dimStyle
|
|
|
|
|
+ if r.Me {
|
|
|
|
|
+ style = boldStyle
|
|
|
|
|
+ }
|
|
|
|
|
+ builder.Write(fmt.Sprintf("%s %d", name, r.Count), style)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (ml *messagesList) drawEmbeds(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style) {
|
|
func (ml *messagesList) drawEmbeds(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style) {
|
|
@@ -647,6 +701,15 @@ func (ml *messagesList) HandleEvent(event tview.Event) tview.Command {
|
|
|
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.DeleteConfirm.Keybind):
|
|
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.DeleteConfirm.Keybind):
|
|
|
ml.confirmDelete()
|
|
ml.confirmDelete()
|
|
|
return nil
|
|
return nil
|
|
|
|
|
+ case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.OpenThread.Keybind):
|
|
|
|
|
+ ml.openThread()
|
|
|
|
|
+ return nil
|
|
|
|
|
+ case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Search.Keybind):
|
|
|
|
|
+ ml.chatView.openSearch()
|
|
|
|
|
+ return nil
|
|
|
|
|
+ case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.UserInfo.Keybind):
|
|
|
|
|
+ ml.showUserInfo()
|
|
|
|
|
+ return nil
|
|
|
}
|
|
}
|
|
|
return ml.Model.HandleEvent(event)
|
|
return ml.Model.HandleEvent(event)
|
|
|
|
|
|
|
@@ -843,6 +906,28 @@ func (ml *messagesList) open() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+func (ml *messagesList) openThread() {
|
|
|
|
|
+ msg, err := ml.selectedMessage()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ slog.Error("failed to get selected message", "err", err)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if msg.Thread == nil {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ thread := *msg.Thread
|
|
|
|
|
+ node := ml.chatView.guildsTree.findNodeByChannelID(thread.ID)
|
|
|
|
|
+ if node == nil {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ml.chatView.guildsTree.expandPathToNode(node)
|
|
|
|
|
+ ml.chatView.guildsTree.SetCurrentNode(node)
|
|
|
|
|
+ ml.chatView.guildsTree.onSelected(node)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
func (ml *messagesList) reply(mention bool) {
|
|
func (ml *messagesList) reply(mention bool) {
|
|
|
message, err := ml.selectedMessage()
|
|
message, err := ml.selectedMessage()
|
|
|
if err != nil {
|
|
if err != nil {
|
|
@@ -1012,6 +1097,7 @@ func (ml *messagesList) ShortHelp() []keybind.Keybind {
|
|
|
cfg.SelectUp.Keybind,
|
|
cfg.SelectUp.Keybind,
|
|
|
cfg.SelectDown.Keybind,
|
|
cfg.SelectDown.Keybind,
|
|
|
cfg.Cancel.Keybind,
|
|
cfg.Cancel.Keybind,
|
|
|
|
|
+ cfg.Search.Keybind,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if msg, err := ml.selectedMessage(); err == nil {
|
|
if msg, err := ml.selectedMessage(); err == nil {
|
|
@@ -1023,6 +1109,9 @@ func (ml *messagesList) ShortHelp() []keybind.Keybind {
|
|
|
if len(messageURLs(*msg)) != 0 || len(msg.Attachments) != 0 {
|
|
if len(messageURLs(*msg)) != 0 || len(msg.Attachments) != 0 {
|
|
|
help = append(help, cfg.Open.Keybind)
|
|
help = append(help, cfg.Open.Keybind)
|
|
|
}
|
|
}
|
|
|
|
|
+ if msg.Thread != nil {
|
|
|
|
|
+ help = append(help, cfg.OpenThread.Keybind)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return help
|
|
return help
|
|
@@ -1036,9 +1125,11 @@ func (ml *messagesList) FullHelp() [][]keybind.Keybind {
|
|
|
canEdit := false
|
|
canEdit := false
|
|
|
canDelete := false
|
|
canDelete := false
|
|
|
canOpen := false
|
|
canOpen := false
|
|
|
|
|
+ canOpenThread := false
|
|
|
if msg, err := ml.selectedMessage(); err == nil {
|
|
if msg, err := ml.selectedMessage(); err == nil {
|
|
|
canSelectReply = msg.ReferencedMessage != nil
|
|
canSelectReply = msg.ReferencedMessage != nil
|
|
|
canOpen = len(messageURLs(*msg)) != 0 || len(msg.Attachments) != 0
|
|
canOpen = len(messageURLs(*msg)) != 0 || len(msg.Attachments) != 0
|
|
|
|
|
+ canOpenThread = msg.Thread != nil
|
|
|
|
|
|
|
|
if me, err := ml.chatView.state.Cabinet.Me(); err == nil {
|
|
if me, err := ml.chatView.state.Cabinet.Me(); err == nil {
|
|
|
canReply = msg.Author.ID != me.ID
|
|
canReply = msg.Author.ID != me.ID
|
|
@@ -1070,12 +1161,16 @@ func (ml *messagesList) FullHelp() [][]keybind.Keybind {
|
|
|
if canOpen {
|
|
if canOpen {
|
|
|
manage = append(manage, cfg.Open.Keybind, cfg.SaveImage.Keybind)
|
|
manage = append(manage, cfg.Open.Keybind, cfg.SaveImage.Keybind)
|
|
|
}
|
|
}
|
|
|
|
|
+ if canOpenThread {
|
|
|
|
|
+ manage = append(manage, cfg.OpenThread.Keybind)
|
|
|
|
|
+ }
|
|
|
|
|
+ manage = append(manage, cfg.UserInfo.Keybind)
|
|
|
|
|
|
|
|
return [][]keybind.Keybind{
|
|
return [][]keybind.Keybind{
|
|
|
{cfg.SelectUp.Keybind, cfg.SelectDown.Keybind, cfg.SelectTop.Keybind, cfg.SelectBottom.Keybind},
|
|
{cfg.SelectUp.Keybind, cfg.SelectDown.Keybind, cfg.SelectTop.Keybind, cfg.SelectBottom.Keybind},
|
|
|
{cfg.ScrollUp.Keybind, cfg.ScrollDown.Keybind, cfg.ScrollTop.Keybind, cfg.ScrollBottom.Keybind},
|
|
{cfg.ScrollUp.Keybind, cfg.ScrollDown.Keybind, cfg.ScrollTop.Keybind, cfg.ScrollBottom.Keybind},
|
|
|
actions,
|
|
actions,
|
|
|
manage,
|
|
manage,
|
|
|
- {cfg.YankContent.Keybind, cfg.YankURL.Keybind, cfg.YankID.Keybind},
|
|
|
|
|
|
|
+ {cfg.YankContent.Keybind, cfg.YankURL.Keybind, cfg.YankID.Keybind, cfg.Search.Keybind},
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|