Kaynağa Gözat

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

Ayyan 2 ay önce
ebeveyn
işleme
56337067fa

+ 41 - 20
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 {

+ 4 - 0
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().

+ 17 - 30
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
-			}
+	// 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
+		}
+	}
 
-			return true
-		})
+	// 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
 	}
 
-	v.app.Draw()
+	if updated {
+		v.app.Draw()
+	}
 }
 
 func (v *View) clearTypers() {