refactor(chat): extract mentions list

This commit is contained in:
ayn2op 2026-07-08 01:43:59 -04:00
parent 6c3727cb7c
commit 37a6394826
6 changed files with 159 additions and 126 deletions

6
go.mod
View file

@ -10,9 +10,9 @@ require (
github.com/andybalholm/brotli v1.2.2
github.com/ayn2op/arikawa/v3 v3.0.0-20260619211231-9a7da0c518de
github.com/ayn2op/ningen/v3 v3.0.1-0.20260619214224-6a89f2062a83
github.com/ayn2op/tview v0.0.0-20260703004135-684c7bd3e9af
github.com/ayn2op/tview v0.0.0-20260708025551-16f85f97975e
github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb
github.com/gdamore/tcell/v3 v3.4.1-0.20260629051246-fe2979ab8bd1
github.com/gdamore/tcell/v3 v3.4.1-0.20260703160154-d85b1fbd48b8
github.com/gen2brain/beeep v0.11.2
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.6.0
@ -59,6 +59,6 @@ require (
golang.org/x/mobile v0.0.0-20260611195102-4dd8f1dbf5d2 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/term v0.44.0 // indirect
golang.org/x/text v0.38.0 // indirect
golang.org/x/text v0.39.0 // indirect
golang.org/x/time v0.15.0 // indirect
)

12
go.sum
View file

@ -16,8 +16,8 @@ github.com/ayn2op/arikawa/v3 v3.0.0-20260619211231-9a7da0c518de h1:j5qhTMT9t09g7
github.com/ayn2op/arikawa/v3 v3.0.0-20260619211231-9a7da0c518de/go.mod h1:96vOKCn1rSdu7xJrCrKdwYzlgDZ5NmV9cMtPAeGHlGM=
github.com/ayn2op/ningen/v3 v3.0.1-0.20260619214224-6a89f2062a83 h1:Dqap/VgsdiL0CNSBd+H845ZG5y2K3SJ8ji3F5oMTYIE=
github.com/ayn2op/ningen/v3 v3.0.1-0.20260619214224-6a89f2062a83/go.mod h1:uMv/8i6wn0p4paqT2Tf1S4nn5dCoNIs4w1UWvODwvf0=
github.com/ayn2op/tview v0.0.0-20260703004135-684c7bd3e9af h1:TbQH/N3lTftvAL9NPELlIcp8BsVm25lpp6kFF78gHJU=
github.com/ayn2op/tview v0.0.0-20260703004135-684c7bd3e9af/go.mod h1:kBq8YDymQUJsGxUfhatCnwc1XU0z1tXjSLNpGpzcMp8=
github.com/ayn2op/tview v0.0.0-20260708025551-16f85f97975e h1:WVf0fqBMetR5F5VoA1lMvRpSX25YUyekaNwaWXieW7c=
github.com/ayn2op/tview v0.0.0-20260708025551-16f85f97975e/go.mod h1:kBq8YDymQUJsGxUfhatCnwc1XU0z1tXjSLNpGpzcMp8=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
@ -39,8 +39,8 @@ github.com/esiqveland/notify v0.14.0 h1:LgIT/vYnziiw3d4ykaeyytc7jBUhD+Nvv3U7GIuP
github.com/esiqveland/notify v0.14.0/go.mod h1:hesw/IRYTO0x99u1JPweAl4+5mwXJibQVUcP0Iu5ORE=
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
github.com/gdamore/tcell/v3 v3.4.1-0.20260629051246-fe2979ab8bd1 h1:vWuBP/aOVrgFN81SPA8ZEOmVIMs0WQ7OZKlEPg0eZyQ=
github.com/gdamore/tcell/v3 v3.4.1-0.20260629051246-fe2979ab8bd1/go.mod h1:Ev/2PFhL0QtVmu6XPZG9NEuITAZ6XH7i7/BF3wupBdw=
github.com/gdamore/tcell/v3 v3.4.1-0.20260703160154-d85b1fbd48b8 h1:4RhCPcOmn5eM1eD0vdjd1l1IC8WqmGj/R0vVUVJ60XA=
github.com/gdamore/tcell/v3 v3.4.1-0.20260703160154-d85b1fbd48b8/go.mod h1:Ev/2PFhL0QtVmu6XPZG9NEuITAZ6XH7i7/BF3wupBdw=
github.com/gen2brain/beeep v0.11.2 h1:+KfiKQBbQCuhfJFPANZuJ+oxsSKAYNe88hIpJuyKWDA=
github.com/gen2brain/beeep v0.11.2/go.mod h1:jQVvuwnLuwOcdctHn/uyh8horSBNJ8uGb9Cn2W4tvoc=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
@ -155,8 +155,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View file

@ -24,6 +24,7 @@ import (
"github.com/ayn2op/discordo/internal/config"
"github.com/ayn2op/discordo/internal/consts"
"github.com/ayn2op/discordo/internal/ui"
"github.com/ayn2op/discordo/internal/ui/chat/mentionslist"
"github.com/ayn2op/ningen/v3"
"github.com/ayn2op/ningen/v3/discordmd"
"github.com/ayn2op/tview"
@ -53,7 +54,7 @@ type composer struct {
edit bool
sendMessageData *api.SendMessageData
cache *cache.Cache
mentionsList *mentionsList
mentionsList *mentionslist.Model
lastSearch time.Time
typingTimerMu sync.Mutex
@ -71,7 +72,7 @@ func newComposer(cfg *config.Config, chat *Model) *composer {
chat: chat,
sendMessageData: &api.SendMessageData{},
cache: cache.NewCache(),
mentionsList: newMentionsList(cfg),
mentionsList: mentionslist.NewModel(cfg),
}
c.Box = ui.ConfigureBox(c.Box, &cfg.Theme)
c.
@ -487,10 +488,10 @@ func (c *composer) tabComplete() tview.Cmd {
}
return nil
}
if c.mentionsList.itemCount() == 0 {
if c.mentionsList.ItemCount() == 0 {
return nil
}
name, ok = c.mentionsList.selectedInsertText()
name, ok = c.mentionsList.SelectedInsertText()
if !ok {
return nil
}
@ -509,7 +510,7 @@ func (c *composer) tabSuggest() tview.Cmd {
}
gID := selectedChannel.GuildID
cID := selectedChannel.ID
c.mentionsList.clear()
c.mentionsList.Clear()
var shown map[string]struct{}
var userDone struct{}
@ -577,16 +578,16 @@ func (c *composer) tabSuggest() tview.Cmd {
break
}
}
if c.mentionsList.itemCount() == 0 {
if c.mentionsList.ItemCount() == 0 {
return tview.Batch(c.stopTabCompletion(), searchCmd)
}
}
if c.mentionsList.itemCount() == 0 {
if c.mentionsList.ItemCount() == 0 {
return c.stopTabCompletion()
}
c.mentionsList.rebuild()
c.mentionsList.Rebuild()
return c.showMentionsList()
}
@ -687,14 +688,14 @@ func (c *composer) showMentionsList() tview.Cmd {
if t := int(c.cfg.Theme.MentionsList.MaxHeight); t != 0 {
maxH = min(maxH, t)
}
count := c.mentionsList.itemCount() + borders
count := c.mentionsList.ItemCount() + borders
h := min(count, maxH) + borders + c.cfg.Theme.Border.Padding[1]
y -= h
w := int(c.cfg.Theme.MentionsList.MinWidth)
if w == 0 {
w = maxW
} else {
w = max(w, c.mentionsList.maxDisplayWidth())
w = max(w, c.mentionsList.MaxDisplayWidth())
w = min(w+borders*2, maxW)
_, col, _, _ := c.GetCursor()
@ -734,12 +735,12 @@ func (c *composer) addMentionMember(gID discord.GuildID, m *discord.Member) bool
style = style.Dim(true)
}
c.mentionsList.append(mentionsListItem{
insertText: m.User.Username,
displayText: name,
style: style,
c.mentionsList.Append(mentionslist.Item{
InsertText: m.User.Username,
DisplayText: name,
Style: style,
})
return c.mentionsList.itemCount() > int(c.cfg.AutocompleteLimit)
return c.mentionsList.ItemCount() > int(c.cfg.AutocompleteLimit)
}
func (c *composer) addMentionUser(user *discord.User) {
@ -756,10 +757,10 @@ func (c *composer) addMentionUser(user *discord.User) {
style = style.Dim(true)
}
c.mentionsList.append(mentionsListItem{
insertText: user.Username,
displayText: name,
style: style,
c.mentionsList.Append(mentionslist.Item{
InsertText: user.Username,
DisplayText: name,
Style: style,
})
}
@ -772,7 +773,7 @@ func (c *composer) removeMentionsList() {
func (c *composer) stopTabCompletion() tview.Cmd {
if c.cfg.AutocompleteLimit > 0 {
c.mentionsList.clear()
c.mentionsList.Clear()
c.removeMentionsList()
return tview.SetFocus(c)
}

View file

@ -1,97 +0,0 @@
package chat
import (
"github.com/ayn2op/discordo/internal/config"
"github.com/ayn2op/discordo/internal/ui"
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/list"
"github.com/gdamore/tcell/v3"
"github.com/rivo/uniseg"
)
type mentionsListItem struct {
insertText string
displayText string
style tcell.Style
}
type mentionsList struct {
*list.Model
items []mentionsListItem
}
func newMentionsList(cfg *config.Config) *mentionsList {
m := &mentionsList{
Model: list.NewModel(),
}
m.SetSelectedStyle(tcell.StyleDefault.Reverse(true))
m.SetKeybinds(list.Keybinds{
SelectUp: cfg.Keybinds.MentionsList.SelectUp.Keybind,
SelectDown: cfg.Keybinds.MentionsList.SelectDown.Keybind,
SelectTop: cfg.Keybinds.MentionsList.SelectTop.Keybind,
SelectBottom: cfg.Keybinds.MentionsList.SelectBottom.Keybind,
})
m.Box = ui.ConfigureBox(m.Box, &cfg.Theme)
m.SetSnapToItems(true).SetTitle("Mentions")
b := m.BorderSet()
b.BottomLeft, b.BottomRight = b.BottomT, b.BottomT
m.SetBorderSet(b)
return m
}
func (m *mentionsList) append(item mentionsListItem) {
m.items = append(m.items, item)
}
func (m *mentionsList) clear() {
m.items = nil
m.Clear()
}
func (m *mentionsList) rebuild() {
m.SetBuilder(func(index int) list.Item {
if index < 0 || index >= len(m.items) {
return nil
}
item := m.items[index]
style := item.style
line := tview.NewLine(tview.NewSegment(item.displayText, style))
return tview.NewTextView().
SetScrollable(false).
SetWrap(false).
SetWordWrap(false).
SetTextStyle(style).
SetLines([]tview.Line{line})
})
if len(m.items) == 0 {
m.SetCursor(-1)
return
}
m.SetCursor(0)
}
func (m *mentionsList) itemCount() int {
return len(m.items)
}
func (m *mentionsList) selectedInsertText() (string, bool) {
index := m.Cursor()
if index < 0 || index >= len(m.items) {
return "", false
}
return m.items[index].insertText, true
}
func (m *mentionsList) maxDisplayWidth() int {
width := 0
for _, item := range m.items {
width = max(width, uniseg.StringWidth(item.displayText))
}
return width
}

View file

@ -0,0 +1,9 @@
package mentionslist
import "github.com/gdamore/tcell/v3"
type Item struct {
InsertText string
DisplayText string
Style tcell.Style
}

View file

@ -0,0 +1,120 @@
package mentionslist
import (
"github.com/ayn2op/discordo/internal/config"
"github.com/ayn2op/discordo/internal/ui"
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/list"
"github.com/gdamore/tcell/v3"
"github.com/rivo/uniseg"
)
type Model struct {
list *list.Model
items []Item
}
func NewModel(cfg *config.Config) *Model {
l := list.NewModel()
l.Box = ui.ConfigureBox(l.Box, &cfg.Theme)
l.
SetSelectedStyle(tcell.StyleDefault.Reverse(true)).
SetSnapToItems(true).
SetTitle("Mentions")
kbs := cfg.Keybinds.MentionsList
l.SetKeybinds(list.Keybinds{
SelectUp: kbs.SelectUp.Keybind,
SelectDown: kbs.SelectDown.Keybind,
SelectTop: kbs.SelectTop.Keybind,
SelectBottom: kbs.SelectBottom.Keybind,
})
b := l.BorderSet()
b.BottomLeft, b.BottomRight = b.BottomT, b.BottomT
l.SetBorderSet(b)
return &Model{list: l}
}
func (m *Model) Append(item Item) {
m.items = append(m.items, item)
}
func (m *Model) Clear() {
m.items = nil
m.list.Clear()
}
func (m *Model) ItemCount() int {
return len(m.items)
}
func (m *Model) SelectedInsertText() (string, bool) {
index := m.list.Cursor()
if index < 0 || index >= len(m.items) {
return "", false
}
return m.items[index].InsertText, true
}
func (m *Model) MaxDisplayWidth() int {
width := 0
for _, item := range m.items {
width = max(width, uniseg.StringWidth(item.DisplayText))
}
return width
}
func (m *Model) Rebuild() {
m.list.SetBuilder(func(index int) list.Item {
if index < 0 || index >= len(m.items) {
return nil
}
item := m.items[index]
style := item.Style
line := tview.NewLine(tview.NewSegment(item.DisplayText, style))
return tview.NewTextView().
SetScrollable(false).
SetWrap(false).
SetWordWrap(false).
SetTextStyle(style).
SetLines([]tview.Line{line})
})
if len(m.items) == 0 {
m.list.SetCursor(-1)
return
}
m.list.SetCursor(0)
}
var _ tview.Model = (*Model)(nil)
func (m *Model) Blur() {
m.list.Blur()
}
func (m *Model) Focus(delegate func(tview.Model)) {
m.list.Focus(delegate)
}
func (m *Model) HasFocus() bool {
return m.list.HasFocus()
}
func (m *Model) Rect() (int, int, int, int) {
return m.list.Rect()
}
func (m *Model) SetRect(x int, y int, width int, height int) {
m.list.SetRect(x, y, width, height)
}
func (m *Model) Update(msg tview.Msg) tview.Cmd {
return m.list.Update(msg)
}
func (m *Model) View(screen tcell.Screen) {
m.list.View(screen)
}