refactor(ui): rename chat view type and file
This commit is contained in:
parent
f46f357eff
commit
07fa9b172a
7 changed files with 438 additions and 438 deletions
|
|
@ -15,7 +15,7 @@ import (
|
|||
|
||||
type App struct {
|
||||
inner *tview.Application
|
||||
chatView *chat.ChatView
|
||||
chatView *chat.View
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ func (a *App) Run(token string) error {
|
|||
}
|
||||
|
||||
func (a *App) showChatView(token string) error {
|
||||
a.chatView = chat.NewChatView(a.inner, a.cfg, a.quit)
|
||||
a.chatView = chat.NewView(a.inner, a.cfg, a.quit)
|
||||
if err := a.chatView.OpenState(token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,316 +0,0 @@
|
|||
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)
|
||||
}
|
||||
|
|
@ -19,14 +19,14 @@ import (
|
|||
type guildsTree struct {
|
||||
*tview.TreeView
|
||||
cfg *config.Config
|
||||
chat *ChatView
|
||||
chatView *View
|
||||
}
|
||||
|
||||
func newGuildsTree(cfg *config.Config, chat *ChatView) *guildsTree {
|
||||
func newGuildsTree(cfg *config.Config, chatView *View) *guildsTree {
|
||||
gt := &guildsTree{
|
||||
TreeView: tview.NewTreeView(),
|
||||
cfg: cfg,
|
||||
chat: chat,
|
||||
chatView: chatView,
|
||||
}
|
||||
|
||||
gt.Box = ui.ConfigureBox(gt.Box, &cfg.Theme)
|
||||
|
|
@ -52,7 +52,7 @@ func (gt *guildsTree) createFolderNode(folder gateway.GuildFolder) {
|
|||
gt.GetRoot().AddChild(folderNode)
|
||||
|
||||
for _, gID := range folder.GuildIDs {
|
||||
guild, err := gt.chat.state.Cabinet.Guild(gID)
|
||||
guild, err := gt.chatView.state.Cabinet.Guild(gID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get guild from state", "guild_id", gID, "err", err)
|
||||
continue
|
||||
|
|
@ -78,12 +78,12 @@ func (gt *guildsTree) unreadStyle(indication ningen.UnreadIndication) tcell.Styl
|
|||
}
|
||||
|
||||
func (gt *guildsTree) getGuildNodeStyle(guildID discord.GuildID) tcell.Style {
|
||||
indication := gt.chat.state.GuildIsUnread(guildID, ningen.GuildUnreadOpts{UnreadOpts: ningen.UnreadOpts{IncludeMutedCategories: true}})
|
||||
indication := gt.chatView.state.GuildIsUnread(guildID, ningen.GuildUnreadOpts{UnreadOpts: ningen.UnreadOpts{IncludeMutedCategories: true}})
|
||||
return gt.unreadStyle(indication)
|
||||
}
|
||||
|
||||
func (gt *guildsTree) getChannelNodeStyle(channelID discord.ChannelID) tcell.Style {
|
||||
indication := gt.chat.state.ChannelIsUnread(channelID, ningen.UnreadOpts{IncludeMutedCategories: true})
|
||||
indication := gt.chatView.state.ChannelIsUnread(channelID, ningen.UnreadOpts{IncludeMutedCategories: true})
|
||||
return gt.unreadStyle(indication)
|
||||
}
|
||||
|
||||
|
|
@ -95,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 && !gt.chat.state.HasPermissions(channel.ID, discord.PermissionViewChannel) {
|
||||
if channel.Type != discord.DirectMessage && channel.Type != discord.GroupDM && !gt.chatView.state.HasPermissions(channel.ID, discord.PermissionViewChannel) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -151,9 +151,9 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) {
|
|||
|
||||
switch ref := node.GetReference().(type) {
|
||||
case discord.GuildID:
|
||||
go gt.chat.state.MemberState.Subscribe(ref)
|
||||
go gt.chatView.state.MemberState.Subscribe(ref)
|
||||
|
||||
channels, err := gt.chat.state.Cabinet.Channels(ref)
|
||||
channels, err := gt.chatView.state.Cabinet.Channels(ref)
|
||||
if err != nil {
|
||||
slog.Error("failed to get channels", "err", err, "guild_id", ref)
|
||||
return
|
||||
|
|
@ -165,7 +165,7 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) {
|
|||
|
||||
gt.createChannelNodes(node, channels)
|
||||
case discord.ChannelID:
|
||||
channel, err := gt.chat.state.Cabinet.Channel(ref)
|
||||
channel, err := gt.chatView.state.Cabinet.Channel(ref)
|
||||
if err != nil {
|
||||
slog.Error("failed to get channel", "channel_id", ref)
|
||||
return
|
||||
|
|
@ -174,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 := gt.chat.state.Cabinet.Channels(channel.GuildID)
|
||||
allChannels, err := gt.chatView.state.Cabinet.Channels(channel.GuildID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get channels for forum threads", "err", err, "guild_id", channel.GuildID)
|
||||
return
|
||||
|
|
@ -200,38 +200,38 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) {
|
|||
return
|
||||
}
|
||||
|
||||
go gt.chat.state.ReadState.MarkRead(channel.ID, channel.LastMessageID)
|
||||
go gt.chatView.state.ReadState.MarkRead(channel.ID, channel.LastMessageID)
|
||||
|
||||
messages, err := gt.chat.state.Messages(channel.ID, uint(gt.cfg.MessagesLimit))
|
||||
messages, err := gt.chatView.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() {
|
||||
gt.chat.messagesList.requestGuildMembers(guildID, messages)
|
||||
gt.chatView.messagesList.requestGuildMembers(guildID, messages)
|
||||
}
|
||||
|
||||
gt.chat.SetSelectedChannel(channel)
|
||||
gt.chatView.SetSelectedChannel(channel)
|
||||
|
||||
gt.chat.messagesList.reset()
|
||||
gt.chat.messagesList.setTitle(*channel)
|
||||
gt.chat.messagesList.drawMessages(messages)
|
||||
gt.chat.messagesList.ScrollToEnd()
|
||||
gt.chatView.messagesList.reset()
|
||||
gt.chatView.messagesList.setTitle(*channel)
|
||||
gt.chatView.messagesList.drawMessages(messages)
|
||||
gt.chatView.messagesList.ScrollToEnd()
|
||||
|
||||
hasNoPerm := channel.Type != discord.DirectMessage && channel.Type != discord.GroupDM && !gt.chat.state.HasPermissions(channel.ID, discord.PermissionSendMessages)
|
||||
gt.chat.messageInput.SetDisabled(hasNoPerm)
|
||||
hasNoPerm := channel.Type != discord.DirectMessage && channel.Type != discord.GroupDM && !gt.chatView.state.HasPermissions(channel.ID, discord.PermissionSendMessages)
|
||||
gt.chatView.messageInput.SetDisabled(hasNoPerm)
|
||||
if hasNoPerm {
|
||||
gt.chat.messageInput.SetPlaceholder("You do not have permission to send messages in this channel.")
|
||||
gt.chatView.messageInput.SetPlaceholder("You do not have permission to send messages in this channel.")
|
||||
} else {
|
||||
gt.chat.messageInput.SetPlaceholder("Message...")
|
||||
gt.chatView.messageInput.SetPlaceholder("Message...")
|
||||
if gt.cfg.AutoFocus {
|
||||
gt.chat.app.SetFocus(gt.chat.messageInput)
|
||||
gt.chatView.app.SetFocus(gt.chatView.messageInput)
|
||||
}
|
||||
}
|
||||
|
||||
case nil: // Direct messages folder
|
||||
channels, err := gt.chat.state.PrivateChannels()
|
||||
channels, err := gt.chatView.state.PrivateChannels()
|
||||
if err != nil {
|
||||
slog.Error("failed to get private channels", "err", err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ var mentionRegex = regexp.MustCompile("@[a-zA-Z0-9._]+")
|
|||
type messageInput struct {
|
||||
*tview.TextArea
|
||||
cfg *config.Config
|
||||
chat *ChatView
|
||||
chatView *View
|
||||
|
||||
edit bool
|
||||
sendMessageData *api.SendMessageData
|
||||
|
|
@ -49,11 +49,11 @@ type messageInput struct {
|
|||
lastSearch time.Time
|
||||
}
|
||||
|
||||
func newMessageInput(cfg *config.Config, chat *ChatView) *messageInput {
|
||||
func newMessageInput(cfg *config.Config, chatView *View) *messageInput {
|
||||
mi := &messageInput{
|
||||
TextArea: tview.NewTextArea(),
|
||||
cfg: cfg,
|
||||
chat: chat,
|
||||
chatView: chatView,
|
||||
sendMessageData: &api.SendMessageData{},
|
||||
cache: cache.NewCache(),
|
||||
mentionsList: tview.NewList(),
|
||||
|
|
@ -96,7 +96,7 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
|||
return tcell.NewEventKey(tcell.KeyCtrlV, "", tcell.ModNone)
|
||||
|
||||
case mi.cfg.Keys.MessageInput.Send:
|
||||
if mi.chat.GetVisibile(mentionsListPageName) {
|
||||
if mi.chatView.GetVisibile(mentionsListPageName) {
|
||||
mi.tabComplete()
|
||||
return nil
|
||||
}
|
||||
|
|
@ -112,7 +112,7 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
|||
mi.openFilePicker()
|
||||
return nil
|
||||
case mi.cfg.Keys.MessageInput.Cancel:
|
||||
if mi.chat.GetVisibile(mentionsListPageName) {
|
||||
if mi.chatView.GetVisibile(mentionsListPageName) {
|
||||
mi.stopTabCompletion()
|
||||
} else {
|
||||
mi.reset()
|
||||
|
|
@ -120,12 +120,12 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
|||
|
||||
return nil
|
||||
case mi.cfg.Keys.MessageInput.TabComplete:
|
||||
go mi.chat.app.QueueUpdateDraw(func() { mi.tabComplete() })
|
||||
go mi.chatView.app.QueueUpdateDraw(func() { mi.tabComplete() })
|
||||
return nil
|
||||
}
|
||||
|
||||
if mi.cfg.AutocompleteLimit > 0 {
|
||||
if mi.chat.GetVisibile(mentionsListPageName) {
|
||||
if mi.chatView.GetVisibile(mentionsListPageName) {
|
||||
switch event.Name() {
|
||||
case mi.cfg.Keys.MentionsList.Up:
|
||||
mi.mentionsList.InputHandler()(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone), nil)
|
||||
|
|
@ -136,7 +136,7 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
}
|
||||
|
||||
go mi.chat.app.QueueUpdateDraw(func() { mi.tabSuggestion() })
|
||||
go mi.chatView.app.QueueUpdateDraw(func() { mi.tabSuggestion() })
|
||||
}
|
||||
|
||||
return event
|
||||
|
|
@ -150,7 +150,7 @@ func (mi *messageInput) paste() {
|
|||
}
|
||||
|
||||
func (mi *messageInput) send() {
|
||||
selected := mi.chat.SelectedChannel()
|
||||
selected := mi.chatView.SelectedChannel()
|
||||
if selected == nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -169,17 +169,17 @@ func (mi *messageInput) send() {
|
|||
}
|
||||
}()
|
||||
|
||||
text = processText(mi.chat.state, selected, []byte(text))
|
||||
text = processText(mi.chatView.state, selected, []byte(text))
|
||||
|
||||
if mi.edit {
|
||||
m, err := mi.chat.messagesList.selectedMessage()
|
||||
m, err := mi.chatView.messagesList.selectedMessage()
|
||||
if err != nil {
|
||||
slog.Error("failed to get selected message", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
data := api.EditMessageData{Content: option.NewNullableString(text)}
|
||||
if _, err := mi.chat.state.EditMessageComplex(m.ChannelID, m.ID, data); err != nil {
|
||||
if _, err := mi.chatView.state.EditMessageComplex(m.ChannelID, m.ID, data); err != nil {
|
||||
slog.Error("failed to edit message", "err", err)
|
||||
}
|
||||
|
||||
|
|
@ -187,14 +187,14 @@ func (mi *messageInput) send() {
|
|||
} else {
|
||||
data := mi.sendMessageData
|
||||
data.Content = text
|
||||
if _, err := mi.chat.state.SendMessageComplex(selected.ID, *data); err != nil {
|
||||
if _, err := mi.chatView.state.SendMessageComplex(selected.ID, *data); err != nil {
|
||||
slog.Error("failed to send message in channel", "channel_id", selected.ID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
mi.reset()
|
||||
mi.chat.messagesList.Highlight()
|
||||
mi.chat.messagesList.ScrollToEnd()
|
||||
mi.chatView.messagesList.Highlight()
|
||||
mi.chatView.messagesList.ScrollToEnd()
|
||||
}
|
||||
|
||||
func processText(state *ningen.State, channel *discord.Channel, src []byte) string {
|
||||
|
|
@ -269,7 +269,7 @@ func (mi *messageInput) tabComplete() {
|
|||
}
|
||||
pos := posEnd - (len(name) + 1)
|
||||
|
||||
selected := mi.chat.SelectedChannel()
|
||||
selected := mi.chatView.SelectedChannel()
|
||||
if selected == nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -284,7 +284,7 @@ func (mi *messageInput) tabComplete() {
|
|||
}
|
||||
} else {
|
||||
mi.searchMember(gID, name)
|
||||
members, err := mi.chat.state.Cabinet.Members(gID)
|
||||
members, err := mi.chatView.state.Cabinet.Members(gID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get members from state", "guild_id", gID, "err", err)
|
||||
return
|
||||
|
|
@ -292,7 +292,7 @@ func (mi *messageInput) tabComplete() {
|
|||
|
||||
res := fuzzy.FindFrom(name, memberList(members))
|
||||
for _, r := range res {
|
||||
if channelHasUser(mi.chat.state, selected.ID, members[r.Index].User.ID) {
|
||||
if channelHasUser(mi.chatView.state, selected.ID, members[r.Index].User.ID) {
|
||||
mi.Replace(pos, posEnd, "@"+members[r.Index].User.Username+" ")
|
||||
return
|
||||
}
|
||||
|
|
@ -316,7 +316,7 @@ func (mi *messageInput) tabSuggestion() {
|
|||
mi.stopTabCompletion()
|
||||
return
|
||||
}
|
||||
selected := mi.chat.SelectedChannel()
|
||||
selected := mi.chatView.SelectedChannel()
|
||||
if selected == nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -329,7 +329,7 @@ func (mi *messageInput) tabSuggestion() {
|
|||
if name == "" {
|
||||
shown = make(map[string]struct{})
|
||||
// Don't show @me in the list of recent authors
|
||||
me, err := mi.chat.state.Cabinet.Me()
|
||||
me, err := mi.chatView.state.Cabinet.Me()
|
||||
if err != nil {
|
||||
slog.Error("failed to get client user (me)", "err", err)
|
||||
} else {
|
||||
|
|
@ -340,7 +340,7 @@ func (mi *messageInput) tabSuggestion() {
|
|||
// DMs have recipients, not members
|
||||
if !gID.IsValid() {
|
||||
if name == "" { // show recent messages' authors
|
||||
msgs, err := mi.chat.state.Cabinet.Messages(cID)
|
||||
msgs, err := mi.chatView.state.Cabinet.Messages(cID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -353,7 +353,7 @@ func (mi *messageInput) tabSuggestion() {
|
|||
}
|
||||
} else {
|
||||
users := selected.DMRecipients
|
||||
me, err := mi.chat.state.Cabinet.Me()
|
||||
me, err := mi.chatView.state.Cabinet.Me()
|
||||
if err != nil {
|
||||
slog.Error("failed to get client user (me)", "err", err)
|
||||
} else {
|
||||
|
|
@ -365,7 +365,7 @@ func (mi *messageInput) tabSuggestion() {
|
|||
}
|
||||
}
|
||||
} else if name == "" { // show recent messages' authors
|
||||
msgs, err := mi.chat.state.Cabinet.Messages(cID)
|
||||
msgs, err := mi.chatView.state.Cabinet.Messages(cID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -374,8 +374,8 @@ func (mi *messageInput) tabSuggestion() {
|
|||
continue
|
||||
}
|
||||
shown[m.Author.Username] = userDone
|
||||
mi.chat.state.MemberState.RequestMember(gID, m.Author.ID)
|
||||
if mem, err := mi.chat.state.Cabinet.Member(gID, m.Author.ID); err == nil {
|
||||
mi.chatView.state.MemberState.RequestMember(gID, m.Author.ID)
|
||||
if mem, err := mi.chatView.state.Cabinet.Member(gID, m.Author.ID); err == nil {
|
||||
if mi.addMentionMember(gID, mem) {
|
||||
break
|
||||
}
|
||||
|
|
@ -383,7 +383,7 @@ func (mi *messageInput) tabSuggestion() {
|
|||
}
|
||||
} else {
|
||||
mi.searchMember(gID, name)
|
||||
mems, err := mi.chat.state.Cabinet.Members(gID)
|
||||
mems, err := mi.chatView.state.Cabinet.Members(gID)
|
||||
if err != nil {
|
||||
slog.Error("fetching members failed", "err", err)
|
||||
return
|
||||
|
|
@ -393,7 +393,7 @@ func (mi *messageInput) tabSuggestion() {
|
|||
res = res[:int(mi.cfg.AutocompleteLimit)]
|
||||
}
|
||||
for _, r := range res {
|
||||
if channelHasUser(mi.chat.state, cID, mems[r.Index].User.ID) &&
|
||||
if channelHasUser(mi.chatView.state, cID, mems[r.Index].User.ID) &&
|
||||
mi.addMentionMember(gID, &mems[r.Index]) {
|
||||
break
|
||||
}
|
||||
|
|
@ -447,22 +447,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 < mi.chat.state.MemberState.SearchLimit {
|
||||
if c := mi.cache.Get(k); c < mi.chatView.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(mi.chat.state.MemberState.SearchFrequency).After(time.Now()) {
|
||||
if mi.lastSearch.Add(mi.chatView.state.MemberState.SearchFrequency).After(time.Now()) {
|
||||
return
|
||||
}
|
||||
|
||||
mi.lastSearch = time.Now()
|
||||
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())
|
||||
mi.chatView.messagesList.waitForChunkEvent()
|
||||
mi.chatView.messagesList.setFetchingChunk(true, 0)
|
||||
mi.chatView.state.MemberState.SearchMember(gID, name)
|
||||
mi.cache.Create(key, mi.chatView.messagesList.waitForChunkEvent())
|
||||
}
|
||||
|
||||
func (mi *messageInput) showMentionList() {
|
||||
|
|
@ -473,7 +473,7 @@ func (mi *messageInput) showMentionList() {
|
|||
l := mi.mentionsList
|
||||
x, _, _, _ := mi.GetInnerRect()
|
||||
_, y, _, _ := mi.GetRect()
|
||||
_, _, maxW, maxH := mi.chat.messagesList.GetInnerRect()
|
||||
_, _, maxW, maxH := mi.chatView.messagesList.GetInnerRect()
|
||||
if t := int(mi.cfg.Theme.MentionsList.MaxHeight); t != 0 {
|
||||
maxH = min(maxH, t)
|
||||
}
|
||||
|
|
@ -496,10 +496,10 @@ func (mi *messageInput) showMentionList() {
|
|||
|
||||
l.SetRect(x, y, w, h)
|
||||
|
||||
mi.chat.
|
||||
mi.chatView.
|
||||
AddAndSwitchToPage(mentionsListPageName, l, false).
|
||||
ShowPage(flexPageName)
|
||||
mi.chat.app.SetFocus(mi)
|
||||
mi.chatView.app.SetFocus(mi)
|
||||
}
|
||||
|
||||
func (mi *messageInput) addMentionMember(gID discord.GuildID, m *discord.Member) bool {
|
||||
|
|
@ -514,14 +514,14 @@ func (mi *messageInput) addMentionMember(gID discord.GuildID, m *discord.Member)
|
|||
|
||||
// This avoids a slower member color lookup path.
|
||||
color, ok := state.MemberColor(m, func(id discord.RoleID) *discord.Role {
|
||||
r, _ := mi.chat.state.Cabinet.Role(gID, id)
|
||||
r, _ := mi.chatView.state.Cabinet.Role(gID, id)
|
||||
return r
|
||||
})
|
||||
if ok {
|
||||
name = fmt.Sprintf("[%s]%s[-]", color, name)
|
||||
}
|
||||
|
||||
presence, err := mi.chat.state.Cabinet.Presence(gID, m.User.ID)
|
||||
presence, err := mi.chatView.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 {
|
||||
|
|
@ -538,7 +538,7 @@ func (mi *messageInput) addMentionUser(user *discord.User) {
|
|||
}
|
||||
|
||||
name := user.DisplayOrUsername()
|
||||
presence, err := mi.chat.state.Cabinet.Presence(discord.NullGuildID, user.ID)
|
||||
presence, err := mi.chatView.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 {
|
||||
|
|
@ -550,7 +550,7 @@ func (mi *messageInput) addMentionUser(user *discord.User) {
|
|||
|
||||
// used by chatView
|
||||
func (mi *messageInput) removeMentionsList() {
|
||||
mi.chat.
|
||||
mi.chatView.
|
||||
RemovePage(mentionsListPageName).
|
||||
SwitchToPage(flexPageName)
|
||||
}
|
||||
|
|
@ -559,7 +559,7 @@ func (mi *messageInput) stopTabCompletion() {
|
|||
if mi.cfg.AutocompleteLimit > 0 {
|
||||
mi.mentionsList.Clear()
|
||||
mi.removeMentionsList()
|
||||
mi.chat.app.SetFocus(mi)
|
||||
mi.chatView.app.SetFocus(mi)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -579,7 +579,7 @@ func (mi *messageInput) editor() {
|
|||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
mi.chat.app.Suspend(func() {
|
||||
mi.chatView.app.Suspend(func() {
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
slog.Error("failed to run command", "args", cmd.Args, "err", err)
|
||||
|
|
@ -597,7 +597,7 @@ func (mi *messageInput) editor() {
|
|||
}
|
||||
|
||||
func (mi *messageInput) openFilePicker() {
|
||||
if mi.chat.SelectedChannel() == nil {
|
||||
if mi.chatView.SelectedChannel() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import (
|
|||
type messagesList struct {
|
||||
*tview.TextView
|
||||
cfg *config.Config
|
||||
chat *ChatView
|
||||
chatView *View
|
||||
selectedMessageID discord.MessageID
|
||||
|
||||
renderer *markdown.Renderer
|
||||
|
|
@ -49,11 +49,11 @@ type messagesList struct {
|
|||
}
|
||||
}
|
||||
|
||||
func newMessagesList(cfg *config.Config, chat *ChatView) *messagesList {
|
||||
func newMessagesList(cfg *config.Config, chatView *View) *messagesList {
|
||||
ml := &messagesList{
|
||||
TextView: tview.NewTextView(),
|
||||
cfg: cfg,
|
||||
chat: chat,
|
||||
chatView: chatView,
|
||||
renderer: markdown.NewRenderer(cfg.Theme.MessagesList),
|
||||
}
|
||||
|
||||
|
|
@ -100,7 +100,7 @@ func (ml *messagesList) drawMessage(writer io.Writer, message discord.Message) {
|
|||
fmt.Fprintf(writer, `["%s"]`, message.ID)
|
||||
|
||||
if ml.cfg.HideBlockedUsers {
|
||||
isBlocked := ml.chat.state.UserIsBlocked(message.Author.ID)
|
||||
isBlocked := ml.chatView.state.UserIsBlocked(message.Author.ID)
|
||||
if isBlocked {
|
||||
io.WriteString(writer, "[:red:b]Blocked message[:-:-]")
|
||||
return
|
||||
|
|
@ -148,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 := ml.chat.state.Cabinet.Member(message.GuildID, message.Author.ID)
|
||||
member, err := ml.chatView.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 {
|
||||
|
|
@ -157,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, _ := ml.chat.state.Cabinet.Role(message.GuildID, id)
|
||||
r, _ := ml.chatView.state.Cabinet.Role(message.GuildID, id)
|
||||
return r
|
||||
})
|
||||
if ok {
|
||||
|
|
@ -171,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 ml.chat.cfg.Markdown {
|
||||
ast := discordmd.ParseWithMessage(c, *ml.chat.state.Cabinet, &message, false)
|
||||
if ml.chatView.cfg.Markdown {
|
||||
ast := discordmd.ParseWithMessage(c, *ml.chatView.state.Cabinet, &message, false)
|
||||
ml.renderer.Render(w, c, ast)
|
||||
} else {
|
||||
w.Write(c) // write the content as is
|
||||
|
|
@ -243,12 +243,12 @@ func (ml *messagesList) selectedMessage() (*discord.Message, error) {
|
|||
return nil, errors.New("no message is currently selected")
|
||||
}
|
||||
|
||||
selected := ml.chat.SelectedChannel()
|
||||
selected := ml.chatView.SelectedChannel()
|
||||
if selected == nil {
|
||||
return nil, errors.New("no channel is currently selected")
|
||||
}
|
||||
|
||||
m, err := ml.chat.state.Cabinet.Message(selected.ID, ml.selectedMessageID)
|
||||
m, err := ml.chatView.state.Cabinet.Message(selected.ID, ml.selectedMessageID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve selected message: %w", err)
|
||||
}
|
||||
|
|
@ -288,12 +288,12 @@ func (ml *messagesList) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
|
||||
func (ml *messagesList) _select(name string) {
|
||||
selectedChannel := ml.chat.SelectedChannel()
|
||||
selectedChannel := ml.chatView.SelectedChannel()
|
||||
if selectedChannel == nil {
|
||||
return
|
||||
}
|
||||
|
||||
messages, err := ml.chat.state.Cabinet.Messages(selectedChannel.ID)
|
||||
messages, err := ml.chatView.state.Cabinet.Messages(selectedChannel.ID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get messages", "err", err, "channel_id", selectedChannel.ID)
|
||||
return
|
||||
|
|
@ -455,8 +455,8 @@ func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord
|
|||
SetHighlightFullLine(true).
|
||||
ShowSecondaryText(false).
|
||||
SetDoneFunc(func() {
|
||||
ml.chat.RemovePage(attachmentsListPageName).SwitchToPage(flexPageName)
|
||||
ml.chat.app.SetFocus(ml)
|
||||
ml.chatView.RemovePage(attachmentsListPageName).SwitchToPage(flexPageName)
|
||||
ml.chatView.app.SetFocus(ml)
|
||||
})
|
||||
list.
|
||||
SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
|
|
@ -491,7 +491,7 @@ func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord
|
|||
})
|
||||
}
|
||||
|
||||
ml.chat.
|
||||
ml.chatView.
|
||||
AddAndSwitchToPage(attachmentsListPageName, ui.Centered(list, 0, 0), true).
|
||||
ShowPage(flexPageName)
|
||||
}
|
||||
|
|
@ -544,7 +544,7 @@ func (ml *messagesList) reply(mention bool) {
|
|||
|
||||
name := msg.Author.DisplayOrUsername()
|
||||
if msg.GuildID.IsValid() {
|
||||
member, err := ml.chat.state.Cabinet.Member(msg.GuildID, msg.Author.ID)
|
||||
member, err := ml.chatView.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 {
|
||||
|
|
@ -554,7 +554,7 @@ func (ml *messagesList) reply(mention bool) {
|
|||
}
|
||||
}
|
||||
|
||||
data := ml.chat.messageInput.sendMessageData
|
||||
data := ml.chatView.messageInput.sendMessageData
|
||||
data.Reference = &discord.MessageReference{MessageID: ml.selectedMessageID}
|
||||
data.AllowedMentions = &api.AllowedMentions{RepliedUser: option.False}
|
||||
|
||||
|
|
@ -564,9 +564,9 @@ func (ml *messagesList) reply(mention bool) {
|
|||
title = "[@] " + title
|
||||
}
|
||||
|
||||
ml.chat.messageInput.sendMessageData = data
|
||||
ml.chat.messageInput.SetTitle(title + name)
|
||||
ml.chat.app.SetFocus(ml.chat.messageInput)
|
||||
ml.chatView.messageInput.sendMessageData = data
|
||||
ml.chatView.messageInput.SetTitle(title + name)
|
||||
ml.chatView.app.SetFocus(ml.chatView.messageInput)
|
||||
}
|
||||
|
||||
func (ml *messagesList) edit() {
|
||||
|
|
@ -576,7 +576,7 @@ func (ml *messagesList) edit() {
|
|||
return
|
||||
}
|
||||
|
||||
me, err := ml.chat.state.Cabinet.Me()
|
||||
me, err := ml.chatView.state.Cabinet.Me()
|
||||
if err != nil {
|
||||
slog.Error("failed to get client user (me)", "err", err)
|
||||
return
|
||||
|
|
@ -587,10 +587,10 @@ func (ml *messagesList) edit() {
|
|||
return
|
||||
}
|
||||
|
||||
ml.chat.messageInput.SetTitle("Editing")
|
||||
ml.chat.messageInput.edit = true
|
||||
ml.chat.messageInput.SetText(message.Content, true)
|
||||
ml.chat.app.SetFocus(ml.chat.messageInput)
|
||||
ml.chatView.messageInput.SetTitle("Editing")
|
||||
ml.chatView.messageInput.edit = true
|
||||
ml.chatView.messageInput.SetText(message.Content, true)
|
||||
ml.chatView.app.SetFocus(ml.chatView.messageInput)
|
||||
}
|
||||
|
||||
func (ml *messagesList) confirmDelete() {
|
||||
|
|
@ -600,7 +600,7 @@ func (ml *messagesList) confirmDelete() {
|
|||
}
|
||||
}
|
||||
|
||||
ml.chat.showConfirmModal(
|
||||
ml.chatView.showConfirmModal(
|
||||
"Are you sure you want to delete this message?",
|
||||
[]string{"Yes", "No"},
|
||||
onChoice,
|
||||
|
|
@ -615,24 +615,24 @@ func (ml *messagesList) delete() {
|
|||
}
|
||||
|
||||
if msg.GuildID.IsValid() {
|
||||
me, err := ml.chat.state.Cabinet.Me()
|
||||
me, err := ml.chatView.state.Cabinet.Me()
|
||||
if err != nil {
|
||||
slog.Error("failed to get client user (me)", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
if msg.Author.ID != me.ID && !ml.chat.state.HasPermissions(msg.ChannelID, discord.PermissionManageMessages) {
|
||||
if msg.Author.ID != me.ID && !ml.chatView.state.HasPermissions(msg.ChannelID, discord.PermissionManageMessages) {
|
||||
slog.Error("failed to delete message; missing relevant permissions", "channel_id", msg.ChannelID, "message_id", msg.ID)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
selected := ml.chat.SelectedChannel()
|
||||
selected := ml.chatView.SelectedChannel()
|
||||
if selected == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := ml.chat.state.DeleteMessage(selected.ID, msg.ID, ""); err != nil {
|
||||
if err := ml.chatView.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
|
||||
}
|
||||
|
|
@ -640,7 +640,7 @@ func (ml *messagesList) delete() {
|
|||
ml.selectedMessageID = 0
|
||||
ml.Highlight()
|
||||
|
||||
if err := ml.chat.state.MessageRemove(selected.ID, msg.ID); err != nil {
|
||||
if err := ml.chatView.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
|
||||
}
|
||||
|
|
@ -657,13 +657,13 @@ func (ml *messagesList) requestGuildMembers(gID discord.GuildID, ms []discord.Me
|
|||
continue
|
||||
}
|
||||
|
||||
if member, _ := ml.chat.state.Cabinet.Member(gID, m.Author.ID); member == nil {
|
||||
if member, _ := ml.chatView.state.Cabinet.Member(gID, m.Author.ID); member == nil {
|
||||
usersToFetch = append(usersToFetch, m.Author.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(usersToFetch) > 0 {
|
||||
err := ml.chat.state.SendGateway(context.TODO(), &gateway.RequestGuildMembersCommand{
|
||||
err := ml.chatView.state.SendGateway(context.TODO(), &gateway.RequestGuildMembersCommand{
|
||||
GuildIDs: []discord.GuildID{gID},
|
||||
UserIDs: slices.Compact(usersToFetch),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ import (
|
|||
"github.com/diamondburned/ningen/v3"
|
||||
)
|
||||
|
||||
func (cv *ChatView) OpenState(token string) error {
|
||||
func (v *View) OpenState(token string) error {
|
||||
identifyProps := http.IdentifyProperties()
|
||||
gateway.DefaultIdentity = identifyProps
|
||||
gateway.DefaultPresence = &gateway.UpdatePresenceCommand{
|
||||
Status: cv.cfg.Status,
|
||||
Status: v.cfg.Status,
|
||||
}
|
||||
|
||||
id := gateway.DefaultIdentifier(token)
|
||||
|
|
@ -28,34 +28,34 @@ func (cv *ChatView) OpenState(token string) error {
|
|||
|
||||
session := session.NewCustom(id, http.NewClient(token), handler.New())
|
||||
state := state.NewFromSession(session, defaultstore.New())
|
||||
cv.state = ningen.FromState(state)
|
||||
v.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)
|
||||
v.state.AddHandler(v.onRaw)
|
||||
v.state.AddHandler(v.onReady)
|
||||
v.state.AddHandler(v.onMessageCreate)
|
||||
v.state.AddHandler(v.onMessageUpdate)
|
||||
v.state.AddHandler(v.onMessageDelete)
|
||||
v.state.AddHandler(v.onReadUpdate)
|
||||
v.state.AddHandler(v.onGuildMembersChunk)
|
||||
v.state.AddHandler(v.onGuildMemberRemove)
|
||||
|
||||
cv.state.StateLog = func(err error) {
|
||||
v.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())
|
||||
v.state.OnRequest = append(v.state.OnRequest, httputil.WithHeaders(http.Headers()), v.onRequest)
|
||||
return v.state.Open(context.TODO())
|
||||
}
|
||||
|
||||
func (cv *ChatView) CloseState() error {
|
||||
if cv.state == nil {
|
||||
func (v *View) CloseState() error {
|
||||
if v.state == nil {
|
||||
return nil
|
||||
}
|
||||
return cv.state.Close()
|
||||
return v.state.Close()
|
||||
}
|
||||
|
||||
func (cv *ChatView) onRequest(r httpdriver.Request) error {
|
||||
func (v *View) onRequest(r httpdriver.Request) error {
|
||||
if req, ok := r.(*httpdriver.DefaultRequest); ok {
|
||||
slog.Debug("new HTTP request", "method", req.Method, "url", req.URL)
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ func (cv *ChatView) onRequest(r httpdriver.Request) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (cv *ChatView) onRaw(event *ws.RawEvent) {
|
||||
func (v *View) onRaw(event *ws.RawEvent) {
|
||||
slog.Debug(
|
||||
"new raw event",
|
||||
"code", event.OriginalCode,
|
||||
|
|
|
|||
316
internal/ui/chat/view.go
Normal file
316
internal/ui/chat/view.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 View 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 NewView(app *tview.Application, cfg *config.Config, onLogout func()) *View {
|
||||
v := &View{
|
||||
Pages: tview.NewPages(),
|
||||
|
||||
mainFlex: tview.NewFlex(),
|
||||
rightFlex: tview.NewFlex(),
|
||||
|
||||
app: app,
|
||||
cfg: cfg,
|
||||
onLogout: onLogout,
|
||||
}
|
||||
v.guildsTree = newGuildsTree(cfg, v)
|
||||
v.messagesList = newMessagesList(cfg, v)
|
||||
v.messageInput = newMessageInput(cfg, v)
|
||||
|
||||
v.SetInputCapture(v.onInputCapture)
|
||||
v.buildLayout()
|
||||
return v
|
||||
}
|
||||
|
||||
func (v *View) SelectedChannel() *discord.Channel {
|
||||
v.selectedChannelMu.RLock()
|
||||
defer v.selectedChannelMu.RUnlock()
|
||||
return v.selectedChannel
|
||||
}
|
||||
|
||||
func (v *View) SetSelectedChannel(channel *discord.Channel) {
|
||||
v.selectedChannelMu.Lock()
|
||||
v.selectedChannel = channel
|
||||
v.selectedChannelMu.Unlock()
|
||||
}
|
||||
|
||||
func (v *View) buildLayout() {
|
||||
v.Clear()
|
||||
v.rightFlex.Clear()
|
||||
v.mainFlex.Clear()
|
||||
|
||||
v.rightFlex.
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(v.messagesList, 0, 1, false).
|
||||
AddItem(v.messageInput, 3, 1, false)
|
||||
// The guilds tree is always focused first at start-up.
|
||||
v.mainFlex.
|
||||
AddItem(v.guildsTree, 0, 1, true).
|
||||
AddItem(v.rightFlex, 0, 4, false)
|
||||
|
||||
v.AddAndSwitchToPage(flexPageName, v.mainFlex, true)
|
||||
}
|
||||
|
||||
func (v *View) toggleGuildsTree() {
|
||||
// The guilds tree is visible if the number of items is two.
|
||||
if v.mainFlex.GetItemCount() == 2 {
|
||||
v.mainFlex.RemoveItem(v.guildsTree)
|
||||
if v.guildsTree.HasFocus() {
|
||||
v.app.SetFocus(v.mainFlex)
|
||||
}
|
||||
} else {
|
||||
v.buildLayout()
|
||||
v.app.SetFocus(v.guildsTree)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) focusGuildsTree() bool {
|
||||
// The guilds tree is not hidden if the number of items is two.
|
||||
if v.mainFlex.GetItemCount() == 2 {
|
||||
v.app.SetFocus(v.guildsTree)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (v *View) focusMessageInput() bool {
|
||||
if !v.messageInput.GetDisabled() {
|
||||
v.app.SetFocus(v.messageInput)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (v *View) focusPrevious() {
|
||||
switch v.app.GetFocus() {
|
||||
case v.guildsTree:
|
||||
v.focusMessageInput()
|
||||
case v.messagesList: // Handle both a.messagesList and a.flex as well as other edge cases (if there is).
|
||||
if ok := v.focusGuildsTree(); !ok {
|
||||
v.app.SetFocus(v.messageInput)
|
||||
}
|
||||
case v.messageInput:
|
||||
v.app.SetFocus(v.messagesList)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) focusNext() {
|
||||
switch v.app.GetFocus() {
|
||||
case v.guildsTree:
|
||||
v.app.SetFocus(v.messagesList)
|
||||
case v.messagesList:
|
||||
v.focusMessageInput()
|
||||
case v.messageInput: // Handle both a.messageInput and a.flex as well as other edge cases (if there is).
|
||||
if ok := v.focusGuildsTree(); !ok {
|
||||
v.app.SetFocus(v.messagesList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch event.Name() {
|
||||
case v.cfg.Keys.FocusGuildsTree:
|
||||
v.messageInput.removeMentionsList()
|
||||
v.focusGuildsTree()
|
||||
return nil
|
||||
case v.cfg.Keys.FocusMessagesList:
|
||||
v.messageInput.removeMentionsList()
|
||||
v.app.SetFocus(v.messagesList)
|
||||
return nil
|
||||
case v.cfg.Keys.FocusMessageInput:
|
||||
v.focusMessageInput()
|
||||
return nil
|
||||
case v.cfg.Keys.FocusPrevious:
|
||||
v.focusPrevious()
|
||||
return nil
|
||||
case v.cfg.Keys.FocusNext:
|
||||
v.focusNext()
|
||||
return nil
|
||||
case v.cfg.Keys.Logout:
|
||||
if v.onLogout != nil {
|
||||
v.onLogout()
|
||||
}
|
||||
|
||||
if err := keyring.DeleteToken(); err != nil {
|
||||
slog.Error("failed to delete token from keyring", "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
case v.cfg.Keys.ToggleGuildsTree:
|
||||
v.toggleGuildsTree()
|
||||
return nil
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func (v *View) showConfirmModal(prompt string, buttons []string, onDone func(label string)) {
|
||||
previousFocus := v.app.GetFocus()
|
||||
|
||||
modal := tview.NewModal().
|
||||
SetText(prompt).
|
||||
AddButtons(buttons).
|
||||
SetDoneFunc(func(_ int, buttonLabel string) {
|
||||
v.RemovePage(confirmModalPageName).SwitchToPage(flexPageName)
|
||||
v.app.SetFocus(previousFocus)
|
||||
|
||||
if onDone != nil {
|
||||
onDone(buttonLabel)
|
||||
}
|
||||
})
|
||||
|
||||
v.
|
||||
AddAndSwitchToPage(confirmModalPageName, ui.Centered(modal, 0, 0), true).
|
||||
ShowPage(flexPageName)
|
||||
}
|
||||
|
||||
func (v *View) onReadUpdate(event *read.UpdateEvent) {
|
||||
var guildNode *tview.TreeNode
|
||||
v.guildsTree.
|
||||
GetRoot().
|
||||
Walk(func(node, parent *tview.TreeNode) bool {
|
||||
switch node.GetReference() {
|
||||
case event.GuildID:
|
||||
node.SetTextStyle(v.guildsTree.getGuildNodeStyle(event.GuildID))
|
||||
guildNode = node
|
||||
return false
|
||||
case event.ChannelID:
|
||||
// private channel
|
||||
if !event.GuildID.IsValid() {
|
||||
style := v.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(v.guildsTree.getChannelNodeStyle(event.ChannelID))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
v.app.Draw()
|
||||
}
|
||||
|
||||
func (v *View) onReady(r *gateway.ReadyEvent) {
|
||||
dmNode := tview.NewTreeNode("Direct Messages")
|
||||
root := v.guildsTree.
|
||||
GetRoot().
|
||||
ClearChildren().
|
||||
AddChild(dmNode)
|
||||
|
||||
for _, folder := range r.UserSettings.GuildFolders {
|
||||
if folder.ID == 0 && len(folder.GuildIDs) == 1 {
|
||||
guild, err := v.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
|
||||
}
|
||||
|
||||
v.guildsTree.createGuildNode(root, *guild)
|
||||
} else {
|
||||
v.guildsTree.createFolderNode(folder)
|
||||
}
|
||||
}
|
||||
|
||||
v.guildsTree.SetCurrentNode(root)
|
||||
v.app.SetFocus(v.guildsTree)
|
||||
v.app.Draw()
|
||||
}
|
||||
|
||||
func (v *View) onMessageCreate(message *gateway.MessageCreateEvent) {
|
||||
if selected := v.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
|
||||
v.messagesList.drawMessage(v.messagesList, message.Message)
|
||||
v.app.Draw()
|
||||
}
|
||||
|
||||
if err := notifications.Notify(v.state, message, v.cfg); err != nil {
|
||||
slog.Error("failed to notify", "err", err, "channel_id", message.ChannelID, "message_id", message.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) onMessageUpdate(message *gateway.MessageUpdateEvent) {
|
||||
if selected := v.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
|
||||
v.onMessageDelete(&gateway.MessageDeleteEvent{ID: message.ID, ChannelID: message.ChannelID, GuildID: message.GuildID})
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) onMessageDelete(message *gateway.MessageDeleteEvent) {
|
||||
if selected := v.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
|
||||
messages, err := v.state.Cabinet.Messages(message.ChannelID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get messages from state", "err", err, "channel_id", message.ChannelID)
|
||||
return
|
||||
}
|
||||
|
||||
v.messagesList.reset()
|
||||
v.messagesList.drawMessages(messages)
|
||||
v.app.Draw()
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) onGuildMembersChunk(event *gateway.GuildMembersChunkEvent) {
|
||||
v.messagesList.setFetchingChunk(false, uint(len(event.Members)))
|
||||
}
|
||||
|
||||
func (v *View) onGuildMemberRemove(event *gateway.GuildMemberRemoveEvent) {
|
||||
v.messageInput.cache.Invalidate(event.GuildID.String()+" "+event.User.Username, v.state.MemberState.SearchLimit)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue