diff --git a/internal/ui/chat/guilds_tree.go b/internal/ui/chat/guilds_tree.go index bc6f555..64dafef 100644 --- a/internal/ui/chat/guilds_tree.go +++ b/internal/ui/chat/guilds_tree.go @@ -20,6 +20,13 @@ type guildsTree struct { *tview.TreeView cfg *config.Config chatView *View + + // Fast-path indexes for frequent event handlers (read updates, picker + // navigation). They mirror the current rendered tree and are rebuilt on + // READY before nodes are added. + guildNodeByID map[discord.GuildID]*tview.TreeNode + channelNodeByID map[discord.ChannelID]*tview.TreeNode + dmRootNode *tview.TreeNode } func newGuildsTree(cfg *config.Config, chatView *View) *guildsTree { @@ -27,6 +34,9 @@ func newGuildsTree(cfg *config.Config, chatView *View) *guildsTree { TreeView: tview.NewTreeView(), cfg: cfg, chatView: chatView, + + guildNodeByID: make(map[discord.GuildID]*tview.TreeNode), + channelNodeByID: make(map[discord.ChannelID]*tview.TreeNode), } gt.Box = ui.ConfigureBox(gt.Box, &cfg.Theme) @@ -42,6 +52,13 @@ func newGuildsTree(cfg *config.Config, chatView *View) *guildsTree { return gt } +func (gt *guildsTree) resetNodeIndex() { + // Keep allocated map capacity; READY can rebuild often during reconnects. + clear(gt.guildNodeByID) + clear(gt.channelNodeByID) + gt.dmRootNode = nil +} + func (gt *guildsTree) createFolderNode(folder gateway.GuildFolder) { name := "Folder" if folder.Name != "" { @@ -92,6 +109,7 @@ func (gt *guildsTree) createGuildNode(n *tview.TreeNode, guild discord.Guild) { SetReference(guild.ID). SetTextStyle(gt.getGuildNodeStyle(guild.ID)) n.AddChild(guildNode) + gt.guildNodeByID[guild.ID] = guildNode } func (gt *guildsTree) createChannelNode(node *tview.TreeNode, channel discord.Channel) { @@ -103,6 +121,7 @@ func (gt *guildsTree) createChannelNode(node *tview.TreeNode, channel discord.Ch SetReference(channel.ID). SetTextStyle(gt.getChannelNodeStyle(channel.ID)) node.AddChild(channelNode) + gt.channelNodeByID[channel.ID] = channelNode } func (gt *guildsTree) createChannelNodes(node *tview.TreeNode, channels []discord.Channel) { @@ -126,16 +145,9 @@ PARENT_CHANNELS: for _, channel := range channels { if channel.ParentID.IsValid() { - var parent *tview.TreeNode - node.Walk(func(node, _ *tview.TreeNode) bool { - if node.GetReference() == channel.ParentID { - parent = node - return false - } - - return true - }) - + // Parent categories are inserted earlier in this function, so this + // lookup is O(1) and avoids per-channel subtree walks. + parent := gt.channelNodeByID[channel.ParentID] if parent != nil { gt.createChannelNode(parent, channel) } @@ -297,16 +309,25 @@ func (gt *guildsTree) yankID() { } func (gt *guildsTree) findNodeByReference(reference any) *tview.TreeNode { - var found *tview.TreeNode - gt.GetRoot().Walk(func(node, _ *tview.TreeNode) bool { - if node.GetReference() == reference { - found = node - return false - } - - return true - }) - return found + switch ref := reference.(type) { + case discord.GuildID: + return gt.guildNodeByID[ref] + case discord.ChannelID: + return gt.channelNodeByID[ref] + case dmNode: + return gt.dmRootNode + default: + // Fallback keeps this helper safe for non-indexed custom references. + var found *tview.TreeNode + gt.GetRoot().Walk(func(node, _ *tview.TreeNode) bool { + if node.GetReference() == reference { + found = node + return false + } + return true + }) + return found + } } func (gt *guildsTree) findNodeByChannelID(channelID discord.ChannelID) *tview.TreeNode { diff --git a/internal/ui/chat/state.go b/internal/ui/chat/state.go index 026d0c6..b31f30f 100644 --- a/internal/ui/chat/state.go +++ b/internal/ui/chat/state.go @@ -82,6 +82,10 @@ func (v *View) onRaw(event *ws.RawEvent) { func (v *View) onReady(r *gateway.ReadyEvent) { dmNode := tview.NewTreeNode("Direct Messages").SetReference(dmNode{}) + // Rebuild indexes from scratch so reconnects and account switches do not + // retain stale pointers to detached tree nodes. + v.guildsTree.resetNodeIndex() + v.guildsTree.dmRootNode = dmNode root := v.guildsTree. GetRoot(). ClearChildren(). diff --git a/internal/ui/chat/view.go b/internal/ui/chat/view.go index c5059e3..0109214 100644 --- a/internal/ui/chat/view.go +++ b/internal/ui/chat/view.go @@ -263,39 +263,26 @@ func (v *View) showConfirmModal(prompt string, buttons []string, onDone func(lab } 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 - }) + // Use indexed node lookup to avoid walking the whole tree on every read + // event. This runs frequently while reading/typing across channels. + var updated bool + if event.GuildID.IsValid() { + if guildNode := v.guildsTree.findNodeByReference(event.GuildID); guildNode != nil { + guildNode.SetTextStyle(v.guildsTree.getGuildNodeStyle(event.GuildID)) + updated = true + } } - v.app.Draw() + // Channel style is always updated for the target channel regardless of + // whether it's in a guild or DM. + if channelNode := v.guildsTree.findNodeByReference(event.ChannelID); channelNode != nil { + channelNode.SetTextStyle(v.guildsTree.getChannelNodeStyle(event.ChannelID)) + updated = true + } + + if updated { + v.app.Draw() + } } func (v *View) clearTypers() {