refactor(ui): rename chat view type and file

This commit is contained in:
ayn2op 2026-01-01 17:44:35 -05:00
parent f46f357eff
commit 07fa9b172a
7 changed files with 438 additions and 438 deletions

View file

@ -15,7 +15,7 @@ import (
type App struct {
inner *tview.Application
chatView *chat.ChatView
chatView *chat.View
cfg *config.Config
}
@ -70,7 +70,7 @@ func (a *App) Run(token string) error {
}
func (a *App) showChatView(token string) error {
a.chatView = chat.NewChatView(a.inner, a.cfg, a.quit)
a.chatView = chat.NewView(a.inner, a.cfg, a.quit)
if err := a.chatView.OpenState(token); err != nil {
return err
}

View file

@ -1,316 +0,0 @@
package chat
import (
"log/slog"
"sync"
"github.com/ayn2op/discordo/internal/config"
"github.com/ayn2op/discordo/internal/keyring"
"github.com/ayn2op/discordo/internal/notifications"
"github.com/ayn2op/discordo/internal/ui"
"github.com/ayn2op/tview"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/gateway"
"github.com/diamondburned/ningen/v3"
"github.com/diamondburned/ningen/v3/states/read"
"github.com/gdamore/tcell/v3"
)
const (
flexPageName = "flex"
mentionsListPageName = "mentionsList"
attachmentsListPageName = "attachmentsList"
confirmModalPageName = "confirmModal"
)
type ChatView struct {
*tview.Pages
mainFlex *tview.Flex
rightFlex *tview.Flex
guildsTree *guildsTree
messagesList *messagesList
messageInput *messageInput
selectedChannel *discord.Channel
selectedChannelMu sync.RWMutex
app *tview.Application
cfg *config.Config
state *ningen.State
onLogout func()
}
func NewChatView(app *tview.Application, cfg *config.Config, onLogout func()) *ChatView {
chatView := &ChatView{
Pages: tview.NewPages(),
mainFlex: tview.NewFlex(),
rightFlex: tview.NewFlex(),
app: app,
cfg: cfg,
onLogout: onLogout,
}
chatView.guildsTree = newGuildsTree(cfg, chatView)
chatView.messagesList = newMessagesList(cfg, chatView)
chatView.messageInput = newMessageInput(cfg, chatView)
chatView.SetInputCapture(chatView.onInputCapture)
chatView.buildLayout()
return chatView
}
func (cv *ChatView) SelectedChannel() *discord.Channel {
cv.selectedChannelMu.RLock()
defer cv.selectedChannelMu.RUnlock()
return cv.selectedChannel
}
func (cv *ChatView) SetSelectedChannel(channel *discord.Channel) {
cv.selectedChannelMu.Lock()
cv.selectedChannel = channel
cv.selectedChannelMu.Unlock()
}
func (cv *ChatView) buildLayout() {
cv.Clear()
cv.rightFlex.Clear()
cv.mainFlex.Clear()
cv.rightFlex.
SetDirection(tview.FlexRow).
AddItem(cv.messagesList, 0, 1, false).
AddItem(cv.messageInput, 3, 1, false)
// The guilds tree is always focused first at start-up.
cv.mainFlex.
AddItem(cv.guildsTree, 0, 1, true).
AddItem(cv.rightFlex, 0, 4, false)
cv.AddAndSwitchToPage(flexPageName, cv.mainFlex, true)
}
func (cv *ChatView) toggleGuildsTree() {
// The guilds tree is visible if the number of items is two.
if cv.mainFlex.GetItemCount() == 2 {
cv.mainFlex.RemoveItem(cv.guildsTree)
if cv.guildsTree.HasFocus() {
cv.app.SetFocus(cv.mainFlex)
}
} else {
cv.buildLayout()
cv.app.SetFocus(cv.guildsTree)
}
}
func (cv *ChatView) focusGuildsTree() bool {
// The guilds tree is not hidden if the number of items is two.
if cv.mainFlex.GetItemCount() == 2 {
cv.app.SetFocus(cv.guildsTree)
return true
}
return false
}
func (cv *ChatView) focusMessageInput() bool {
if !cv.messageInput.GetDisabled() {
cv.app.SetFocus(cv.messageInput)
return true
}
return false
}
func (cv *ChatView) focusPrevious() {
switch cv.app.GetFocus() {
case cv.guildsTree:
cv.focusMessageInput()
case cv.messagesList: // Handle both a.messagesList and a.flex as well as other edge cases (if there is).
if ok := cv.focusGuildsTree(); !ok {
cv.app.SetFocus(cv.messageInput)
}
case cv.messageInput:
cv.app.SetFocus(cv.messagesList)
}
}
func (cv *ChatView) focusNext() {
switch cv.app.GetFocus() {
case cv.guildsTree:
cv.app.SetFocus(cv.messagesList)
case cv.messagesList:
cv.focusMessageInput()
case cv.messageInput: // Handle both a.messageInput and a.flex as well as other edge cases (if there is).
if ok := cv.focusGuildsTree(); !ok {
cv.app.SetFocus(cv.messagesList)
}
}
}
func (cv *ChatView) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
switch event.Name() {
case cv.cfg.Keys.FocusGuildsTree:
cv.messageInput.removeMentionsList()
cv.focusGuildsTree()
return nil
case cv.cfg.Keys.FocusMessagesList:
cv.messageInput.removeMentionsList()
cv.app.SetFocus(cv.messagesList)
return nil
case cv.cfg.Keys.FocusMessageInput:
cv.focusMessageInput()
return nil
case cv.cfg.Keys.FocusPrevious:
cv.focusPrevious()
return nil
case cv.cfg.Keys.FocusNext:
cv.focusNext()
return nil
case cv.cfg.Keys.Logout:
if cv.onLogout != nil {
cv.onLogout()
}
if err := keyring.DeleteToken(); err != nil {
slog.Error("failed to delete token from keyring", "err", err)
return nil
}
return nil
case cv.cfg.Keys.ToggleGuildsTree:
cv.toggleGuildsTree()
return nil
}
return event
}
func (cv *ChatView) showConfirmModal(prompt string, buttons []string, onDone func(label string)) {
previousFocus := cv.app.GetFocus()
modal := tview.NewModal().
SetText(prompt).
AddButtons(buttons).
SetDoneFunc(func(_ int, buttonLabel string) {
cv.RemovePage(confirmModalPageName).SwitchToPage(flexPageName)
cv.app.SetFocus(previousFocus)
if onDone != nil {
onDone(buttonLabel)
}
})
cv.
AddAndSwitchToPage(confirmModalPageName, ui.Centered(modal, 0, 0), true).
ShowPage(flexPageName)
}
func (cv *ChatView) onReadUpdate(event *read.UpdateEvent) {
var guildNode *tview.TreeNode
cv.guildsTree.
GetRoot().
Walk(func(node, parent *tview.TreeNode) bool {
switch node.GetReference() {
case event.GuildID:
node.SetTextStyle(cv.guildsTree.getGuildNodeStyle(event.GuildID))
guildNode = node
return false
case event.ChannelID:
// private channel
if !event.GuildID.IsValid() {
style := cv.guildsTree.getChannelNodeStyle(event.ChannelID)
node.SetTextStyle(style)
return false
}
}
return true
})
if guildNode != nil {
guildNode.Walk(func(node, parent *tview.TreeNode) bool {
if node.GetReference() == event.ChannelID {
node.SetTextStyle(cv.guildsTree.getChannelNodeStyle(event.ChannelID))
return false
}
return true
})
}
cv.app.Draw()
}
func (cv *ChatView) onReady(r *gateway.ReadyEvent) {
dmNode := tview.NewTreeNode("Direct Messages")
root := cv.guildsTree.
GetRoot().
ClearChildren().
AddChild(dmNode)
for _, folder := range r.UserSettings.GuildFolders {
if folder.ID == 0 && len(folder.GuildIDs) == 1 {
guild, err := cv.state.Cabinet.Guild(folder.GuildIDs[0])
if err != nil {
slog.Error(
"failed to get guild from state",
"guild_id",
folder.GuildIDs[0],
"err",
err,
)
continue
}
cv.guildsTree.createGuildNode(root, *guild)
} else {
cv.guildsTree.createFolderNode(folder)
}
}
cv.guildsTree.SetCurrentNode(root)
cv.app.SetFocus(cv.guildsTree)
cv.app.Draw()
}
func (cv *ChatView) onMessageCreate(message *gateway.MessageCreateEvent) {
if selected := cv.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
cv.messagesList.drawMessage(cv.messagesList, message.Message)
cv.app.Draw()
}
if err := notifications.Notify(cv.state, message, cv.cfg); err != nil {
slog.Error("failed to notify", "err", err, "channel_id", message.ChannelID, "message_id", message.ID)
}
}
func (cv *ChatView) onMessageUpdate(message *gateway.MessageUpdateEvent) {
if selected := cv.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
cv.onMessageDelete(&gateway.MessageDeleteEvent{ID: message.ID, ChannelID: message.ChannelID, GuildID: message.GuildID})
}
}
func (cv *ChatView) onMessageDelete(message *gateway.MessageDeleteEvent) {
if selected := cv.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
messages, err := cv.state.Cabinet.Messages(message.ChannelID)
if err != nil {
slog.Error("failed to get messages from state", "err", err, "channel_id", message.ChannelID)
return
}
cv.messagesList.reset()
cv.messagesList.drawMessages(messages)
cv.app.Draw()
}
}
func (cv *ChatView) onGuildMembersChunk(event *gateway.GuildMembersChunkEvent) {
cv.messagesList.setFetchingChunk(false, uint(len(event.Members)))
}
func (cv *ChatView) onGuildMemberRemove(event *gateway.GuildMemberRemoveEvent) {
cv.messageInput.cache.Invalidate(event.GuildID.String()+" "+event.User.Username, cv.state.MemberState.SearchLimit)
}

