From 0dc3f21501a6c793d41146e8f51f0723b960fdeb Mon Sep 17 00:00:00 2001 From: ayn2op Date: Tue, 6 Oct 2026 16:55:51 -0400 Subject: [PATCH] refactor(ui): give command methods value receivers --- internal/ui/chat/composer/model.go | 330 +++++++-------- internal/ui/chat/composer/msg.go | 136 +++---- internal/ui/chat/guildstree/keymap.go | 8 +- internal/ui/chat/guildstree/model.go | 30 +- internal/ui/chat/guildstree/msg.go | 2 +- internal/ui/chat/memberstree/model.go | 20 +- internal/ui/chat/memberstree/msg.go | 4 +- internal/ui/chat/messageslist/model.go | 494 +++++++++++------------ internal/ui/chat/messageslist/msg.go | 116 +++--- internal/ui/chat/messageslist/preview.go | 18 +- internal/ui/chat/msg.go | 2 +- internal/ui/chat/state.go | 6 +- 12 files changed, 583 insertions(+), 583 deletions(-) diff --git a/internal/ui/chat/composer/model.go b/internal/ui/chat/composer/model.go index b821347..f813c1a 100644 --- a/internal/ui/chat/composer/model.go +++ b/internal/ui/chat/composer/model.go @@ -93,34 +93,34 @@ func NewModel(cfg *config.Config, state *ningen.State) Model { } // SetChannel sets the channel messages are sent to, disabling the composer if the user may not send messages there. -func (c *Model) SetChannel(channel *discord.Channel) { - c.channel, c.typingUntil = channel, time.Time{} +func (m *Model) SetChannel(channel *discord.Channel) { + m.channel, m.typingUntil = channel, time.Time{} isDM := channel.Type == discord.DirectMessage || channel.Type == discord.GroupDM - c.disabled = !isDM && !c.state.HasPermissions(channel.ID, discord.PermissionSendMessages) + m.disabled = !isDM && !m.state.HasPermissions(channel.ID, discord.PermissionSendMessages) } // Height returns the number of rows the composer takes, including its border. -func (c *Model) Height() int { - _, frame := c.frame() - return min(strings.Count(c.editState.Value(), "\n")+1, max(c.cfg.Composer.MaxHeight, 1)) + frame +func (m *Model) Height() int { + _, frame := m.frame() + return min(strings.Count(m.editState.Value(), "\n")+1, max(m.cfg.Composer.MaxHeight, 1)) + frame } -func (c *Model) Disabled() bool { - return c.disabled +func (m Model) Disabled() bool { + return m.disabled } -func (c *Model) placeholder() string { +func (m Model) placeholder() string { switch { - case c.channel == nil: + case m.channel == nil: return noChannelPlaceholder - case c.disabled: + case m.disabled: return noPermissionPlaceholder } return messagePlaceholder } // View shows the text area in a box, taking keys if focused. -func (c Model) View(focused bool) tview.Widget { return view{&c, focused} } +func (m Model) View(focused bool) tview.Widget { return view{&m, focused} } // view is the composer as shown, with or without the focus. type view struct { @@ -149,177 +149,177 @@ func (v view) Handle(msg tview.Msg, area tview.Rectangle) tview.Msg { } // box returns the text area in a box with the title and footer. -func (c *Model) box(focused bool) box.Widget { - text := textarea.New(&c.editState). - Placeholder(c.placeholder()). - Keybind(c.editAction). - Focused(focused && !c.disabled). +func (m *Model) box(focused bool) box.Widget { + text := textarea.New(&m.editState). + Placeholder(m.placeholder()). + Keybind(m.editAction). + Focused(focused && !m.disabled). OnChange(func(a textarea.Change) tview.Msg { return editMsg(a) }) - return ui.Box(text, &c.cfg.Theme, focused).Title(c.title()).Footer(c.footer()) + return ui.Box(text, &m.cfg.Theme, focused).Title(m.title()).Footer(m.footer()) } // title returns what the composer is doing: editing, replying, or nothing. -func (c *Model) title() string { - switch data := c.sendMessageData; { - case c.editing != nil: +func (m Model) title() string { + switch data := m.sendMessageData; { + case m.editing != nil: return "Editing" case data.Reference != nil: if mention := data.AllowedMentions.RepliedUser; mention != nil && *mention { - return "[@] Replying to " + c.replyName + return "[@] Replying to " + m.replyName } - return "Replying to " + c.replyName + return "Replying to " + m.replyName } return "" } // footer returns the names of the attached files, or "" if there are none. -func (c *Model) footer() string { - if len(c.sendMessageData.Files) == 0 { +func (m Model) footer() string { + if len(m.sendMessageData.Files) == 0 { return "" } var names []string - for _, file := range c.sendMessageData.Files { + for _, file := range m.sendMessageData.Files { names = append(names, file.Name) } return "Attached " + humanJoin(names) } // isBound reports whether key is one of the composer's keybinds. -func (c *Model) isBound(key tview.KeyMsg) bool { - k := c.cfg.Keybinds.Composer +func (m Model) isBound(key tview.KeyMsg) bool { + k := m.cfg.Keybinds.Composer if keybind.Matches(key, k.Paste.Keybind, k.Send.Keybind, k.OpenEditor.Keybind, k.OpenFilePicker.Keybind, k.Cancel.Keybind, k.TabComplete.Keybind, k.ToggleReplyMention.Keybind, k.Undo.Keybind) { return true } - return keybind.Matches(key, k.EditLast.Keybind) && c.canEditLastMessage() + return keybind.Matches(key, k.EditLast.Keybind) && m.canEditLastMessage() } // insert inserts text at the cursor. -func (c *Model) insert(text string) { - c.editState.Replace(c.editState.Cursor(), c.editState.Cursor(), text) +func (m *Model) insert(text string) { + m.editState.Replace(m.editState.Cursor(), m.editState.Cursor(), text) } // pasteText inserts the text on the clipboard at the cursor. -func (c *Model) pasteText() { +func (m *Model) pasteText() { data, err := clipboard.Read(context.Background(), clipboard.FmtText) if err != nil { slog.Error("failed to read from clipboard", "err", err) return } - c.insert(string(data)) + m.insert(string(data)) } // frame returns the cells the composer's border and padding take on each side. -func (c *Model) frame() (left, vertical int) { +func (m *Model) frame() (left, vertical int) { const size = 100 - inner := c.box(false).InnerArea(tview.Rectangle{Width: size, Height: size}) + inner := m.box(false).InnerArea(tview.Rectangle{Width: size, Height: size}) return inner.X, size - inner.Height } -func (c *Model) reset() { - c.editing = nil - c.sendMessageData = &api.SendMessageData{} - c.editState.SetValue("") +func (m *Model) reset() { + m.editing = nil + m.sendMessageData = &api.SendMessageData{} + m.editState.SetValue("") } // Update returns c changed in response to msg and a command to run, or nil. -func (c Model) Update(msg tview.Msg) (Model, tview.Cmd) { +func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) { switch msg := msg.(type) { case TabSuggestMsg: - return c, c.tabSuggest() + return m, m.tabSuggest() case imagePastedMsg: if len(msg) == 0 { - return c, nil + return m, nil } - c.attach(imageAttachmentName, bytes.NewReader(msg)) - return c, nil + m.attach(imageAttachmentName, bytes.NewReader(msg)) + return m, nil case filesPickedMsg: - selectedChannel := c.channel + selectedChannel := m.channel if selectedChannel == nil || selectedChannel.ID != msg.channelID { - return c, closeFiles(msg.files) + return m, closeFiles(msg.files) } for _, file := range msg.files { - c.attach(file.Name, file.Reader) + m.attach(file.Name, file.Reader) } - return c, nil + return m, nil case editorMsg: - c.editState.SetValue(string(msg)) - return c, nil + m.editState.SetValue(string(msg)) + return m, nil case editMsg: - c.editState.Apply(textarea.Change(msg)) - typingCmd := c.sendTyping() - if c.cfg.AutocompleteLimit > 0 { - return c, tview.Batch(typingCmd, c.tabSuggest()) + m.editState.Apply(textarea.Change(msg)) + typingCmd := m.sendTyping() + if m.cfg.AutocompleteLimit > 0 { + return m, tview.Batch(typingCmd, m.tabSuggest()) } - return c, typingCmd + return m, typingCmd case tview.KeyMsg: switch { - case keybind.Matches(msg, c.cfg.Keybinds.Composer.EditLast.Keybind) && c.canEditLastMessage(): - return c, c.editLastMessage() - case keybind.Matches(msg, c.cfg.Keybinds.Composer.Paste.Keybind): - c.pasteText() - return c, pasteImage() - case keybind.Matches(msg, c.cfg.Keybinds.Composer.Send.Keybind): - if c.mentionsVisible { - return c, c.tabComplete() + case keybind.Matches(msg, m.cfg.Keybinds.Composer.EditLast.Keybind) && m.canEditLastMessage(): + return m, m.editLastMessage() + case keybind.Matches(msg, m.cfg.Keybinds.Composer.Paste.Keybind): + m.pasteText() + return m, pasteImage() + case keybind.Matches(msg, m.cfg.Keybinds.Composer.Send.Keybind): + if m.mentionsVisible { + return m, m.tabComplete() } - return c, c.send() - case keybind.Matches(msg, c.cfg.Keybinds.Composer.OpenEditor.Keybind): - c.stopTabCompletion() - return c, c.openEditor() - case keybind.Matches(msg, c.cfg.Keybinds.Composer.OpenFilePicker.Keybind): - c.stopTabCompletion() - return c, c.pickFiles() - case keybind.Matches(msg, c.cfg.Keybinds.Composer.Cancel.Keybind): - if c.mentionsVisible { - c.stopTabCompletion() - return c, nil + return m, m.send() + case keybind.Matches(msg, m.cfg.Keybinds.Composer.OpenEditor.Keybind): + m.stopTabCompletion() + return m, m.openEditor() + case keybind.Matches(msg, m.cfg.Keybinds.Composer.OpenFilePicker.Keybind): + m.stopTabCompletion() + return m, m.pickFiles() + case keybind.Matches(msg, m.cfg.Keybinds.Composer.Cancel.Keybind): + if m.mentionsVisible { + m.stopTabCompletion() + return m, nil } - c.reset() - return c, nil - case keybind.Matches(msg, c.cfg.Keybinds.Composer.TabComplete.Keybind): - if c.mentionsVisible { - return c, c.tabComplete() + m.reset() + return m, nil + case keybind.Matches(msg, m.cfg.Keybinds.Composer.TabComplete.Keybind): + if m.mentionsVisible { + return m, m.tabComplete() } - c.insert("\t") - return c, nil - case keybind.Matches(msg, c.cfg.Keybinds.Composer.ToggleReplyMention.Keybind): - c.toggleReplyMention() - return c, nil - case keybind.Matches(msg, c.cfg.Keybinds.Composer.Undo.Keybind): - c.editState.Undo() - return c, nil + m.insert("\t") + return m, nil + case keybind.Matches(msg, m.cfg.Keybinds.Composer.ToggleReplyMention.Keybind): + m.toggleReplyMention() + return m, nil + case keybind.Matches(msg, m.cfg.Keybinds.Composer.Undo.Keybind): + m.editState.Undo() + return m, nil } case mentionslist.Msg: var cmd tview.Cmd - c.mentionsList, cmd = c.mentionsList.Update(msg) - return c, cmd + m.mentionsList, cmd = m.mentionsList.Update(msg) + return m, cmd } - return c, nil + return m, nil } -func (c *Model) canEditLastMessage() bool { - return !c.Disabled() && c.editing == nil && c.editState.Value() == "" && - c.sendMessageData.Reference == nil && len(c.sendMessageData.Files) == 0 && - !c.mentionsVisible +func (m Model) canEditLastMessage() bool { + return !m.Disabled() && m.editing == nil && m.editState.Value() == "" && + m.sendMessageData.Reference == nil && len(m.sendMessageData.Files) == 0 && + !m.mentionsVisible } // StartEdit puts message in the composer to be edited. -func (c *Model) StartEdit(message discord.Message) { - c.editing = &message - c.editState.SetValue(message.Content) +func (m *Model) StartEdit(message discord.Message) { + m.editing = &message + m.editState.SetValue(message.Content) } // StartReply makes the next message a reply to message by name, mentioning its author if mention is set. -func (c *Model) StartReply(message discord.Message, name string, mention bool) { - data := c.sendMessageData +func (m *Model) StartReply(message discord.Message, name string, mention bool) { + data := m.sendMessageData data.Reference = &discord.MessageReference{MessageID: message.ID} data.AllowedMentions = &api.AllowedMentions{RepliedUser: option.Some(mention)} - c.replyName = name + m.replyName = name } -func (c *Model) toggleReplyMention() { - data := c.sendMessageData +func (m Model) toggleReplyMention() { + data := m.sendMessageData if data.Reference == nil || data.AllowedMentions == nil || data.AllowedMentions.RepliedUser == nil { return } @@ -327,7 +327,7 @@ func (c *Model) toggleReplyMention() { data.AllowedMentions.RepliedUser = option.Some(!*data.AllowedMentions.RepliedUser) } -func (c *Model) processText(channel *discord.Channel, src []byte) string { +func (m Model) processText(channel *discord.Channel, src []byte) string { // Fast path: no mentions to expand. if bytes.IndexByte(src, '@') == -1 { return string(src) @@ -335,7 +335,7 @@ func (c *Model) processText(channel *discord.Channel, src []byte) string { // Fast path: no back ticks (code blocks), so expand mentions directly. if bytes.IndexByte(src, '`') == -1 { - return string(c.expandMentions(channel, src)) + return string(m.expandMentions(channel, src)) } var ( @@ -361,14 +361,14 @@ func (c *Model) processText(channel *discord.Channel, src []byte) string { // Replace back to front so that expanding a mention does not shift the ranges before it. for _, rng := range slices.Backward(ranges) { - src = slices.Replace(src, rng[0], rng[1], c.expandMentions(channel, src[rng[0]:rng[1]])...) + src = slices.Replace(src, rng[0], rng[1], m.expandMentions(channel, src[rng[0]:rng[1]])...) } return string(src) } -func (c *Model) expandMentions(channel *discord.Channel, src []byte) []byte { - state := c.state +func (m Model) expandMentions(channel *discord.Channel, src []byte) []byte { + state := m.state return mentionRegex.ReplaceAllFunc(src, func(input []byte) []byte { output := input name := string(input[1:]) @@ -415,24 +415,24 @@ func isMentionChar(r rune) bool { } // suggestRecentAuthors suggests the authors of the channel's recent messages, other than the user. -func (c *Model) suggestRecentAuthors(channel *discord.Channel) { - messages, err := c.state.Cabinet.Messages(channel.ID) +func (m *Model) suggestRecentAuthors(channel *discord.Channel) { + messages, err := m.state.Cabinet.Messages(channel.ID) if err != nil { return } - me, _ := c.state.Cabinet.Me() + me, _ := m.state.Cabinet.Me() shown := map[discord.UserID]bool{me.ID: true} - for _, m := range messages { - if shown[m.Author.ID] { + for _, message := range messages { + if shown[message.Author.ID] { continue } - shown[m.Author.ID] = true + shown[message.Author.ID] = true if !channel.GuildID.IsValid() { - c.addMentionUser(&m.Author) + m.addMentionUser(&message.Author) continue } - c.state.MemberState.RequestMember(channel.GuildID, m.Author.ID) - if member, err := c.state.Cabinet.Member(channel.GuildID, m.Author.ID); err == nil && c.addMentionMember(channel.GuildID, member) { + m.state.MemberState.RequestMember(channel.GuildID, message.Author.ID) + if member, err := m.state.Cabinet.Member(channel.GuildID, message.Author.ID); err == nil && m.addMentionMember(channel.GuildID, member) { return } } @@ -467,25 +467,25 @@ func channelHasUser(state *ningen.State, channelID discord.ChannelID, userID dis return perms.Has(discord.PermissionViewChannel) } -func (c *Model) CacheMemberSearch(event *gateway.GuildMembersChunkEvent) tview.Cmd { +func (m Model) CacheMemberSearch(event *gateway.GuildMembersChunkEvent) tview.Cmd { key, ok := strings.CutPrefix(event.Nonce, memberSearchNonce) if !ok { return nil } - c.memberSearchCache[key] = uint(len(event.Members)) + m.memberSearchCache[key] = uint(len(event.Members)) return func() tview.Msg { return TabSuggestMsg{} } } // OnGuildMemberRemove forgets cached member searches that the removed member may have filled. -func (c *Model) OnGuildMemberRemove(event *gateway.GuildMemberRemoveEvent) { +func (m Model) OnGuildMemberRemove(event *gateway.GuildMemberRemoveEvent) { for name := event.GuildID.String() + " " + event.User.Username; name != ""; name = name[:len(name)-1] { - if count, ok := c.memberSearchCache[name]; ok && count >= c.state.MemberState.SearchLimit { + if count, ok := m.memberSearchCache[name]; ok && count >= m.state.MemberState.SearchLimit { // A full result set may now be missing members; search these prefixes again. for name != "" { - delete(c.memberSearchCache, name) + delete(m.memberSearchCache, name) name = name[:len(name)-1] } return @@ -494,11 +494,11 @@ func (c *Model) OnGuildMemberRemove(event *gateway.GuildMemberRemoveEvent) { } // MentionsView places the mentions list just above the composer, near the cursor, or returns nil if it is hidden. -func (c *Model) MentionsView() tview.Widget { - if !c.mentionsVisible { +func (m *Model) MentionsView() tview.Widget { + if !m.mentionsVisible { return nil } - return mentionsPopup{c} + return mentionsPopup{m} } // mentionsPopup lays out the mentions list within the area above the composer, where it is drawn over the messages. @@ -547,54 +547,54 @@ func (p mentionsPopup) area(area tview.Rectangle) tview.Rectangle { return tview.Rectangle{X: area.X + x, Y: area.Y + area.Height - c.Height() - h, Width: w, Height: h} } -func (c *Model) addMentionMember(gID discord.GuildID, m *discord.Member) bool { - if m == nil { +func (m *Model) addMentionMember(gID discord.GuildID, member *discord.Member) bool { + if member == nil { return false } - name := cmp.Or(m.Nick, m.User.DisplayName, m.User.Username) + name := cmp.Or(member.Nick, member.User.DisplayName, member.User.Username) style := tcell.StyleDefault // This avoids a slower member color lookup path. - color, ok := state.MemberColor(m, func(id discord.RoleID) *discord.Role { - r, _ := c.state.Cabinet.Role(gID, id) + color, ok := state.MemberColor(member, func(id discord.RoleID) *discord.Role { + r, _ := m.state.Cabinet.Role(gID, id) return r }) if ok { style = style.Foreground(tcell.NewHexColor(int32(color))) } - presence, err := c.state.Cabinet.Presence(gID, m.User.ID) + presence, err := m.state.Cabinet.Presence(gID, member.User.ID) if err != nil { - slog.Info("failed to get presence from state", "guild_id", gID, "user_id", m.User.ID, "err", err) + slog.Info("failed to get presence from state", "guild_id", gID, "user_id", member.User.ID, "err", err) } else if presence.Status == discord.OfflineStatus { style = style.Dim(true) } - c.mentionsList.Append(mentionslist.Item{ - InsertText: m.User.Username, + m.mentionsList.Append(mentionslist.Item{ + InsertText: member.User.Username, DisplayText: name, Style: style, }) - return c.mentionsList.ItemCount() > int(c.cfg.AutocompleteLimit) + return m.mentionsList.ItemCount() > int(m.cfg.AutocompleteLimit) } -func (c *Model) addMentionUser(user *discord.User) { +func (m *Model) addMentionUser(user *discord.User) { if user == nil { return } name := user.DisplayOrUsername() style := tcell.StyleDefault - presence, err := c.state.Cabinet.Presence(discord.NullGuildID, user.ID) + presence, err := m.state.Cabinet.Presence(discord.NullGuildID, user.ID) if err != nil { slog.Info("failed to get presence from state", "user_id", user.ID, "err", err) } else if presence.Status == discord.OfflineStatus { style = style.Dim(true) } - c.mentionsList.Append(mentionslist.Item{ + m.mentionsList.Append(mentionslist.Item{ InsertText: user.Username, DisplayText: name, Style: style, @@ -602,28 +602,28 @@ func (c *Model) addMentionUser(user *discord.User) { } // stopTabCompletion clears and closes the mentions list when it is in use. -func (c *Model) stopTabCompletion() { - if c.cfg.AutocompleteLimit > 0 { - c.mentionsList.Clear() - c.CloseMentions() +func (m *Model) stopTabCompletion() { + if m.cfg.AutocompleteLimit > 0 { + m.mentionsList.Clear() + m.CloseMentions() } } -func (c *Model) CloseMentions() { - c.mentionsVisible = false +func (m *Model) CloseMentions() { + m.mentionsVisible = false } -func (c *Model) attach(name string, reader io.Reader) { - c.sendMessageData.Files = append(c.sendMessageData.Files, sendpart.File{Name: name, Reader: reader}) +func (m Model) attach(name string, reader io.Reader) { + m.sendMessageData.Files = append(m.sendMessageData.Files, sendpart.File{Name: name, Reader: reader}) } -func (c *Model) canAttachFiles() bool { - return c.channel != nil && c.state.HasPermissions(c.channel.ID, discord.PermissionAttachFiles) +func (m Model) canAttachFiles() bool { + return m.channel != nil && m.state.HasPermissions(m.channel.ID, discord.PermissionAttachFiles) } // editAction binds the configured newline key and textarea's other default keys. -func (c Model) editAction(key tview.KeyMsg) textarea.Action { - if keybind.Matches(key, c.cfg.Keybinds.Composer.Newline.Keybind) { +func (m Model) editAction(key tview.KeyMsg) textarea.Action { + if keybind.Matches(key, m.cfg.Keybinds.Composer.Newline.Keybind) { return textarea.ActionNewline } if action := textarea.DefaultKeybind(key); action != textarea.ActionNewline { @@ -632,51 +632,51 @@ func (c Model) editAction(key tview.KeyMsg) textarea.Action { return textarea.ActionNone } -func (c Model) ShortHelp() []keybind.Keybind { - if c.mentionsVisible { - cfg := c.cfg.Keybinds.MentionsList - ccfg := c.cfg.Keybinds.Composer +func (m Model) ShortHelp() []keybind.Keybind { + if m.mentionsVisible { + cfg := m.cfg.Keybinds.MentionsList + ccfg := m.cfg.Keybinds.Composer short := []keybind.Keybind{cfg.SelectUp.Keybind, cfg.SelectDown.Keybind, ccfg.TabComplete.Keybind, ccfg.Cancel.Keybind} - if c.canAttachFiles() { + if m.canAttachFiles() { short = append(short, ccfg.OpenFilePicker.Keybind) } return short } - cfg := c.cfg.Keybinds.Composer + cfg := m.cfg.Keybinds.Composer short := []keybind.Keybind{cfg.Send.Keybind, cfg.Newline.Keybind, cfg.Cancel.Keybind, cfg.Paste.Keybind, cfg.OpenEditor.Keybind} - if c.canEditLastMessage() { + if m.canEditLastMessage() { short = append(short, cfg.EditLast.Keybind) } - if c.sendMessageData.Reference != nil { + if m.sendMessageData.Reference != nil { short = append(short, cfg.ToggleReplyMention.Keybind) } - if c.canAttachFiles() { + if m.canAttachFiles() { short = append(short, cfg.OpenFilePicker.Keybind) } return short } -func (c Model) FullHelp() [][]keybind.Keybind { - if c.mentionsVisible { - mcfg := c.cfg.Keybinds.MentionsList - ccfg := c.cfg.Keybinds.Composer +func (m Model) FullHelp() [][]keybind.Keybind { + if m.mentionsVisible { + mcfg := m.cfg.Keybinds.MentionsList + ccfg := m.cfg.Keybinds.Composer return [][]keybind.Keybind{ {mcfg.SelectUp.Keybind, mcfg.SelectDown.Keybind, mcfg.SelectTop.Keybind, mcfg.SelectBottom.Keybind}, {ccfg.TabComplete.Keybind, ccfg.Cancel.Keybind}, } } - cfg := c.cfg.Keybinds.Composer + cfg := m.cfg.Keybinds.Composer openEditor := []keybind.Keybind{cfg.Paste.Keybind, cfg.OpenEditor.Keybind} - if c.canAttachFiles() { + if m.canAttachFiles() { openEditor = append(openEditor, cfg.OpenFilePicker.Keybind) } compose := []keybind.Keybind{cfg.Send.Keybind, cfg.Newline.Keybind, cfg.Cancel.Keybind, cfg.Undo.Keybind} compose = append(compose, cfg.EditLast.Keybind) - if c.sendMessageData.Reference != nil { + if m.sendMessageData.Reference != nil { compose = append(compose, cfg.ToggleReplyMention.Keybind) } diff --git a/internal/ui/chat/composer/msg.go b/internal/ui/chat/composer/msg.go index 44cfff3..4693832 100644 --- a/internal/ui/chat/composer/msg.go +++ b/internal/ui/chat/composer/msg.go @@ -36,7 +36,7 @@ type editorMsg string // editMsg edits the composer's text. type editMsg textarea.Change -func (c *Model) editLastMessage() tview.Cmd { +func (m Model) editLastMessage() tview.Cmd { return func() tview.Msg { return EditLastMsg{} } } @@ -58,8 +58,8 @@ type filesPickedMsg struct { files []sendpart.File } -func (c *Model) pickFiles() tview.Cmd { - selectedChannel := c.channel +func (m Model) pickFiles() tview.Cmd { + selectedChannel := m.channel if selectedChannel == nil { return nil } @@ -99,89 +99,89 @@ func closeFiles(files []sendpart.File) tview.Cmd { } } -func (c *Model) sendTyping() tview.Cmd { - if !c.cfg.TypingIndicator.Send { +func (m *Model) sendTyping() tview.Cmd { + if !m.cfg.TypingIndicator.Send { return nil } now := time.Now() - if now.Before(c.typingUntil) { + if now.Before(m.typingUntil) { return nil } - c.typingUntil = now.Add(TypingDuration) + m.typingUntil = now.Add(TypingDuration) - selectedChannel := c.channel + selectedChannel := m.channel if selectedChannel == nil { return nil } channelID := selectedChannel.ID return func() tview.Msg { - c.state.Typing(channelID) + m.state.Typing(channelID) return nil } } -func (c *Model) send() tview.Cmd { - selectedChannel := c.channel +func (m *Model) send() tview.Cmd { + selectedChannel := m.channel if selectedChannel == nil { return nil } - text := strings.TrimSpace(c.editState.Value()) - if text == "" && len(c.sendMessageData.Files) == 0 { + text := strings.TrimSpace(m.editState.Value()) + if text == "" && len(m.sendMessageData.Files) == 0 { return nil } - text = c.processText(selectedChannel, []byte(text)) - data := *c.sendMessageData + text = m.processText(selectedChannel, []byte(text)) + data := *m.sendMessageData data.Files = slices.Clone(data.Files) - editing := c.editing - c.typingUntil = time.Time{} - c.reset() + editing := m.editing + m.typingUntil = time.Time{} + m.reset() sent := func() tview.Msg { return SentMsg{} } return tview.Batch(sent, func() tview.Msg { defer closeFiles(data.Files)() if editing != nil { editData := api.EditMessageData{Content: option.SomeNullable(text)} - if _, err := c.state.EditMessageComplex(editing.ChannelID, editing.ID, editData); err != nil { + if _, err := m.state.EditMessageComplex(editing.ChannelID, editing.ID, editData); err != nil { slog.Error("failed to edit message", "err", err) } return nil } data.Content = text - if _, err := c.state.SendMessageComplex(selectedChannel.ID, data); err != nil { + if _, err := m.state.SendMessageComplex(selectedChannel.ID, data); err != nil { slog.Error("failed to send message in channel", "channel_id", selectedChannel.ID, "err", err) } return nil }) } -func (c *Model) tabComplete() tview.Cmd { - posEnd, name, r := wordBeforeCursor(c.editState.Value(), c.editState.Cursor(), isMentionChar) +func (m *Model) tabComplete() tview.Cmd { + posEnd, name, r := wordBeforeCursor(m.editState.Value(), m.editState.Cursor(), isMentionChar) if r != '@' { - c.stopTabCompletion() + m.stopTabCompletion() return nil } pos := posEnd - (len(name) + 1) - selectedChannel := c.channel + selectedChannel := m.channel if selectedChannel == nil { return nil } gID := selectedChannel.GuildID - if c.cfg.AutocompleteLimit == 0 { + if m.cfg.AutocompleteLimit == 0 { if !gID.IsValid() { users := selectedChannel.DMRecipients res := fuzzy.FindFrom(name, userList(users)) if len(res) > 0 { - c.editState.Replace(pos, posEnd, "@"+users[res[0].Index].Username+" ") + m.editState.Replace(pos, posEnd, "@"+users[res[0].Index].Username+" ") } } else { - cmd := c.searchMember(gID, name) - members, err := c.state.Cabinet.Members(gID) + cmd := m.searchMember(gID, name) + members, err := m.state.Cabinet.Members(gID) if err != nil { slog.Error("failed to get members from state", "guild_id", gID, "err", err) return cmd @@ -189,8 +189,8 @@ func (c *Model) tabComplete() tview.Cmd { res := fuzzy.FindFrom(name, memberList(members)) for _, r := range res { - if channelHasUser(c.state, selectedChannel.ID, members[r.Index].User.ID) { - c.editState.Replace(pos, posEnd, "@"+members[r.Index].User.Username+" ") + if channelHasUser(m.state, selectedChannel.ID, members[r.Index].User.ID) { + m.editState.Replace(pos, posEnd, "@"+members[r.Index].User.Username+" ") return cmd } } @@ -198,107 +198,107 @@ func (c *Model) tabComplete() tview.Cmd { } return nil } - if c.mentionsList.ItemCount() == 0 { + if m.mentionsList.ItemCount() == 0 { return nil } - name, ok := c.mentionsList.SelectedInsertText() + name, ok := m.mentionsList.SelectedInsertText() if !ok { return nil } - c.editState.Replace(pos, posEnd, "@"+name+" ") - c.stopTabCompletion() + m.editState.Replace(pos, posEnd, "@"+name+" ") + m.stopTabCompletion() return nil } -func (c *Model) tabSuggest() tview.Cmd { - _, name, r := wordBeforeCursor(c.editState.Value(), c.editState.Cursor(), isMentionChar) +func (m *Model) tabSuggest() tview.Cmd { + _, name, r := wordBeforeCursor(m.editState.Value(), m.editState.Cursor(), isMentionChar) if r != '@' { - c.stopTabCompletion() + m.stopTabCompletion() return nil } - channel := c.channel + channel := m.channel if channel == nil { return nil } - c.mentionsList.Clear() + m.mentionsList.Clear() gID := channel.GuildID switch { case name == "": - c.suggestRecentAuthors(channel) + m.suggestRecentAuthors(channel) case !gID.IsValid(): // DMs have recipients, not members. - me, _ := c.state.Cabinet.Me() + me, _ := m.state.Cabinet.Me() users := append(slices.Clone(channel.DMRecipients), *me) for _, r := range fuzzy.FindFrom(name, userList(users)) { - c.addMentionUser(&users[r.Index]) + m.addMentionUser(&users[r.Index]) } default: - searchCmd := c.searchMember(gID, name) - mems, err := c.state.Cabinet.Members(gID) + searchCmd := m.searchMember(gID, name) + mems, err := m.state.Cabinet.Members(gID) if err != nil { slog.Error("fetching members failed", "err", err) return searchCmd } res := fuzzy.FindFrom(name, memberList(mems)) - if len(res) > int(c.cfg.AutocompleteLimit) { - res = res[:int(c.cfg.AutocompleteLimit)] + if len(res) > int(m.cfg.AutocompleteLimit) { + res = res[:int(m.cfg.AutocompleteLimit)] } for _, r := range res { - if channelHasUser(c.state, channel.ID, mems[r.Index].User.ID) && - c.addMentionMember(gID, &mems[r.Index]) { + if channelHasUser(m.state, channel.ID, mems[r.Index].User.ID) && + m.addMentionMember(gID, &mems[r.Index]) { break } } - if c.mentionsList.ItemCount() == 0 { - c.stopTabCompletion() + if m.mentionsList.ItemCount() == 0 { + m.stopTabCompletion() return searchCmd } } - if c.mentionsList.ItemCount() == 0 { - c.stopTabCompletion() + if m.mentionsList.ItemCount() == 0 { + m.stopTabCompletion() return nil } - c.mentionsList.Rebuild() - c.mentionsVisible = true + m.mentionsList.Rebuild() + m.mentionsVisible = true return nil } // searchMember performs member discovery in a command goroutine. // It emits a follow-up suggestion message once results are loaded. -func (c *Model) searchMember(gID discord.GuildID, name string) tview.Cmd { +func (m *Model) searchMember(gID discord.GuildID, name string) tview.Cmd { if name == "" { return nil } key := gID.String() + " " + name - if _, ok := c.memberSearchCache[key]; ok { + if _, ok := m.memberSearchCache[key]; ok { return nil } // If searching for "ab" returns less than SearchLimit, then "abc" would not return anything new because we already searched everything starting with "ab". // This will still be true even if a new member joins because arikawa loads new members into the state. - if count, ok := c.memberSearchCache[key[:len(key)-1]]; ok { - if count < c.state.MemberState.SearchLimit { - c.memberSearchCache[key] = count + if count, ok := m.memberSearchCache[key[:len(key)-1]]; ok { + if count < m.state.MemberState.SearchLimit { + m.memberSearchCache[key] = count return nil } } now := time.Now() // Rate limit on our side because we can't distinguish between a successful search and SearchMember not doing anything because of its internal rate limit that we can't detect - if c.lastSearch.Add(c.state.MemberState.SearchFrequency).After(now) { + if m.lastSearch.Add(m.state.MemberState.SearchFrequency).After(now) { return nil } - c.lastSearch = now + m.lastSearch = now nonce := memberSearchNonce + key return func() tview.Msg { - if err := c.state.SendGateway(context.Background(), &gateway.RequestGuildMembersCommand{ + if err := m.state.SendGateway(context.Background(), &gateway.RequestGuildMembersCommand{ GuildIDs: []discord.GuildID{gID}, Query: option.Some(name), - Presences: c.state.MemberState.RequestPresences, - Limit: c.state.MemberState.SearchLimit, + Presences: m.state.MemberState.RequestPresences, + Limit: m.state.MemberState.SearchLimit, Nonce: nonce, }); err != nil { slog.Error("failed to search guild members", "err", err, "guild_id", gID, "query", name) @@ -307,15 +307,15 @@ func (c *Model) searchMember(gID discord.GuildID, name string) tview.Cmd { } } -func (c *Model) openEditor() tview.Cmd { - if c.cfg.Editor == "" { +func (m Model) openEditor() tview.Cmd { + if m.cfg.Editor == "" { return func() tview.Msg { slog.Warn("Attempt to open file with editor, but no editor is set") return nil } } - text := c.editState.Value() - cfg := c.cfg + text := m.editState.Value() + cfg := m.cfg return tview.Suspend(func() tview.Msg { file, err := os.CreateTemp("", tmpFilePattern) if err != nil { diff --git a/internal/ui/chat/guildstree/keymap.go b/internal/ui/chat/guildstree/keymap.go index 7f2dbc1..eeb64f4 100644 --- a/internal/ui/chat/guildstree/keymap.go +++ b/internal/ui/chat/guildstree/keymap.go @@ -11,7 +11,7 @@ import ( var _ help.KeyMap = Model{} -func (m *Model) selectCurrentKeybind() keybind.Keybind { +func (m Model) selectCurrentKeybind() keybind.Keybind { selectCurrent := m.cfg.Keybinds.GuildsTree.SelectCurrent.Keybind selectHelp := selectCurrent.Help() selectDesc := selectHelp.Desc @@ -54,7 +54,7 @@ func (m Model) FullHelp() [][]keybind.Keybind { } } -func (m *Model) collapseKeybinds() []keybind.Keybind { +func (m Model) collapseKeybinds() []keybind.Keybind { cfg := m.cfg.Keybinds.GuildsTree var keybinds []keybind.Keybind @@ -67,10 +67,10 @@ func (m *Model) collapseKeybinds() []keybind.Keybind { return keybinds } -func (m *Model) canCollapseParent(node *tree.Node) bool { +func (m Model) canCollapseParent(node *tree.Node) bool { return node != nil && len(m.root.PathTo(node)) >= 3 } -func (m *Model) canCollapseAll() bool { +func (m Model) canCollapseAll() bool { return slices.ContainsFunc(m.root.Children(), (*tree.Node).Expanded) } diff --git a/internal/ui/chat/guildstree/model.go b/internal/ui/chat/guildstree/model.go index 9b07450..e7da169 100644 --- a/internal/ui/chat/guildstree/model.go +++ b/internal/ui/chat/guildstree/model.go @@ -42,7 +42,7 @@ func NewModel(cfg *config.Config, state *ningen.State) Model { } // CurrentNode returns the selected node, or nil for none. -func (m *Model) CurrentNode() *tree.Node { +func (m Model) CurrentNode() *tree.Node { return m.selectionState.CurrentNode() } @@ -111,7 +111,7 @@ func (m *Model) rebuild(event *gateway.ReadyEvent) { m.selectionState.SetCurrentNode(root) } -func (m *Model) refreshReadStyles(event *read.UpdateEvent) { +func (m Model) refreshReadStyles(event *read.UpdateEvent) { if event.GuildID.IsValid() { if node := m.findNodeByReference(event.GuildID); node != nil { m.setNodeLineStyle(node, m.guildNodeStyle(event.GuildID)) @@ -131,7 +131,7 @@ func (m *Model) refreshReadStyles(event *read.UpdateEvent) { m.setNodeLineStyle(node, m.channelNodeStyle(*channel)) } -func (m *Model) updateDMNodeStyle(userID discord.UserID) { +func (m Model) updateDMNodeStyle(userID discord.UserID) { channel, err := m.state.Cabinet.CreatePrivateChannel(userID) if err != nil { return @@ -144,7 +144,7 @@ func (m *Model) updateDMNodeStyle(userID discord.UserID) { m.setNodeLineStyle(node, m.channelNodeStyle(*channel)) } -func (m *Model) moveDMToFront(channelID discord.ChannelID) { +func (m Model) moveDMToFront(channelID discord.ChannelID) { if m.dmRootNode == nil { return } @@ -157,7 +157,7 @@ func (m *Model) moveDMToFront(channelID discord.ChannelID) { } } -func (m *Model) createFolderNode(folder gateway.GuildFolder, guildsByID map[discord.GuildID]*gateway.GuildCreateEvent) { +func (m Model) createFolderNode(folder gateway.GuildFolder, guildsByID map[discord.GuildID]*gateway.GuildCreateEvent) { name := "Folder" if folder.Name != "" { name = folder.Name @@ -177,12 +177,12 @@ func (m *Model) createFolderNode(folder gateway.GuildFolder, guildsByID map[disc } } -func (m *Model) guildNodeStyle(guildID discord.GuildID) tview.Style { +func (m Model) guildNodeStyle(guildID discord.GuildID) tview.Style { indication := m.state.GuildIsUnread(guildID, ningen.GuildUnreadOpts{IncludeMutedCategories: true}) return unreadStyle(indication) } -func (m *Model) channelNodeStyle(channel discord.Channel) tview.Style { +func (m Model) channelNodeStyle(channel discord.Channel) tview.Style { unread := unreadStyle(m.state.ChannelIsUnread(channel.ID, ningen.UnreadOpts{IncludeMutedCategories: true})) if channel.Type != discord.DirectMessage || len(channel.DMRecipients) != 1 { return unread @@ -197,7 +197,7 @@ func (m *Model) channelNodeStyle(channel discord.Channel) tview.Style { return tview.MergeStyle(m.cfg.Theme.GuildsTree.StatusStyle(presence.Status), unread) } -func (m *Model) createGuildNode(parent *tree.Node, guild discord.Guild) { +func (m Model) createGuildNode(parent *tree.Node, guild discord.Guild) { guildNode := tree.NewNode(guild.Name). SetReference(guild.ID). SetExpandable(true). @@ -208,7 +208,7 @@ func (m *Model) createGuildNode(parent *tree.Node, guild discord.Guild) { m.nodes[discord.Snowflake(guild.ID)] = guildNode } -func (m *Model) createChannelNode(parent *tree.Node, channel discord.Channel) { +func (m Model) createChannelNode(parent *tree.Node, channel discord.Channel) { if channel.Type != discord.DirectMessage && channel.Type != discord.GroupDM && channel.Type != discord.GuildCategory && !m.state.HasPermissions(channel.ID, discord.PermissionViewChannel) { return } @@ -234,7 +234,7 @@ func (m *Model) createChannelNode(parent *tree.Node, channel discord.Channel) { m.nodes[discord.Snowflake(channel.ID)] = channelNode } -func (m *Model) setNodeLineStyle(node *tree.Node, style tview.Style) { +func (m Model) setNodeLineStyle(node *tree.Node, style tview.Style) { line := node.Line() for i := range line { line[i].Style = style @@ -242,7 +242,7 @@ func (m *Model) setNodeLineStyle(node *tree.Node, style tview.Style) { node.SetLine(line) } -func (m *Model) createChannelNodes(node *tree.Node, channels []discord.Channel) { +func (m Model) createChannelNodes(node *tree.Node, channels []discord.Channel) { // Preserve exact ordering semantics: // 1) top-level non-categories (in input order), // 2) categories that have at least one child in the source slice (in input order), @@ -358,7 +358,7 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) { } // isUnread reports whether node is an unread channel, or a guild or the direct messages whose channels are not shown yet and has one. -func (m *Model) isUnread(node *tree.Node) bool { +func (m Model) isUnread(node *tree.Node) bool { if len(node.Children()) > 0 { return false } @@ -396,7 +396,7 @@ func adjacentNode(root, current *tree.Node, match func(*tree.Node) bool, previou return nodes[after%len(nodes)] } -func (m *Model) findNodeByReference(reference any) *tree.Node { +func (m Model) findNodeByReference(reference any) *tree.Node { switch ref := reference.(type) { case discord.GuildID: return m.nodes[discord.Snowflake(ref)] @@ -418,7 +418,7 @@ func (m *Model) findNodeByReference(reference any) *tree.Node { } } -func (m *Model) findNodeByChannelID(channelID discord.ChannelID) *tree.Node { +func (m Model) findNodeByChannelID(channelID discord.ChannelID) *tree.Node { channel, err := m.state.Cabinet.Channel(channelID) if err != nil { slog.Error("failed to get channel", "channel_id", channelID, "err", err) @@ -441,7 +441,7 @@ func (m *Model) findNodeByChannelID(channelID discord.ChannelID) *tree.Node { return node } -func (m *Model) expandPathToNode(node *tree.Node) { +func (m Model) expandPathToNode(node *tree.Node) { if node == nil { return } diff --git a/internal/ui/chat/guildstree/msg.go b/internal/ui/chat/guildstree/msg.go index d2d470a..dc8b436 100644 --- a/internal/ui/chat/guildstree/msg.go +++ b/internal/ui/chat/guildstree/msg.go @@ -44,7 +44,7 @@ func (m *Model) navigate(channelID discord.ChannelID) tview.Cmd { return m.selectNode(node) } -func (m *Model) selectNode(node *tree.Node) tview.Cmd { +func (m Model) selectNode(node *tree.Node) tview.Cmd { if len(node.Children()) != 0 { node.SetExpanded(!node.Expanded()) return nil diff --git a/internal/ui/chat/memberstree/model.go b/internal/ui/chat/memberstree/model.go index e528db4..3b32100 100644 --- a/internal/ui/chat/memberstree/model.go +++ b/internal/ui/chat/memberstree/model.go @@ -135,7 +135,7 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) { } // rebuild recreates the tree from the member list of the selected channel. -func (m *Model) rebuild() { +func (m Model) rebuild() { m.root.ClearChildren() channel := m.channel switch { @@ -155,7 +155,7 @@ func (m *Model) rebuild() { }) } -func (m *Model) addGuildMembers(channel *discord.Channel) { +func (m Model) addGuildMembers(channel *discord.Channel) { list, err := m.state.MemberState.GetMemberList(channel.GuildID, channel.ID) if err != nil { // The list has yet to arrive. @@ -181,7 +181,7 @@ func (m *Model) addGuildMembers(channel *discord.Channel) { }) } -func (m *Model) addRecipients(recipients []discord.User) { +func (m Model) addRecipients(recipients []discord.User) { recipients = slices.Clone(recipients) // Discord leaves the current user out of the recipients. if me, err := m.state.Cabinet.Me(); err == nil { @@ -200,7 +200,7 @@ func (m *Model) addRecipients(recipients []discord.User) { } } -func (m *Model) addGroup(guildID discord.GuildID, group gateway.GuildMemberListGroup) *tree.Node { +func (m Model) addGroup(guildID discord.GuildID, group gateway.GuildMemberListGroup) *tree.Node { name := "Members" var style tview.Style switch group.ID { @@ -232,7 +232,7 @@ func (m *Model) addGroup(guildID discord.GuildID, group gateway.GuildMemberListG } // addMember adds a member as Discord shows it: a status dot, then the name in the color of their highest colored role, dimmed when offline. -func (m *Model) addMember(group *tree.Node, guildID discord.GuildID, member discord.Member, index int) { +func (m Model) addMember(group *tree.Node, guildID discord.GuildID, member discord.Member, index int) { name := cmp.Or(member.Nick, member.User.DisplayName, member.User.Username) var nameStyle tview.Style color, ok := state.MemberColor(&member, func(id discord.RoleID) *discord.Role { @@ -254,13 +254,13 @@ func (m *Model) addMember(group *tree.Node, guildID discord.GuildID, member disc const statusMarker = "●" // setStatus styles the status dot and name of a member line for status. -func (m *Model) setStatus(line richtext.Line, status discord.Status) { +func (m Model) setStatus(line richtext.Line, status discord.Status) { line[0].Style = m.cfg.Theme.MembersTree.StatusStyle(status) line[2].Style = line[2].Style.Dim(status == discord.OfflineStatus) } // reuse takes the node of id out of the index, so that it is reused once, or else returns a new node. Either way the node has no children. -func (m *Model) reuse(id string) *tree.Node { +func (m Model) reuse(id string) *tree.Node { node, ok := m.nodes[id] if !ok { return tree.NewNode("") @@ -269,7 +269,7 @@ func (m *Model) reuse(id string) *tree.Node { return node.ClearChildren() } -func (m *Model) refreshStatus(userID discord.UserID) { +func (m Model) refreshStatus(userID discord.UserID) { if node, ok := m.nodes[userID.String()]; ok { line := node.Line() m.setStatus(line, m.status(m.channel.GuildID, userID)) @@ -277,7 +277,7 @@ func (m *Model) refreshStatus(userID discord.UserID) { } } -func (m *Model) status(guildID discord.GuildID, userID discord.UserID) discord.Status { +func (m Model) status(guildID discord.GuildID, userID discord.UserID) discord.Status { presence, err := m.state.Cabinet.Presence(guildID, userID) if err != nil { return discord.OfflineStatus @@ -299,7 +299,7 @@ func statusRank(status discord.Status) int { } } -func (m *Model) parent(node *tree.Node) *tree.Node { +func (m Model) parent(node *tree.Node) *tree.Node { path := m.root.PathTo(node) if len(path) < 3 { return nil diff --git a/internal/ui/chat/memberstree/msg.go b/internal/ui/chat/memberstree/msg.go index c446cac..037c867 100644 --- a/internal/ui/chat/memberstree/msg.go +++ b/internal/ui/chat/memberstree/msg.go @@ -15,7 +15,7 @@ type SelectedMsg struct { } // requestSelectedMembers requests the chunk of the member list after the selected member, so that the members below it load before they are reached. -func (m *Model) requestSelectedMembers() tview.Cmd { +func (m Model) requestSelectedMembers() tview.Cmd { node := m.selectionState.CurrentNode() if node == nil { return nil @@ -28,7 +28,7 @@ func (m *Model) requestSelectedMembers() tview.Cmd { } // requestMembers requests the member list of the selected guild channel up to chunk. -func (m *Model) requestMembers(chunk int) tview.Cmd { +func (m Model) requestMembers(chunk int) tview.Cmd { channel := m.channel if !m.Shown() || !channel.GuildID.IsValid() { return nil diff --git a/internal/ui/chat/messageslist/model.go b/internal/ui/chat/messageslist/model.go index 1c1e4e0..de24cc7 100644 --- a/internal/ui/chat/messageslist/model.go +++ b/internal/ui/chat/messageslist/model.go @@ -80,33 +80,33 @@ func NewModel(cfg *config.Config, state *ningen.State) Model { } // View shows the messages in a box titled with the channel and footed with footer, such as who is typing. -func (ml Model) View(focused bool, footer string) tview.Widget { - return ui.Box(ml.listView(focused), &ml.cfg.Theme, focused).Title(ml.title()).Footer(footer) +func (m Model) View(focused bool, footer string) tview.Widget { + return ui.Box(m.listView(focused), &m.cfg.Theme, focused).Title(m.title()).Footer(footer) } // title returns the selected channel and its topic, or "Messages" when none is selected. -func (ml *Model) title() string { - if ml.channel == nil { +func (m Model) title() string { + if m.channel == nil { return "Messages" } - title := ui.ChannelToString(*ml.channel, ml.cfg.Icons, ml.state) - if topic := ml.channel.Topic; topic != "" { + title := ui.ChannelToString(*m.channel, m.cfg.Icons, m.state) + if topic := m.channel.Topic; topic != "" { title += " - " + topic } return title } -func (ml *Model) listView(focused bool) list.Widget { - cfg := ml.cfg.Theme.ScrollBar +func (m Model) listView(focused bool) list.Widget { + cfg := m.cfg.Theme.ScrollBar bar := scrollbar.New(). SymbolSet(cfg.SymbolSet.SymbolSet). Style(cfg.TrackStyle.Style). ThumbStyle(cfg.ThumbStyle.Style) - return list.New(ml.selectionState, len(ml.items), ml.buildItem). - SelectedStyle(ml.cfg.Theme.MessagesList.SelectedMessageStyle.Style). + return list.New(m.selectionState, len(m.items), m.buildItem). + SelectedStyle(m.cfg.Theme.MessagesList.SelectedMessageStyle.Style). ScrollBar(bar, cfg.Visibility.ScrollBarVisibility). TrackEnd(true). - Keybind(scrollKeybind(ml.cfg.Keybinds.MessagesList.ScrollKeybinds)). + Keybind(scrollKeybind(m.cfg.Keybinds.MessagesList.ScrollKeybinds)). Focused(focused). OnChange(func(a list.Change) tview.Msg { return listMsg(a) }) } @@ -128,127 +128,127 @@ func scrollKeybind(kbs config.ScrollKeybinds) func(tview.KeyMsg) list.Action { } } -func (ml *Model) cursor() int { - return ml.selectionState.Cursor() +func (m Model) cursor() int { + return m.selectionState.Cursor() } -func (ml *Model) setCursor(index int) { - ml.selectionState.SetCursor(index) +func (m *Model) setCursor(index int) { + m.selectionState.SetCursor(index) } // SetChannel shows messages of channel, newest first as Discord sends them, scrolled to the newest, and requests the members who wrote them. -func (ml *Model) SetChannel(channel *discord.Channel, messages []discord.Message) tview.Cmd { - ml.channel = channel - ml.reset() - ml.setMessages(messages) +func (m *Model) SetChannel(channel *discord.Channel, messages []discord.Message) tview.Cmd { + m.channel = channel + m.reset() + m.setMessages(messages) if channel.GuildID.IsValid() { - return tview.Batch(ml.requestGuildMembers(channel.GuildID, messages), ml.loadPreviews()) + return tview.Batch(m.requestGuildMembers(channel.GuildID, messages), m.loadPreviews()) } - return ml.loadPreviews() + return m.loadPreviews() } // ShowNewest clears the selection and scrolls to the newest message, which then stays in view as messages arrive until the user scrolls up. -func (ml *Model) ShowNewest() { - ml.clearSelection() - ml.selectionState.ScrollToEnd() +func (m *Model) ShowNewest() { + m.clearSelection() + m.selectionState.ScrollToEnd() } // UpdateMessage replaces the shown message with the same ID. -func (ml *Model) UpdateMessage(message discord.Message) tview.Cmd { - ml.setMessage(ml.indexOf(message.ChannelID, message.ID), message) - return ml.loadPreviews() +func (m *Model) UpdateMessage(message discord.Message) tview.Cmd { + m.setMessage(m.indexOf(message.ChannelID, message.ID), message) + return m.loadPreviews() } // RefreshMessage shows the cached state of a message, such as after a reaction. -func (ml *Model) RefreshMessage(channelID discord.ChannelID, id discord.MessageID) { - if message, err := ml.state.Cabinet.Message(channelID, id); err == nil { - ml.setMessage(ml.indexOf(channelID, id), *message) +func (m *Model) RefreshMessage(channelID discord.ChannelID, id discord.MessageID) { + if message, err := m.state.Cabinet.Message(channelID, id); err == nil { + m.setMessage(m.indexOf(channelID, id), *message) } } // DeleteMessage removes the shown message with id. -func (ml *Model) DeleteMessage(channelID discord.ChannelID, id discord.MessageID) { - ml.deleteMessage(ml.indexOf(channelID, id)) +func (m *Model) DeleteMessage(channelID discord.ChannelID, id discord.MessageID) { + m.deleteMessage(m.indexOf(channelID, id)) } // indexOf returns the index of the message with id in the channel, or -1 if it is not shown. -func (ml *Model) indexOf(channelID discord.ChannelID, id discord.MessageID) int { - if ml.channel == nil || ml.channel.ID != channelID { +func (m Model) indexOf(channelID discord.ChannelID, id discord.MessageID) int { + if m.channel == nil || m.channel.ID != channelID { return -1 } - return slices.IndexFunc(ml.items, func(item messageItem) bool { return !item.separator && item.message.ID == id }) + return slices.IndexFunc(m.items, func(item messageItem) bool { return !item.separator && item.message.ID == id }) } -func (ml *Model) reset() { - ml.items = nil - ml.selectionState = list.NewSelectionState() - ml.selectionState.ScrollToEnd() +func (m *Model) reset() { + m.items = nil + m.selectionState = list.NewSelectionState() + m.selectionState.ScrollToEnd() } -func (ml *Model) setMessages(messages []discord.Message) { - ml.items = make([]messageItem, 0, len(messages)) +func (m *Model) setMessages(messages []discord.Message) { + m.items = make([]messageItem, 0, len(messages)) for _, message := range slices.Backward(messages) { - ml.items = append(ml.items, messageItem{message: message}) + m.items = append(m.items, messageItem{message: message}) } - ml.rebuildItems() + m.rebuildItems() } -func (ml *Model) AddMessage(message discord.Message) tview.Cmd { +func (m *Model) AddMessage(message discord.Message) tview.Cmd { // Appending keeps the items before it as they are, so only the separator of a new day is added rather than all rebuilt. - if ml.cfg.DateSeparator.Enabled && (len(ml.items) == 0 || !sameLocalDate(ml.items[len(ml.items)-1].message.Timestamp, message.Timestamp)) { - ml.items = append(ml.items, messageItem{separator: true}) + if m.cfg.DateSeparator.Enabled && (len(m.items) == 0 || !sameLocalDate(m.items[len(m.items)-1].message.Timestamp, message.Timestamp)) { + m.items = append(m.items, messageItem{separator: true}) } - ml.items = append(ml.items, messageItem{message: message}) - return ml.loadPreviews() + m.items = append(m.items, messageItem{message: message}) + return m.loadPreviews() } -func (ml *Model) setMessage(index int, message discord.Message) { - if index < 0 || index >= len(ml.items) || ml.items[index].separator { +func (m *Model) setMessage(index int, message discord.Message) { + if index < 0 || index >= len(m.items) || m.items[index].separator { return } - ml.items[index] = messageItem{message: message, previews: ml.items[index].previews} - ml.rebuildItems() + m.items[index] = messageItem{message: message, previews: m.items[index].previews} + m.rebuildItems() } -func (ml *Model) deleteMessage(index int) { - if index < 0 || index >= len(ml.items) || ml.items[index].separator { +func (m *Model) deleteMessage(index int) { + if index < 0 || index >= len(m.items) || m.items[index].separator { return } - cursor := ml.cursor() + cursor := m.cursor() if cursor == index { - cursor = ml.messageIndex(index, -1) + cursor = m.messageIndex(index, -1) if cursor == -1 { - cursor = ml.messageIndex(index, 1) + cursor = m.messageIndex(index, 1) } } if cursor > index { cursor-- } - ml.items = slices.Delete(ml.items, index, index+1) - ml.setCursor(cursor) - ml.rebuildItems() + m.items = slices.Delete(m.items, index, index+1) + m.setCursor(cursor) + m.rebuildItems() } -func (ml *Model) clearSelection() { - ml.setCursor(-1) +func (m *Model) clearSelection() { + m.setCursor(-1) } -func (ml *Model) buildItem(index int) tview.Widget { - if index < 0 || index >= len(ml.items) { +func (m Model) buildItem(index int) tview.Widget { + if index < 0 || index >= len(m.items) { return nil } - item := &ml.items[index] + item := &m.items[index] if item.separator { - date := ml.items[index+1].message.Timestamp.Time().In(time.Local).Format(ml.cfg.DateSeparator.Format) - return dateSeparator{date: date, fill: ml.cfg.DateSeparator.Character, style: ml.cfg.Theme.MessagesList.MessageStyle.Style.Dim(true)} + date := m.items[index+1].message.Timestamp.Time().In(time.Local).Format(m.cfg.DateSeparator.Format) + return dateSeparator{date: date, fill: m.cfg.DateSeparator.Character, style: m.cfg.Theme.MessagesList.MessageStyle.Style.Dim(true)} } if item.view == nil { message := item.message view := &messageView{render: func(width int) richtext.Text { - return ml.renderMessage(message, ml.cfg.Theme.MessagesList.MessageStyle.Style, width) + return m.renderMessage(message, m.cfg.Theme.MessagesList.MessageStyle.Style, width) }} for _, s := range previewSources(message) { if preview, ok := item.previews[s.proxy]; ok { @@ -333,22 +333,22 @@ func (d dateSeparator) Draw(screen tview.Screen, area tview.Rectangle) { func (dateSeparator) Handle(msg tview.Msg, area tview.Rectangle) tview.Msg { return msg } -func (ml *Model) renderMessage(message discord.Message, baseStyle tview.Style, width int) richtext.Text { +func (m Model) renderMessage(message discord.Message, baseStyle tview.Style, width int) richtext.Text { builder := new(richtext.Builder) - ml.writeMessage(builder, message, baseStyle, width) + m.writeMessage(builder, message, baseStyle, width) return builder.Finish() } // rebuildItems replaces date separators while retaining message views and selection. -func (ml *Model) rebuildItems() { - items := make([]messageItem, 0, len(ml.items)) - cursor, selected := ml.cursor(), -1 +func (m *Model) rebuildItems() { + items := make([]messageItem, 0, len(m.items)) + cursor, selected := m.cursor(), -1 var previous discord.Timestamp - for i, item := range ml.items { + for i, item := range m.items { if item.separator { continue } - if ml.cfg.DateSeparator.Enabled && (len(items) == 0 || !sameLocalDate(previous, item.message.Timestamp)) { + if m.cfg.DateSeparator.Enabled && (len(items) == 0 || !sameLocalDate(previous, item.message.Timestamp)) { items = append(items, messageItem{separator: true}) } if i == cursor { @@ -357,8 +357,8 @@ func (ml *Model) rebuildItems() { items = append(items, item) previous = item.message.Timestamp } - ml.items = items - ml.setCursor(selected) + m.items = items + m.setCursor(selected) } func sameLocalDate(a discord.Timestamp, b discord.Timestamp) bool { @@ -368,29 +368,29 @@ func sameLocalDate(a discord.Timestamp, b discord.Timestamp) bool { } // messageIndex finds the next message in direction (-1 or 1), excluding start. -func (ml *Model) messageIndex(start, direction int) int { - for i := start + direction; i >= 0 && i < len(ml.items); i += direction { - if !ml.items[i].separator { +func (m Model) messageIndex(start, direction int) int { + for i := start + direction; i >= 0 && i < len(m.items); i += direction { + if !m.items[i].separator { return i } } return -1 } -func (ml *Model) onRowCursorChanged(index int) { - if index < 0 || index >= len(ml.items) || !ml.items[index].separator { +func (m *Model) onRowCursorChanged(index int) { + if index < 0 || index >= len(m.items) || !m.items[index].separator { return } - target := ml.messageIndex(index, -1) + target := m.messageIndex(index, -1) if target == -1 { - target = ml.messageIndex(index, 1) + target = m.messageIndex(index, 1) } - ml.setCursor(target) + m.setCursor(target) } -func (ml *Model) writeMessage(builder *richtext.Builder, message discord.Message, baseStyle tview.Style, width int) { - if ml.cfg.HideBlockedUsers { - isBlocked := ml.state.UserIsBlocked(message.Author.ID) +func (m Model) writeMessage(builder *richtext.Builder, message discord.Message, baseStyle tview.Style, width int) { + if m.cfg.HideBlockedUsers { + isBlocked := m.state.UserIsBlocked(message.Author.ID) if isBlocked { builder.Write("Blocked message", baseStyle.Foreground(color.Red).Bold(true)) return @@ -400,26 +400,26 @@ func (ml *Model) writeMessage(builder *richtext.Builder, message discord.Message switch message.Type { case discord.DefaultMessage: if message.Reference != nil && message.Reference.Type == discord.MessageReferenceTypeForward { - ml.drawForwardedMessage(builder, message, baseStyle) + m.drawForwardedMessage(builder, message, baseStyle) } else { - ml.drawDefaultMessage(builder, message, baseStyle, width) + m.drawDefaultMessage(builder, message, baseStyle, width) } case discord.GuildMemberJoinMessage: - ml.drawTimestamps(builder, message.Timestamp, baseStyle) - ml.drawAuthor(builder, message, baseStyle) + m.drawTimestamps(builder, message.Timestamp, baseStyle) + m.drawAuthor(builder, message, baseStyle) builder.Write("joined the server.", baseStyle) case discord.InlinedReplyMessage: - ml.drawReplyMessage(builder, message, baseStyle, width) + m.drawReplyMessage(builder, message, baseStyle, width) case discord.ChannelPinnedMessage: - ml.drawPinnedMessage(builder, message, baseStyle) + m.drawPinnedMessage(builder, message, baseStyle) default: - ml.drawTimestamps(builder, message.Timestamp, baseStyle) - ml.drawAuthor(builder, message, baseStyle) + m.drawTimestamps(builder, message.Timestamp, baseStyle) + m.drawAuthor(builder, message, baseStyle) } - ml.drawReactions(builder, message.Reactions, baseStyle) + m.drawReactions(builder, message.Reactions, baseStyle) } -func (ml *Model) drawReactions(builder *richtext.Builder, reactions []discord.Reaction, baseStyle tview.Style) { +func (m Model) drawReactions(builder *richtext.Builder, reactions []discord.Reaction, baseStyle tview.Style) { if len(reactions) == 0 { return } @@ -434,34 +434,34 @@ func (ml *Model) drawReactions(builder *richtext.Builder, reactions []discord.Re if reaction.Emoji.IsCustom() { name = ":" + name + ":" } - style := ml.cfg.Theme.MessagesList.ReactionStyle.Style + style := m.cfg.Theme.MessagesList.ReactionStyle.Style if reaction.Me { - style = ml.cfg.Theme.MessagesList.OwnReactionStyle.Style + style = m.cfg.Theme.MessagesList.OwnReactionStyle.Style } builder.Write(name+" "+strconv.Itoa(reaction.Count), tview.MergeStyle(baseStyle, style)) } } -func (ml *Model) formatTimestamp(ts discord.Timestamp) string { - return ts.Time().In(time.Local).Format(ml.cfg.Timestamps.Format) +func (m Model) formatTimestamp(ts discord.Timestamp) string { + return ts.Time().In(time.Local).Format(m.cfg.Timestamps.Format) } -func (ml *Model) drawTimestamps(builder *richtext.Builder, ts discord.Timestamp, baseStyle tview.Style) { +func (m Model) drawTimestamps(builder *richtext.Builder, ts discord.Timestamp, baseStyle tview.Style) { dimStyle := baseStyle.Dim(true) - builder.Write(ml.formatTimestamp(ts)+" ", dimStyle) + builder.Write(m.formatTimestamp(ts)+" ", dimStyle) } -func (ml *Model) drawAuthor(builder *richtext.Builder, message discord.Message, baseStyle tview.Style) { +func (m Model) drawAuthor(builder *richtext.Builder, message discord.Message, baseStyle tview.Style) { name := message.Author.DisplayOrUsername() foreground := tcell.ColorDefault - if member := ml.memberForMessage(message); member != nil { + if member := m.memberForMessage(message); member != nil { if member.Nick != "" { name = member.Nick } color, ok := state.MemberColor(member, func(id discord.RoleID) *discord.Role { - r, _ := ml.state.Cabinet.Role(message.GuildID, id) + r, _ := m.state.Cabinet.Role(message.GuildID, id) return r }) if ok { @@ -474,13 +474,13 @@ func (ml *Model) drawAuthor(builder *richtext.Builder, message discord.Message, builder.Write(" ", baseStyle) } -func (ml *Model) memberForMessage(message discord.Message) *discord.Member { +func (m Model) memberForMessage(message discord.Message) *discord.Member { // Webhooks do not have nicknames or roles. if !message.GuildID.IsValid() || message.WebhookID.IsValid() { return nil } - member, err := ml.state.Cabinet.Member(message.GuildID, message.Author.ID) + member, err := m.state.Cabinet.Member(message.GuildID, message.Author.ID) if err != nil { slog.Error("failed to get member from state", "guild_id", message.GuildID, "member_id", message.Author.ID, "err", err) return nil @@ -490,9 +490,9 @@ func (ml *Model) memberForMessage(message discord.Message) *discord.Member { // drawContent renders the message body and returns the parsed markdown AST together with the source bytes it indexes into, so callers can reuse them instead of re-parsing the same content (see drawEmbeds). // root is nil when markdown rendering is disabled. -func (ml *Model) drawContent(builder *richtext.Builder, message discord.Message, baseStyle tview.Style) (ast.Node, []byte) { - content, root, source := ml.renderContent(message, baseStyle, false) - if ml.cfg.Markdown.Enabled && !builder.LineEmpty() { +func (m Model) drawContent(builder *richtext.Builder, message discord.Message, baseStyle tview.Style) (ast.Node, []byte) { + content, root, source := m.renderContent(message, baseStyle, false) + if m.cfg.Markdown.Enabled && !builder.LineEmpty() { startsWithCodeBlock := false if root != nil { if first := root.FirstChild(); first != nil { @@ -516,12 +516,12 @@ func (ml *Model) drawContent(builder *richtext.Builder, message discord.Message, return root, source } -func (ml *Model) renderContent(message discord.Message, baseStyle tview.Style, forceMarkdown bool) (richtext.Text, ast.Node, []byte) { +func (m Model) renderContent(message discord.Message, baseStyle tview.Style, forceMarkdown bool) (richtext.Text, ast.Node, []byte) { // Keep one rendering path for both normal messages and embed fragments so we preserve mention/link parsing behavior consistently across both. - if forceMarkdown || ml.cfg.Markdown.Enabled { + if forceMarkdown || m.cfg.Markdown.Enabled { c := []byte(message.Content) - root := md.ParseWithMessage(c, *ml.state.Cabinet, &message) - return ml.renderer.RenderText(c, root, baseStyle), root, c + root := md.ParseWithMessage(c, *m.state.Cabinet, &message) + return m.renderer.RenderText(c, root, baseStyle), root, c } b := new(richtext.Builder) @@ -529,7 +529,7 @@ func (ml *Model) renderContent(message discord.Message, baseStyle tview.Style, f return b.Finish(), nil, nil } -func (ml *Model) drawSnapshotContent(builder *richtext.Builder, parent discord.Message, snapshot discord.MessageSnapshotMessage, baseStyle tview.Style) { +func (m Model) drawSnapshotContent(builder *richtext.Builder, parent discord.Message, snapshot discord.MessageSnapshotMessage, baseStyle tview.Style) { // Convert discord.MessageSnapshotMessage to discord.Message with common fields. message := discord.Message{ Type: snapshot.Type, @@ -546,28 +546,28 @@ func (ml *Model) drawSnapshotContent(builder *richtext.Builder, parent discord.M ChannelID: parent.ChannelID, GuildID: parent.GuildID, } - ml.drawContent(builder, message, baseStyle) + m.drawContent(builder, message, baseStyle) } -func (ml *Model) drawDefaultMessage(builder *richtext.Builder, message discord.Message, baseStyle tview.Style, width int) { - if ml.cfg.Timestamps.Enabled { - ml.drawTimestamps(builder, message.Timestamp, baseStyle) +func (m Model) drawDefaultMessage(builder *richtext.Builder, message discord.Message, baseStyle tview.Style, width int) { + if m.cfg.Timestamps.Enabled { + m.drawTimestamps(builder, message.Timestamp, baseStyle) } - ml.drawAuthor(builder, message, baseStyle) - contentRoot, contentSource := ml.drawContent(builder, message, baseStyle) + m.drawAuthor(builder, message, baseStyle) + contentRoot, contentSource := m.drawContent(builder, message, baseStyle) if message.EditedTimestamp.IsValid() { dimStyle := baseStyle.Dim(true) builder.Write(" (edited)", dimStyle) } - ml.drawEmbeds(builder, message, baseStyle, width, contentRoot, contentSource) + m.drawEmbeds(builder, message, baseStyle, width, contentRoot, contentSource) - attachmentStyle := tview.MergeStyle(baseStyle, ml.cfg.Theme.MessagesList.AttachmentStyle.Style) + attachmentStyle := tview.MergeStyle(baseStyle, m.cfg.Theme.MessagesList.AttachmentStyle.Style) for _, a := range message.Attachments { builder.NewLine() - if ml.cfg.Attachments.ShowLinks { + if m.cfg.Attachments.ShowLinks { builder.Write(a.Filename+":", attachmentStyle) builder.NewLine() builder.Write(a.URL, attachmentStyle.Url(a.URL)) @@ -577,7 +577,7 @@ func (ml *Model) drawDefaultMessage(builder *richtext.Builder, message discord.M } } -func (ml *Model) drawEmbeds(builder *richtext.Builder, message discord.Message, baseStyle tview.Style, width int, contentRoot ast.Node, contentSource []byte) { +func (m Model) drawEmbeds(builder *richtext.Builder, message discord.Message, baseStyle tview.Style, width int, contentRoot ast.Node, contentSource []byte) { if len(message.Embeds) == 0 { return } @@ -595,7 +595,7 @@ func (ml *Model) drawEmbeds(builder *richtext.Builder, message discord.Message, contentURLs[u] = struct{}{} } - lineStyles := embedLineStyles(baseStyle, ml.cfg.Theme.MessagesList.Embeds) + lineStyles := embedLineStyles(baseStyle, m.cfg.Theme.MessagesList.Embeds) defaultBarStyle := baseStyle.Dim(true) prefixText := " ▎ " prefixWidth := uniseg.StringWidth(prefixText) @@ -623,7 +623,7 @@ func (ml *Model) drawEmbeds(builder *richtext.Builder, message discord.Message, msg.Content = line.Text lineStyle := lineStyles[line.Kind] // Embed descriptions are always markdown-rendered to match Discord's rich embed semantics, even when message markdown is globally disabled. - rendered, _, _ := ml.renderContent(msg, lineStyle, line.Kind == embedLineDescription) + rendered, _, _ := m.renderContent(msg, lineStyle, line.Kind == embedLineDescription) for _, renderedLine := range rendered { if line.URL != "" { renderedLine = lineWithURL(renderedLine, line.URL) @@ -799,174 +799,174 @@ func isMarkdownEscapable(c byte) bool { } } -func (ml *Model) drawForwardedMessage(builder *richtext.Builder, message discord.Message, baseStyle tview.Style) { +func (m Model) drawForwardedMessage(builder *richtext.Builder, message discord.Message, baseStyle tview.Style) { dimStyle := baseStyle.Dim(true) - ml.drawTimestamps(builder, message.Timestamp, baseStyle) - ml.drawAuthor(builder, message, baseStyle) - builder.Write(ml.cfg.Theme.MessagesList.ForwardedIndicator+" ", dimStyle) - ml.drawSnapshotContent(builder, message, message.MessageSnapshots[0].Message, baseStyle) - builder.Write(" ("+ml.formatTimestamp(message.MessageSnapshots[0].Message.Timestamp)+") ", dimStyle) + m.drawTimestamps(builder, message.Timestamp, baseStyle) + m.drawAuthor(builder, message, baseStyle) + builder.Write(m.cfg.Theme.MessagesList.ForwardedIndicator+" ", dimStyle) + m.drawSnapshotContent(builder, message, message.MessageSnapshots[0].Message, baseStyle) + builder.Write(" ("+m.formatTimestamp(message.MessageSnapshots[0].Message.Timestamp)+") ", dimStyle) } -func (ml *Model) drawReplyMessage(builder *richtext.Builder, message discord.Message, baseStyle tview.Style, width int) { +func (m Model) drawReplyMessage(builder *richtext.Builder, message discord.Message, baseStyle tview.Style, width int) { dimStyle := baseStyle.Dim(true) - builder.Write(ml.cfg.Theme.MessagesList.ReplyIndicator+" ", dimStyle) + builder.Write(m.cfg.Theme.MessagesList.ReplyIndicator+" ", dimStyle) - if m := message.ReferencedMessage; m != nil { - m.GuildID = message.GuildID - ml.drawAuthor(builder, *m, dimStyle) - ml.drawContent(builder, *m, dimStyle) + if reference := message.ReferencedMessage; reference != nil { + reference.GuildID = message.GuildID + m.drawAuthor(builder, *reference, dimStyle) + m.drawContent(builder, *reference, dimStyle) } else { builder.Write("Original message was deleted", dimStyle) } builder.NewLine() - ml.drawDefaultMessage(builder, message, baseStyle, width) + m.drawDefaultMessage(builder, message, baseStyle, width) } -func (ml *Model) drawPinnedMessage(builder *richtext.Builder, message discord.Message, baseStyle tview.Style) { +func (m Model) drawPinnedMessage(builder *richtext.Builder, message discord.Message, baseStyle tview.Style) { builder.Write(message.Author.DisplayOrUsername()+" pinned a message.", baseStyle) } -func (ml *Model) selectedMessage() (*discord.Message, bool) { - cursor := ml.cursor() - if cursor < 0 || cursor >= len(ml.items) || ml.items[cursor].separator { +func (m Model) selectedMessage() (*discord.Message, bool) { + cursor := m.cursor() + if cursor < 0 || cursor >= len(m.items) || m.items[cursor].separator { return nil, false } - return &ml.items[cursor].message, true + return &m.items[cursor].message, true } // Update returns ml changed in response to msg and a command to run, or nil. -func (ml Model) Update(msg tview.Msg) (Model, tview.Cmd) { +func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) { switch msg := msg.(type) { case tview.KeyMsg: switch { - case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.Cancel.Keybind): - ml.clearSelection() - case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.SelectUp.Keybind): - return ml, ml.selectUp() - case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.SelectDown.Keybind): - ml.selectDown() - case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.SelectTop.Keybind): - return ml, ml.selectTop() - case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.SelectBottom.Keybind): - ml.selectBottom() - case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.SelectReply.Keybind): - ml.selectReply() - case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.YankID.Keybind): - return ml, ml.yankMessageID() - case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.YankContent.Keybind): - return ml, ml.yankContent() - case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.YankURL.Keybind): - return ml, ml.yankURL() - case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.Open.Keybind): - return ml, ml.open() - case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.OpenInBrowser.Keybind): - return ml, ml.openInBrowser() - case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.Download.Keybind): - return ml, ml.download() - case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.Reply.Keybind): - return ml, ml.reply(false) - case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.ReplyMention.Keybind): - return ml, ml.reply(true) - case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.Edit.Keybind): - return ml, ml.editSelectedMessage() - case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.Delete.Keybind): - return ml, ml.deleteSelectedMessage() - case keybind.Matches(msg, ml.cfg.Keybinds.MessagesList.DeleteConfirm.Keybind): - return ml, ml.confirmDelete() + case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.Cancel.Keybind): + m.clearSelection() + case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.SelectUp.Keybind): + return m, m.selectUp() + case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.SelectDown.Keybind): + m.selectDown() + case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.SelectTop.Keybind): + return m, m.selectTop() + case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.SelectBottom.Keybind): + m.selectBottom() + case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.SelectReply.Keybind): + m.selectReply() + case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.YankID.Keybind): + return m, m.yankMessageID() + case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.YankContent.Keybind): + return m, m.yankContent() + case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.YankURL.Keybind): + return m, m.yankURL() + case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.Open.Keybind): + return m, m.open() + case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.OpenInBrowser.Keybind): + return m, m.openInBrowser() + case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.Download.Keybind): + return m, m.download() + case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.Reply.Keybind): + return m, m.reply(false) + case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.ReplyMention.Keybind): + return m, m.reply(true) + case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.Edit.Keybind): + return m, m.editSelectedMessage() + case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.Delete.Keybind): + return m, m.deleteSelectedMessage() + case keybind.Matches(msg, m.cfg.Keybinds.MessagesList.DeleteConfirm.Keybind): + return m, m.confirmDelete() } case olderMessagesLoadedMsg: - selectedChannel := ml.channel + selectedChannel := m.channel if selectedChannel == nil || selectedChannel.ID != msg.ChannelID { - return ml, nil + return m, nil } - prevCursor := ml.cursor() + prevCursor := m.cursor() older := make([]messageItem, len(msg.Older)) for i, message := range msg.Older { older[i] = messageItem{message: message} } - ml.items = slices.Concat(older, ml.items) + m.items = slices.Concat(older, m.items) if prevCursor >= 0 { - ml.setCursor(prevCursor + len(older)) + m.setCursor(prevCursor + len(older)) } - ml.rebuildItems() + m.rebuildItems() if selectedChannel.GuildID.IsValid() { - return ml, tview.Batch(ml.requestGuildMembers(selectedChannel.GuildID, msg.Older), ml.loadPreviews()) + return m, tview.Batch(m.requestGuildMembers(selectedChannel.GuildID, msg.Older), m.loadPreviews()) } - return ml, ml.loadPreviews() + return m, m.loadPreviews() case previewLoadedMsg: - index := slices.IndexFunc(ml.items, func(item messageItem) bool { return !item.separator && item.message.ID == msg.messageID }) - if index < 0 || ml.items[index].previews == nil { - return ml, nil + index := slices.IndexFunc(m.items, func(item messageItem) bool { return !item.separator && item.message.ID == msg.messageID }) + if index < 0 || m.items[index].previews == nil { + return m, nil } preview, cmd := msg.preview, tview.Cmd(nil) - if ml.kitty { - ml.kittyImages++ - preview = preview.Kitty(ml.kittyImages%63488 + 1) + if m.kitty { + m.kittyImages++ + preview = preview.Kitty(m.kittyImages%63488 + 1) cmd = preview.Transmit() } - ml.items[index].previews[msg.proxy] = preview + m.items[index].previews[msg.proxy] = preview // Invalidate the view so it is laid out with the preview. - ml.items[index].view = nil - return ml, cmd + m.items[index].view = nil + return m, cmd case tview.ResizeMsg: cols, rows := msg.Size() width, height := msg.PixelSize() if cols > 0 && rows > 0 { - ml.cellWidth, ml.cellHeight = uint(width/cols), uint(height/rows) + m.cellWidth, m.cellHeight = uint(width/cols), uint(height/rows) } case tview.TerminalInfoMsg: - switch ml.cfg.Attachments.Protocol { + switch m.cfg.Attachments.Protocol { case "kitty": - ml.kitty = true + m.kitty = true case "halfblocks": - ml.kitty = false + m.kitty = false default: // WezTerm and Konsole support kitty graphics but not Unicode placeholders. - ml.kitty = msg.Name == "kitty" || msg.Name == "ghostty" + m.kitty = msg.Name == "kitty" || msg.Name == "ghostty" } case listMsg: - top := ml.atTop() - ml.selectionState.Apply(list.Change(msg)) - ml.onRowCursorChanged(ml.cursor()) - if !top && ml.atTop() { - return ml, ml.fetchOlderMessages() + top := m.atTop() + m.selectionState.Apply(list.Change(msg)) + m.onRowCursorChanged(m.cursor()) + if !top && m.atTop() { + return m, m.fetchOlderMessages() } } - return ml, nil + return m, nil } // atTop reports whether the oldest message shown is selected. -func (ml *Model) atTop() bool { - return ml.cursor() >= 0 && ml.cursor() == ml.messageIndex(-1, 1) +func (m Model) atTop() bool { + return m.cursor() >= 0 && m.cursor() == m.messageIndex(-1, 1) } -func (ml *Model) selectDown() { - if ml.cursor() == -1 { - ml.selectBottom() - } else if next := ml.messageIndex(ml.cursor(), 1); next >= 0 { - ml.setCursor(next) +func (m *Model) selectDown() { + if m.cursor() == -1 { + m.selectBottom() + } else if next := m.messageIndex(m.cursor(), 1); next >= 0 { + m.setCursor(next) } } -func (ml *Model) selectTop() tview.Cmd { - if ml.atTop() { +func (m *Model) selectTop() tview.Cmd { + if m.atTop() { return nil } - ml.setCursor(ml.messageIndex(-1, 1)) - return ml.fetchOlderMessages() + m.setCursor(m.messageIndex(-1, 1)) + return m.fetchOlderMessages() } -func (ml *Model) selectBottom() { - ml.setCursor(ml.messageIndex(len(ml.items), -1)) +func (m *Model) selectBottom() { + m.setCursor(m.messageIndex(len(m.items), -1)) } -func (ml *Model) selectReply() { - messages := ml.items - cursor := ml.cursor() +func (m *Model) selectReply() { + messages := m.items + cursor := m.cursor() if cursor < 0 || cursor >= len(messages) { return } @@ -976,7 +976,7 @@ func (ml *Model) selectReply() { return !m.separator && m.message.ID == ref.ID }) if refIdx != -1 { - ml.setCursor(refIdx) + m.setCursor(refIdx) } } } @@ -1065,45 +1065,45 @@ func downloadAttachment(attachment discord.Attachment, destination string) error } // SelectLastOwn selects the last message in the selected channel that the user can edit, and returns it. -func (ml *Model) SelectLastOwn() (discord.Message, bool) { - channel := ml.channel +func (m *Model) SelectLastOwn() (discord.Message, bool) { + channel := m.channel if channel == nil { return discord.Message{}, false } - for i, item := range slices.Backward(ml.items) { + for i, item := range slices.Backward(m.items) { message := item.message - if item.separator || message.ChannelID != channel.ID || !ui.IsMe(ml.state, message.Author.ID) || + if item.separator || message.ChannelID != channel.ID || !ui.IsMe(m.state, message.Author.ID) || !message.ID.IsValid() || message.Content == "" || len(message.MessageSnapshots) > 0 || (message.Type != discord.DefaultMessage && message.Type != discord.InlinedReplyMessage) { continue } - ml.setCursor(i) + m.setCursor(i) return message, true } return discord.Message{}, false } -func (ml *Model) canDeleteMessage(message discord.Message) bool { - return ui.IsMe(ml.state, message.Author.ID) || - (message.GuildID.IsValid() && ml.state.HasPermissions(message.ChannelID, discord.PermissionManageMessages)) +func (m Model) canDeleteMessage(message discord.Message) bool { + return ui.IsMe(m.state, message.Author.ID) || + (message.GuildID.IsValid() && m.state.HasPermissions(message.ChannelID, discord.PermissionManageMessages)) } -func (ml *Model) InvalidateRendered() { - for i := range ml.items { - ml.items[i].view = nil +func (m Model) InvalidateRendered() { + for i := range m.items { + m.items[i].view = nil } } -func (ml Model) ShortHelp() []keybind.Keybind { - cfg := ml.cfg.Keybinds.MessagesList +func (m Model) ShortHelp() []keybind.Keybind { + cfg := m.cfg.Keybinds.MessagesList help := []keybind.Keybind{ cfg.SelectUp.Keybind, cfg.SelectDown.Keybind, cfg.Cancel.Keybind, } - if selectedMessage, ok := ml.selectedMessage(); ok { - if !ui.IsMe(ml.state, selectedMessage.Author.ID) { + if selectedMessage, ok := m.selectedMessage(); ok { + if !ui.IsMe(m.state, selectedMessage.Author.ID) { help = append(help, cfg.Reply.Keybind) } if len(selectedMessage.Attachments) != 0 || len(messageURLs(*selectedMessage)) != 0 { @@ -1114,11 +1114,11 @@ func (ml Model) ShortHelp() []keybind.Keybind { return help } -func (ml Model) FullHelp() [][]keybind.Keybind { - cfg := ml.cfg.Keybinds.MessagesList +func (m Model) FullHelp() [][]keybind.Keybind { + cfg := m.cfg.Keybinds.MessagesList var actions, manage, attachments []keybind.Keybind - if message, ok := ml.selectedMessage(); ok { - mine := ui.IsMe(ml.state, message.Author.ID) + if message, ok := m.selectedMessage(); ok { + mine := ui.IsMe(m.state, message.Author.ID) if !mine { actions = append(actions, cfg.Reply.Keybind, cfg.ReplyMention.Keybind) } @@ -1128,7 +1128,7 @@ func (ml Model) FullHelp() [][]keybind.Keybind { if mine { manage = append(manage, cfg.Edit.Keybind) } - if ml.canDeleteMessage(*message) { + if m.canDeleteMessage(*message) { manage = append(manage, cfg.DeleteConfirm.Keybind) if len(cfg.Delete.Keys()) != 0 { manage = append(manage, cfg.Delete.Keybind) diff --git a/internal/ui/chat/messageslist/msg.go b/internal/ui/chat/messageslist/msg.go index d1167d8..0a6c024 100644 --- a/internal/ui/chat/messageslist/msg.go +++ b/internal/ui/chat/messageslist/msg.go @@ -47,35 +47,35 @@ type ShowAttachmentsMsg picker.Items // listMsg moves or scrolls the messages list. type listMsg list.Change -func (ml *Model) selectUp() tview.Cmd { - cursor := ml.cursor() +func (m *Model) selectUp() tview.Cmd { + cursor := m.cursor() if cursor == -1 { - ml.selectBottom() - } else if previous := ml.messageIndex(cursor, -1); previous >= 0 { - ml.setCursor(previous) + m.selectBottom() + } else if previous := m.messageIndex(cursor, -1); previous >= 0 { + m.setCursor(previous) // Older messages are fetched once as the cursor reaches the top, rather than on each key press there. - if ml.atTop() { - return ml.fetchOlderMessages() + if m.atTop() { + return m.fetchOlderMessages() } } return nil } -func (ml *Model) fetchOlderMessages() tview.Cmd { - selectedChannel := ml.channel +func (m Model) fetchOlderMessages() tview.Cmd { + selectedChannel := m.channel if selectedChannel == nil { return nil } channelID := selectedChannel.ID - first := ml.messageIndex(-1, 1) + first := m.messageIndex(-1, 1) if first == -1 { return nil } - before := ml.items[first].message.ID - limit := uint(ml.cfg.MessagesLimit) + before := m.items[first].message.ID + limit := uint(m.cfg.MessagesLimit) return func() tview.Msg { - messages, err := ml.state.MessagesBefore(channelID, before, limit) + messages, err := m.state.MessagesBefore(channelID, before, limit) if err != nil { slog.Error("failed to fetch older messages", "err", err) return nil @@ -90,8 +90,8 @@ func (ml *Model) fetchOlderMessages() tview.Cmd { } } -func (ml *Model) yankMessageID() tview.Cmd { - selectedMessage, ok := ml.selectedMessage() +func (m Model) yankMessageID() tview.Cmd { + selectedMessage, ok := m.selectedMessage() if !ok { return nil } @@ -104,8 +104,8 @@ func (ml *Model) yankMessageID() tview.Cmd { } } -func (ml *Model) yankContent() tview.Cmd { - selectedMessage, ok := ml.selectedMessage() +func (m Model) yankContent() tview.Cmd { + selectedMessage, ok := m.selectedMessage() if !ok { return nil } @@ -118,8 +118,8 @@ func (ml *Model) yankContent() tview.Cmd { } } -func (ml *Model) yankURL() tview.Cmd { - selectedMessage, ok := ml.selectedMessage() +func (m Model) yankURL() tview.Cmd { + selectedMessage, ok := m.selectedMessage() if !ok { return nil } @@ -132,16 +132,16 @@ func (ml *Model) yankURL() tview.Cmd { } } -func (ml *Model) open() tview.Cmd { - return ml.openWith(ml.openAttachment) +func (m Model) open() tview.Cmd { + return m.openWith(m.openAttachment) } -func (ml *Model) openInBrowser() tview.Cmd { - return ml.openWith(func(attachment discord.Attachment) tview.Cmd { return openURL(attachment.URL) }) +func (m Model) openInBrowser() tview.Cmd { + return m.openWith(func(attachment discord.Attachment) tview.Cmd { return openURL(attachment.URL) }) } -func (ml *Model) openWith(openAttachment func(discord.Attachment) tview.Cmd) tview.Cmd { - selectedMessage, ok := ml.selectedMessage() +func (m Model) openWith(openAttachment func(discord.Attachment) tview.Cmd) tview.Cmd { + selectedMessage, ok := m.selectedMessage() if !ok { return nil } @@ -151,7 +151,7 @@ func (ml *Model) openWith(openAttachment func(discord.Attachment) tview.Cmd) tvi case total == 0: return nil case total > 1: - return ml.showAttachmentsList(urls, selectedMessage.Attachments, openAttachment) + return m.showAttachmentsList(urls, selectedMessage.Attachments, openAttachment) case len(urls) == 1: return openURL(urls[0]) } @@ -159,24 +159,24 @@ func (ml *Model) openWith(openAttachment func(discord.Attachment) tview.Cmd) tvi return openAttachment(selectedMessage.Attachments[0]) } -func (ml *Model) download() tview.Cmd { - selectedMessage, ok := ml.selectedMessage() +func (m Model) download() tview.Cmd { + selectedMessage, ok := m.selectedMessage() if !ok || len(selectedMessage.Attachments) == 0 { return nil } if len(selectedMessage.Attachments) == 1 { attachment := selectedMessage.Attachments[0] - return ml.confirmAttachment(attachment, saveAttachment(attachment)) + return m.confirmAttachment(attachment, saveAttachment(attachment)) } items := make(picker.Items, len(selectedMessage.Attachments)) for i, attachment := range selectedMessage.Attachments { - items[i] = picker.Item{Text: attachment.Filename, Reference: ml.confirmAttachment(attachment, saveAttachment(attachment))} + items[i] = picker.Item{Text: attachment.Filename, Reference: m.confirmAttachment(attachment, saveAttachment(attachment))} } - return ml.showAttachmentsPicker(items) + return m.showAttachmentsPicker(items) } -func (ml *Model) showAttachmentsList(urls []string, attachments []discord.Attachment, openAttachment func(discord.Attachment) tview.Cmd) tview.Cmd { +func (m Model) showAttachmentsList(urls []string, attachments []discord.Attachment, openAttachment func(discord.Attachment) tview.Cmd) tview.Cmd { var items picker.Items for _, attachment := range attachments { items = append(items, picker.Item{Text: attachment.Filename, Reference: openAttachment(attachment)}) @@ -184,19 +184,19 @@ func (ml *Model) showAttachmentsList(urls []string, attachments []discord.Attach for _, url := range urls { items = append(items, picker.Item{Text: url, Reference: openURL(url)}) } - return ml.showAttachmentsPicker(items) + return m.showAttachmentsPicker(items) } -func (ml *Model) showAttachmentsPicker(items picker.Items) tview.Cmd { +func (m Model) showAttachmentsPicker(items picker.Items) tview.Cmd { return func() tview.Msg { return ShowAttachmentsMsg(items) } } -func (ml *Model) openAttachment(attachment discord.Attachment) tview.Cmd { - return ml.confirmAttachment(attachment, openDownloadedAttachment(attachment)) +func (m Model) openAttachment(attachment discord.Attachment) tview.Cmd { + return m.confirmAttachment(attachment, openDownloadedAttachment(attachment)) } -func (ml *Model) confirmAttachment(attachment discord.Attachment, action tview.Cmd) tview.Cmd { - if !ml.cfg.Attachments.AllowedMIMETypes.Has(attachment.ContentType) { +func (m Model) confirmAttachment(attachment discord.Attachment, action tview.Cmd) tview.Cmd { + if !m.cfg.Attachments.AllowedMIMETypes.Has(attachment.ContentType) { return ui.ShowModal( "This attachment type is not allowed and may be unsafe. Continue anyway?", ui.ModalButton{Label: "No"}, @@ -266,14 +266,14 @@ func openURL(url string) tview.Cmd { } } -func (ml *Model) reply(mention bool) tview.Cmd { - selectedMessage, ok := ml.selectedMessage() +func (m Model) reply(mention bool) tview.Cmd { + selectedMessage, ok := m.selectedMessage() if !ok { return nil } name := selectedMessage.Author.DisplayOrUsername() - if member := ml.memberForMessage(*selectedMessage); member != nil && member.Nick != "" { + if member := m.memberForMessage(*selectedMessage); member != nil && member.Nick != "" { name = member.Nick } @@ -281,13 +281,13 @@ func (ml *Model) reply(mention bool) tview.Cmd { return func() tview.Msg { return ReplyMsg{Message: message, Name: name, Mention: mention} } } -func (ml *Model) editSelectedMessage() tview.Cmd { - selectedMessage, ok := ml.selectedMessage() +func (m Model) editSelectedMessage() tview.Cmd { + selectedMessage, ok := m.selectedMessage() if !ok { return nil } - if !ui.IsMe(ml.state, selectedMessage.Author.ID) { + if !ui.IsMe(m.state, selectedMessage.Author.ID) { slog.Error("failed to edit message; not the author", "channel_id", selectedMessage.ChannelID, "message_id", selectedMessage.ID) return nil } @@ -296,47 +296,47 @@ func (ml *Model) editSelectedMessage() tview.Cmd { return func() tview.Msg { return EditMsg(message) } } -func (ml *Model) confirmDelete() tview.Cmd { - selectedMessage, ok := ml.selectedMessage() - if !ok || !ml.canDeleteMessage(*selectedMessage) { +func (m Model) confirmDelete() tview.Cmd { + selectedMessage, ok := m.selectedMessage() + if !ok || !m.canDeleteMessage(*selectedMessage) { return nil } message := *selectedMessage return ui.ShowModal( "Are you sure you want to delete this message?", - ui.ModalButton{Label: "Yes", Cmd: ml.requestDelete(message)}, + ui.ModalButton{Label: "Yes", Cmd: m.requestDelete(message)}, ui.ModalButton{Label: "No"}, ) } -func (ml *Model) deleteSelectedMessage() tview.Cmd { - selectedMessage, ok := ml.selectedMessage() +func (m Model) deleteSelectedMessage() tview.Cmd { + selectedMessage, ok := m.selectedMessage() if !ok { return nil } - return ml.requestDelete(*selectedMessage) + return m.requestDelete(*selectedMessage) } -func (ml *Model) requestDelete(message discord.Message) tview.Cmd { +func (m Model) requestDelete(message discord.Message) tview.Cmd { return func() tview.Msg { - if !ml.canDeleteMessage(message) { + if !m.canDeleteMessage(message) { slog.Error("failed to delete message; missing relevant permissions", "channel_id", message.ChannelID, "message_id", message.ID) return nil } - if err := ml.state.DeleteMessage(message.ChannelID, message.ID, ""); err != nil { + if err := m.state.DeleteMessage(message.ChannelID, message.ID, ""); err != nil { slog.Error("failed to delete message", "channel_id", message.ChannelID, "message_id", message.ID, "err", err) return nil } - if err := ml.state.MessageRemove(message.ChannelID, message.ID); err != nil { + if err := m.state.MessageRemove(message.ChannelID, message.ID); err != nil { slog.Error("failed to delete message", "channel_id", message.ChannelID, "message_id", message.ID, "err", err) } return nil } } -func (ml *Model) requestGuildMembers(guildID discord.GuildID, messages []discord.Message) tview.Cmd { +func (m Model) requestGuildMembers(guildID discord.GuildID, messages []discord.Message) tview.Cmd { usersToFetch := make([]discord.UserID, 0, len(messages)) seen := make(map[discord.UserID]struct{}, len(messages)) @@ -346,7 +346,7 @@ func (ml *Model) requestGuildMembers(guildID discord.GuildID, messages []discord continue } - if member, _ := ml.state.Cabinet.Member(guildID, message.Author.ID); member == nil { + if member, _ := m.state.Cabinet.Member(guildID, message.Author.ID); member == nil { userID := message.Author.ID if _, ok := seen[userID]; !ok { seen[userID] = struct{}{} @@ -360,7 +360,7 @@ func (ml *Model) requestGuildMembers(guildID discord.GuildID, messages []discord } return func() tview.Msg { - if err := ml.state.SendGateway(context.Background(), &gateway.RequestGuildMembersCommand{ + if err := m.state.SendGateway(context.Background(), &gateway.RequestGuildMembersCommand{ GuildIDs: []discord.GuildID{guildID}, UserIDs: usersToFetch, }); err != nil { diff --git a/internal/ui/chat/messageslist/preview.go b/internal/ui/chat/messageslist/preview.go index 0e9fe60..6fb6241 100644 --- a/internal/ui/chat/messageslist/preview.go +++ b/internal/ui/chat/messageslist/preview.go @@ -63,24 +63,24 @@ func (s previewSource) cols(cellWidth, cellHeight uint) int { } // previewCell returns the size of a cell in image pixels: the terminal's for kitty, and one pixel by two for half blocks. -func (ml *Model) previewCell() (width, height uint) { +func (m Model) previewCell() (width, height uint) { switch { - case !ml.kitty: + case !m.kitty: return 1, 2 - case ml.cellWidth == 0 || ml.cellHeight == 0: + case m.cellWidth == 0 || m.cellHeight == 0: return kittyPixels, kittyPixels * 2 } - return ml.cellWidth, ml.cellHeight + return m.cellWidth, m.cellHeight } -func (ml *Model) loadPreviews() tview.Cmd { - if !ml.cfg.Attachments.Preview { +func (m Model) loadPreviews() tview.Cmd { + if !m.cfg.Attachments.Preview { return nil } - cellWidth, cellHeight := ml.previewCell() + cellWidth, cellHeight := m.previewCell() var cmds []tview.Cmd - for i := range ml.items { - item := &ml.items[i] + for i := range m.items { + item := &m.items[i] for _, s := range previewSources(item.message) { if _, ok := item.previews[s.proxy]; ok { continue diff --git a/internal/ui/chat/msg.go b/internal/ui/chat/msg.go index 1f3b984..55e49fc 100644 --- a/internal/ui/chat/msg.go +++ b/internal/ui/chat/msg.go @@ -80,7 +80,7 @@ type typingExpiredMsg struct { } // addTyper shows userID as typing and returns a command that ends it after the typing duration. -func (m *Model) addTyper(userID discord.UserID) tview.Cmd { +func (m Model) addTyper(userID discord.UserID) tview.Cmd { m.typers[userID] = time.Now().Add(composer.TypingDuration) return func() tview.Msg { time.Sleep(composer.TypingDuration) diff --git a/internal/ui/chat/state.go b/internal/ui/chat/state.go index 3a5bd17..05297bf 100644 --- a/internal/ui/chat/state.go +++ b/internal/ui/chat/state.go @@ -38,7 +38,7 @@ func (m *Model) addMessageOrNotify(message *gateway.MessageCreateEvent) tview.Cm return tview.Batch(cmd, m.notify(*message)) } -func (m *Model) notify(message gateway.MessageCreateEvent) tview.Cmd { +func (m Model) notify(message gateway.MessageCreateEvent) tview.Cmd { return func() tview.Msg { if !m.cfg.Notifications.Enabled || m.cfg.Status == discord.DoNotDisturbStatus { return nil @@ -84,12 +84,12 @@ func (m *Model) notify(message gateway.MessageCreateEvent) tview.Cmd { } } -func (m *Model) refreshMemberNames(event *gateway.GuildMembersChunkEvent) tview.Cmd { +func (m Model) refreshMemberNames(event *gateway.GuildMembersChunkEvent) tview.Cmd { m.messagesList.InvalidateRendered() return m.composer.CacheMemberSearch(event) } -func (m *Model) showTypingIndicator(event *gateway.TypingStartEvent) tview.Cmd { +func (m Model) showTypingIndicator(event *gateway.TypingStartEvent) tview.Cmd { if channel := m.selectedChannel; channel == nil || channel.ID != event.ChannelID || ui.IsMe(m.state, event.UserID) { return nil }