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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -15,6 +16,7 @@ import (
|
||||||
|
|
||||||
"github.com/ayn2op/arikawa/v3/api"
|
"github.com/ayn2op/arikawa/v3/api"
|
||||||
"github.com/ayn2op/arikawa/v3/discord"
|
"github.com/ayn2op/arikawa/v3/discord"
|
||||||
|
"github.com/ayn2op/arikawa/v3/gateway"
|
||||||
"github.com/ayn2op/arikawa/v3/state"
|
"github.com/ayn2op/arikawa/v3/state"
|
||||||
"github.com/ayn2op/arikawa/v3/utils/json/option"
|
"github.com/ayn2op/arikawa/v3/utils/json/option"
|
||||||
"github.com/ayn2op/arikawa/v3/utils/sendpart"
|
"github.com/ayn2op/arikawa/v3/utils/sendpart"
|
||||||
|
|
@ -37,6 +39,7 @@ import (
|
||||||
const (
|
const (
|
||||||
tmpFilePattern = consts.Name + "_*.md"
|
tmpFilePattern = consts.Name + "_*.md"
|
||||||
imageAttachmentName = "clipboard.png"
|
imageAttachmentName = "clipboard.png"
|
||||||
|
memberSearchNonce = "autocomplete:"
|
||||||
)
|
)
|
||||||
|
|
||||||
var mentionRegex = regexp.MustCompile("@[a-zA-Z0-9._]+")
|
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.
|
// searchMember performs member discovery in a command goroutine.
|
||||||
// It emits a follow-up suggestion message once results are loaded.
|
// It emits a follow-up suggestion message once results are loaded.
|
||||||
func (c *composer) searchMember(gID discord.GuildID, name string) tview.Cmd {
|
func (c *composer) searchMember(gID discord.GuildID, name string) tview.Cmd {
|
||||||
|
if name == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
key := gID.String() + " " + name
|
key := gID.String() + " " + name
|
||||||
if _, ok := c.cache.Get(key); ok {
|
if _, ok := c.cache.Get(key); ok {
|
||||||
return nil
|
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
|
// 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
|
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 {
|
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{}
|
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)
|
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}
|
return channelLoadedMsg{Channel: channel, Messages: messages}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
|
@ -55,13 +54,6 @@ type messagesList struct {
|
||||||
itemByID map[discord.MessageID]*tview.TextView
|
itemByID map[discord.MessageID]*tview.TextView
|
||||||
|
|
||||||
attachmentsPicker *attachmentsPicker
|
attachmentsPicker *attachmentsPicker
|
||||||
|
|
||||||
fetchingMembers struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
value bool
|
|
||||||
count uint
|
|
||||||
done chan struct{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ help.KeyMap = (*messagesList)(nil)
|
var _ help.KeyMap = (*messagesList)(nil)
|
||||||
|
|
@ -920,6 +912,9 @@ func (ml *messagesList) Update(msg tview.Msg) tview.Cmd {
|
||||||
default:
|
default:
|
||||||
ml.SetCursor(prevCursor)
|
ml.SetCursor(prevCursor)
|
||||||
}
|
}
|
||||||
|
if selectedChannel.GuildID.IsValid() {
|
||||||
|
return ml.requestGuildMembers(selectedChannel.GuildID, msg.Older)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return ml.Model.Update(msg)
|
return ml.Model.Update(msg)
|
||||||
|
|
@ -1016,10 +1011,6 @@ func (ml *messagesList) fetchOlderMessages() tview.Cmd {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if guildID := selectedChannel.GuildID; guildID.IsValid() {
|
|
||||||
ml.requestGuildMembers(guildID, messages)
|
|
||||||
}
|
|
||||||
|
|
||||||
older := slices.Clone(messages)
|
older := slices.Clone(messages)
|
||||||
slices.Reverse(older)
|
slices.Reverse(older)
|
||||||
return olderMessagesLoadedMsg{ChannelID: channelID, Older: 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))
|
usersToFetch := make([]discord.UserID, 0, len(messages))
|
||||||
seen := make(map[discord.UserID]struct{}, 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 {
|
if len(usersToFetch) == 0 {
|
||||||
err := ml.chat.state.SendGateway(context.Background(), &gateway.RequestGuildMembersCommand{
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() tview.Msg {
|
||||||
|
if err := ml.chat.state.SendGateway(context.Background(), &gateway.RequestGuildMembersCommand{
|
||||||
GuildIDs: []discord.GuildID{guildID},
|
GuildIDs: []discord.GuildID{guildID},
|
||||||
UserIDs: usersToFetch,
|
UserIDs: usersToFetch,
|
||||||
})
|
}); err != nil {
|
||||||
if err != nil {
|
|
||||||
slog.Error("failed to request guild members", "guild_id", guildID, "err", err)
|
slog.Error("failed to request guild members", "guild_id", guildID, "err", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
ml.setFetchingChunk(true, 0)
|
|
||||||
ml.waitForChunkEvent()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ml *messagesList) setFetchingChunk(value bool, count uint) {
|
func (ml *messagesList) invalidateRenderedMessages() {
|
||||||
ml.fetchingMembers.mu.Lock()
|
clear(ml.itemByID)
|
||||||
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) ShortHelp() []keybind.Keybind {
|
func (ml *messagesList) ShortHelp() []keybind.Keybind {
|
||||||
|
|
|
||||||
|
|
@ -267,7 +267,7 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
||||||
m.onMessageDelete(eventMsg)
|
m.onMessageDelete(eventMsg)
|
||||||
|
|
||||||
case *gateway.GuildMembersChunkEvent:
|
case *gateway.GuildMembersChunkEvent:
|
||||||
m.onGuildMembersChunk(eventMsg)
|
return tview.Batch(m.onGuildMembersChunk(eventMsg), m.listen())
|
||||||
case *gateway.GuildMemberRemoveEvent:
|
case *gateway.GuildMemberRemoveEvent:
|
||||||
m.onGuildMemberRemove(eventMsg)
|
m.onGuildMemberRemove(eventMsg)
|
||||||
|
|
||||||
|
|
@ -312,6 +312,9 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
||||||
focusCmd = m.focusComposer()
|
focusCmd = m.focusComposer()
|
||||||
}
|
}
|
||||||
m.composer.SetPlaceholder(tview.NewLine(tview.NewSegment(text, tcell.StyleDefault.Dim(true))))
|
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
|
return focusCmd
|
||||||
case QuitMsg:
|
case QuitMsg:
|
||||||
return m.closeState()
|
return m.closeState()
|
||||||
|
|
|
||||||
|
|
@ -167,8 +167,9 @@ func cursorAfterDelete(prevCursor, deletedIndex, remaining int) int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) onGuildMembersChunk(event *gateway.GuildMembersChunkEvent) {
|
func (m *Model) onGuildMembersChunk(event *gateway.GuildMembersChunkEvent) tview.Cmd {
|
||||||
m.messagesList.setFetchingChunk(false, uint(len(event.Members)))
|
m.messagesList.invalidateRenderedMessages()
|
||||||
|
return m.composer.onGuildMembersChunk(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) onGuildMemberRemove(event *gateway.GuildMemberRemoveEvent) {
|
func (m *Model) onGuildMemberRemove(event *gateway.GuildMemberRemoveEvent) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue