diff --git a/cmd/messages_text.go b/cmd/messages_text.go index 5a67837..7835d0a 100644 --- a/cmd/messages_text.go +++ b/cmd/messages_text.go @@ -1,12 +1,14 @@ package cmd import ( + "context" "errors" "fmt" "io" "log/slog" "slices" "strconv" + "sync" "time" "github.com/atotto/clipboard" @@ -14,6 +16,7 @@ import ( "github.com/ayn2op/discordo/internal/markdown" "github.com/ayn2op/discordo/internal/ui" "github.com/diamondburned/arikawa/v3/discord" + "github.com/diamondburned/arikawa/v3/gateway" "github.com/diamondburned/ningen/v3/discordmd" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" @@ -29,6 +32,11 @@ type MessagesText struct { cfg *config.Config app *tview.Application selectedMessageID discord.MessageID + fetchingMembers struct { + value bool + done chan struct{} + mu sync.Mutex + } } func newMessagesText(app *tview.Application, cfg *config.Config) *MessagesText { @@ -57,6 +65,7 @@ func newMessagesText(app *tview.Application, cfg *config.Config) *MessagesText { markdown.DefaultRenderer.AddOptions( renderer.WithOption("emojiColor", t.MessagesText.EmojiColor), renderer.WithOption("linkColor", t.MessagesText.LinkColor), + renderer.WithOption("showNicknames", t.MessagesText.ShowNicknames), ) return mt @@ -69,6 +78,12 @@ func (mt *MessagesText) drawMsgs(cID discord.ChannelID) { return } + if app.cfg.Theme.MessagesText.ShowNicknames || app.cfg.Theme.MessagesText.ShowUsernameColors { + if ch, _ := discordState.Cabinet.Channel(cID); ch.GuildID.IsValid() { + mt.fetchMembers(ch.GuildID, ms) + } + } + for _, m := range slices.Backward(ms) { app.messagesText.createMessage(m) } @@ -111,23 +126,23 @@ func (mt *MessagesText) createMessage(m discord.Message) { fmt.Fprint(mt, "["+mt.cfg.Theme.MessagesText.ContentColor+"]"+m.Author.Username+" pinned a message"+"[-:-:-]") case discord.DefaultMessage, discord.InlinedReplyMessage: if m.ReferencedMessage != nil { - mt.createHeader(mt, *m.ReferencedMessage, true) + mt.createHeader(mt, *m.ReferencedMessage, m.GuildID, true) mt.createBody(mt, *m.ReferencedMessage, true) fmt.Fprint(mt, "[::-]\n") } - mt.createHeader(mt, m, false) + mt.createHeader(mt, m, m.GuildID, false) mt.createBody(mt, m, false) mt.createFooter(mt, m) default: - mt.createHeader(mt, m, false) + mt.createHeader(mt, m, m.GuildID, false) } fmt.Fprintln(mt) } -func (mt *MessagesText) createHeader(w io.Writer, m discord.Message, isReply bool) { +func (mt *MessagesText) createHeader(w io.Writer, m discord.Message, gID discord.GuildID, isReply bool) { if mt.cfg.Timestamps.Enabled { time := m.Timestamp.Time().In(time.Local).Format(mt.cfg.Timestamps.Format) fmt.Fprintf(w, "[::d]%s[::-] ", time) @@ -137,7 +152,10 @@ func (mt *MessagesText) createHeader(w io.Writer, m discord.Message, isReply boo fmt.Fprintf(mt, "[::d]%s", mt.cfg.Theme.MessagesText.ReplyIndicator) } - fmt.Fprintf(w, "[%s]%s[-:-:-] ", mt.cfg.Theme.MessagesText.AuthorColor, m.Author.Username) + displayName := mt.getAuthorDisplayName(m, gID) + displayColor := mt.getAuthorDisplayColor(m, gID) + + fmt.Fprintf(w, "[%s]%s[-:-:-] ", displayColor, displayName) } func (mt *MessagesText) createBody(w io.Writer, m discord.Message, isReply bool) { @@ -193,6 +211,32 @@ func (mt *MessagesText) getSelectedMessageIndex() (int, error) { return -1, nil } +func (mt *MessagesText) getAuthorDisplayName(m discord.Message, gID discord.GuildID) string { + name := m.Author.DisplayOrUsername() + + if app.cfg.Theme.MessagesText.ShowNicknames && gID.IsValid() { + // Use guild nickname if present + if member, _ := discordState.Cabinet.Member(gID, m.Author.ID); member != nil && member.Nick != "" { + name = member.Nick + } + } + + return name +} + +func (mt *MessagesText) getAuthorDisplayColor(m discord.Message, gID discord.GuildID) string { + color := mt.cfg.Theme.MessagesText.AuthorColor + + if app.cfg.Theme.MessagesText.ShowUsernameColors && gID.IsValid() { + // Use color from highest role in guild + if c, ok := discordState.MemberColor(gID, m.Author.ID); ok { + color = c.String() + } + } + + return color +} + func (mt *MessagesText) onInputCapture(event *tcell.EventKey) *tcell.EventKey { switch event.Name() { case mt.cfg.Keys.MessagesText.Cancel: @@ -460,7 +504,7 @@ func (mt *MessagesText) reply(mention bool) { return } - title += msg.Author.Tag() + title += mt.getAuthorDisplayName(*msg, msg.GuildID) app.messageInput.SetTitle(title) app.messageInput.replyMessageID = mt.selectedMessageID mt.app.SetFocus(app.messageInput) @@ -511,3 +555,57 @@ func (mt *MessagesText) delete() { app.messagesText.createMessage(m) } } + +func (mt *MessagesText) fetchMembers(gID discord.GuildID, ms []discord.Message) { + var usersToFetch []discord.UserID + for _, m := range ms { + if member, _ := discordState.Cabinet.Member(gID, m.Author.ID); member == nil { + usersToFetch = append(usersToFetch, m.Author.ID) + } + } + + if usersToFetch != nil { + err := discordState.Gateway().Send(context.Background(), &gateway.RequestGuildMembersCommand{ + GuildIDs: []discord.GuildID{gID}, + UserIDs: slices.Compact(usersToFetch), + }) + if err != nil { + slog.Error("Failed to request guild members", "err", err) + return + } + + mt.setFetchingChunk(true) + mt.waitForChunkEvent() + } +} + +func (mt *MessagesText) setFetchingChunk(value bool) { + mt.fetchingMembers.mu.Lock() + if mt.fetchingMembers.value == value { + mt.fetchingMembers.mu.Unlock() + return + } + mt.fetchingMembers.value = value + mt.fetchingMembers.mu.Unlock() + + if value { + mt.fetchingMembers.done = make(chan struct{}) + } else { + close(mt.fetchingMembers.done) + } +} + +func (mt *MessagesText) waitForChunkEvent() { + mt.fetchingMembers.mu.Lock() + if !mt.fetchingMembers.value { + mt.fetchingMembers.mu.Unlock() + return + } + mt.fetchingMembers.mu.Unlock() + + select { + case <-mt.fetchingMembers.done: + default: + <-mt.fetchingMembers.done + } +} diff --git a/cmd/state.go b/cmd/state.go index 3d017e7..9dd6430 100644 --- a/cmd/state.go +++ b/cmd/state.go @@ -43,6 +43,7 @@ func openState(token string) error { discordState.AddHandler(discordState.onReady) discordState.AddHandler(discordState.onMessageCreate) discordState.AddHandler(discordState.onMessageDelete) + discordState.AddHandler(discordState.onGetMemberChunk) discordState.OnRequest = append(discordState.OnRequest, discordState.onRequest) return discordState.Open(context.TODO()) @@ -106,3 +107,7 @@ func (s *State) onMessageDelete(m *gateway.MessageDeleteEvent) { app.messagesText.drawMsgs(m.ChannelID) } } + +func (s *State) onGetMemberChunk(g *gateway.GuildMembersChunkEvent) { + app.messagesText.setFetchingChunk(false) +} diff --git a/internal/config/theme.go b/internal/config/theme.go index 19b2d11..909327a 100644 --- a/internal/config/theme.go +++ b/internal/config/theme.go @@ -55,6 +55,9 @@ type ( } MessagesTextTheme struct { + ShowNicknames bool `toml:"show_user_nicks"` + ShowUsernameColors bool `toml:"show_user_colors"` + ReplyIndicator string `toml:"reply_indicator"` AuthorColor string `toml:"author_color"` @@ -92,6 +95,9 @@ func defaultTheme() Theme { PrivateChannelColor: tview.Styles.PrimaryTextColor.String(), }, MessagesText: MessagesTextTheme{ + ShowNicknames: true, + ShowUsernameColors: true, + ReplyIndicator: string(tview.BoxDrawingsLightArcDownAndRight) + " ", AuthorColor: "aqua", diff --git a/internal/markdown/renderer.go b/internal/markdown/renderer.go index 6781ef5..d7b6694 100644 --- a/internal/markdown/renderer.go +++ b/internal/markdown/renderer.go @@ -96,7 +96,11 @@ func (r *renderer) Render(w io.Writer, source []byte, n ast.Node) error { case n.Channel != nil: io.WriteString(w, "#"+n.Channel.Name) case n.GuildUser != nil: - io.WriteString(w, "@"+n.GuildUser.Username) + username := n.GuildUser.DisplayOrUsername() + if r.config.Options["showNicknames"].(bool) && n.GuildUser.Member != nil && n.GuildUser.Member.Nick != "" { + username = n.GuildUser.Member.Nick + } + io.WriteString(w, "@"+username) case n.GuildRole != nil: io.WriteString(w, "@"+n.GuildRole.Name) }