refactor(chat): avoid blocking member chunks
This commit is contained in:
parent
bb3ee02edc
commit
6c3727cb7c
5 changed files with 53 additions and 61 deletions
|
|
@ -2,6 +2,7 @@ package chat
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
|
@ -15,6 +16,7 @@ import (
|
|||
|
||||
"github.com/ayn2op/arikawa/v3/api"
|
||||
"github.com/ayn2op/arikawa/v3/discord"
|
||||
"github.com/ayn2op/arikawa/v3/gateway"
|
||||
"github.com/ayn2op/arikawa/v3/state"
|
||||
"github.com/ayn2op/arikawa/v3/utils/json/option"
|
||||
"github.com/ayn2op/arikawa/v3/utils/sendpart"
|
||||
|
|
@ -37,6 +39,7 @@ import (
|
|||
const (
|
||||
tmpFilePattern = consts.Name + "_*.md"
|
||||
imageAttachmentName = "clipboard.png"
|
||||
memberSearchNonce = "autocomplete:"
|
||||
)
|
||||
|
||||
var mentionRegex = regexp.MustCompile("@[a-zA-Z0-9._]+")
|
||||
|
|
@ -619,6 +622,10 @@ func channelHasUser(state *ningen.State, channelID discord.ChannelID, userID dis
|
|||
// searchMember performs member discovery in a command goroutine.
|
||||
// It emits a follow-up suggestion message once results are loaded.
|
||||
func (c *composer) searchMember(gID discord.GuildID, name string) tview.Cmd {
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
key := gID.String() + " " + name
|
||||
if _, ok := c.cache.Get(key); ok {
|
||||
return nil
|
||||
|
|
@ -634,17 +641,36 @@ func (c *composer) searchMember(gID discord.GuildID, name string) tview.Cmd {
|
|||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
// Rate limit on our side because we can't distinguish between a successful search and SearchMember not doing anything because of its internal rate limit that we can't detect
|
||||
if c.lastSearch.Add(c.chat.state.MemberState.SearchFrequency).After(time.Now()) {
|
||||
if c.lastSearch.Add(c.chat.state.MemberState.SearchFrequency).After(now) {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.lastSearch = time.Now()
|
||||
c.lastSearch = now
|
||||
nonce := memberSearchNonce + key
|
||||
return func() tview.Msg {
|
||||
if err := c.chat.state.SendGateway(context.Background(), &gateway.RequestGuildMembersCommand{
|
||||
GuildIDs: []discord.GuildID{gID},
|
||||
Query: option.NewString(name),
|
||||
Presences: c.chat.state.MemberState.RequestPresences,
|
||||
Limit: c.chat.state.MemberState.SearchLimit,
|
||||
Nonce: nonce,
|
||||
}); err != nil {
|
||||
slog.Error("failed to search guild members", "err", err, "guild_id", gID, "query", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *composer) onGuildMembersChunk(event *gateway.GuildMembersChunkEvent) tview.Cmd {
|
||||
key, ok := strings.CutPrefix(event.Nonce, memberSearchNonce)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.cache.Create(key, uint(len(event.Members)))
|
||||
return func() tview.Msg {
|
||||
c.chat.messagesList.waitForChunkEvent()
|
||||
c.chat.messagesList.setFetchingChunk(true, 0)
|
||||
c.chat.state.MemberState.SearchMember(gID, name)
|
||||
c.cache.Create(key, c.chat.messagesList.waitForChunkEvent())
|
||||
return tabSuggestMsg{}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -309,9 +309,6 @@ func (gt *guildsTree) loadChannel(channel discord.Channel) tview.Cmd {
|
|||
|
||||
go gt.chat.state.ReadState.MarkRead(channel.ID, channel.LastMessageID)
|
||||
|
||||
if guildID := channel.GuildID; guildID.IsValid() {
|
||||
gt.chat.messagesList.requestGuildMembers(guildID, messages)
|
||||
}
|
||||
return channelLoadedMsg{Channel: channel, Messages: messages}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import (
|
|||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
|
|
@ -55,13 +54,6 @@ type messagesList struct {
|
|||
itemByID map[discord.MessageID]*tview.TextView
|
||||
|
||||
attachmentsPicker *attachmentsPicker
|
||||
|
||||
fetchingMembers struct {
|
||||
mu sync.Mutex
|
||||
value bool
|
||||
count uint
|
||||
done chan struct{}
|
||||
}
|
||||
}
|
||||
|
||||
var _ help.KeyMap = (*messagesList)(nil)
|
||||
|
|
@ -920,6 +912,9 @@ func (ml *messagesList) Update(msg tview.Msg) tview.Cmd {
|
|||
default:
|
||||
ml.SetCursor(prevCursor)
|
||||
}
|
||||
if selectedChannel.GuildID.IsValid() {
|
||||
return ml.requestGuildMembers(selectedChannel.GuildID, msg.Older)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return ml.Model.Update(msg)
|
||||
|
|
@ -1016,10 +1011,6 @@ func (ml *messagesList) fetchOlderMessages() tview.Cmd {
|
|||
return nil
|
||||
}
|
||||
|
||||
if guildID := selectedChannel.GuildID; guildID.IsValid() {
|
||||
ml.requestGuildMembers(guildID, messages)
|
||||
}
|
||||
|
||||
older := slices.Clone(messages)
|
||||
slices.Reverse(older)
|
||||
return olderMessagesLoadedMsg{ChannelID: channelID, Older: older}
|
||||
|
|
@ -1327,7 +1318,7 @@ func (ml *messagesList) deleteSelectedMessage() tview.Cmd {
|
|||
}
|
||||
}
|
||||
|
||||
func (ml *messagesList) requestGuildMembers(guildID discord.GuildID, messages []discord.Message) {
|
||||
func (ml *messagesList) requestGuildMembers(guildID discord.GuildID, messages []discord.Message) tview.Cmd {
|
||||
usersToFetch := make([]discord.UserID, 0, len(messages))
|
||||
seen := make(map[discord.UserID]struct{}, len(messages))
|
||||
|
||||
|
|
@ -1346,49 +1337,23 @@ func (ml *messagesList) requestGuildMembers(guildID discord.GuildID, messages []
|
|||
}
|
||||
}
|
||||
|
||||
if len(usersToFetch) > 0 {
|
||||
err := ml.chat.state.SendGateway(context.Background(), &gateway.RequestGuildMembersCommand{
|
||||
if len(usersToFetch) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return func() tview.Msg {
|
||||
if err := ml.chat.state.SendGateway(context.Background(), &gateway.RequestGuildMembersCommand{
|
||||
GuildIDs: []discord.GuildID{guildID},
|
||||
UserIDs: usersToFetch,
|
||||
})
|
||||
if err != nil {
|
||||
}); err != nil {
|
||||
slog.Error("failed to request guild members", "guild_id", guildID, "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
ml.setFetchingChunk(true, 0)
|
||||
ml.waitForChunkEvent()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (ml *messagesList) setFetchingChunk(value bool, count uint) {
|
||||
ml.fetchingMembers.mu.Lock()
|
||||
defer ml.fetchingMembers.mu.Unlock()
|
||||
|
||||
if ml.fetchingMembers.value == value {
|
||||
return
|
||||
}
|
||||
|
||||
ml.fetchingMembers.value = value
|
||||
|
||||
if value {
|
||||
ml.fetchingMembers.done = make(chan struct{})
|
||||
} else {
|
||||
ml.fetchingMembers.count = count
|
||||
close(ml.fetchingMembers.done)
|
||||
}
|
||||
}
|
||||
|
||||
func (ml *messagesList) waitForChunkEvent() uint {
|
||||
ml.fetchingMembers.mu.Lock()
|
||||
if !ml.fetchingMembers.value {
|
||||
ml.fetchingMembers.mu.Unlock()
|
||||
return 0
|
||||
}
|
||||
ml.fetchingMembers.mu.Unlock()
|
||||
|
||||
<-ml.fetchingMembers.done
|
||||
return ml.fetchingMembers.count
|
||||
func (ml *messagesList) invalidateRenderedMessages() {
|
||||
clear(ml.itemByID)
|
||||
}
|
||||
|
||||
func (ml *messagesList) ShortHelp() []keybind.Keybind {
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
|||
m.onMessageDelete(eventMsg)
|
||||
|
||||
case *gateway.GuildMembersChunkEvent:
|
||||
m.onGuildMembersChunk(eventMsg)
|
||||
return tview.Batch(m.onGuildMembersChunk(eventMsg), m.listen())
|
||||
case *gateway.GuildMemberRemoveEvent:
|
||||
m.onGuildMemberRemove(eventMsg)
|
||||
|
||||
|
|
@ -312,6 +312,9 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
|||
focusCmd = m.focusComposer()
|
||||
}
|
||||
m.composer.SetPlaceholder(tview.NewLine(tview.NewSegment(text, tcell.StyleDefault.Dim(true))))
|
||||
if msg.Channel.GuildID.IsValid() {
|
||||
return tview.Batch(focusCmd, m.messagesList.requestGuildMembers(msg.Channel.GuildID, msg.Messages))
|
||||
}
|
||||
return focusCmd
|
||||
case QuitMsg:
|
||||
return m.closeState()
|
||||
|
|
|
|||
|
|
@ -167,8 +167,9 @@ func cursorAfterDelete(prevCursor, deletedIndex, remaining int) int {
|
|||
}
|
||||
}
|
||||
|
||||
func (m *Model) onGuildMembersChunk(event *gateway.GuildMembersChunkEvent) {
|
||||
m.messagesList.setFetchingChunk(false, uint(len(event.Members)))
|
||||
func (m *Model) onGuildMembersChunk(event *gateway.GuildMembersChunkEvent) tview.Cmd {
|
||||
m.messagesList.invalidateRenderedMessages()
|
||||
return m.composer.onGuildMembersChunk(event)
|
||||
}
|
||||
|
||||
func (m *Model) onGuildMemberRemove(event *gateway.GuildMemberRemoveEvent) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue