perf(ui/chat): index guild tree nodes for read updates (#738)

This commit is contained in:
Ayyan 2026-02-07 01:49:16 -05:00 • committed by GitHub
parent 1428b58f68
commit 56337067fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 63 additions and 51 deletions

View file

@ -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 {

View file

@ -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().

View file

@ -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() {