View file

@ -19,14 +19,14 @@ import (
type guildsTree struct {
*tview.TreeView
cfg *config.Config
chat *ChatView
chatView *View
}
func newGuildsTree(cfg *config.Config, chat *ChatView) *guildsTree {
func newGuildsTree(cfg *config.Config, chatView *View) *guildsTree {
gt := &guildsTree{
TreeView: tview.NewTreeView(),
cfg: cfg,
chat: chat,
chatView: chatView,
}
gt.Box = ui.ConfigureBox(gt.Box, &cfg.Theme)
@ -52,7 +52,7 @@ func (gt *guildsTree) createFolderNode(folder gateway.GuildFolder) {
gt.GetRoot().AddChild(folderNode)
for _, gID := range folder.GuildIDs {
guild, err := gt.chat.state.Cabinet.Guild(gID)
guild, err := gt.chatView.state.Cabinet.Guild(gID)
if err != nil {
slog.Error("failed to get guild from state", "guild_id", gID, "err", err)
continue
@ -78,12 +78,12 @@ func (gt *guildsTree) unreadStyle(indication ningen.UnreadIndication) tcell.Styl
}
func (gt *guildsTree) getGuildNodeStyle(guildID discord.GuildID) tcell.Style {
indication := gt.chat.state.GuildIsUnread(guildID, ningen.GuildUnreadOpts{UnreadOpts: ningen.UnreadOpts{IncludeMutedCategories: true}})
indication := gt.chatView.state.GuildIsUnread(guildID, ningen.GuildUnreadOpts{UnreadOpts: ningen.UnreadOpts{IncludeMutedCategories: true}})
return gt.unreadStyle(indication)
}
func (gt *guildsTree) getChannelNodeStyle(channelID discord.ChannelID) tcell.Style {
indication := gt.chat.state.ChannelIsUnread(channelID, ningen.UnreadOpts{IncludeMutedCategories: true})
indication := gt.chatView.state.ChannelIsUnread(channelID, ningen.UnreadOpts{IncludeMutedCategories: true})
return gt.unreadStyle(indication)
}
@ -95,7 +95,7 @@ func (gt *guildsTree) createGuildNode(n *tview.TreeNode, guild discord.Guild) {
}
func (gt *guildsTree) createChannelNode(node *tview.TreeNode, channel discord.Channel) {
if channel.Type != discord.DirectMessage && channel.Type != discord.GroupDM && !gt.chat.state.HasPermissions(channel.ID, discord.PermissionViewChannel) {
if channel.Type != discord.DirectMessage && channel.Type != discord.GroupDM && !gt.chatView.state.HasPermissions(channel.ID, discord.PermissionViewChannel) {
return
}
@ -151,9 +151,9 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) {
switch ref := node.GetReference().(type) {
case discord.GuildID:
go gt.chat.state.MemberState.Subscribe(ref)
go gt.chatView.state.MemberState.Subscribe(ref)
channels, err := gt.chat.state.Cabinet.Channels(ref)
channels, err := gt.chatView.state.Cabinet.Channels(ref)
if err != nil {
slog.Error("failed to get channels", "err", err, "guild_id", ref)
return
@ -165,7 +165,7 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) {
gt.createChannelNodes(node, channels)
case discord.ChannelID:
channel, err := gt.chat.state.Cabinet.Channel(ref)
channel, err := gt.chatView.state.Cabinet.Channel(ref)
if err != nil {
slog.Error("failed to get channel", "channel_id", ref)
return
@ -174,7 +174,7 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) {
// Handle forum channels differently - they contain threads, not direct messages
if channel.Type == discord.GuildForum {
// Get all channels from the guild - this includes active threads from GuildCreateEvent
allChannels, err := gt.chat.state.Cabinet.Channels(channel.GuildID)
allChannels, err := gt.chatView.state.Cabinet.Channels(channel.GuildID)
if err != nil {
slog.Error("failed to get channels for forum threads", "err", err, "guild_id", channel.GuildID)
return
@ -200,38 +200,38 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) {
return
}
go gt.chat.state.ReadState.MarkRead(channel.ID, channel.LastMessageID)
go gt.chatView.state.ReadState.MarkRead(channel.ID, channel.LastMessageID)
messages, err := gt.chat.state.Messages(channel.ID, uint(gt.cfg.MessagesLimit))
messages, err := gt.chatView.state.Messages(channel.ID, uint(gt.cfg.MessagesLimit))
if err != nil {
slog.Error("failed to get messages", "err", err, "channel_id", channel.ID, "limit", gt.cfg.MessagesLimit)
return
}
if guildID := channel.GuildID; guildID.IsValid() {
gt.chat.messagesList.requestGuildMembers(guildID, messages)
gt.chatView.messagesList.requestGuildMembers(guildID, messages)
}
gt.chat.SetSelectedChannel(channel)
gt.chatView.SetSelectedChannel(channel)
gt.chat.messagesList.reset()
gt.chat.messagesList.setTitle(*channel)
gt.chat.messagesList.drawMessages(messages)
gt.chat.messagesList.ScrollToEnd()
gt.chatView.messagesList.reset()
gt.chatView.messagesList.setTitle(*channel)
gt.chatView.messagesList.drawMessages(messages)
gt.chatView.messagesList.ScrollToEnd()
hasNoPerm := channel.Type != discord.DirectMessage && channel.Type != discord.GroupDM && !gt.chat.state.HasPermissions(channel.ID, discord.PermissionSendMessages)
gt.chat.messageInput.SetDisabled(hasNoPerm)
hasNoPerm := channel.Type != discord.DirectMessage && channel.Type != discord.GroupDM && !gt.chatView.state.HasPermissions(channel.ID, discord.PermissionSendMessages)
gt.chatView.messageInput.SetDisabled(hasNoPerm)
if hasNoPerm {
gt.chat.messageInput.SetPlaceholder("You do not have permission to send messages in this channel.")
gt.chatView.messageInput.SetPlaceholder("You do not have permission to send messages in this channel.")
} else {
gt.chat.messageInput.SetPlaceholder("Message...")
gt.chatView.messageInput.SetPlaceholder("Message...")
if gt.cfg.AutoFocus {
gt.chat.app.SetFocus(gt.chat.messageInput)
gt.chatView.app.SetFocus(gt.chatView.messageInput)
}
}
case nil: // Direct messages folder
channels, err := gt.chat.state.PrivateChannels()
channels, err := gt.chatView.state.PrivateChannels()
if err != nil {
slog.Error("failed to get private channels", "err", err)
return

View file

@ -40,7 +40,7 @@ var mentionRegex = regexp.MustCompile("@[a-zA-Z0-9._]+")
type messageInput struct {
*tview.TextArea
cfg *config.Config
chat *ChatView
chatView *View
edit bool
sendMessageData *api.SendMessageData
@ -49,11 +49,11 @@ type messageInput struct {
lastSearch time.Time
}
func newMessageInput(cfg *config.Config, chat *ChatView) *messageInput {
func newMessageInput(cfg *config.Config, chatView *View) *messageInput {
mi := &messageInput{
TextArea: tview.NewTextArea(),
cfg: cfg,
chat: chat,
chatView: chatView,
sendMessageData: &api.SendMessageData{},
cache: cache.NewCache(),
mentionsList: tview.NewList(),
@ -96,7 +96,7 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
return tcell.NewEventKey(tcell.KeyCtrlV, "", tcell.ModNone)
case mi.cfg.Keys.MessageInput.Send:
if mi.chat.GetVisibile(mentionsListPageName) {
if mi.chatView.GetVisibile(mentionsListPageName) {
mi.tabComplete()
return nil
}
@ -112,7 +112,7 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
mi.openFilePicker()
return nil
case mi.cfg.Keys.MessageInput.Cancel:
if mi.chat.GetVisibile(mentionsListPageName) {
if mi.chatView.GetVisibile(mentionsListPageName) {
mi.stopTabCompletion()
} else {
mi.reset()
@ -120,12 +120,12 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
return nil
case mi.cfg.Keys.MessageInput.TabComplete:
go mi.chat.app.QueueUpdateDraw(func() { mi.tabComplete() })
go mi.chatView.app.QueueUpdateDraw(func() { mi.tabComplete() })
return nil
}
if mi.cfg.AutocompleteLimit > 0 {
if mi.chat.GetVisibile(mentionsListPageName) {
if mi.chatView.GetVisibile(mentionsListPageName) {
switch event.Name() {
case mi.cfg.Keys.MentionsList.Up:
mi.mentionsList.InputHandler()(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone), nil)
@ -136,7 +136,7 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
}
}
go mi.chat.app.QueueUpdateDraw(func() { mi.tabSuggestion() })
go mi.chatView.app.QueueUpdateDraw(func() { mi.tabSuggestion() })
}
return event
@ -150,7 +150,7 @@ func (mi *messageInput) paste() {
}
func (mi *messageInput) send() {
selected := mi.chat.SelectedChannel()
selected := mi.chatView.SelectedChannel()
if selected == nil {
return
}
@ -169,17 +169,17 @@ func (mi *messageInput) send() {
}
}()
text = processText(mi.chat.state, selected, []byte(text))
text = processText(mi.chatView.state, selected, []byte(text))
if mi.edit {
m, err := mi.chat.messagesList.selectedMessage()
m, err := mi.chatView.messagesList.selectedMessage()
if err != nil {
slog.Error("failed to get selected message", "err", err)
return
}
data := api.EditMessageData{Content: option.NewNullableString(text)}
if _, err := mi.chat.state.EditMessageComplex(m.ChannelID, m.ID, data); err != nil {
if _, err := mi.chatView.state.EditMessageComplex(m.ChannelID, m.ID, data); err != nil {
slog.Error("failed to edit message", "err", err)
}
@ -187,14 +187,14 @@ func (mi *messageInput) send() {
} else {
data := mi.sendMessageData
data.Content = text
if _, err := mi.chat.state.SendMessageComplex(selected.ID, *data); err != nil {
if _, err := mi.chatView.state.SendMessageComplex(selected.ID, *data); err != nil {
slog.Error("failed to send message in channel", "channel_id", selected.ID, "err", err)
}
}
mi.reset()
mi.chat.messagesList.Highlight()
mi.chat.messagesList.ScrollToEnd()
mi.chatView.messagesList.Highlight()
mi.chatView.messagesList.ScrollToEnd()
}
func processText(state *ningen.State, channel *discord.Channel, src []byte) string {
@ -269,7 +269,7 @@ func (mi *messageInput) tabComplete() {
}
pos := posEnd - (len(name) + 1)
selected := mi.chat.SelectedChannel()
selected := mi.chatView.SelectedChannel()
if selected == nil {
return
}
@ -284,7 +284,7 @@ func (mi *messageInput) tabComplete() {
}
} else {
mi.searchMember(gID, name)
members, err := mi.chat.state.Cabinet.Members(gID)
members, err := mi.chatView.state.Cabinet.Members(gID)
if err != nil {
slog.Error("failed to get members from state", "guild_id", gID, "err", err)
return
@ -292,7 +292,7 @@ func (mi *messageInput) tabComplete() {
res := fuzzy.FindFrom(name, memberList(members))
for _, r := range res {
if channelHasUser(mi.chat.state, selected.ID, members[r.Index].User.ID) {
if channelHasUser(mi.chatView.state, selected.ID, members[r.Index].User.ID) {
mi.Replace(pos, posEnd, "@"+members[r.Index].User.Username+" ")
return
}
@ -316,7 +316,7 @@ func (mi *messageInput) tabSuggestion() {
mi.stopTabCompletion()
return
}
selected := mi.chat.SelectedChannel()
selected := mi.chatView.SelectedChannel()
if selected == nil {
return
}
@ -329,7 +329,7 @@ func (mi *messageInput) tabSuggestion() {
if name == "" {
shown = make(map[string]struct{})
// Don't show @me in the list of recent authors
me, err := mi.chat.state.Cabinet.Me()
me, err := mi.chatView.state.Cabinet.Me()
if err != nil {
slog.Error("failed to get client user (me)", "err", err)
} else {
@ -340,7 +340,7 @@ func (mi *messageInput) tabSuggestion() {
// DMs have recipients, not members
if !gID.IsValid() {
if name == "" { // show recent messages' authors
msgs, err := mi.chat.state.Cabinet.Messages(cID)
msgs, err := mi.chatView.state.Cabinet.Messages(cID)
if err != nil {
return
}
@ -353,7 +353,7 @@ func (mi *messageInput) tabSuggestion() {
}
} else {
users := selected.DMRecipients
me, err := mi.chat.state.Cabinet.Me()
me, err := mi.chatView.state.Cabinet.Me()
if err != nil {
slog.Error("failed to get client user (me)", "err", err)
} else {
@ -365,7 +365,7 @@ func (mi *messageInput) tabSuggestion() {
}
}
} else if name == "" { // show recent messages' authors
msgs, err := mi.chat.state.Cabinet.Messages(cID)
msgs, err := mi.chatView.state.Cabinet.Messages(cID)
if err != nil {
return
}
@ -374,8 +374,8 @@ func (mi *messageInput) tabSuggestion() {
continue
}
shown[m.Author.Username] = userDone
mi.chat.state.MemberState.RequestMember(gID, m.Author.ID)
if mem, err := mi.chat.state.Cabinet.Member(gID, m.Author.ID); err == nil {
mi.chatView.state.MemberState.RequestMember(gID, m.Author.ID)
if mem, err := mi.chatView.state.Cabinet.Member(gID, m.Author.ID); err == nil {
if mi.addMentionMember(gID, mem) {
break
}
@ -383,7 +383,7 @@ func (mi *messageInput) tabSuggestion() {
}
} else {
mi.searchMember(gID, name)
mems, err := mi.chat.state.Cabinet.Members(gID)
mems, err := mi.chatView.state.Cabinet.Members(gID)
if err != nil {
slog.Error("fetching members failed", "err", err)
return
@ -393,7 +393,7 @@ func (mi *messageInput) tabSuggestion() {
res = res[:int(mi.cfg.AutocompleteLimit)]
}
for _, r := range res {
if channelHasUser(mi.chat.state, cID, mems[r.Index].User.ID) &&
if channelHasUser(mi.chatView.state, cID, mems[r.Index].User.ID) &&
mi.addMentionMember(gID, &mems[r.Index]) {
break
}
@ -447,22 +447,22 @@ func (mi *messageInput) searchMember(gID discord.GuildID, name string) {
// everything starting with "ab". This will still be true even if a new
// member joins because arikawa loads new members into the state.
if k := key[:len(key)-1]; mi.cache.Exists(k) {
if c := mi.cache.Get(k); c < mi.chat.state.MemberState.SearchLimit {
if c := mi.cache.Get(k); c < mi.chatView.state.MemberState.SearchLimit {
mi.cache.Create(key, c)
return
}
}
// 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 mi.lastSearch.Add(mi.chat.state.MemberState.SearchFrequency).After(time.Now()) {
if mi.lastSearch.Add(mi.chatView.state.MemberState.SearchFrequency).After(time.Now()) {
return
}
mi.lastSearch = time.Now()
mi.chat.messagesList.waitForChunkEvent()
mi.chat.messagesList.setFetchingChunk(true, 0)
mi.chat.state.MemberState.SearchMember(gID, name)
mi.cache.Create(key, mi.chat.messagesList.waitForChunkEvent())
mi.chatView.messagesList.waitForChunkEvent()
mi.chatView.messagesList.setFetchingChunk(true, 0)
mi.chatView.state.MemberState.SearchMember(gID, name)
mi.cache.Create(key, mi.chatView.messagesList.waitForChunkEvent())
}
func (mi *messageInput) showMentionList() {
@ -473,7 +473,7 @@ func (mi *messageInput) showMentionList() {
l := mi.mentionsList
x, _, _, _ := mi.GetInnerRect()
_, y, _, _ := mi.GetRect()
_, _, maxW, maxH := mi.chat.messagesList.GetInnerRect()
_, _, maxW, maxH := mi.chatView.messagesList.GetInnerRect()
if t := int(mi.cfg.Theme.MentionsList.MaxHeight); t != 0 {
maxH = min(maxH, t)
}
@ -496,10 +496,10 @@ func (mi *messageInput) showMentionList() {
l.SetRect(x, y, w, h)
mi.chat.
mi.chatView.
AddAndSwitchToPage(mentionsListPageName, l, false).
ShowPage(flexPageName)
mi.chat.app.SetFocus(mi)
mi.chatView.app.SetFocus(mi)
}
func (mi *messageInput) addMentionMember(gID discord.GuildID, m *discord.Member) bool {
@ -514,14 +514,14 @@ func (mi *messageInput) addMentionMember(gID discord.GuildID, m *discord.Member)
// This avoids a slower member color lookup path.
color, ok := state.MemberColor(m, func(id discord.RoleID) *discord.Role {
r, _ := mi.chat.state.Cabinet.Role(gID, id)
r, _ := mi.chatView.state.Cabinet.Role(gID, id)
return r
})
if ok {
name = fmt.Sprintf("[%s]%s[-]", color, name)
}
presence, err := mi.chat.state.Cabinet.Presence(gID, m.User.ID)
presence, err := mi.chatView.state.Cabinet.Presence(gID, m.User.ID)
if err != nil {
slog.Info("failed to get presence from state", "guild_id", gID, "user_id", m.User.ID, "err", err)
} else if presence.Status == discord.OfflineStatus {
@ -538,7 +538,7 @@ func (mi *messageInput) addMentionUser(user *discord.User) {
}
name := user.DisplayOrUsername()
presence, err := mi.chat.state.Cabinet.Presence(discord.NullGuildID, user.ID)
presence, err := mi.chatView.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 {
@ -550,7 +550,7 @@ func (mi *messageInput) addMentionUser(user *discord.User) {
// used by chatView
func (mi *messageInput) removeMentionsList() {
mi.chat.
mi.chatView.
RemovePage(mentionsListPageName).
SwitchToPage(flexPageName)
}
@ -559,7 +559,7 @@ func (mi *messageInput) stopTabCompletion() {
if mi.cfg.AutocompleteLimit > 0 {
mi.mentionsList.Clear()
mi.removeMentionsList()
mi.chat.app.SetFocus(mi)
mi.chatView.app.SetFocus(mi)
}
}
@ -579,7 +579,7 @@ func (mi *messageInput) editor() {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
mi.chat.app.Suspend(func() {
mi.chatView.app.Suspend(func() {
err := cmd.Run()
if err != nil {
slog.Error("failed to run command", "args", cmd.Args, "err", err)
@ -597,7 +597,7 @@ func (mi *messageInput) editor() {
}
func (mi *messageInput) openFilePicker() {
if mi.chat.SelectedChannel() == nil {
if mi.chatView.SelectedChannel() == nil {
return
}

View file

@ -36,7 +36,7 @@ import (
type messagesList struct {
*tview.TextView
cfg *config.Config
chat *ChatView
chatView *View
selectedMessageID discord.MessageID
renderer *markdown.Renderer
@ -49,11 +49,11 @@ type messagesList struct {
}
}
func newMessagesList(cfg *config.Config, chat *ChatView) *messagesList {
func newMessagesList(cfg *config.Config, chatView *View) *messagesList {
ml := &messagesList{
TextView: tview.NewTextView(),
cfg: cfg,
chat: chat,
chatView: chatView,
renderer: markdown.NewRenderer(cfg.Theme.MessagesList),
}
@ -100,7 +100,7 @@ func (ml *messagesList) drawMessage(writer io.Writer, message discord.Message) {
fmt.Fprintf(writer, `["%s"]`, message.ID)
if ml.cfg.HideBlockedUsers {
isBlocked := ml.chat.state.UserIsBlocked(message.Author.ID)
isBlocked := ml.chatView.state.UserIsBlocked(message.Author.ID)
if isBlocked {
io.WriteString(writer, "[:red:b]Blocked message[:-:-]")
return
@ -148,7 +148,7 @@ func (ml *messagesList) drawAuthor(w io.Writer, message discord.Message) {
// Webhooks do not have nicknames or roles.
if message.GuildID.IsValid() && !message.WebhookID.IsValid() {
member, err := ml.chat.state.Cabinet.Member(message.GuildID, message.Author.ID)
member, err := ml.chatView.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)
} else {
@ -157,7 +157,7 @@ func (ml *messagesList) drawAuthor(w io.Writer, message discord.Message) {
}
color, ok := state.MemberColor(member, func(id discord.RoleID) *discord.Role {
r, _ := ml.chat.state.Cabinet.Role(message.GuildID, id)
r, _ := ml.chatView.state.Cabinet.Role(message.GuildID, id)
return r
})
if ok {
@ -171,8 +171,8 @@ func (ml *messagesList) drawAuthor(w io.Writer, message discord.Message) {
func (ml *messagesList) drawContent(w io.Writer, message discord.Message) {
c := []byte(tview.Escape(message.Content))
if ml.chat.cfg.Markdown {
ast := discordmd.ParseWithMessage(c, *ml.chat.state.Cabinet, &message, false)
if ml.chatView.cfg.Markdown {
ast := discordmd.ParseWithMessage(c, *ml.chatView.state.Cabinet, &message, false)
ml.renderer.Render(w, c, ast)
} else {
w.Write(c) // write the content as is
@ -243,12 +243,12 @@ func (ml *messagesList) selectedMessage() (*discord.Message, error) {
return nil, errors.New("no message is currently selected")
}
selected := ml.chat.SelectedChannel()
selected := ml.chatView.SelectedChannel()
if selected == nil {
return nil, errors.New("no channel is currently selected")
}
m, err := ml.chat.state.Cabinet.Message(selected.ID, ml.selectedMessageID)
m, err := ml.chatView.state.Cabinet.Message(selected.ID, ml.selectedMessageID)
if err != nil {
return nil, fmt.Errorf("failed to retrieve selected message: %w", err)
}
@ -288,12 +288,12 @@ func (ml *messagesList) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
}
func (ml *messagesList) _select(name string) {
selectedChannel := ml.chat.SelectedChannel()
selectedChannel := ml.chatView.SelectedChannel()
if selectedChannel == nil {
return
}
messages, err := ml.chat.state.Cabinet.Messages(selectedChannel.ID)
messages, err := ml.chatView.state.Cabinet.Messages(selectedChannel.ID)
if err != nil {
slog.Error("failed to get messages", "err", err, "channel_id", selectedChannel.ID)
return
@ -455,8 +455,8 @@ func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord
SetHighlightFullLine(true).
ShowSecondaryText(false).
SetDoneFunc(func() {
ml.chat.RemovePage(attachmentsListPageName).SwitchToPage(flexPageName)
ml.chat.app.SetFocus(ml)
ml.chatView.RemovePage(attachmentsListPageName).SwitchToPage(flexPageName)
ml.chatView.app.SetFocus(ml)
})
list.
SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
@ -491,7 +491,7 @@ func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord
})
}
ml.chat.
ml.chatView.
AddAndSwitchToPage(attachmentsListPageName, ui.Centered(list, 0, 0), true).
ShowPage(flexPageName)
}
@ -544,7 +544,7 @@ func (ml *messagesList) reply(mention bool) {
name := msg.Author.DisplayOrUsername()
if msg.GuildID.IsValid() {
member, err := ml.chat.state.Cabinet.Member(msg.GuildID, msg.Author.ID)
member, err := ml.chatView.state.Cabinet.Member(msg.GuildID, msg.Author.ID)
if err != nil {
slog.Error("failed to get member from state", "guild_id", msg.GuildID, "member_id", msg.Author.ID, "err", err)
} else {
@ -554,7 +554,7 @@ func (ml *messagesList) reply(mention bool) {
}
}
data := ml.chat.messageInput.sendMessageData
data := ml.chatView.messageInput.sendMessageData
data.Reference = &discord.MessageReference{MessageID: ml.selectedMessageID}
data.AllowedMentions = &api.AllowedMentions{RepliedUser: option.False}
@ -564,9 +564,9 @@ func (ml *messagesList) reply(mention bool) {
title = "[@] " + title
}
ml.chat.messageInput.sendMessageData = data
ml.chat.messageInput.SetTitle(title + name)
ml.chat.app.SetFocus(ml.chat.messageInput)
ml.chatView.messageInput.sendMessageData = data
ml.chatView.messageInput.SetTitle(title + name)
ml.chatView.app.SetFocus(ml.chatView.messageInput)
}
func (ml *messagesList) edit() {
@ -576,7 +576,7 @@ func (ml *messagesList) edit() {
return
}
me, err := ml.chat.state.Cabinet.Me()
me, err := ml.chatView.state.Cabinet.Me()
if err != nil {
slog.Error("failed to get client user (me)", "err", err)
return
@ -587,10 +587,10 @@ func (ml *messagesList) edit() {
return
}
ml.chat.messageInput.SetTitle("Editing")
ml.chat.messageInput.edit = true
ml.chat.messageInput.SetText(message.Content, true)
ml.chat.app.SetFocus(ml.chat.messageInput)
ml.chatView.messageInput.SetTitle("Editing")
ml.chatView.messageInput.edit = true
ml.chatView.messageInput.SetText(message.Content, true)
ml.chatView.app.SetFocus(ml.chatView.messageInput)
}
func (ml *messagesList) confirmDelete() {
@ -600,7 +600,7 @@ func (ml *messagesList) confirmDelete() {
}
}
ml.chat.showConfirmModal(
ml.chatView.showConfirmModal(
"Are you sure you want to delete this message?",
[]string{"Yes", "No"},
onChoice,
@ -615,24 +615,24 @@ func (ml *messagesList) delete() {
}
if msg.GuildID.IsValid() {
me, err := ml.chat.state.Cabinet.Me()
me, err := ml.chatView.state.Cabinet.Me()
if err != nil {
slog.Error("failed to get client user (me)", "err", err)
return
}
if msg.Author.ID != me.ID && !ml.chat.state.HasPermissions(msg.ChannelID, discord.PermissionManageMessages) {
if msg.Author.ID != me.ID && !ml.chatView.state.HasPermissions(msg.ChannelID, discord.PermissionManageMessages) {
slog.Error("failed to delete message; missing relevant permissions", "channel_id", msg.ChannelID, "message_id", msg.ID)
return
}
}
selected := ml.chat.SelectedChannel()
selected := ml.chatView.SelectedChannel()
if selected == nil {
return
}
if err := ml.chat.state.DeleteMessage(selected.ID, msg.ID, ""); err != nil {
if err := ml.chatView.state.DeleteMessage(selected.ID, msg.ID, ""); err != nil {
slog.Error("failed to delete message", "channel_id", selected.ID, "message_id", msg.ID, "err", err)
return
}
@ -640,7 +640,7 @@ func (ml *messagesList) delete() {
ml.selectedMessageID = 0
ml.Highlight()
if err := ml.chat.state.MessageRemove(selected.ID, msg.ID); err != nil {
if err := ml.chatView.state.MessageRemove(selected.ID, msg.ID); err != nil {
slog.Error("failed to delete message", "channel_id", selected.ID, "message_id", msg.ID, "err", err)
return
}
@ -657,13 +657,13 @@ func (ml *messagesList) requestGuildMembers(gID discord.GuildID, ms []discord.Me
continue
}
if member, _ := ml.chat.state.Cabinet.Member(gID, m.Author.ID); member == nil {
if member, _ := ml.chatView.state.Cabinet.Member(gID, m.Author.ID); member == nil {
usersToFetch = append(usersToFetch, m.Author.ID)
}
}
if len(usersToFetch) > 0 {
err := ml.chat.state.SendGateway(context.TODO(), &gateway.RequestGuildMembersCommand{
err := ml.chatView.state.SendGateway(context.TODO(), &gateway.RequestGuildMembersCommand{
GuildIDs: []discord.GuildID{gID},
UserIDs: slices.Compact(usersToFetch),
})

View file

@ -16,11 +16,11 @@ import (
"github.com/diamondburned/ningen/v3"
)
func (cv *ChatView) OpenState(token string) error {
func (v *View) OpenState(token string) error {
identifyProps := http.IdentifyProperties()
gateway.DefaultIdentity = identifyProps
gateway.DefaultPresence = &gateway.UpdatePresenceCommand{
Status: cv.cfg.Status,
Status: v.cfg.Status,
}
id := gateway.DefaultIdentifier(token)
@ -28,34 +28,34 @@ func (cv *ChatView) OpenState(token string) error {
session := session.NewCustom(id, http.NewClient(token), handler.New())
state := state.NewFromSession(session, defaultstore.New())
cv.state = ningen.FromState(state)
v.state = ningen.FromState(state)
// Handlers
cv.state.AddHandler(cv.onRaw)
cv.state.AddHandler(cv.onReady)
cv.state.AddHandler(cv.onMessageCreate)
cv.state.AddHandler(cv.onMessageUpdate)
cv.state.AddHandler(cv.onMessageDelete)
cv.state.AddHandler(cv.onReadUpdate)
cv.state.AddHandler(cv.onGuildMembersChunk)
cv.state.AddHandler(cv.onGuildMemberRemove)
v.state.AddHandler(v.onRaw)
v.state.AddHandler(v.onReady)
v.state.AddHandler(v.onMessageCreate)
v.state.AddHandler(v.onMessageUpdate)
v.state.AddHandler(v.onMessageDelete)
v.state.AddHandler(v.onReadUpdate)
v.state.AddHandler(v.onGuildMembersChunk)
v.state.AddHandler(v.onGuildMemberRemove)
cv.state.StateLog = func(err error) {
v.state.StateLog = func(err error) {
slog.Error("state log", "err", err)
}
cv.state.OnRequest = append(cv.state.OnRequest, httputil.WithHeaders(http.Headers()), cv.onRequest)
return cv.state.Open(context.TODO())
v.state.OnRequest = append(v.state.OnRequest, httputil.WithHeaders(http.Headers()), v.onRequest)
return v.state.Open(context.TODO())
}
func (cv *ChatView) CloseState() error {
if cv.state == nil {
func (v *View) CloseState() error {
if v.state == nil {
return nil
}
return cv.state.Close()
return v.state.Close()
}
func (cv *ChatView) onRequest(r httpdriver.Request) error {
func (v *View) onRequest(r httpdriver.Request) error {
if req, ok := r.(*httpdriver.DefaultRequest); ok {
slog.Debug("new HTTP request", "method", req.Method, "url", req.URL)
}
@ -63,7 +63,7 @@ func (cv *ChatView) onRequest(r httpdriver.Request) error {
return nil
}
func (cv *ChatView) onRaw(event *ws.RawEvent) {
func (v *View) onRaw(event *ws.RawEvent) {
slog.Debug(
"new raw event",
"code", event.OriginalCode,

316
internal/ui/chat/view.go Normal file
View file

@ -0,0 +1,316 @@
package chat
import (
"log/slog"
"sync"
"github.com/ayn2op/discordo/internal/config"
"github.com/ayn2op/discordo/internal/keyring"
"github.com/ayn2op/discordo/internal/notifications"
"github.com/ayn2op/discordo/internal/ui"
"github.com/ayn2op/tview"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/gateway"
"github.com/diamondburned/ningen/v3"
"github.com/diamondburned/ningen/v3/states/read"
"github.com/gdamore/tcell/v3"
)
const (
flexPageName = "flex"
mentionsListPageName = "mentionsList"
attachmentsListPageName = "attachmentsList"
confirmModalPageName = "confirmModal"
)
type View struct {
*tview.Pages
mainFlex *tview.Flex
rightFlex *tview.Flex
guildsTree *guildsTree
messagesList *messagesList
messageInput *messageInput
selectedChannel *discord.Channel
selectedChannelMu sync.RWMutex
app *tview.Application
cfg *config.Config
state *ningen.State
onLogout func()
}
func NewView(app *tview.Application, cfg *config.Config, onLogout func()) *View {
v := &View{
Pages: tview.NewPages(),
mainFlex: tview.NewFlex(),
rightFlex: tview.NewFlex(),
app: app,
cfg: cfg,
onLogout: onLogout,
}
v.guildsTree = newGuildsTree(cfg, v)
v.messagesList = newMessagesList(cfg, v)
v.messageInput = newMessageInput(cfg, v)
v.SetInputCapture(v.onInputCapture)
v.buildLayout()
return v
}
func (v *View) SelectedChannel() *discord.Channel {
v.selectedChannelMu.RLock()
defer v.selectedChannelMu.RUnlock()
return v.selectedChannel
}
func (v *View) SetSelectedChannel(channel *discord.Channel) {
v.selectedChannelMu.Lock()
v.selectedChannel = channel
v.selectedChannelMu.Unlock()
}
func (v *View) buildLayout() {
v.Clear()
v.rightFlex.Clear()
v.mainFlex.Clear()
v.rightFlex.
SetDirection(tview.FlexRow).
AddItem(v.messagesList, 0, 1, false).
AddItem(v.messageInput, 3, 1, false)
// The guilds tree is always focused first at start-up.
v.mainFlex.
AddItem(v.guildsTree, 0, 1, true).
AddItem(v.rightFlex, 0, 4, false)
v.AddAndSwitchToPage(flexPageName, v.mainFlex, true)
}
func (v *View) toggleGuildsTree() {
// The guilds tree is visible if the number of items is two.
if v.mainFlex.GetItemCount() == 2 {
v.mainFlex.RemoveItem(v.guildsTree)
if v.guildsTree.HasFocus() {
v.app.SetFocus(v.mainFlex)
}
} else {
v.buildLayout()
v.app.SetFocus(v.guildsTree)
}
}
func (v *View) focusGuildsTree() bool {
// The guilds tree is not hidden if the number of items is two.
if v.mainFlex.GetItemCount() == 2 {
v.app.SetFocus(v.guildsTree)
return true
}
return false
}
func (v *View) focusMessageInput() bool {
if !v.messageInput.GetDisabled() {
v.app.SetFocus(v.messageInput)
return true
}
return false
}
func (v *View) focusPrevious() {
switch v.app.GetFocus() {
case v.guildsTree:
v.focusMessageInput()
case v.messagesList: // Handle both a.messagesList and a.flex as well as other edge cases (if there is).
if ok := v.focusGuildsTree(); !ok {
v.app.SetFocus(v.messageInput)
}
case v.messageInput:
v.app.SetFocus(v.messagesList)
}
}
func (v *View) focusNext() {
switch v.app.GetFocus() {
case v.guildsTree:
v.app.SetFocus(v.messagesList)
case v.messagesList:
v.focusMessageInput()
case v.messageInput: // Handle both a.messageInput and a.flex as well as other edge cases (if there is).
if ok := v.focusGuildsTree(); !ok {
v.app.SetFocus(v.messagesList)
}
}
}
func (v *View) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
switch event.Name() {
case v.cfg.Keys.FocusGuildsTree:
v.messageInput.removeMentionsList()
v.focusGuildsTree()
return nil
case v.cfg.Keys.FocusMessagesList:
v.messageInput.removeMentionsList()
v.app.SetFocus(v.messagesList)
return nil
case v.cfg.Keys.FocusMessageInput:
v.focusMessageInput()
return nil
case v.cfg.Keys.FocusPrevious:
v.focusPrevious()
return nil
case v.cfg.Keys.FocusNext:
v.focusNext()
return nil
case v.cfg.Keys.Logout:
if v.onLogout != nil {
v.onLogout()
}
if err := keyring.DeleteToken(); err != nil {
slog.Error("failed to delete token from keyring", "err", err)
return nil
}
return nil
case v.cfg.Keys.ToggleGuildsTree:
v.toggleGuildsTree()
return nil
}
return event
}
func (v *View) showConfirmModal(prompt string, buttons []string, onDone func(label string)) {
previousFocus := v.app.GetFocus()
modal := tview.NewModal().
SetText(prompt).
AddButtons(buttons).
SetDoneFunc(func(_ int, buttonLabel string) {
v.RemovePage(confirmModalPageName).SwitchToPage(flexPageName)
v.app.SetFocus(previousFocus)
if onDone != nil {
onDone(buttonLabel)
}
})
v.
AddAndSwitchToPage(confirmModalPageName, ui.Centered(modal, 0, 0), true).
ShowPage(flexPageName)
}
func (v *View) onReadUpdate(event *read.UpdateEvent) {
var guildNode *tview.TreeNode
v.guildsTree.
GetRoot().
Walk(func(node, parent *tview.TreeNode) bool {
switch node.GetReference() {
case event.GuildID:
node.SetTextStyle(v.guildsTree.getGuildNodeStyle(event.GuildID))
guildNode = node
return false
case event.ChannelID:
// private channel
if !event.GuildID.IsValid() {
style := v.guildsTree.getChannelNodeStyle(event.ChannelID)
node.SetTextStyle(style)
return false
}
}
return true
})
if guildNode != nil {
guildNode.Walk(func(node, parent *tview.TreeNode) bool {
if node.GetReference() == event.ChannelID {
node.SetTextStyle(v.guildsTree.getChannelNodeStyle(event.ChannelID))
return false
}
return true
})
}
v.app.Draw()
}
func (v *View) onReady(r *gateway.ReadyEvent) {
dmNode := tview.NewTreeNode("Direct Messages")
root := v.guildsTree.
GetRoot().
ClearChildren().
AddChild(dmNode)
for _, folder := range r.UserSettings.GuildFolders {
if folder.ID == 0 && len(folder.GuildIDs) == 1 {
guild, err := v.state.Cabinet.Guild(folder.GuildIDs[0])
if err != nil {
slog.Error(
"failed to get guild from state",
"guild_id",
folder.GuildIDs[0],
"err",
err,
)
continue
}
v.guildsTree.createGuildNode(root, *guild)
} else {
v.guildsTree.createFolderNode(folder)
}
}
v.guildsTree.SetCurrentNode(root)
v.app.SetFocus(v.guildsTree)
v.app.Draw()
}
func (v *View) onMessageCreate(message *gateway.MessageCreateEvent) {
if selected := v.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
v.messagesList.drawMessage(v.messagesList, message.Message)
v.app.Draw()
}
if err := notifications.Notify(v.state, message, v.cfg); err != nil {
slog.Error("failed to notify", "err", err, "channel_id", message.ChannelID, "message_id", message.ID)
}
}
func (v *View) onMessageUpdate(message *gateway.MessageUpdateEvent) {
if selected := v.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
v.onMessageDelete(&gateway.MessageDeleteEvent{ID: message.ID, ChannelID: message.ChannelID, GuildID: message.GuildID})
}
}
func (v *View) onMessageDelete(message *gateway.MessageDeleteEvent) {
if selected := v.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
messages, err := v.state.Cabinet.Messages(message.ChannelID)
if err != nil {
slog.Error("failed to get messages from state", "err", err, "channel_id", message.ChannelID)
return
}
v.messagesList.reset()
v.messagesList.drawMessages(messages)
v.app.Draw()
}
}
func (v *View) onGuildMembersChunk(event *gateway.GuildMembersChunkEvent) {
v.messagesList.setFetchingChunk(false, uint(len(event.Members)))
}
func (v *View) onGuildMemberRemove(event *gateway.GuildMemberRemoveEvent) {
v.messageInput.cache.Invalidate(event.GuildID.String()+" "+event.User.Username, v.state.MemberState.SearchLimit)
}