refactor: move app and chat UI out of cmd (#680)
This commit is contained in:
parent
870bc240ee
commit
91ca816ea0
11 changed files with 563 additions and 511 deletions
187
cmd/chatview.go
187
cmd/chatview.go
|
|
@ -1,187 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/ayn2op/discordo/internal/config"
|
||||
"github.com/ayn2op/discordo/internal/keyring"
|
||||
"github.com/ayn2op/discordo/internal/ui"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/gdamore/tcell/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
flexPageName = "flex"
|
||||
mentionsListPageName = "mentionsList"
|
||||
attachmentsListPageName = "attachmentsList"
|
||||
confirmModalPageName = "confirmModal"
|
||||
)
|
||||
|
||||
type chatView struct {
|
||||
*tview.Pages
|
||||
|
||||
mainFlex *tview.Flex
|
||||
rightFlex *tview.Flex
|
||||
|
||||
guildsTree *guildsTree
|
||||
messagesList *messagesList
|
||||
messageInput *messageInput
|
||||
|
||||
selectedChannel *discord.Channel
|
||||
|
||||
app *tview.Application
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func newChatView(app *tview.Application, cfg *config.Config) *chatView {
|
||||
chatView := &chatView{
|
||||
Pages: tview.NewPages(),
|
||||
|
||||
mainFlex: tview.NewFlex(),
|
||||
rightFlex: tview.NewFlex(),
|
||||
|
||||
guildsTree: newGuildsTree(cfg),
|
||||
messagesList: newMessagesList(cfg),
|
||||
messageInput: newMessageInput(cfg),
|
||||
|
||||
app: app,
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
chatView.SetInputCapture(chatView.onInputCapture)
|
||||
|
||||
chatView.buildLayout()
|
||||
return chatView
|
||||
}
|
||||
|
||||
func (cv *chatView) buildLayout() {
|
||||
cv.Clear()
|
||||
cv.rightFlex.Clear()
|
||||
cv.mainFlex.Clear()
|
||||
|
||||
cv.rightFlex.
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(cv.messagesList, 0, 1, false).
|
||||
AddItem(cv.messageInput, 3, 1, false)
|
||||
// The guilds tree is always focused first at start-up.
|
||||
cv.mainFlex.
|
||||
AddItem(cv.guildsTree, 0, 1, true).
|
||||
AddItem(cv.rightFlex, 0, 4, false)
|
||||
|
||||
cv.AddAndSwitchToPage(flexPageName, cv.mainFlex, true)
|
||||
}
|
||||
|
||||
func (cv *chatView) toggleGuildsTree() {
|
||||
// The guilds tree is visible if the number of items is two.
|
||||
if cv.mainFlex.GetItemCount() == 2 {
|
||||
cv.mainFlex.RemoveItem(cv.guildsTree)
|
||||
if cv.guildsTree.HasFocus() {
|
||||
cv.app.SetFocus(cv.mainFlex)
|
||||
}
|
||||
} else {
|
||||
cv.buildLayout()
|
||||
cv.app.SetFocus(cv.guildsTree)
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *chatView) focusGuildsTree() bool {
|
||||
// The guilds tree is not hidden if the number of items is two.
|
||||
if cv.mainFlex.GetItemCount() == 2 {
|
||||
cv.app.SetFocus(cv.guildsTree)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (cv *chatView) focusMessageInput() bool {
|
||||
if !cv.messageInput.GetDisabled() {
|
||||
cv.app.SetFocus(cv.messageInput)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (cv *chatView) focusPrevious() {
|
||||
switch cv.app.GetFocus() {
|
||||
case cv.guildsTree:
|
||||
cv.focusMessageInput()
|
||||
case cv.messagesList: // Handle both a.messagesList and a.flex as well as other edge cases (if there is).
|
||||
if ok := cv.focusGuildsTree(); !ok {
|
||||
cv.app.SetFocus(cv.messageInput)
|
||||
}
|
||||
case cv.messageInput:
|
||||
cv.app.SetFocus(cv.messagesList)
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *chatView) focusNext() {
|
||||
switch cv.app.GetFocus() {
|
||||
case cv.guildsTree:
|
||||
cv.app.SetFocus(cv.messagesList)
|
||||
case cv.messagesList:
|
||||
cv.focusMessageInput()
|
||||
case cv.messageInput: // Handle both a.messageInput and a.flex as well as other edge cases (if there is).
|
||||
if ok := cv.focusGuildsTree(); !ok {
|
||||
cv.app.SetFocus(cv.messagesList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *chatView) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch event.Name() {
|
||||
case cv.cfg.Keys.FocusGuildsTree:
|
||||
cv.messageInput.removeMentionsList()
|
||||
cv.focusGuildsTree()
|
||||
return nil
|
||||
case cv.cfg.Keys.FocusMessagesList:
|
||||
cv.messageInput.removeMentionsList()
|
||||
cv.app.SetFocus(cv.messagesList)
|
||||
return nil
|
||||
case cv.cfg.Keys.FocusMessageInput:
|
||||
cv.focusMessageInput()
|
||||
return nil
|
||||
case cv.cfg.Keys.FocusPrevious:
|
||||
cv.focusPrevious()
|
||||
return nil
|
||||
case cv.cfg.Keys.FocusNext:
|
||||
cv.focusNext()
|
||||
return nil
|
||||
case cv.cfg.Keys.Logout:
|
||||
app.quit()
|
||||
|
||||
if err := keyring.DeleteToken(); err != nil {
|
||||
slog.Error("failed to delete token from keyring", "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
case cv.cfg.Keys.ToggleGuildsTree:
|
||||
cv.toggleGuildsTree()
|
||||
return nil
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func (cv *chatView) showConfirmModal(prompt string, buttons []string, onDone func(label string)) {
|
||||
previousFocus := cv.app.GetFocus()
|
||||
|
||||
modal := tview.NewModal().
|
||||
SetText(prompt).
|
||||
AddButtons(buttons).
|
||||
SetDoneFunc(func(_ int, buttonLabel string) {
|
||||
cv.RemovePage(confirmModalPageName).SwitchToPage(flexPageName)
|
||||
cv.app.SetFocus(previousFocus)
|
||||
|
||||
if onDone != nil {
|
||||
onDone(buttonLabel)
|
||||
}
|
||||
})
|
||||
|
||||
cv.
|
||||
AddAndSwitchToPage(confirmModalPageName, ui.Centered(modal, 0, 0), true).
|
||||
ShowPage(flexPageName)
|
||||
}
|
||||
10
cmd/root.go
10
cmd/root.go
|
|
@ -6,20 +6,15 @@ import (
|
|||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/ayn2op/discordo/internal/app"
|
||||
"github.com/ayn2op/discordo/internal/config"
|
||||
"github.com/ayn2op/discordo/internal/consts"
|
||||
"github.com/ayn2op/discordo/internal/keyring"
|
||||
"github.com/ayn2op/discordo/internal/logger"
|
||||
"github.com/diamondburned/arikawa/v3/utils/ws"
|
||||
"github.com/diamondburned/ningen/v3"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
discordState *ningen.State
|
||||
app *application
|
||||
)
|
||||
|
||||
var (
|
||||
token string
|
||||
configPath string
|
||||
|
|
@ -62,8 +57,7 @@ var (
|
|||
}
|
||||
}
|
||||
|
||||
app = newApplication(cfg)
|
||||
return app.run(token)
|
||||
return app.New(cfg).Run(token)
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
177
cmd/state.go
177
cmd/state.go
|
|
@ -1,177 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/ayn2op/discordo/internal/http"
|
||||
"github.com/ayn2op/discordo/internal/notifications"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/session"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
"github.com/diamondburned/arikawa/v3/state/store/defaultstore"
|
||||
"github.com/diamondburned/arikawa/v3/utils/handler"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil/httpdriver"
|
||||
"github.com/diamondburned/arikawa/v3/utils/ws"
|
||||
"github.com/diamondburned/ningen/v3"
|
||||
"github.com/diamondburned/ningen/v3/states/read"
|
||||
)
|
||||
|
||||
func openState(token string) error {
|
||||
identifyProps := http.IdentifyProperties()
|
||||
gateway.DefaultIdentity = identifyProps
|
||||
gateway.DefaultPresence = &gateway.UpdatePresenceCommand{
|
||||
Status: app.cfg.Status,
|
||||
}
|
||||
|
||||
id := gateway.DefaultIdentifier(token)
|
||||
id.Compress = false
|
||||
|
||||
session := session.NewCustom(id, http.NewClient(token), handler.New())
|
||||
state := state.NewFromSession(session, defaultstore.New())
|
||||
discordState = ningen.FromState(state)
|
||||
|
||||
// Handlers
|
||||
discordState.AddHandler(onRaw)
|
||||
discordState.AddHandler(onReady)
|
||||
discordState.AddHandler(onMessageCreate)
|
||||
discordState.AddHandler(onMessageUpdate)
|
||||
discordState.AddHandler(onMessageDelete)
|
||||
discordState.AddHandler(onReadUpdate)
|
||||
|
||||
discordState.AddHandler(func(event *gateway.GuildMembersChunkEvent) {
|
||||
app.chatView.messagesList.setFetchingChunk(false, uint(len(event.Members)))
|
||||
})
|
||||
|
||||
discordState.AddHandler(func(event *gateway.GuildMemberRemoveEvent) {
|
||||
app.chatView.messageInput.cache.Invalidate(event.GuildID.String()+" "+event.User.Username, discordState.MemberState.SearchLimit)
|
||||
})
|
||||
|
||||
discordState.StateLog = func(err error) {
|
||||
slog.Error("state log", "err", err)
|
||||
}
|
||||
|
||||
discordState.OnRequest = append(discordState.OnRequest, httputil.WithHeaders(http.Headers()), onRequest)
|
||||
return discordState.Open(context.TODO())
|
||||
}
|
||||
|
||||
func onRequest(r httpdriver.Request) error {
|
||||
if req, ok := r.(*httpdriver.DefaultRequest); ok {
|
||||
slog.Debug("new HTTP request", "method", req.Method, "url", req.URL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func onRaw(event *ws.RawEvent) {
|
||||
slog.Debug(
|
||||
"new raw event",
|
||||
"code", event.OriginalCode,
|
||||
"type", event.OriginalType,
|
||||
// "data", event.Raw,
|
||||
)
|
||||
}
|
||||
|
||||
func onReadUpdate(event *read.UpdateEvent) {
|
||||
var guildNode *tview.TreeNode
|
||||
app.chatView.guildsTree.
|
||||
GetRoot().
|
||||
Walk(func(node, parent *tview.TreeNode) bool {
|
||||
switch node.GetReference() {
|
||||
case event.GuildID:
|
||||
node.SetTextStyle(app.chatView.guildsTree.getGuildNodeStyle(event.GuildID))
|
||||
guildNode = node
|
||||
return false
|
||||
case event.ChannelID:
|
||||
// private channel
|
||||
if !event.GuildID.IsValid() {
|
||||
style := app.chatView.guildsTree.getChannelNodeStyle(event.ChannelID)
|
||||
node.SetTextStyle(style)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if guildNode != nil {
|
||||
guildNode.Walk(func(node, parent *tview.TreeNode) bool {
|
||||
if node.GetReference() == event.ChannelID {
|
||||
node.SetTextStyle(app.chatView.guildsTree.getChannelNodeStyle(event.ChannelID))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
app.Draw()
|
||||
}
|
||||
|
||||
func onReady(r *gateway.ReadyEvent) {
|
||||
dmNode := tview.NewTreeNode("Direct Messages")
|
||||
root := app.chatView.guildsTree.
|
||||
GetRoot().
|
||||
ClearChildren().
|
||||
AddChild(dmNode)
|
||||
|
||||
for _, folder := range r.UserSettings.GuildFolders {
|
||||
if folder.ID == 0 && len(folder.GuildIDs) == 1 {
|
||||
guild, err := discordState.Cabinet.Guild(folder.GuildIDs[0])
|
||||
if err != nil {
|
||||
slog.Error(
|
||||
"failed to get guild from state",
|
||||
"guild_id",
|
||||
folder.GuildIDs[0],
|
||||
"err",
|
||||
err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
app.chatView.guildsTree.createGuildNode(root, *guild)
|
||||
} else {
|
||||
app.chatView.guildsTree.createFolderNode(folder)
|
||||
}
|
||||
}
|
||||
|
||||
app.chatView.guildsTree.SetCurrentNode(root)
|
||||
app.SetFocus(app.chatView.guildsTree)
|
||||
app.Draw()
|
||||
}
|
||||
|
||||
func onMessageCreate(message *gateway.MessageCreateEvent) {
|
||||
if app.chatView.selectedChannel != nil &&
|
||||
app.chatView.selectedChannel.ID == message.ChannelID {
|
||||
app.chatView.messagesList.drawMessage(app.chatView.messagesList, message.Message)
|
||||
app.Draw()
|
||||
}
|
||||
|
||||
if err := notifications.Notify(discordState, message, app.cfg); err != nil {
|
||||
slog.Error("failed to notify", "err", err, "channel_id", message.ChannelID, "message_id", message.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func onMessageUpdate(message *gateway.MessageUpdateEvent) {
|
||||
if app.chatView.selectedChannel != nil &&
|
||||
app.chatView.selectedChannel.ID == message.ChannelID {
|
||||
onMessageDelete(&gateway.MessageDeleteEvent{ID: message.ID, ChannelID: message.ChannelID, GuildID: message.GuildID})
|
||||
}
|
||||
}
|
||||
|
||||
func onMessageDelete(message *gateway.MessageDeleteEvent) {
|
||||
if app.chatView.selectedChannel != nil &&
|
||||
app.chatView.selectedChannel.ID == message.ChannelID {
|
||||
messages, err := discordState.Cabinet.Messages(message.ChannelID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get messages from state", "err", err, "channel_id", message.ChannelID)
|
||||
return
|
||||
}
|
||||
|
||||
app.chatView.messagesList.reset()
|
||||
app.chatView.messagesList.drawMessages(messages)
|
||||
app.Draw()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package cmd
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
@ -7,33 +7,34 @@ import (
|
|||
"github.com/ayn2op/discordo/internal/clipboard"
|
||||
"github.com/ayn2op/discordo/internal/config"
|
||||
"github.com/ayn2op/discordo/internal/consts"
|
||||
"github.com/ayn2op/discordo/internal/login"
|
||||
"github.com/ayn2op/discordo/internal/ui/chat"
|
||||
"github.com/ayn2op/discordo/internal/ui/login"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/gdamore/tcell/v3"
|
||||
)
|
||||
|
||||
type application struct {
|
||||
*tview.Application
|
||||
chatView *chatView
|
||||
type App struct {
|
||||
inner *tview.Application
|
||||
chatView *chat.ChatView
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func newApplication(cfg *config.Config) *application {
|
||||
func New(cfg *config.Config) *App {
|
||||
tview.Styles = tview.Theme{}
|
||||
app := &application{
|
||||
Application: tview.NewApplication(),
|
||||
cfg: cfg,
|
||||
app := &App{
|
||||
inner: tview.NewApplication(),
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
if err := clipboard.Init(); err != nil {
|
||||
slog.Error("failed to init clipboard", "err", err)
|
||||
}
|
||||
|
||||
app.SetInputCapture(app.onInputCapture)
|
||||
app.inner.SetInputCapture(app.onInputCapture)
|
||||
return app
|
||||
}
|
||||
|
||||
func (a *application) run(token string) error {
|
||||
func (a *App) Run(token string) error {
|
||||
screen, err := tcell.NewScreen()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create screen: %w", err)
|
||||
|
|
@ -50,44 +51,44 @@ func (a *application) run(token string) error {
|
|||
screen.SetTitle(consts.Name)
|
||||
screen.EnablePaste()
|
||||
screen.EnableFocus()
|
||||
a.SetScreen(screen)
|
||||
a.inner.SetScreen(screen)
|
||||
|
||||
if token == "" {
|
||||
loginForm := login.NewForm(a.Application, a.cfg, func(token string) {
|
||||
loginForm := login.NewForm(a.inner, a.cfg, func(token string) {
|
||||
if err := a.showChatView(token); err != nil {
|
||||
slog.Error("failed to show chat view", "err", err)
|
||||
}
|
||||
})
|
||||
a.SetRoot(loginForm)
|
||||
a.inner.SetRoot(loginForm)
|
||||
} else {
|
||||
if err := a.showChatView(token); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return a.Run()
|
||||
return a.inner.Run()
|
||||
}
|
||||
|
||||
func (a *application) showChatView(token string) error {
|
||||
a.chatView = newChatView(a.Application, a.cfg)
|
||||
if err := openState(token); err != nil {
|
||||
func (a *App) showChatView(token string) error {
|
||||
a.chatView = chat.NewChatView(a.inner, a.cfg, a.quit)
|
||||
if err := a.chatView.OpenState(token); err != nil {
|
||||
return err
|
||||
}
|
||||
a.SetRoot(a.chatView)
|
||||
a.inner.SetRoot(a.chatView)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *application) quit() {
|
||||
if discordState != nil {
|
||||
if err := discordState.Close(); err != nil {
|
||||
func (a *App) quit() {
|
||||
if a.chatView != nil {
|
||||
if err := a.chatView.CloseState(); err != nil {
|
||||
slog.Error("failed to close the session", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
a.Stop()
|
||||
a.inner.Stop()
|
||||
}
|
||||
|
||||
func (a *application) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
||||
func (a *App) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch event.Name() {
|
||||
case a.cfg.Keys.Quit:
|
||||
a.quit()
|
||||
316
internal/ui/chat/chatview.go
Normal file
316
internal/ui/chat/chatview.go
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
package chat
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
"github.com/ayn2op/discordo/internal/config"
|
||||
"github.com/ayn2op/discordo/internal/keyring"
|
||||
"github.com/ayn2op/discordo/internal/notifications"
|
||||
"github.com/ayn2op/discordo/internal/ui"
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/ningen/v3"
|
||||
"github.com/diamondburned/ningen/v3/states/read"
|
||||
"github.com/gdamore/tcell/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
flexPageName = "flex"
|
||||
mentionsListPageName = "mentionsList"
|
||||
attachmentsListPageName = "attachmentsList"
|
||||
confirmModalPageName = "confirmModal"
|
||||
)
|
||||
|
||||
type ChatView struct {
|
||||
*tview.Pages
|
||||
|
||||
mainFlex *tview.Flex
|
||||
rightFlex *tview.Flex
|
||||
|
||||
guildsTree *guildsTree
|
||||
messagesList *messagesList
|
||||
messageInput *messageInput
|
||||
|
||||
selectedChannel *discord.Channel
|
||||
selectedChannelMu sync.RWMutex
|
||||
|
||||
app *tview.Application
|
||||
cfg *config.Config
|
||||
state *ningen.State
|
||||
|
||||
onLogout func()
|
||||
}
|
||||
|
||||
func NewChatView(app *tview.Application, cfg *config.Config, onLogout func()) *ChatView {
|
||||
chatView := &ChatView{
|
||||
Pages: tview.NewPages(),
|
||||
|
||||
mainFlex: tview.NewFlex(),
|
||||
rightFlex: tview.NewFlex(),
|
||||
|
||||
app: app,
|
||||
cfg: cfg,
|
||||
onLogout: onLogout,
|
||||
}
|
||||
chatView.guildsTree = newGuildsTree(cfg, chatView)
|
||||
chatView.messagesList = newMessagesList(cfg, chatView)
|
||||
chatView.messageInput = newMessageInput(cfg, chatView)
|
||||
|
||||
chatView.SetInputCapture(chatView.onInputCapture)
|
||||
chatView.buildLayout()
|
||||
return chatView
|
||||
}
|
||||
|
||||
func (cv *ChatView) SelectedChannel() *discord.Channel {
|
||||
cv.selectedChannelMu.RLock()
|
||||
defer cv.selectedChannelMu.RUnlock()
|
||||
return cv.selectedChannel
|
||||
}
|
||||
|
||||
func (cv *ChatView) SetSelectedChannel(channel *discord.Channel) {
|
||||
cv.selectedChannelMu.Lock()
|
||||
cv.selectedChannel = channel
|
||||
cv.selectedChannelMu.Unlock()
|
||||
}
|
||||
|
||||
func (cv *ChatView) buildLayout() {
|
||||
cv.Clear()
|
||||
cv.rightFlex.Clear()
|
||||
cv.mainFlex.Clear()
|
||||
|
||||
cv.rightFlex.
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(cv.messagesList, 0, 1, false).
|
||||
AddItem(cv.messageInput, 3, 1, false)
|
||||
// The guilds tree is always focused first at start-up.
|
||||
cv.mainFlex.
|
||||
AddItem(cv.guildsTree, 0, 1, true).
|
||||
AddItem(cv.rightFlex, 0, 4, false)
|
||||
|
||||
cv.AddAndSwitchToPage(flexPageName, cv.mainFlex, true)
|
||||
}
|
||||
|
||||
func (cv *ChatView) toggleGuildsTree() {
|
||||
// The guilds tree is visible if the number of items is two.
|
||||
if cv.mainFlex.GetItemCount() == 2 {
|
||||
cv.mainFlex.RemoveItem(cv.guildsTree)
|
||||
if cv.guildsTree.HasFocus() {
|
||||
cv.app.SetFocus(cv.mainFlex)
|
||||
}
|
||||
} else {
|
||||
cv.buildLayout()
|
||||
cv.app.SetFocus(cv.guildsTree)
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ChatView) focusGuildsTree() bool {
|
||||
// The guilds tree is not hidden if the number of items is two.
|
||||
if cv.mainFlex.GetItemCount() == 2 {
|
||||
cv.app.SetFocus(cv.guildsTree)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (cv *ChatView) focusMessageInput() bool {
|
||||
if !cv.messageInput.GetDisabled() {
|
||||
cv.app.SetFocus(cv.messageInput)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (cv *ChatView) focusPrevious() {
|
||||
switch cv.app.GetFocus() {
|
||||
case cv.guildsTree:
|
||||
cv.focusMessageInput()
|
||||
case cv.messagesList: // Handle both a.messagesList and a.flex as well as other edge cases (if there is).
|
||||
if ok := cv.focusGuildsTree(); !ok {
|
||||
cv.app.SetFocus(cv.messageInput)
|
||||
}
|
||||
case cv.messageInput:
|
||||
cv.app.SetFocus(cv.messagesList)
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ChatView) focusNext() {
|
||||
switch cv.app.GetFocus() {
|
||||
case cv.guildsTree:
|
||||
cv.app.SetFocus(cv.messagesList)
|
||||
case cv.messagesList:
|
||||
cv.focusMessageInput()
|
||||
case cv.messageInput: // Handle both a.messageInput and a.flex as well as other edge cases (if there is).
|
||||
if ok := cv.focusGuildsTree(); !ok {
|
||||
cv.app.SetFocus(cv.messagesList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ChatView) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch event.Name() {
|
||||
case cv.cfg.Keys.FocusGuildsTree:
|
||||
cv.messageInput.removeMentionsList()
|
||||
cv.focusGuildsTree()
|
||||
return nil
|
||||
case cv.cfg.Keys.FocusMessagesList:
|
||||
cv.messageInput.removeMentionsList()
|
||||
cv.app.SetFocus(cv.messagesList)
|
||||
return nil
|
||||
case cv.cfg.Keys.FocusMessageInput:
|
||||
cv.focusMessageInput()
|
||||
return nil
|
||||
case cv.cfg.Keys.FocusPrevious:
|
||||
cv.focusPrevious()
|
||||
return nil
|
||||
case cv.cfg.Keys.FocusNext:
|
||||
cv.focusNext()
|
||||
return nil
|
||||
case cv.cfg.Keys.Logout:
|
||||
if cv.onLogout != nil {
|
||||
cv.onLogout()
|
||||
}
|
||||
|
||||
if err := keyring.DeleteToken(); err != nil {
|
||||
slog.Error("failed to delete token from keyring", "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
case cv.cfg.Keys.ToggleGuildsTree:
|
||||
cv.toggleGuildsTree()
|
||||
return nil
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func (cv *ChatView) showConfirmModal(prompt string, buttons []string, onDone func(label string)) {
|
||||
previousFocus := cv.app.GetFocus()
|
||||
|
||||
modal := tview.NewModal().
|
||||
SetText(prompt).
|
||||
AddButtons(buttons).
|
||||
SetDoneFunc(func(_ int, buttonLabel string) {
|
||||
cv.RemovePage(confirmModalPageName).SwitchToPage(flexPageName)
|
||||
cv.app.SetFocus(previousFocus)
|
||||
|
||||
if onDone != nil {
|
||||
onDone(buttonLabel)
|
||||
}
|
||||
})
|
||||
|
||||
cv.
|
||||
AddAndSwitchToPage(confirmModalPageName, ui.Centered(modal, 0, 0), true).
|
||||
ShowPage(flexPageName)
|
||||
}
|
||||
|
||||
func (cv *ChatView) onReadUpdate(event *read.UpdateEvent) {
|
||||
var guildNode *tview.TreeNode
|
||||
cv.guildsTree.
|
||||
GetRoot().
|
||||
Walk(func(node, parent *tview.TreeNode) bool {
|
||||
switch node.GetReference() {
|
||||
case event.GuildID:
|
||||
node.SetTextStyle(cv.guildsTree.getGuildNodeStyle(event.GuildID))
|
||||
guildNode = node
|
||||
return false
|
||||
case event.ChannelID:
|
||||
// private channel
|
||||
if !event.GuildID.IsValid() {
|
||||
style := cv.guildsTree.getChannelNodeStyle(event.ChannelID)
|
||||
node.SetTextStyle(style)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if guildNode != nil {
|
||||
guildNode.Walk(func(node, parent *tview.TreeNode) bool {
|
||||
if node.GetReference() == event.ChannelID {
|
||||
node.SetTextStyle(cv.guildsTree.getChannelNodeStyle(event.ChannelID))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
cv.app.Draw()
|
||||
}
|
||||
|
||||
func (cv *ChatView) onReady(r *gateway.ReadyEvent) {
|
||||
dmNode := tview.NewTreeNode("Direct Messages")
|
||||
root := cv.guildsTree.
|
||||
GetRoot().
|
||||
ClearChildren().
|
||||
AddChild(dmNode)
|
||||
|
||||
for _, folder := range r.UserSettings.GuildFolders {
|
||||
if folder.ID == 0 && len(folder.GuildIDs) == 1 {
|
||||
guild, err := cv.state.Cabinet.Guild(folder.GuildIDs[0])
|
||||
if err != nil {
|
||||
slog.Error(
|
||||
"failed to get guild from state",
|
||||
"guild_id",
|
||||
folder.GuildIDs[0],
|
||||
"err",
|
||||
err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
cv.guildsTree.createGuildNode(root, *guild)
|
||||
} else {
|
||||
cv.guildsTree.createFolderNode(folder)
|
||||
}
|
||||
}
|
||||
|
||||
cv.guildsTree.SetCurrentNode(root)
|
||||
cv.app.SetFocus(cv.guildsTree)
|
||||
cv.app.Draw()
|
||||
}
|
||||
|
||||
func (cv *ChatView) onMessageCreate(message *gateway.MessageCreateEvent) {
|
||||
if selected := cv.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
|
||||
cv.messagesList.drawMessage(cv.messagesList, message.Message)
|
||||
cv.app.Draw()
|
||||
}
|
||||
|
||||
if err := notifications.Notify(cv.state, message, cv.cfg); err != nil {
|
||||
slog.Error("failed to notify", "err", err, "channel_id", message.ChannelID, "message_id", message.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ChatView) onMessageUpdate(message *gateway.MessageUpdateEvent) {
|
||||
if selected := cv.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
|
||||
cv.onMessageDelete(&gateway.MessageDeleteEvent{ID: message.ID, ChannelID: message.ChannelID, GuildID: message.GuildID})
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ChatView) onMessageDelete(message *gateway.MessageDeleteEvent) {
|
||||
if selected := cv.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
|
||||
messages, err := cv.state.Cabinet.Messages(message.ChannelID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get messages from state", "err", err, "channel_id", message.ChannelID)
|
||||
return
|
||||
}
|
||||
|
||||
cv.messagesList.reset()
|
||||
cv.messagesList.drawMessages(messages)
|
||||
cv.app.Draw()
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ChatView) onGuildMembersChunk(event *gateway.GuildMembersChunkEvent) {
|
||||
cv.messagesList.setFetchingChunk(false, uint(len(event.Members)))
|
||||
}
|
||||
|
||||
func (cv *ChatView) onGuildMemberRemove(event *gateway.GuildMemberRemoveEvent) {
|
||||
cv.messageInput.cache.Invalidate(event.GuildID.String()+" "+event.User.Username, cv.state.MemberState.SearchLimit)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package cmd
|
||||
package chat
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
|
|
@ -18,13 +18,15 @@ import (
|
|||
|
||||
type guildsTree struct {
|
||||
*tview.TreeView
|
||||
cfg *config.Config
|
||||
cfg *config.Config
|
||||
chat *ChatView
|
||||
}
|
||||
|
||||
func newGuildsTree(cfg *config.Config) *guildsTree {
|
||||
func newGuildsTree(cfg *config.Config, chat *ChatView) *guildsTree {
|
||||
gt := &guildsTree{
|
||||
TreeView: tview.NewTreeView(),
|
||||
cfg: cfg,
|
||||
chat: chat,
|
||||
}
|
||||
|
||||
gt.Box = ui.ConfigureBox(gt.Box, &cfg.Theme)
|
||||
|
|
@ -50,7 +52,7 @@ func (gt *guildsTree) createFolderNode(folder gateway.GuildFolder) {
|
|||
gt.GetRoot().AddChild(folderNode)
|
||||
|
||||
for _, gID := range folder.GuildIDs {
|
||||
guild, err := discordState.Cabinet.Guild(gID)
|
||||
guild, err := gt.chat.state.Cabinet.Guild(gID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get guild from state", "guild_id", gID, "err", err)
|
||||
continue
|
||||
|
|
@ -76,12 +78,12 @@ func (gt *guildsTree) unreadStyle(indication ningen.UnreadIndication) tcell.Styl
|
|||
}
|
||||
|
||||
func (gt *guildsTree) getGuildNodeStyle(guildID discord.GuildID) tcell.Style {
|
||||
indication := discordState.GuildIsUnread(guildID, ningen.GuildUnreadOpts{UnreadOpts: ningen.UnreadOpts{IncludeMutedCategories: true}})
|
||||
indication := gt.chat.state.GuildIsUnread(guildID, ningen.GuildUnreadOpts{UnreadOpts: ningen.UnreadOpts{IncludeMutedCategories: true}})
|
||||
return gt.unreadStyle(indication)
|
||||
}
|
||||
|
||||
func (gt *guildsTree) getChannelNodeStyle(channelID discord.ChannelID) tcell.Style {
|
||||
indication := discordState.ChannelIsUnread(channelID, ningen.UnreadOpts{IncludeMutedCategories: true})
|
||||
indication := gt.chat.state.ChannelIsUnread(channelID, ningen.UnreadOpts{IncludeMutedCategories: true})
|
||||
return gt.unreadStyle(indication)
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +95,7 @@ func (gt *guildsTree) createGuildNode(n *tview.TreeNode, guild discord.Guild) {
|
|||
}
|
||||
|
||||
func (gt *guildsTree) createChannelNode(node *tview.TreeNode, channel discord.Channel) {
|
||||
if channel.Type != discord.DirectMessage && channel.Type != discord.GroupDM && !discordState.HasPermissions(channel.ID, discord.PermissionViewChannel) {
|
||||
if channel.Type != discord.DirectMessage && channel.Type != discord.GroupDM && !gt.chat.state.HasPermissions(channel.ID, discord.PermissionViewChannel) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -149,9 +151,9 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) {
|
|||
|
||||
switch ref := node.GetReference().(type) {
|
||||
case discord.GuildID:
|
||||
go discordState.MemberState.Subscribe(ref)
|
||||
go gt.chat.state.MemberState.Subscribe(ref)
|
||||
|
||||
channels, err := discordState.Cabinet.Channels(ref)
|
||||
channels, err := gt.chat.state.Cabinet.Channels(ref)
|
||||
if err != nil {
|
||||
slog.Error("failed to get channels", "err", err, "guild_id", ref)
|
||||
return
|
||||
|
|
@ -163,7 +165,7 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) {
|
|||
|
||||
gt.createChannelNodes(node, channels)
|
||||
case discord.ChannelID:
|
||||
channel, err := discordState.Cabinet.Channel(ref)
|
||||
channel, err := gt.chat.state.Cabinet.Channel(ref)
|
||||
if err != nil {
|
||||
slog.Error("failed to get channel", "channel_id", ref)
|
||||
return
|
||||
|
|
@ -172,7 +174,7 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) {
|
|||
// Handle forum channels differently - they contain threads, not direct messages
|
||||
if channel.Type == discord.GuildForum {
|
||||
// Get all channels from the guild - this includes active threads from GuildCreateEvent
|
||||
allChannels, err := discordState.Cabinet.Channels(channel.GuildID)
|
||||
allChannels, err := gt.chat.state.Cabinet.Channels(channel.GuildID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get channels for forum threads", "err", err, "guild_id", channel.GuildID)
|
||||
return
|
||||
|
|
@ -198,38 +200,38 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) {
|
|||
return
|
||||
}
|
||||
|
||||
go discordState.ReadState.MarkRead(channel.ID, channel.LastMessageID)
|
||||
go gt.chat.state.ReadState.MarkRead(channel.ID, channel.LastMessageID)
|
||||
|
||||
messages, err := discordState.Messages(channel.ID, uint(gt.cfg.MessagesLimit))
|
||||
messages, err := gt.chat.state.Messages(channel.ID, uint(gt.cfg.MessagesLimit))
|
||||
if err != nil {
|
||||
slog.Error("failed to get messages", "err", err, "channel_id", channel.ID, "limit", gt.cfg.MessagesLimit)
|
||||
return
|
||||
}
|
||||
|
||||
if guildID := channel.GuildID; guildID.IsValid() {
|
||||
app.chatView.messagesList.requestGuildMembers(guildID, messages)
|
||||
gt.chat.messagesList.requestGuildMembers(guildID, messages)
|
||||
}
|
||||
|
||||
app.chatView.selectedChannel = channel
|
||||
gt.chat.SetSelectedChannel(channel)
|
||||
|
||||
app.chatView.messagesList.reset()
|
||||
app.chatView.messagesList.setTitle(*channel)
|
||||
app.chatView.messagesList.drawMessages(messages)
|
||||
app.chatView.messagesList.ScrollToEnd()
|
||||
gt.chat.messagesList.reset()
|
||||
gt.chat.messagesList.setTitle(*channel)
|
||||
gt.chat.messagesList.drawMessages(messages)
|
||||
gt.chat.messagesList.ScrollToEnd()
|
||||
|
||||
hasNoPerm := channel.Type != discord.DirectMessage && channel.Type != discord.GroupDM && !discordState.HasPermissions(channel.ID, discord.PermissionSendMessages)
|
||||
app.chatView.messageInput.SetDisabled(hasNoPerm)
|
||||
hasNoPerm := channel.Type != discord.DirectMessage && channel.Type != discord.GroupDM && !gt.chat.state.HasPermissions(channel.ID, discord.PermissionSendMessages)
|
||||
gt.chat.messageInput.SetDisabled(hasNoPerm)
|
||||
if hasNoPerm {
|
||||
app.chatView.messageInput.SetPlaceholder("You do not have permission to send messages in this channel.")
|
||||
gt.chat.messageInput.SetPlaceholder("You do not have permission to send messages in this channel.")
|
||||
} else {
|
||||
app.chatView.messageInput.SetPlaceholder("Message...")
|
||||
gt.chat.messageInput.SetPlaceholder("Message...")
|
||||
if gt.cfg.AutoFocus {
|
||||
app.SetFocus(app.chatView.messageInput)
|
||||
gt.chat.app.SetFocus(gt.chat.messageInput)
|
||||
}
|
||||
}
|
||||
|
||||
case nil: // Direct messages folder
|
||||
channels, err := discordState.PrivateChannels()
|
||||
channels, err := gt.chat.state.PrivateChannels()
|
||||
if err != nil {
|
||||
slog.Error("failed to get private channels", "err", err)
|
||||
return
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package cmd
|
||||
package chat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
@ -25,6 +25,7 @@ import (
|
|||
"github.com/diamondburned/arikawa/v3/state"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/diamondburned/arikawa/v3/utils/sendpart"
|
||||
"github.com/diamondburned/ningen/v3"
|
||||
"github.com/diamondburned/ningen/v3/discordmd"
|
||||
"github.com/gdamore/tcell/v3"
|
||||
"github.com/ncruces/zenity"
|
||||
|
|
@ -38,7 +39,8 @@ var mentionRegex = regexp.MustCompile("@[a-zA-Z0-9._]+")
|
|||
|
||||
type messageInput struct {
|
||||
*tview.TextArea
|
||||
cfg *config.Config
|
||||
cfg *config.Config
|
||||
chat *ChatView
|
||||
|
||||
edit bool
|
||||
sendMessageData *api.SendMessageData
|
||||
|
|
@ -47,10 +49,11 @@ type messageInput struct {
|
|||
lastSearch time.Time
|
||||
}
|
||||
|
||||
func newMessageInput(cfg *config.Config) *messageInput {
|
||||
func newMessageInput(cfg *config.Config, chat *ChatView) *messageInput {
|
||||
mi := &messageInput{
|
||||
TextArea: tview.NewTextArea(),
|
||||
cfg: cfg,
|
||||
chat: chat,
|
||||
sendMessageData: &api.SendMessageData{},
|
||||
cache: cache.NewCache(),
|
||||
mentionsList: tview.NewList(),
|
||||
|
|
@ -94,7 +97,7 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
|||
return tcell.NewEventKey(tcell.KeyCtrlV, "", tcell.ModNone)
|
||||
|
||||
case mi.cfg.Keys.MessageInput.Send:
|
||||
if app.chatView.GetVisibile(mentionsListPageName) {
|
||||
if mi.chat.GetVisibile(mentionsListPageName) {
|
||||
mi.tabComplete()
|
||||
return nil
|
||||
}
|
||||
|
|
@ -110,7 +113,7 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
|||
mi.openFilePicker()
|
||||
return nil
|
||||
case mi.cfg.Keys.MessageInput.Cancel:
|
||||
if app.chatView.GetVisibile(mentionsListPageName) {
|
||||
if mi.chat.GetVisibile(mentionsListPageName) {
|
||||
mi.stopTabCompletion()
|
||||
} else {
|
||||
mi.reset()
|
||||
|
|
@ -118,12 +121,12 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
|||
|
||||
return nil
|
||||
case mi.cfg.Keys.MessageInput.TabComplete:
|
||||
go app.QueueUpdateDraw(func() { mi.tabComplete() })
|
||||
go mi.chat.app.QueueUpdateDraw(func() { mi.tabComplete() })
|
||||
return nil
|
||||
}
|
||||
|
||||
if mi.cfg.AutocompleteLimit > 0 {
|
||||
if app.chatView.GetVisibile(mentionsListPageName) {
|
||||
if mi.chat.GetVisibile(mentionsListPageName) {
|
||||
switch event.Name() {
|
||||
case mi.cfg.Keys.MentionsList.Up:
|
||||
mi.mentionsList.InputHandler()(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone), nil)
|
||||
|
|
@ -134,7 +137,7 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
}
|
||||
|
||||
go app.QueueUpdateDraw(func() { mi.tabSuggestion() })
|
||||
go mi.chat.app.QueueUpdateDraw(func() { mi.tabSuggestion() })
|
||||
}
|
||||
|
||||
return event
|
||||
|
|
@ -148,7 +151,8 @@ func (mi *messageInput) paste() {
|
|||
}
|
||||
|
||||
func (mi *messageInput) send() {
|
||||
if app.chatView.selectedChannel == nil {
|
||||
selected := mi.chat.SelectedChannel()
|
||||
if selected == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -166,17 +170,17 @@ func (mi *messageInput) send() {
|
|||
}
|
||||
}()
|
||||
|
||||
text = processText(app.chatView.selectedChannel, []byte(text))
|
||||
text = processText(mi.chat.state, selected, []byte(text))
|
||||
|
||||
if mi.edit {
|
||||
m, err := app.chatView.messagesList.selectedMessage()
|
||||
m, err := mi.chat.messagesList.selectedMessage()
|
||||
if err != nil {
|
||||
slog.Error("failed to get selected message", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
data := api.EditMessageData{Content: option.NewNullableString(text)}
|
||||
if _, err := discordState.EditMessageComplex(m.ChannelID, m.ID, data); err != nil {
|
||||
if _, err := mi.chat.state.EditMessageComplex(m.ChannelID, m.ID, data); err != nil {
|
||||
slog.Error("failed to edit message", "err", err)
|
||||
}
|
||||
|
||||
|
|
@ -184,17 +188,17 @@ func (mi *messageInput) send() {
|
|||
} else {
|
||||
data := mi.sendMessageData
|
||||
data.Content = text
|
||||
if _, err := discordState.SendMessageComplex(app.chatView.selectedChannel.ID, *data); err != nil {
|
||||
slog.Error("failed to send message in channel", "channel_id", app.chatView.selectedChannel.ID, "err", err)
|
||||
if _, err := mi.chat.state.SendMessageComplex(selected.ID, *data); err != nil {
|
||||
slog.Error("failed to send message in channel", "channel_id", selected.ID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
mi.reset()
|
||||
app.chatView.messagesList.Highlight()
|
||||
app.chatView.messagesList.ScrollToEnd()
|
||||
mi.chat.messagesList.Highlight()
|
||||
mi.chat.messagesList.ScrollToEnd()
|
||||
}
|
||||
|
||||
func processText(channel *discord.Channel, src []byte) string {
|
||||
func processText(state *ningen.State, channel *discord.Channel, src []byte) string {
|
||||
var (
|
||||
ranges [][2]int
|
||||
canMention = true
|
||||
|
|
@ -218,13 +222,13 @@ func processText(channel *discord.Channel, src []byte) string {
|
|||
})
|
||||
|
||||
for _, rng := range ranges {
|
||||
src = slices.Replace(src, rng[0], rng[1], expandMentions(channel, src[rng[0]:rng[1]])...)
|
||||
src = slices.Replace(src, rng[0], rng[1], expandMentions(state, channel, src[rng[0]:rng[1]])...)
|
||||
}
|
||||
|
||||
return string(src)
|
||||
}
|
||||
|
||||
func expandMentions(c *discord.Channel, src []byte) []byte {
|
||||
func expandMentions(state *ningen.State, c *discord.Channel, src []byte) []byte {
|
||||
return mentionRegex.ReplaceAllFunc(src, func(input []byte) []byte {
|
||||
output := input
|
||||
name := string(input[1:])
|
||||
|
|
@ -235,7 +239,7 @@ func expandMentions(c *discord.Channel, src []byte) []byte {
|
|||
}
|
||||
}
|
||||
// self ping
|
||||
me, err := discordState.Cabinet.Me()
|
||||
me, err := state.Cabinet.Me()
|
||||
if err != nil {
|
||||
slog.Error("failed to get client user (me)", "err", err)
|
||||
} else if strings.EqualFold(me.Username, name) {
|
||||
|
|
@ -243,9 +247,9 @@ func expandMentions(c *discord.Channel, src []byte) []byte {
|
|||
}
|
||||
return output
|
||||
}
|
||||
discordState.MemberStore.Each(c.GuildID, func(m *discord.Member) bool {
|
||||
state.MemberStore.Each(c.GuildID, func(m *discord.Member) bool {
|
||||
if strings.EqualFold(m.User.Username, name) {
|
||||
if channelHasUser(c.ID, m.User.ID) {
|
||||
if channelHasUser(state, c.ID, m.User.ID) {
|
||||
output = []byte(m.User.ID.Mention())
|
||||
}
|
||||
return true
|
||||
|
|
@ -266,18 +270,22 @@ func (mi *messageInput) tabComplete() {
|
|||
}
|
||||
pos := posEnd - (len(name) + 1)
|
||||
|
||||
gID := app.chatView.selectedChannel.GuildID
|
||||
selected := mi.chat.SelectedChannel()
|
||||
if selected == nil {
|
||||
return
|
||||
}
|
||||
gID := selected.GuildID
|
||||
|
||||
if mi.cfg.AutocompleteLimit == 0 {
|
||||
if !gID.IsValid() {
|
||||
users := app.chatView.selectedChannel.DMRecipients
|
||||
users := selected.DMRecipients
|
||||
res := fuzzy.FindFrom(name, userList(users))
|
||||
if len(res) > 0 {
|
||||
mi.Replace(pos, posEnd, "@"+users[res[0].Index].Username+" ")
|
||||
}
|
||||
} else {
|
||||
mi.searchMember(gID, name)
|
||||
members, err := discordState.Cabinet.Members(gID)
|
||||
members, err := mi.chat.state.Cabinet.Members(gID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get members from state", "guild_id", gID, "err", err)
|
||||
return
|
||||
|
|
@ -285,7 +293,7 @@ func (mi *messageInput) tabComplete() {
|
|||
|
||||
res := fuzzy.FindFrom(name, memberList(members))
|
||||
for _, r := range res {
|
||||
if channelHasUser(app.chatView.selectedChannel.ID, members[r.Index].User.ID) {
|
||||
if channelHasUser(mi.chat.state, selected.ID, members[r.Index].User.ID) {
|
||||
mi.Replace(pos, posEnd, "@"+members[r.Index].User.Username+" ")
|
||||
return
|
||||
}
|
||||
|
|
@ -309,8 +317,12 @@ func (mi *messageInput) tabSuggestion() {
|
|||
mi.stopTabCompletion()
|
||||
return
|
||||
}
|
||||
gID := app.chatView.selectedChannel.GuildID
|
||||
cID := app.chatView.selectedChannel.ID
|
||||
selected := mi.chat.SelectedChannel()
|
||||
if selected == nil {
|
||||
return
|
||||
}
|
||||
gID := selected.GuildID
|
||||
cID := selected.ID
|
||||
mi.mentionsList.Clear()
|
||||
|
||||
var shown map[string]struct{}
|
||||
|
|
@ -318,7 +330,7 @@ func (mi *messageInput) tabSuggestion() {
|
|||
if name == "" {
|
||||
shown = make(map[string]struct{})
|
||||
// Don't show @me in the list of recent authors
|
||||
me, err := discordState.Cabinet.Me()
|
||||
me, err := mi.chat.state.Cabinet.Me()
|
||||
if err != nil {
|
||||
slog.Error("failed to get client user (me)", "err", err)
|
||||
} else {
|
||||
|
|
@ -329,7 +341,7 @@ func (mi *messageInput) tabSuggestion() {
|
|||
// DMs have recipients, not members
|
||||
if !gID.IsValid() {
|
||||
if name == "" { // show recent messages' authors
|
||||
msgs, err := discordState.Cabinet.Messages(cID)
|
||||
msgs, err := mi.chat.state.Cabinet.Messages(cID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -341,8 +353,8 @@ func (mi *messageInput) tabSuggestion() {
|
|||
mi.addMentionUser(&m.Author)
|
||||
}
|
||||
} else {
|
||||
users := app.chatView.selectedChannel.DMRecipients
|
||||
me, err := discordState.Cabinet.Me()
|
||||
users := selected.DMRecipients
|
||||
me, err := mi.chat.state.Cabinet.Me()
|
||||
if err != nil {
|
||||
slog.Error("failed to get client user (me)", "err", err)
|
||||
} else {
|
||||
|
|
@ -354,7 +366,7 @@ func (mi *messageInput) tabSuggestion() {
|
|||
}
|
||||
}
|
||||
} else if name == "" { // show recent messages' authors
|
||||
msgs, err := discordState.Cabinet.Messages(cID)
|
||||
msgs, err := mi.chat.state.Cabinet.Messages(cID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -363,8 +375,8 @@ func (mi *messageInput) tabSuggestion() {
|
|||
continue
|
||||
}
|
||||
shown[m.Author.Username] = userDone
|
||||
discordState.MemberState.RequestMember(gID, m.Author.ID)
|
||||
if mem, err := discordState.Cabinet.Member(gID, m.Author.ID); err == nil {
|
||||
mi.chat.state.MemberState.RequestMember(gID, m.Author.ID)
|
||||
if mem, err := mi.chat.state.Cabinet.Member(gID, m.Author.ID); err == nil {
|
||||
if mi.addMentionMember(gID, mem) {
|
||||
break
|
||||
}
|
||||
|
|
@ -372,7 +384,7 @@ func (mi *messageInput) tabSuggestion() {
|
|||
}
|
||||
} else {
|
||||
mi.searchMember(gID, name)
|
||||
mems, err := discordState.Cabinet.Members(gID)
|
||||
mems, err := mi.chat.state.Cabinet.Members(gID)
|
||||
if err != nil {
|
||||
slog.Error("fetching members failed", "err", err)
|
||||
return
|
||||
|
|
@ -382,7 +394,7 @@ func (mi *messageInput) tabSuggestion() {
|
|||
res = res[:int(mi.cfg.AutocompleteLimit)]
|
||||
}
|
||||
for _, r := range res {
|
||||
if channelHasUser(cID, mems[r.Index].User.ID) &&
|
||||
if channelHasUser(mi.chat.state, cID, mems[r.Index].User.ID) &&
|
||||
mi.addMentionMember(gID, &mems[r.Index]) {
|
||||
break
|
||||
}
|
||||
|
|
@ -417,8 +429,8 @@ func (ul userList) Len() int {
|
|||
}
|
||||
|
||||
// channelHasUser checks if a user has permission to view the specified channel
|
||||
func channelHasUser(channelID discord.ChannelID, userID discord.UserID) bool {
|
||||
perms, err := discordState.Permissions(channelID, userID)
|
||||
func channelHasUser(state *ningen.State, channelID discord.ChannelID, userID discord.UserID) bool {
|
||||
perms, err := state.Permissions(channelID, userID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get permissions", "err", err, "channel", channelID, "user", userID)
|
||||
return false
|
||||
|
|
@ -436,22 +448,22 @@ func (mi *messageInput) searchMember(gID discord.GuildID, name string) {
|
|||
// everything starting with "ab". This will still be true even if a new
|
||||
// member joins because arikawa loads new members into the state.
|
||||
if k := key[:len(key)-1]; mi.cache.Exists(k) {
|
||||
if c := mi.cache.Get(k); c < discordState.MemberState.SearchLimit {
|
||||
if c := mi.cache.Get(k); c < mi.chat.state.MemberState.SearchLimit {
|
||||
mi.cache.Create(key, c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 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 mi.lastSearch.Add(discordState.MemberState.SearchFrequency).After(time.Now()) {
|
||||
if mi.lastSearch.Add(mi.chat.state.MemberState.SearchFrequency).After(time.Now()) {
|
||||
return
|
||||
}
|
||||
|
||||
mi.lastSearch = time.Now()
|
||||
app.chatView.messagesList.waitForChunkEvent()
|
||||
app.chatView.messagesList.setFetchingChunk(true, 0)
|
||||
discordState.MemberState.SearchMember(gID, name)
|
||||
mi.cache.Create(key, app.chatView.messagesList.waitForChunkEvent())
|
||||
mi.chat.messagesList.waitForChunkEvent()
|
||||
mi.chat.messagesList.setFetchingChunk(true, 0)
|
||||
mi.chat.state.MemberState.SearchMember(gID, name)
|
||||
mi.cache.Create(key, mi.chat.messagesList.waitForChunkEvent())
|
||||
}
|
||||
|
||||
func (mi *messageInput) showMentionList() {
|
||||
|
|
@ -462,7 +474,7 @@ func (mi *messageInput) showMentionList() {
|
|||
l := mi.mentionsList
|
||||
x, _, _, _ := mi.GetInnerRect()
|
||||
_, y, _, _ := mi.GetRect()
|
||||
_, _, maxW, maxH := app.chatView.messagesList.GetInnerRect()
|
||||
_, _, maxW, maxH := mi.chat.messagesList.GetInnerRect()
|
||||
if t := int(mi.cfg.Theme.MentionsList.MaxHeight); t != 0 {
|
||||
maxH = min(maxH, t)
|
||||
}
|
||||
|
|
@ -485,10 +497,10 @@ func (mi *messageInput) showMentionList() {
|
|||
|
||||
l.SetRect(x, y, w, h)
|
||||
|
||||
app.chatView.
|
||||
mi.chat.
|
||||
AddAndSwitchToPage(mentionsListPageName, l, false).
|
||||
ShowPage(flexPageName)
|
||||
app.SetFocus(mi)
|
||||
mi.chat.app.SetFocus(mi)
|
||||
}
|
||||
|
||||
func (mi *messageInput) addMentionMember(gID discord.GuildID, m *discord.Member) bool {
|
||||
|
|
@ -501,16 +513,16 @@ func (mi *messageInput) addMentionMember(gID discord.GuildID, m *discord.Member)
|
|||
name = m.Nick
|
||||
}
|
||||
|
||||
// this is WAY faster than discordState.MemberColor
|
||||
// This avoids a slower member color lookup path.
|
||||
color, ok := state.MemberColor(m, func(id discord.RoleID) *discord.Role {
|
||||
r, _ := discordState.Cabinet.Role(gID, id)
|
||||
r, _ := mi.chat.state.Cabinet.Role(gID, id)
|
||||
return r
|
||||
})
|
||||
if ok {
|
||||
name = fmt.Sprintf("[%s]%s[-]", color, name)
|
||||
}
|
||||
|
||||
presence, err := discordState.Cabinet.Presence(gID, m.User.ID)
|
||||
presence, err := mi.chat.state.Cabinet.Presence(gID, m.User.ID)
|
||||
if err != nil {
|
||||
slog.Info("failed to get presence from state", "guild_id", gID, "user_id", m.User.ID, "err", err)
|
||||
} else if presence.Status == discord.OfflineStatus {
|
||||
|
|
@ -527,7 +539,7 @@ func (mi *messageInput) addMentionUser(user *discord.User) {
|
|||
}
|
||||
|
||||
name := user.DisplayOrUsername()
|
||||
presence, err := discordState.Cabinet.Presence(discord.NullGuildID, user.ID)
|
||||
presence, err := mi.chat.state.Cabinet.Presence(discord.NullGuildID, user.ID)
|
||||
if err != nil {
|
||||
slog.Info("failed to get presence from state", "user_id", user.ID, "err", err)
|
||||
} else if presence.Status == discord.OfflineStatus {
|
||||
|
|
@ -539,7 +551,7 @@ func (mi *messageInput) addMentionUser(user *discord.User) {
|
|||
|
||||
// used by chatView
|
||||
func (mi *messageInput) removeMentionsList() {
|
||||
app.chatView.
|
||||
mi.chat.
|
||||
RemovePage(mentionsListPageName).
|
||||
SwitchToPage(flexPageName)
|
||||
}
|
||||
|
|
@ -548,7 +560,7 @@ func (mi *messageInput) stopTabCompletion() {
|
|||
if mi.cfg.AutocompleteLimit > 0 {
|
||||
mi.mentionsList.Clear()
|
||||
mi.removeMentionsList()
|
||||
app.SetFocus(mi)
|
||||
mi.chat.app.SetFocus(mi)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -568,7 +580,7 @@ func (mi *messageInput) editor() {
|
|||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
app.Suspend(func() {
|
||||
mi.chat.app.Suspend(func() {
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
slog.Error("failed to run command", "args", cmd.Args, "err", err)
|
||||
|
|
@ -586,7 +598,7 @@ func (mi *messageInput) editor() {
|
|||
}
|
||||
|
||||
func (mi *messageInput) openFilePicker() {
|
||||
if app.chatView.selectedChannel == nil {
|
||||
if mi.chat.SelectedChannel() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package cmd
|
||||
package chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -36,6 +36,7 @@ import (
|
|||
type messagesList struct {
|
||||
*tview.TextView
|
||||
cfg *config.Config
|
||||
chat *ChatView
|
||||
selectedMessageID discord.MessageID
|
||||
|
||||
renderer *markdown.Renderer
|
||||
|
|
@ -48,10 +49,11 @@ type messagesList struct {
|
|||
}
|
||||
}
|
||||
|
||||
func newMessagesList(cfg *config.Config) *messagesList {
|
||||
func newMessagesList(cfg *config.Config, chat *ChatView) *messagesList {
|
||||
ml := &messagesList{
|
||||
TextView: tview.NewTextView(),
|
||||
cfg: cfg,
|
||||
chat: chat,
|
||||
renderer: markdown.NewRenderer(cfg.Theme.MessagesList),
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +100,7 @@ func (ml *messagesList) drawMessage(writer io.Writer, message discord.Message) {
|
|||
fmt.Fprintf(writer, `["%s"]`, message.ID)
|
||||
|
||||
if ml.cfg.HideBlockedUsers {
|
||||
isBlocked := discordState.UserIsBlocked(message.Author.ID)
|
||||
isBlocked := ml.chat.state.UserIsBlocked(message.Author.ID)
|
||||
if isBlocked {
|
||||
io.WriteString(writer, "[:red:b]Blocked message[:-:-]")
|
||||
return
|
||||
|
|
@ -146,7 +148,7 @@ func (ml *messagesList) drawAuthor(w io.Writer, message discord.Message) {
|
|||
|
||||
// Webhooks do not have nicknames or roles.
|
||||
if message.GuildID.IsValid() && !message.WebhookID.IsValid() {
|
||||
member, err := discordState.Cabinet.Member(message.GuildID, message.Author.ID)
|
||||
member, err := ml.chat.state.Cabinet.Member(message.GuildID, message.Author.ID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get member from state", "guild_id", message.GuildID, "member_id", message.Author.ID, "err", err)
|
||||
} else {
|
||||
|
|
@ -155,7 +157,7 @@ func (ml *messagesList) drawAuthor(w io.Writer, message discord.Message) {
|
|||
}
|
||||
|
||||
color, ok := state.MemberColor(member, func(id discord.RoleID) *discord.Role {
|
||||
r, _ := discordState.Cabinet.Role(message.GuildID, id)
|
||||
r, _ := ml.chat.state.Cabinet.Role(message.GuildID, id)
|
||||
return r
|
||||
})
|
||||
if ok {
|
||||
|
|
@ -169,8 +171,8 @@ func (ml *messagesList) drawAuthor(w io.Writer, message discord.Message) {
|
|||
|
||||
func (ml *messagesList) drawContent(w io.Writer, message discord.Message) {
|
||||
c := []byte(tview.Escape(message.Content))
|
||||
if app.cfg.Markdown {
|
||||
ast := discordmd.ParseWithMessage(c, *discordState.Cabinet, &message, false)
|
||||
if ml.chat.cfg.Markdown {
|
||||
ast := discordmd.ParseWithMessage(c, *ml.chat.state.Cabinet, &message, false)
|
||||
ml.renderer.Render(w, c, ast)
|
||||
} else {
|
||||
w.Write(c) // write the content as is
|
||||
|
|
@ -241,7 +243,12 @@ func (ml *messagesList) selectedMessage() (*discord.Message, error) {
|
|||
return nil, errors.New("no message is currently selected")
|
||||
}
|
||||
|
||||
m, err := discordState.Cabinet.Message(app.chatView.selectedChannel.ID, ml.selectedMessageID)
|
||||
selected := ml.chat.SelectedChannel()
|
||||
if selected == nil {
|
||||
return nil, errors.New("no channel is currently selected")
|
||||
}
|
||||
|
||||
m, err := ml.chat.state.Cabinet.Message(selected.ID, ml.selectedMessageID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve selected message: %w", err)
|
||||
}
|
||||
|
|
@ -250,7 +257,12 @@ func (ml *messagesList) selectedMessage() (*discord.Message, error) {
|
|||
}
|
||||
|
||||
func (ml *messagesList) selectedMessageIndex() (int, error) {
|
||||
ms, err := discordState.Cabinet.Messages(app.chatView.selectedChannel.ID)
|
||||
selected := ml.chat.SelectedChannel()
|
||||
if selected == nil {
|
||||
return -1, errors.New("no channel is currently selected")
|
||||
}
|
||||
|
||||
ms, err := ml.chat.state.Cabinet.Messages(selected.ID)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
|
@ -296,13 +308,14 @@ func (ml *messagesList) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
|
||||
func (ml *messagesList) _select(name string) {
|
||||
if app.chatView.selectedChannel == nil {
|
||||
selected := ml.chat.SelectedChannel()
|
||||
if selected == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ms, err := discordState.Cabinet.Messages(app.chatView.selectedChannel.ID)
|
||||
ms, err := ml.chat.state.Cabinet.Messages(selected.ID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get messages", "err", err, "channel_id", app.chatView.selectedChannel.ID)
|
||||
slog.Error("failed to get messages", "err", err, "channel_id", selected.ID)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -456,8 +469,8 @@ func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord
|
|||
SetHighlightFullLine(true).
|
||||
ShowSecondaryText(false).
|
||||
SetDoneFunc(func() {
|
||||
app.chatView.RemovePage(attachmentsListPageName).SwitchToPage(flexPageName)
|
||||
app.SetFocus(ml)
|
||||
ml.chat.RemovePage(attachmentsListPageName).SwitchToPage(flexPageName)
|
||||
ml.chat.app.SetFocus(ml)
|
||||
})
|
||||
list.
|
||||
SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
|
|
@ -492,7 +505,7 @@ func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord
|
|||
})
|
||||
}
|
||||
|
||||
app.chatView.
|
||||
ml.chat.
|
||||
AddAndSwitchToPage(attachmentsListPageName, ui.Centered(list, 0, 0), true).
|
||||
ShowPage(flexPageName)
|
||||
}
|
||||
|
|
@ -545,7 +558,7 @@ func (ml *messagesList) reply(mention bool) {
|
|||
|
||||
name := msg.Author.DisplayOrUsername()
|
||||
if msg.GuildID.IsValid() {
|
||||
member, err := discordState.Cabinet.Member(msg.GuildID, msg.Author.ID)
|
||||
member, err := ml.chat.state.Cabinet.Member(msg.GuildID, msg.Author.ID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get member from state", "guild_id", msg.GuildID, "member_id", msg.Author.ID, "err", err)
|
||||
} else {
|
||||
|
|
@ -555,7 +568,7 @@ func (ml *messagesList) reply(mention bool) {
|
|||
}
|
||||
}
|
||||
|
||||
data := app.chatView.messageInput.sendMessageData
|
||||
data := ml.chat.messageInput.sendMessageData
|
||||
data.Reference = &discord.MessageReference{MessageID: ml.selectedMessageID}
|
||||
data.AllowedMentions = &api.AllowedMentions{RepliedUser: option.False}
|
||||
|
||||
|
|
@ -565,9 +578,9 @@ func (ml *messagesList) reply(mention bool) {
|
|||
title = "[@] " + title
|
||||
}
|
||||
|
||||
app.chatView.messageInput.sendMessageData = data
|
||||
app.chatView.messageInput.SetTitle(title + name)
|
||||
app.SetFocus(app.chatView.messageInput)
|
||||
ml.chat.messageInput.sendMessageData = data
|
||||
ml.chat.messageInput.SetTitle(title + name)
|
||||
ml.chat.app.SetFocus(ml.chat.messageInput)
|
||||
}
|
||||
|
||||
func (ml *messagesList) edit() {
|
||||
|
|
@ -577,7 +590,7 @@ func (ml *messagesList) edit() {
|
|||
return
|
||||
}
|
||||
|
||||
me, err := discordState.Cabinet.Me()
|
||||
me, err := ml.chat.state.Cabinet.Me()
|
||||
if err != nil {
|
||||
slog.Error("failed to get client user (me)", "err", err)
|
||||
return
|
||||
|
|
@ -588,10 +601,10 @@ func (ml *messagesList) edit() {
|
|||
return
|
||||
}
|
||||
|
||||
app.chatView.messageInput.SetTitle("Editing")
|
||||
app.chatView.messageInput.edit = true
|
||||
app.chatView.messageInput.SetText(message.Content, true)
|
||||
app.SetFocus(app.chatView.messageInput)
|
||||
ml.chat.messageInput.SetTitle("Editing")
|
||||
ml.chat.messageInput.edit = true
|
||||
ml.chat.messageInput.SetText(message.Content, true)
|
||||
ml.chat.app.SetFocus(ml.chat.messageInput)
|
||||
}
|
||||
|
||||
func (ml *messagesList) confirmDelete() {
|
||||
|
|
@ -601,7 +614,7 @@ func (ml *messagesList) confirmDelete() {
|
|||
}
|
||||
}
|
||||
|
||||
app.chatView.showConfirmModal(
|
||||
ml.chat.showConfirmModal(
|
||||
"Are you sure you want to delete this message?",
|
||||
[]string{"Yes", "No"},
|
||||
onChoice,
|
||||
|
|
@ -616,28 +629,33 @@ func (ml *messagesList) delete() {
|
|||
}
|
||||
|
||||
if msg.GuildID.IsValid() {
|
||||
me, err := discordState.Cabinet.Me()
|
||||
me, err := ml.chat.state.Cabinet.Me()
|
||||
if err != nil {
|
||||
slog.Error("failed to get client user (me)", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
if msg.Author.ID != me.ID && !discordState.HasPermissions(msg.ChannelID, discord.PermissionManageMessages) {
|
||||
if msg.Author.ID != me.ID && !ml.chat.state.HasPermissions(msg.ChannelID, discord.PermissionManageMessages) {
|
||||
slog.Error("failed to delete message; missing relevant permissions", "channel_id", msg.ChannelID, "message_id", msg.ID)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := discordState.DeleteMessage(app.chatView.selectedChannel.ID, msg.ID, ""); err != nil {
|
||||
slog.Error("failed to delete message", "channel_id", app.chatView.selectedChannel.ID, "message_id", msg.ID, "err", err)
|
||||
selected := ml.chat.SelectedChannel()
|
||||
if selected == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := ml.chat.state.DeleteMessage(selected.ID, msg.ID, ""); err != nil {
|
||||
slog.Error("failed to delete message", "channel_id", selected.ID, "message_id", msg.ID, "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
ml.selectedMessageID = 0
|
||||
ml.Highlight()
|
||||
|
||||
if err := discordState.MessageRemove(app.chatView.selectedChannel.ID, msg.ID); err != nil {
|
||||
slog.Error("failed to delete message", "channel_id", app.chatView.selectedChannel.ID, "message_id", msg.ID, "err", err)
|
||||
if err := ml.chat.state.MessageRemove(selected.ID, msg.ID); err != nil {
|
||||
slog.Error("failed to delete message", "channel_id", selected.ID, "message_id", msg.ID, "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -653,13 +671,13 @@ func (ml *messagesList) requestGuildMembers(gID discord.GuildID, ms []discord.Me
|
|||
continue
|
||||
}
|
||||
|
||||
if member, _ := discordState.Cabinet.Member(gID, m.Author.ID); member == nil {
|
||||
if member, _ := ml.chat.state.Cabinet.Member(gID, m.Author.ID); member == nil {
|
||||
usersToFetch = append(usersToFetch, m.Author.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(usersToFetch) > 0 {
|
||||
err := discordState.SendGateway(context.TODO(), &gateway.RequestGuildMembersCommand{
|
||||
err := ml.chat.state.SendGateway(context.TODO(), &gateway.RequestGuildMembersCommand{
|
||||
GuildIDs: []discord.GuildID{gID},
|
||||
UserIDs: slices.Compact(usersToFetch),
|
||||
})
|
||||
73
internal/ui/chat/state.go
Normal file
73
internal/ui/chat/state.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/ayn2op/discordo/internal/http"
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/session"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
"github.com/diamondburned/arikawa/v3/state/store/defaultstore"
|
||||
"github.com/diamondburned/arikawa/v3/utils/handler"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil/httpdriver"
|
||||
"github.com/diamondburned/arikawa/v3/utils/ws"
|
||||
"github.com/diamondburned/ningen/v3"
|
||||
)
|
||||
|
||||
func (cv *ChatView) OpenState(token string) error {
|
||||
identifyProps := http.IdentifyProperties()
|
||||
gateway.DefaultIdentity = identifyProps
|
||||
gateway.DefaultPresence = &gateway.UpdatePresenceCommand{
|
||||
Status: cv.cfg.Status,
|
||||
}
|
||||
|
||||
id := gateway.DefaultIdentifier(token)
|
||||
id.Compress = false
|
||||
|
||||
session := session.NewCustom(id, http.NewClient(token), handler.New())
|
||||
state := state.NewFromSession(session, defaultstore.New())
|
||||
cv.state = ningen.FromState(state)
|
||||
|
||||
// Handlers
|
||||
cv.state.AddHandler(cv.onRaw)
|
||||
cv.state.AddHandler(cv.onReady)
|
||||
cv.state.AddHandler(cv.onMessageCreate)
|
||||
cv.state.AddHandler(cv.onMessageUpdate)
|
||||
cv.state.AddHandler(cv.onMessageDelete)
|
||||
cv.state.AddHandler(cv.onReadUpdate)
|
||||
cv.state.AddHandler(cv.onGuildMembersChunk)
|
||||
cv.state.AddHandler(cv.onGuildMemberRemove)
|
||||
|
||||
cv.state.StateLog = func(err error) {
|
||||
slog.Error("state log", "err", err)
|
||||
}
|
||||
|
||||
cv.state.OnRequest = append(cv.state.OnRequest, httputil.WithHeaders(http.Headers()), cv.onRequest)
|
||||
return cv.state.Open(context.TODO())
|
||||
}
|
||||
|
||||
func (cv *ChatView) CloseState() error {
|
||||
if cv.state == nil {
|
||||
return nil
|
||||
}
|
||||
return cv.state.Close()
|
||||
}
|
||||
|
||||
func (cv *ChatView) onRequest(r httpdriver.Request) error {
|
||||
if req, ok := r.(*httpdriver.DefaultRequest); ok {
|
||||
slog.Debug("new HTTP request", "method", req.Method, "url", req.URL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cv *ChatView) onRaw(event *ws.RawEvent) {
|
||||
slog.Debug(
|
||||
"new raw event",
|
||||
"code", event.OriginalCode,
|
||||
"type", event.OriginalType,
|
||||
// "data", event.Raw,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue