fix(messageslist): fetch older messages once on scrolling to the top
This commit is contained in:
parent
fa82fad291
commit
3562b4d546
4 changed files with 88 additions and 15 deletions
4
go.mod
4
go.mod
|
|
@ -15,7 +15,7 @@ require (
|
|||
github.com/andybalholm/brotli v1.2.6
|
||||
github.com/ayn2op/arikawa/v3 v3.0.0-20261014035141-32b58d8f3755
|
||||
github.com/ayn2op/ningen/v3 v3.0.1-0.20261014035153-5e43590a1426
|
||||
github.com/ayn2op/tview v0.0.0-20261013225942-7c4cbe85112a
|
||||
github.com/ayn2op/tview v0.0.0-20261015202917-18bf0788a772
|
||||
github.com/bogdanfinn/fhttp v0.6.9
|
||||
github.com/bogdanfinn/tls-client v1.16.0
|
||||
github.com/gdamore/tcell/v3 v3.6.0
|
||||
|
|
@ -60,7 +60,7 @@ require (
|
|||
github.com/randall77/makefat v0.0.0-20260406194835-1b91746796b7 // indirect
|
||||
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect
|
||||
github.com/thomas-vilte/mls-go v1.6.0 // indirect
|
||||
github.com/tphakala/simd v1.11.0 // indirect
|
||||
github.com/tphakala/simd v1.12.0 // indirect
|
||||
github.com/twmb/murmur3 v1.2.1 // indirect
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||
golang.design/x/x11 v0.2.0 // indirect
|
||||
|
|
|
|||
8
go.sum
8
go.sum
|
|
@ -14,8 +14,8 @@ github.com/ayn2op/arikawa/v3 v3.0.0-20261014035141-32b58d8f3755 h1:XHo9kizImaAEt
|
|||
github.com/ayn2op/arikawa/v3 v3.0.0-20261014035141-32b58d8f3755/go.mod h1:YARcqZmk7hDjXB+lexlXX4wDFHULafbGnF0gsD0tuNw=
|
||||
github.com/ayn2op/ningen/v3 v3.0.1-0.20261014035153-5e43590a1426 h1:0ndRWpAiSSgR0z9b7K+pwMlCHEk3VzQeS5ls+zWtbr0=
|
||||
github.com/ayn2op/ningen/v3 v3.0.1-0.20261014035153-5e43590a1426/go.mod h1:4S2B9frwrjELeAldoqPgasA/VfnOySgRlektwYLuVks=
|
||||
github.com/ayn2op/tview v0.0.0-20261013225942-7c4cbe85112a h1:b92yhUuO4jsY8eTqTEEJO+SjpkIIngSzhnJA//FEw6M=
|
||||
github.com/ayn2op/tview v0.0.0-20261013225942-7c4cbe85112a/go.mod h1:3aWH6S5uOqOkZv7JFePEVHukTj9hPb6Lbynm59Sks2E=
|
||||
github.com/ayn2op/tview v0.0.0-20261015202917-18bf0788a772 h1:XXNOUzYpwH2S/Q5XA/kHJCkbz4HwXtM/Unh3HbhqRIw=
|
||||
github.com/ayn2op/tview v0.0.0-20261015202917-18bf0788a772/go.mod h1:3aWH6S5uOqOkZv7JFePEVHukTj9hPb6Lbynm59Sks2E=
|
||||
github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q=
|
||||
github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM=
|
||||
github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic=
|
||||
|
|
@ -96,8 +96,8 @@ github.com/thomas-vilte/mls-go v1.6.0 h1:HGxrRe0kRfEAXu7MtTemNzqGmC/758rfGkarhYG
|
|||
github.com/thomas-vilte/mls-go v1.6.0/go.mod h1:Cxl407ecAW94wwYLOycpQUp0UyfBk6dNmvs1hkw2yLg=
|
||||
github.com/tphakala/go-opus v1.2.1 h1:j3DlcDf+9VTUzOEZ6kODoZoLMb/MTN2+ft24VuO8FaQ=
|
||||
github.com/tphakala/go-opus v1.2.1/go.mod h1:CzYWGAXjMJHIqu/aTowxgmut2D0+OFJCjQ9+YJHDJV0=
|
||||
github.com/tphakala/simd v1.11.0 h1:3rtUU/6mO76R4rq3hQiEX+DC4yKc/twIGU62bkmIAYA=
|
||||
github.com/tphakala/simd v1.11.0/go.mod h1:eO53GkWg40mRbr2tzs0Xb1IMZQ5U8cMR+Aa1RlXUinE=
|
||||
github.com/tphakala/simd v1.12.0 h1:qjCO4f/ega4+2znt8bp+lb6R8a7bSri+/KpH+cGHOrA=
|
||||
github.com/tphakala/simd v1.12.0/go.mod h1:eO53GkWg40mRbr2tzs0Xb1IMZQ5U8cMR+Aa1RlXUinE=
|
||||
github.com/twmb/murmur3 v1.2.1 h1:TJzZGygOc1VJ7jiBpy8m7KAAzm67zy1jvkUxxEjJp+g=
|
||||
github.com/twmb/murmur3 v1.2.1/go.mod h1:LVqs/avIy0flspVtmBd4VSbogDepFl6Wkj0i0ToccHE=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
|
|
|
|||
|
|
@ -128,7 +128,8 @@ func (m Model) listView(focused bool) viewport.Widget {
|
|||
return viewport.New(l, m.scrollState).
|
||||
Target(l.Target(m.target)).
|
||||
ScrollBar(bar, cfg.Visibility.ScrollBarVisibility).
|
||||
TrackEnd(true).
|
||||
// Older messages are added above, so the position is kept from the newest, which also keeps the newest in view.
|
||||
AnchorBottom().
|
||||
Keybind(ui.ScrollKeybind(m.cfg.Keybinds.MessagesList.ScrollKeybinds)).
|
||||
Focused(focused).
|
||||
OnChange(func(s *viewport.ScrollState) tview.Msg { return scrollMsg(*s) })
|
||||
|
|
@ -359,7 +360,7 @@ func (m Model) renderMessage(message discord.Message, baseStyle tview.Style, wid
|
|||
// rebuildItems replaces date separators while retaining message views and selection.
|
||||
func (m *Model) rebuildItems() {
|
||||
items := make([]messageItem, 0, len(m.items))
|
||||
cursor, selected := m.cursor, -1
|
||||
cursor, target := -1, -1
|
||||
var previous discord.Timestamp
|
||||
for i, item := range m.items {
|
||||
if item.separator {
|
||||
|
|
@ -368,14 +369,18 @@ func (m *Model) rebuildItems() {
|
|||
if m.cfg.DateSeparator.Enabled && (len(items) == 0 || !sameLocalDate(previous, item.message.Timestamp)) {
|
||||
items = append(items, messageItem{separator: true})
|
||||
}
|
||||
if i == cursor {
|
||||
selected = len(items)
|
||||
if i == m.cursor {
|
||||
cursor = len(items)
|
||||
}
|
||||
if i == m.target {
|
||||
target = len(items)
|
||||
}
|
||||
items = append(items, item)
|
||||
previous = item.message.Timestamp
|
||||
}
|
||||
m.items = items
|
||||
m.setCursor(selected)
|
||||
// The selected and target messages only move with the separators, so they are not scrolled to.
|
||||
m.cursor, m.target = cursor, target
|
||||
}
|
||||
|
||||
func sameLocalDate(a discord.Timestamp, b discord.Timestamp) bool {
|
||||
|
|
@ -1014,15 +1019,17 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) {
|
|||
if selectedChannel == nil || selectedChannel.ID != msg.ChannelID {
|
||||
return m, nil
|
||||
}
|
||||
prevCursor := m.cursor
|
||||
|
||||
older := make([]messageItem, len(msg.Older))
|
||||
for i, message := range msg.Older {
|
||||
older[i] = messageItem{message: message}
|
||||
}
|
||||
m.items = slices.Concat(older, m.items)
|
||||
if prevCursor >= 0 {
|
||||
m.setCursor(prevCursor + len(older))
|
||||
// The selected and target messages stay the same, so they are not scrolled to.
|
||||
if m.cursor >= 0 {
|
||||
m.cursor += len(older)
|
||||
}
|
||||
if m.target >= 0 {
|
||||
m.target += len(older)
|
||||
}
|
||||
m.rebuildItems()
|
||||
if selectedChannel.GuildID.IsValid() {
|
||||
|
|
@ -1093,6 +1100,10 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) {
|
|||
}
|
||||
case scrollMsg:
|
||||
m.scrollState = viewport.ScrollState(msg)
|
||||
// Older messages are fetched once as the list is scrolled to the top, rather than on each scroll there.
|
||||
if !scrolledToTop(m.scrollState.Origin()) && scrolledToTop(m.scrollState.Viewport()) {
|
||||
return m, m.fetchOlderMessages()
|
||||
}
|
||||
case listMsg:
|
||||
top := m.atTop()
|
||||
m.cursor, m.target = int(msg), int(msg)
|
||||
|
|
@ -1104,6 +1115,11 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) {
|
|||
return m, nil
|
||||
}
|
||||
|
||||
// scrolledToTop reports whether v is scrolled to the oldest message shown, which is false while the messages fit, as there is nothing to scroll.
|
||||
func scrolledToTop(v viewport.Viewport) bool {
|
||||
return v.Content().Height > v.Bounds().Height && v.AbsoluteOffsetReversed().Y == 0
|
||||
}
|
||||
|
||||
// atTop reports whether the oldest message shown is selected.
|
||||
func (m Model) atTop() bool {
|
||||
return m.cursor >= 0 && m.cursor == m.messageIndex(-1, 1)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/ayn2op/ningen/v3"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/ayn2op/tview/layout"
|
||||
"github.com/gdamore/tcell/v3"
|
||||
)
|
||||
|
||||
func testConfig(t *testing.T) *config.Config {
|
||||
|
|
@ -246,3 +247,59 @@ func TestMessageViewLayout(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelScrollToTop(t *testing.T) {
|
||||
ml := newTestModel(t, testConfig(t))
|
||||
messages := make([]discord.Message, 20)
|
||||
for i := range messages {
|
||||
messages[i] = discord.Message{ID: discord.MessageID(len(messages) + 10 - i), Content: "hi"}
|
||||
}
|
||||
ml.setMessages(messages)
|
||||
area := tview.Rectangle{Width: 40, Height: 5}
|
||||
up := tview.MouseMsg{EventMouse: tcell.NewEventMouse(0, 0, tcell.ButtonNone, tcell.ModNone), Action: tview.MouseScrollUp}
|
||||
// scrollUp scrolls up a row and returns what fetching older messages that does, and whether it scrolled.
|
||||
scrollUp := func() (tview.Cmd, bool) {
|
||||
t.Helper()
|
||||
msg, ok := ml.listView(true).Handle(up, area).(scrollMsg)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
var cmd tview.Cmd
|
||||
*ml, cmd = ml.Update(msg)
|
||||
return cmd, true
|
||||
}
|
||||
// scrollToTop scrolls to the top and returns how many times older messages were fetched on the way.
|
||||
scrollToTop := func() (fetches int) {
|
||||
t.Helper()
|
||||
for {
|
||||
cmd, scrolled := scrollUp()
|
||||
if !scrolled {
|
||||
return fetches
|
||||
}
|
||||
if cmd != nil {
|
||||
fetches++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fetches := scrollToTop(); fetches != 1 {
|
||||
t.Fatalf("scrolling to the top fetched %d times, want once", fetches)
|
||||
}
|
||||
if cmd, _ := scrollUp(); cmd != nil {
|
||||
t.Fatal("older messages were fetched again while at the top")
|
||||
}
|
||||
|
||||
older := []discord.Message{{ID: 1, Content: "hi"}, {ID: 2, Content: "hi"}}
|
||||
*ml, _ = ml.Update(olderMessagesLoadedMsg{ChannelID: 1, Older: older})
|
||||
cmd, _ := scrollUp()
|
||||
if cmd != nil {
|
||||
t.Fatal("older messages were fetched again before scrolling to the new top")
|
||||
}
|
||||
if ml.scrollState.Viewport().AbsoluteOffsetReversed().Y == 0 {
|
||||
t.Fatal("the older messages were scrolled into view rather than added above it")
|
||||
}
|
||||
|
||||
if fetches := scrollToTop(); fetches != 1 {
|
||||
t.Fatalf("scrolling to the new top fetched %d times, want once", fetches)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue