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

This commit is contained in:
Ayyan 2026-02-07 01:43:34 -05:00 • committed by GitHub
parent 0b9b76109c
commit 1428b58f68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

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