Ver código fonte

perf(ui/chat): cache rendered message TextViews in messages list (#737)

Ayyan 2 meses atrás
pai
commit
1428b58f68
1 arquivos alterados com 27 adições e 5 exclusões
  1. 27 5
      internal/ui/chat/messages_list.go

+ 27 - 5
internal/ui/chat/messages_list.go

@@ -41,6 +41,10 @@ type messagesList struct {
 	messages []discord.Message
 
 	renderer *markdown.Renderer
+	// itemByID caches fully built message TextViews. ScrollList may ask the
+	// builder repeatedly during drawing, so we avoid reparsing markdown and
+	// reconstructing TextViews unless a message actually changed.
+	itemByID map[discord.MessageID]*tview.TextView
 
 	fetchingMembers struct {
 		mu    sync.Mutex
@@ -56,6 +60,7 @@ func newMessagesList(cfg *config.Config, chatView *View) *messagesList {
 		cfg:        cfg,
 		chatView:   chatView,
 		renderer:   markdown.NewRenderer(cfg.Theme.MessagesList),
+		itemByID:   make(map[discord.MessageID]*tview.TextView),
 	}
 
 	ml.Box = ui.ConfigureBox(ml.Box, &cfg.Theme)
@@ -68,6 +73,7 @@ func newMessagesList(cfg *config.Config, chatView *View) *messagesList {
 
 func (ml *messagesList) reset() {
 	ml.messages = nil
+	clear(ml.itemByID)
 	ml.
 		Clear().
 		SetBuilder(ml.buildItem).
@@ -86,11 +92,16 @@ func (ml *messagesList) setTitle(channel discord.Channel) {
 func (ml *messagesList) setMessages(messages []discord.Message) {
 	ml.messages = slices.Clone(messages)
 	slices.Reverse(ml.messages)
+	// New channel payload / refetch: replace the cache wholesale to keep it in
+	// lockstep with the current message slice.
+	clear(ml.itemByID)
 	ml.MarkDirty()
 }
 
 func (ml *messagesList) addMessage(message discord.Message) {
 	ml.messages = append(ml.messages, message)
+	// Defensive invalidation for ID reuse/edits delivered out-of-order.
+	delete(ml.itemByID, message.ID)
 	ml.MarkDirty()
 }
 
@@ -100,6 +111,7 @@ func (ml *messagesList) setMessage(index int, message discord.Message) {
 	}
 
 	ml.messages[index] = message
+	delete(ml.itemByID, message.ID)
 	ml.MarkDirty()
 }
 
@@ -108,6 +120,7 @@ func (ml *messagesList) deleteMessage(index int) {
 		return
 	}
 
+	delete(ml.itemByID, ml.messages[index].ID)
 	ml.messages = append(ml.messages[:index], ml.messages[index+1:]...)
 	ml.MarkDirty()
 }
@@ -122,11 +135,16 @@ func (ml *messagesList) buildItem(index int, cursor int) tview.ScrollListItem {
 	}
 
 	message := ml.messages[index]
-	tv := tview.NewTextView().
-		SetWrap(true).
-		SetWordWrap(true).
-		SetDynamicColors(true).
-		SetText(ml.renderMessage(message))
+	tv, ok := ml.itemByID[message.ID]
+	if !ok {
+		tv = tview.NewTextView().
+			SetWrap(true).
+			SetWordWrap(true).
+			SetDynamicColors(true).
+			SetText(ml.renderMessage(message))
+		ml.itemByID[message.ID] = tv
+	}
+	// Selection state is visual only; we mutate style on the cached view.
 	if index == cursor {
 		tv.SetTextStyle(ml.cfg.Theme.MessagesList.SelectedMessageStyle.Style)
 	} else {
@@ -391,6 +409,10 @@ func (ml *messagesList) _select(name string) {
 			older := slices.Clone(messages)
 			slices.Reverse(older)
 
+			// Defensive invalidation if Discord returns overlapping windows.
+			for _, message := range older {
+				delete(ml.itemByID, message.ID)
+			}
 			ml.messages = slices.Concat(older, ml.messages)
 			cursor = len(messages) - 1
 		}