Rename inconsistent widget names (#224)

This commit is contained in:
ayntgl 2022-09-26 19:09:28 +05:30 • committed by GitHub
parent 4b61bbf61d
commit 0bbe6bf93a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 796 additions and 799 deletions

View file

@ -11,8 +11,8 @@ import (
const Name = "discordo" const Name = "discordo"
type MessagesPanelKeysConfig struct { type MessagesViewKeysConfig struct {
OpenActionsList string `yaml:"open_actions_list"` OpenActionsView string `yaml:"open_actions_view"`
SelectPreviousMessage string `yaml:"select_previous_message"` SelectPreviousMessage string `yaml:"select_previous_message"`
SelectNextMessage string `yaml:"select_next_message"` SelectNextMessage string `yaml:"select_next_message"`
@ -20,14 +20,14 @@ type MessagesPanelKeysConfig struct {
SelectLastMessage string `yaml:"select_last_message"` SelectLastMessage string `yaml:"select_last_message"`
} }
type MessageInputKeysConfig struct { type InputViewKeysConfig struct {
OpenExternalEditor string `yaml:"open_external_editor"` OpenExternalEditor string `yaml:"open_external_editor"`
PasteClipboard string `yaml:"paste_clipboard"` PasteClipboard string `yaml:"paste_clipboard"`
} }
type KeysConfig struct { type KeysConfig struct {
MessagesPanel MessagesPanelKeysConfig `yaml:"messages_panel"` MessagesView MessagesViewKeysConfig `yaml:"messages_view"`
MessageInput MessageInputKeysConfig `yaml:"message_input"` InputView InputViewKeysConfig `yaml:"input_view"`
} }
type ThemeConfig struct { type ThemeConfig struct {
@ -39,7 +39,7 @@ type ThemeConfig struct {
type Config struct { type Config struct {
// Whether the mouse is usable or not. // Whether the mouse is usable or not.
Mouse bool `yaml:"mouse"` Mouse bool `yaml:"mouse"`
// The maximum number of messages to fetch and display on the messages panel. Its value must not be lesser than 1 and greater than 100. // The maximum number of messages to fetch and display. Its value must not be lesser than 1 and greater than 100.
MessagesLimit uint `yaml:"messages_limit"` MessagesLimit uint `yaml:"messages_limit"`
// Whether to display the timestamps of the messages beside the displayed message or not. // Whether to display the timestamps of the messages beside the displayed message or not.
Timestamps bool `yaml:"timestamps"` Timestamps bool `yaml:"timestamps"`
@ -63,8 +63,8 @@ func New() *Config {
TimeFormat: time.Kitchen, TimeFormat: time.Kitchen,
Keys: KeysConfig{ Keys: KeysConfig{
MessagesPanel: MessagesPanelKeysConfig{ MessagesView: MessagesViewKeysConfig{
OpenActionsList: "Rune[a]", OpenActionsView: "Rune[a]",
SelectPreviousMessage: "Up", SelectPreviousMessage: "Up",
SelectNextMessage: "Down", SelectNextMessage: "Down",

229
ui/actions_view.go Normal file
View file

@ -0,0 +1,229 @@
package ui
import (
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"github.com/atotto/clipboard"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/rivo/tview"
"github.com/skratchdot/open-golang/open"
)
var linkRegex = regexp.MustCompile("https?://.+")
type ActionsView struct {
*tview.List
core *Core
message *discord.Message
}
func newActionsView(c *Core, m *discord.Message) *ActionsView {
v := &ActionsView{
List: tview.NewList(),
core: c,
message: m,
}
v.ShowSecondaryText(false)
v.SetDoneFunc(func() {
c.App.SetRoot(c.View, true)
c.App.SetFocus(c.MessagesView)
})
// If the client user has the `SEND_MESSAGES` permission, add "Reply" and "Mention Reply" actions.
if hasPermission(c.State, c.ChannelsView.selectedChannel.ID, discord.PermissionSendMessages) {
v.AddItem("Reply", "", 'r', v.replyAction)
v.AddItem("Mention Reply", "", 'R', v.mentionReplyAction)
}
// If the referenced message exists, add a new action to select the reply.
if m.ReferencedMessage != nil {
v.AddItem("Select Reply", "", 'm', v.selectReplyAction)
}
// If the content of the message contains link(s), add the appropriate actions.
links := linkRegex.FindAllString(m.Content, -1)
if len(links) != 0 {
v.AddItem("Open Link", "", 'l', func() {
for _, l := range links {
go open.Run(l)
}
c.App.SetRoot(c.View, true)
c.App.SetFocus(c.MessagesView)
})
}
// If the message contains attachments, add the appropriate actions to the actions view.
if len(m.Attachments) != 0 {
v.AddItem("Open Attachment", "", 'o', v.openAttachmentAction)
v.AddItem("Download Attachment", "", 'd', v.downloadAttachmentAction)
}
// If the client user has the `MANAGE_MESSAGES` permission, add a new action to delete the message.
if hasPermission(c.State, c.ChannelsView.selectedChannel.ID, discord.PermissionManageMessages) {
v.AddItem("Delete", "", 'd', v.deleteAction)
}
v.AddItem("Copy Content", "", 'c', v.copyContentAction)
v.AddItem("Copy ID", "", 'i', v.copyIDAction)
v.AddItem("Copy Link", "", 'k', v.copyLinkAction)
v.SetTitle("Press the Escape key to close")
v.SetTitleAlign(tview.AlignLeft)
v.SetBorder(true)
v.SetBorderPadding(0, 0, 1, 1)
return v
}
func (v *ActionsView) replyAction() {
v.core.InputView.SetTitle("Replying to " + v.message.Author.Tag())
v.core.App.SetRoot(v.core.View, true)
v.core.App.SetFocus(v.core.InputView)
}
func (v *ActionsView) mentionReplyAction() {
v.core.InputView.SetTitle("[@] Replying to " + v.message.Author.Tag())
v.core.App.SetRoot(v.core.View, true)
v.core.App.SetFocus(v.core.InputView)
}
func (v *ActionsView) selectReplyAction() {
ms, err := v.core.State.Cabinet.Messages(v.message.ChannelID)
if err != nil {
return
}
v.core.MessagesView.selectedMessage, _ = findMessageByID(ms, v.message.ReferencedMessage.ID)
v.core.MessagesView.
Highlight(v.message.ReferencedMessage.ID.String()).
ScrollToHighlight()
v.core.App.SetRoot(v.core.View, true)
v.core.App.SetFocus(v.core.MessagesView)
}
func (v *ActionsView) openAttachmentAction() {
for _, a := range v.message.Attachments {
cacheDirPath, _ := os.UserCacheDir()
f, err := os.Create(filepath.Join(cacheDirPath, a.Filename))
if err != nil {
return
}
defer f.Close()
resp, err := http.Get(a.URL)
if err != nil {
return
}
d, err := io.ReadAll(resp.Body)
if err != nil {
return
}
f.Write(d)
go open.Run(f.Name())
}
v.core.App.SetRoot(v.core.View, true)
v.core.App.SetFocus(v.core.MessagesView)
}
func (v *ActionsView) downloadAttachmentAction() {
for _, a := range v.message.Attachments {
path, err := os.UserHomeDir()
if err != nil {
path = os.TempDir()
}
path = filepath.Join(path, "Downloads", a.Filename)
f, err := os.Create(path)
if err != nil {
return
}
defer f.Close()
resp, err := http.Get(a.URL)
if err != nil {
return
}
d, err := io.ReadAll(resp.Body)
if err != nil {
return
}
f.Write(d)
}
v.core.App.SetRoot(v.core.View, true)
v.core.App.SetFocus(v.core.MessagesView)
}
func (v *ActionsView) deleteAction() {
v.core.MessagesView.Clear()
err := v.core.State.MessageRemove(v.message.ChannelID, v.message.ID)
if err != nil {
return
}
err = v.core.State.DeleteMessage(v.message.ChannelID, v.message.ID, "Unknown")
if err != nil {
return
}
// The returned slice will be sorted from latest to oldest.
ms, err := v.core.State.Cabinet.Messages(v.message.ChannelID)
if err != nil {
return
}
for i := len(ms) - 1; i >= 0; i-- {
_, err = v.core.MessagesView.Write(buildMessage(v.core, ms[i]))
if err != nil {
return
}
}
v.core.App.SetRoot(v.core.View, true)
v.core.App.SetFocus(v.core.MessagesView)
}
func (v *ActionsView) copyContentAction() {
err := clipboard.WriteAll(v.message.Content)
if err != nil {
return
}
v.core.App.SetRoot(v.core.View, true)
v.core.App.SetFocus(v.core.MessagesView)
}
func (v *ActionsView) copyIDAction() {
err := clipboard.WriteAll(v.message.ID.String())
if err != nil {
return
}
v.core.App.SetRoot(v.core.View, true)
v.core.App.SetFocus(v.core.MessagesView)
}
func (v *ActionsView) copyLinkAction() {
err := clipboard.WriteAll(v.message.URL())
if err != nil {
return
}
v.core.App.SetRoot(v.core.View, true)
v.core.App.SetFocus(v.core.MessagesView)
}

View file

@ -1,162 +0,0 @@
package ui
import (
"sort"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/rivo/tview"
)
type ChannelsTree struct {
*tview.TreeView
SelectedChannel *discord.Channel
core *Core
}
func NewChannelsTree(c *Core) *ChannelsTree {
ct := &ChannelsTree{
TreeView: tview.NewTreeView(),
core: c,
}
ct.SetRoot(tview.NewTreeNode(""))
ct.SetTopLevel(1)
ct.SetSelectedFunc(ct.onSelected)
ct.SetTitle("Channels")
ct.SetTitleAlign(tview.AlignLeft)
ct.SetBorder(true)
ct.SetBorderPadding(0, 0, 1, 1)
return ct
}
func (ct *ChannelsTree) onSelected(node *tview.TreeNode) {
ct.SelectedChannel = nil
ct.core.MessagesPanel.SelectedMessage = -1
ct.core.MessagesPanel.
Highlight().
Clear().
SetTitle("")
ct.core.MessageInput.SetText("")
ref := node.GetReference()
c, err := ct.core.State.Cabinet.Channel(ref.(discord.ChannelID))
if err != nil {
return
}
// If the channel is a category channel, expand the selected node if it is collapsed, otherwise collapse.
if c.Type == discord.GuildCategory {
node.SetExpanded(!node.IsExpanded())
return
}
ct.SelectedChannel = c
ct.core.App.SetFocus(ct.core.MessageInput)
title := channelToString(*c)
if c.Topic != "" {
title += " - " + parseMarkdown(c.Topic)
}
ct.core.MessagesPanel.SetTitle(title)
go func() {
// The returned slice will be sorted from latest to oldest.
ms, err := ct.core.State.Messages(c.ID, ct.core.Config.MessagesLimit)
if err != nil {
return
}
for i := len(ms) - 1; i >= 0; i-- {
_, err = ct.core.MessagesPanel.Write(buildMessage(ct.core, ms[i]))
if err != nil {
return
}
}
ct.core.MessagesPanel.ScrollToEnd()
}()
}
func (ct *ChannelsTree) createChannelNode(c discord.Channel) *tview.TreeNode {
channelNode := tview.NewTreeNode(channelToString(c))
channelNode.SetReference(c.ID)
return channelNode
}
func (ct *ChannelsTree) createPrivateChannelNodes(rootNode *tview.TreeNode) {
cs, err := ct.core.State.Cabinet.PrivateChannels()
if err != nil {
return
}
sort.Slice(cs, func(i, j int) bool {
return cs[i].LastMessageID > cs[j].LastMessageID
})
for _, c := range cs {
rootNode.AddChild(ct.createChannelNode(c))
}
}
func (ct *ChannelsTree) createGuildChannelNodes(rootNode *tview.TreeNode, gID discord.GuildID) {
cs, err := ct.core.State.Cabinet.Channels(gID)
if err != nil {
return
}
sort.Slice(cs, func(i, j int) bool {
return cs[i].Position < cs[j].Position
})
ct.createOrphanChannelNodes(rootNode, cs)
ct.createCategoryChannelNodes(rootNode, cs)
ct.createChildrenChannelNodes(rootNode, cs)
}
func (ct *ChannelsTree) createOrphanChannelNodes(rootNode *tview.TreeNode, cs []discord.Channel) {
for _, c := range cs {
if (c.Type == discord.GuildText || c.Type == discord.GuildNews) && (!c.ParentID.IsValid()) {
rootNode.AddChild(ct.createChannelNode(c))
}
}
}
func (ct *ChannelsTree) createCategoryChannelNodes(rootNode *tview.TreeNode, cs []discord.Channel) {
CATEGORY:
for _, c := range cs {
if c.Type == discord.GuildCategory {
for _, nestedChannel := range cs {
if nestedChannel.ParentID == c.ID {
rootNode.AddChild(ct.createChannelNode(c))
continue CATEGORY
}
}
rootNode.AddChild(ct.createChannelNode(c))
}
}
}
func (ct *ChannelsTree) createChildrenChannelNodes(rootNode *tview.TreeNode, cs []discord.Channel) {
for _, c := range cs {
if (c.Type == discord.GuildText || c.Type == discord.GuildNews) && (c.ParentID.IsValid()) {
var parentNode *tview.TreeNode
rootNode.Walk(func(node, _ *tview.TreeNode) bool {
if node.GetReference() == c.ParentID {
parentNode = node
return false
}
return true
})
if parentNode != nil {
parentNode.AddChild(ct.createChannelNode(c))
}
}
}
}

161
ui/channels_view.go Normal file
View file

@ -0,0 +1,161 @@
package ui
import (
"sort"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/rivo/tview"
)
type ChannelsView struct {
*tview.TreeView
selectedChannel *discord.Channel
core *Core
}
func newChannelsView(c *Core) *ChannelsView {
v := &ChannelsView{
TreeView: tview.NewTreeView(),
core: c,
}
v.SetRoot(tview.NewTreeNode(""))
v.SetTopLevel(1)
v.SetSelectedFunc(v.onSelected)
v.SetTitle("Channels")
v.SetTitleAlign(tview.AlignLeft)
v.SetBorder(true)
v.SetBorderPadding(0, 0, 1, 1)
return v
}
func (v *ChannelsView) onSelected(node *tview.TreeNode) {
v.selectedChannel = nil
v.core.MessagesView.selectedMessage = -1
v.core.MessagesView.
Highlight().
Clear().
SetTitle("")
v.core.InputView.SetText("")
ref := node.GetReference()
c, err := v.core.State.Cabinet.Channel(ref.(discord.ChannelID))
if err != nil {
return
}
// If the channel is a category channel, expand the selected node if it is collapsed, otherwise collapse.
if c.Type == discord.GuildCategory {
node.SetExpanded(!node.IsExpanded())
return
}
v.selectedChannel = c
v.core.App.SetFocus(v.core.InputView)
title := channelToString(*c)
if c.Topic != "" {
title += " - " + parseMarkdown(c.Topic)
}
v.core.MessagesView.SetTitle(title)
go func() {
// The returned slice will be sorted from latest to oldest.
ms, err := v.core.State.Messages(c.ID, v.core.Config.MessagesLimit)
if err != nil {
return
}
for i := len(ms) - 1; i >= 0; i-- {
_, err = v.core.MessagesView.Write(buildMessage(v.core, ms[i]))
if err != nil {
return
}
}
v.core.MessagesView.ScrollToEnd()
}()
}
func (v *ChannelsView) createChannelNode(c discord.Channel) *tview.TreeNode {
channelNode := tview.NewTreeNode(channelToString(c))
channelNode.SetReference(c.ID)
return channelNode
}
func (v *ChannelsView) createPrivateChannelNodes(root *tview.TreeNode) {
cs, err := v.core.State.Cabinet.PrivateChannels()
if err != nil {
return
}
sort.Slice(cs, func(i, j int) bool {
return cs[i].LastMessageID > cs[j].LastMessageID
})
for _, c := range cs {
root.AddChild(v.createChannelNode(c))
}
}
func (v *ChannelsView) createGuildChannelNodes(root *tview.TreeNode, gID discord.GuildID) {
cs, err := v.core.State.Cabinet.Channels(gID)
if err != nil {
return
}
sort.Slice(cs, func(i, j int) bool {
return cs[i].Position < cs[j].Position
})
v.createOrphanChannelNodes(root, cs)
v.createCategoryChannelNodes(root, cs)
v.createChildrenChannelNodes(root, cs)
}
func (v *ChannelsView) createOrphanChannelNodes(root *tview.TreeNode, cs []discord.Channel) {
for _, c := range cs {
if (c.Type == discord.GuildText || c.Type == discord.GuildNews) && (!c.ParentID.IsValid()) {
root.AddChild(v.createChannelNode(c))
}
}
}
func (v *ChannelsView) createCategoryChannelNodes(root *tview.TreeNode, cs []discord.Channel) {
CATEGORY:
for _, c := range cs {
if c.Type == discord.GuildCategory {
for _, nestedChannel := range cs {
if nestedChannel.ParentID == c.ID {
root.AddChild(v.createChannelNode(c))
continue CATEGORY
}
}
root.AddChild(v.createChannelNode(c))
}
}
}
func (v *ChannelsView) createChildrenChannelNodes(root *tview.TreeNode, cs []discord.Channel) {
for _, c := range cs {
if (c.Type == discord.GuildText || c.Type == discord.GuildNews) && (c.ParentID.IsValid()) {
var parentNode *tview.TreeNode
root.Walk(func(node, _ *tview.TreeNode) bool {
if node.GetReference() == c.ParentID {
parentNode = node
return false
}
return true
})
if parentNode != nil {
parentNode.AddChild(v.createChannelNode(c))
}
}
}
}

View file

@ -15,10 +15,10 @@ import (
type FocusedID int type FocusedID int
const ( const (
guildsTree FocusedID = iota guildsView FocusedID = iota
channelsTree channelsView
messagesPanel messagesView
messageInput inputView
) )
// Core is responsible for the following: // Core is responsible for the following:
@ -26,12 +26,12 @@ const (
// - Configuration of the application and state when Run is called. // - Configuration of the application and state when Run is called.
// - Management of the application and state. // - Management of the application and state.
type Core struct { type Core struct {
App *tview.Application App *tview.Application
View *tview.Flex View *tview.Flex
GuildsTree *GuildsTree GuildsView *GuildsView
ChannelsTree *ChannelsTree ChannelsView *ChannelsView
MessagesPanel *MessagesPanel MessagesView *MessagesView
MessageInput *MessageInput InputView *InputView
Config *config.Config Config *config.Config
State *state.State State *state.State
@ -55,10 +55,10 @@ func NewCore(cfg *config.Config) *Core {
c.View = tview.NewFlex() c.View = tview.NewFlex()
c.View.SetInputCapture(c.onViewInputCapture) c.View.SetInputCapture(c.onViewInputCapture)
c.GuildsTree = NewGuildsTree(c) c.GuildsView = newGuildsView(c)
c.ChannelsTree = NewChannelsTree(c) c.ChannelsView = newChannelsView(c)
c.MessagesPanel = NewMessagesPanel(c) c.MessagesView = newMessagesView(c)
c.MessageInput = NewMessageInput(c) c.InputView = newInputView(c)
return c return c
} }
@ -75,18 +75,18 @@ func (c *Core) Run(token string) error {
func (c *Core) Draw() { func (c *Core) Draw() {
left := tview.NewFlex(). left := tview.NewFlex().
SetDirection(tview.FlexRow). SetDirection(tview.FlexRow).
AddItem(c.GuildsTree, 10, 1, false). AddItem(c.GuildsView, 10, 1, false).
AddItem(c.ChannelsTree, 0, 1, false) AddItem(c.ChannelsView, 0, 1, false)
right := tview.NewFlex(). right := tview.NewFlex().
SetDirection(tview.FlexRow). SetDirection(tview.FlexRow).
AddItem(c.MessagesPanel, 0, 1, false). AddItem(c.MessagesView, 0, 1, false).
AddItem(c.MessageInput, 3, 1, false) AddItem(c.InputView, 3, 1, false)
c.View.AddItem(left, 0, 1, false) c.View.AddItem(left, 0, 1, false)
c.View.AddItem(right, 0, 4, false) c.View.AddItem(right, 0, 4, false)
c.App.SetRoot(c.View, true) c.App.SetRoot(c.View, true)
c.App.SetFocus(c.GuildsTree) c.App.SetFocus(c.GuildsView)
} }
func (c *Core) onAppBeforeDraw(screen tcell.Screen) bool { func (c *Core) onAppBeforeDraw(screen tcell.Screen) bool {
@ -102,18 +102,18 @@ func (c *Core) onViewInputCapture(event *tcell.EventKey) *tcell.EventKey {
case tcell.KeyEsc: case tcell.KeyEsc:
c.focusedID = 0 c.focusedID = 0
case tcell.KeyBacktab: case tcell.KeyBacktab:
// If the currently focused widget is the guilds tree widget (first), then focus the message input widget (last) // If the currently focused view is the guilds view (first), then focus the input view (last)
if c.focusedID == 0 { if c.focusedID == 0 {
c.focusedID = messageInput c.focusedID = inputView
} else { } else {
c.focusedID-- c.focusedID--
} }
c.setFocus() c.setFocus()
case tcell.KeyTab: case tcell.KeyTab:
// If the currently focused widget is the message input widget (last), then focus the guilds tree widget (first) // If the currently focused view is the input view (last), then focus the guilds view (first)
if c.focusedID == messageInput { if c.focusedID == inputView {
c.focusedID = guildsTree c.focusedID = guildsView
} else { } else {
c.focusedID++ c.focusedID++
} }
@ -127,21 +127,21 @@ func (c *Core) onViewInputCapture(event *tcell.EventKey) *tcell.EventKey {
func (c *Core) setFocus() { func (c *Core) setFocus() {
var p tview.Primitive var p tview.Primitive
switch c.focusedID { switch c.focusedID {
case guildsTree: case guildsView:
p = c.GuildsTree p = c.GuildsView
case channelsTree: case channelsView:
p = c.ChannelsTree p = c.ChannelsView
case messagesPanel: case messagesView:
p = c.MessagesPanel p = c.MessagesView
case messageInput: case inputView:
p = c.MessageInput p = c.InputView
} }
c.App.SetFocus(p) c.App.SetFocus(p)
} }
func (c *Core) onReady(r *gateway.ReadyEvent) { func (c *Core) onReady(r *gateway.ReadyEvent) {
rootNode := c.GuildsTree.GetRoot() root := c.GuildsView.GetRoot()
for _, gf := range r.UserSettings.GuildFolders { for _, gf := range r.UserSettings.GuildFolders {
if gf.ID == 0 { if gf.ID == 0 {
for _, gID := range gf.GuildIDs { for _, gID := range gf.GuildIDs {
@ -152,7 +152,7 @@ func (c *Core) onReady(r *gateway.ReadyEvent) {
guildNode := tview.NewTreeNode(g.Name) guildNode := tview.NewTreeNode(g.Name)
guildNode.SetReference(g.ID) guildNode.SetReference(g.ID)
rootNode.AddChild(guildNode) root.AddChild(guildNode)
} }
} else { } else {
var b strings.Builder var b strings.Builder
@ -174,7 +174,7 @@ func (c *Core) onReady(r *gateway.ReadyEvent) {
b.WriteString("[-]") b.WriteString("[-]")
folderNode := tview.NewTreeNode(b.String()) folderNode := tview.NewTreeNode(b.String())
rootNode.AddChild(folderNode) root.AddChild(folderNode)
for _, gID := range gf.GuildIDs { for _, gID := range gf.GuildIDs {
g, err := c.State.Cabinet.Guild(gID) g, err := c.State.Cabinet.Guild(gID)
@ -190,24 +190,24 @@ func (c *Core) onReady(r *gateway.ReadyEvent) {
} }
c.GuildsTree.SetCurrentNode(rootNode) c.GuildsView.SetCurrentNode(root)
c.App.SetFocus(c.GuildsTree) c.App.SetFocus(c.GuildsView)
} }
func (c *Core) onGuildCreate(g *gateway.GuildCreateEvent) { func (c *Core) onGuildCreate(g *gateway.GuildCreateEvent) {
guildNode := tview.NewTreeNode(g.Name) guildNode := tview.NewTreeNode(g.Name)
guildNode.SetReference(g.ID) guildNode.SetReference(g.ID)
rootNode := c.GuildsTree.GetRoot() rootNode := c.GuildsView.GetRoot()
rootNode.AddChild(guildNode) rootNode.AddChild(guildNode)
c.GuildsTree.SetCurrentNode(rootNode) c.GuildsView.SetCurrentNode(rootNode)
c.App.SetFocus(c.GuildsTree) c.App.SetFocus(c.GuildsView)
c.App.Draw() c.App.Draw()
} }
func (c *Core) onGuildDelete(g *gateway.GuildDeleteEvent) { func (c *Core) onGuildDelete(g *gateway.GuildDeleteEvent) {
rootNode := c.GuildsTree.GetRoot() rootNode := c.GuildsView.GetRoot()
var parentNode *tview.TreeNode var parentNode *tview.TreeNode
rootNode.Walk(func(node, _ *tview.TreeNode) bool { rootNode.Walk(func(node, _ *tview.TreeNode) bool {
if node.GetReference() == g.ID { if node.GetReference() == g.ID {
@ -226,14 +226,14 @@ func (c *Core) onGuildDelete(g *gateway.GuildDeleteEvent) {
} }
func (c *Core) onMessageCreate(m *gateway.MessageCreateEvent) { func (c *Core) onMessageCreate(m *gateway.MessageCreateEvent) {
if c.ChannelsTree.SelectedChannel != nil && c.ChannelsTree.SelectedChannel.ID == m.ChannelID { if c.ChannelsView.selectedChannel != nil && c.ChannelsView.selectedChannel.ID == m.ChannelID {
_, err := c.MessagesPanel.Write(buildMessage(c, m.Message)) _, err := c.MessagesView.Write(buildMessage(c, m.Message))
if err != nil { if err != nil {
return return
} }
if len(c.MessagesPanel.GetHighlights()) == 0 { if len(c.MessagesView.GetHighlights()) == 0 {
c.MessagesPanel.ScrollToEnd() c.MessagesView.ScrollToEnd()
} }
} }
} }

View file

@ -1,61 +0,0 @@
package ui
import (
"github.com/diamondburned/arikawa/v3/discord"
"github.com/rivo/tview"
)
type GuildsTree struct {
*tview.TreeView
core *Core
}
func NewGuildsTree(c *Core) *GuildsTree {
gt := &GuildsTree{
TreeView: tview.NewTreeView(),
core: c,
}
rootNode := tview.NewTreeNode("")
rootNode.AddChild(tview.NewTreeNode("Direct Messages"))
gt.SetRoot(rootNode)
gt.SetTopLevel(1)
gt.SetSelectedFunc(gt.onSelected)
gt.SetTitle("Guilds")
gt.SetTitleAlign(tview.AlignLeft)
gt.SetBorder(true)
gt.SetBorderPadding(0, 0, 1, 1)
return gt
}
func (gt *GuildsTree) onSelected(node *tview.TreeNode) {
gt.core.ChannelsTree.SelectedChannel = nil
gt.core.MessagesPanel.SelectedMessage = -1
rootNode := gt.core.ChannelsTree.GetRoot()
rootNode.ClearChildren()
gt.core.MessagesPanel.
Highlight().
Clear().
SetTitle("")
gt.core.MessageInput.SetText("")
// If the selected node has children (guild folder), expand the selected node if it is collapsed, otherwise collapse.
if len(node.GetChildren()) != 0 {
node.SetExpanded(!node.IsExpanded())
return
}
ref := node.GetReference()
// If the reference of the selected node is nil, it must be the direct messages node.
if ref == nil {
gt.core.ChannelsTree.createPrivateChannelNodes(rootNode)
} else { // Guild
gt.core.ChannelsTree.createGuildChannelNodes(rootNode, ref.(discord.GuildID))
}
gt.core.ChannelsTree.SetCurrentNode(rootNode)
gt.core.App.SetFocus(gt.core.ChannelsTree)
}

61
ui/guilds_view.go Normal file
View file

@ -0,0 +1,61 @@
package ui
import (
"github.com/diamondburned/arikawa/v3/discord"
"github.com/rivo/tview"
)
type GuildsView struct {
*tview.TreeView
core *Core
}
func newGuildsView(c *Core) *GuildsView {
v := &GuildsView{
TreeView: tview.NewTreeView(),
core: c,
}
root := tview.NewTreeNode("")
root.AddChild(tview.NewTreeNode("Direct Messages"))
v.SetRoot(root)
v.SetTopLevel(1)
v.SetSelectedFunc(v.onSelected)
v.SetTitle("Guilds")
v.SetTitleAlign(tview.AlignLeft)
v.SetBorder(true)
v.SetBorderPadding(0, 0, 1, 1)
return v
}
func (v *GuildsView) onSelected(node *tview.TreeNode) {
v.core.ChannelsView.selectedChannel = nil
v.core.MessagesView.selectedMessage = -1
rootNode := v.core.ChannelsView.GetRoot()
rootNode.ClearChildren()
v.core.MessagesView.
Highlight().
Clear().
SetTitle("")
v.core.InputView.SetText("")
// If the selected node has children (guild folder), expand the selected node if it is collapsed, otherwise collapse.
if len(node.GetChildren()) != 0 {
node.SetExpanded(!node.IsExpanded())
return
}
ref := node.GetReference()
// If the reference of the selected node is nil, it must be the direct messages node.
if ref == nil {
v.core.ChannelsView.createPrivateChannelNodes(rootNode)
} else { // Guild
v.core.ChannelsView.createGuildChannelNodes(rootNode, ref.(discord.GuildID))
}
v.core.ChannelsView.SetCurrentNode(rootNode)
v.core.App.SetFocus(v.core.ChannelsView)
}

144
ui/input_view.go Normal file
View file

@ -0,0 +1,144 @@
package ui
import (
"io"
"os"
"os/exec"
"strings"
"github.com/atotto/clipboard"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/utils/json/option"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type InputView struct {
*tview.InputField
core *Core
}
func newInputView(c *Core) *InputView {
v := &InputView{
InputField: tview.NewInputField(),
core: c,
}
v.SetFieldBackgroundColor(tview.Styles.PrimitiveBackgroundColor)
v.SetPlaceholder("Message...")
v.SetPlaceholderStyle(tcell.StyleDefault.Background(tview.Styles.PrimitiveBackgroundColor))
v.SetInputCapture(v.inputCapture)
v.SetTitleAlign(tview.AlignLeft)
v.SetBorder(true)
v.SetBorderPadding(0, 0, 1, 1)
return v
}
func (v *InputView) inputCapture(event *tcell.EventKey) *tcell.EventKey {
switch event.Name() {
case "Enter":
return v.sendMessage()
case v.core.Config.Keys.InputView.OpenExternalEditor:
return v.openExternalEditor()
case v.core.Config.Keys.InputView.PasteClipboard:
return v.pasteClipboard()
case "Esc":
v.
SetText("").
SetTitle("")
v.core.MessagesView.selectedMessage = -1
v.core.MessagesView.Highlight()
return nil
}
return event
}
func (v *InputView) sendMessage() *tcell.EventKey {
if v.core.ChannelsView.selectedChannel == nil {
return nil
}
t := strings.TrimSpace(v.GetText())
if t == "" {
return nil
}
ms, err := v.core.State.Messages(v.core.ChannelsView.selectedChannel.ID, v.core.Config.MessagesLimit)
if err != nil {
return nil
}
if len(v.core.MessagesView.GetHighlights()) != 0 {
mID, err := discord.ParseSnowflake(v.core.MessagesView.GetHighlights()[0])
if err != nil {
return nil
}
_, m := findMessageByID(ms, discord.MessageID(mID))
d := api.SendMessageData{
Content: t,
Reference: m.Reference,
AllowedMentions: &api.AllowedMentions{RepliedUser: option.False},
}
// If the title of the input view has "[@]" as a prefix, send the message as a reply and mention the replied user.
if strings.HasPrefix(v.GetTitle(), "[@]") {
d.AllowedMentions.RepliedUser = option.True
}
go v.core.State.SendMessageComplex(m.ChannelID, d)
v.core.MessagesView.selectedMessage = -1
v.core.MessagesView.Highlight()
v.SetTitle("")
} else {
go v.core.State.SendMessage(v.core.ChannelsView.selectedChannel.ID, t)
}
v.SetText("")
return nil
}
func (v *InputView) pasteClipboard() *tcell.EventKey {
text, _ := clipboard.ReadAll()
text = v.GetText() + text
v.SetText(text)
return nil
}
func (v *InputView) openExternalEditor() *tcell.EventKey {
e := os.Getenv("EDITOR")
if e == "" {
return nil
}
f, err := os.CreateTemp(os.TempDir(), "discordo-*.md")
if err != nil {
return nil
}
defer os.Remove(f.Name())
cmd := exec.Command(e, f.Name())
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
v.core.App.Suspend(func() {
err = cmd.Run()
if err != nil {
return
}
})
b, err := io.ReadAll(f)
if err != nil {
return nil
}
v.SetText(string(b))
return nil
}

View file

@ -1,229 +0,0 @@
package ui
import (
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"github.com/atotto/clipboard"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/rivo/tview"
"github.com/skratchdot/open-golang/open"
)
var linkRegex = regexp.MustCompile("https?://.+")
type MessageActionsList struct {
*tview.List
core *Core
message *discord.Message
}
func NewMessageActionsList(c *Core, m *discord.Message) *MessageActionsList {
mal := &MessageActionsList{
List: tview.NewList(),
core: c,
message: m,
}
mal.ShowSecondaryText(false)
mal.SetDoneFunc(func() {
c.App.SetRoot(c.View, true)
c.App.SetFocus(c.MessagesPanel)
})
// If the client user has the `SEND_MESSAGES` permission, add "Reply" and "Mention Reply" actions.
if hasPermission(c.State, c.ChannelsTree.SelectedChannel.ID, discord.PermissionSendMessages) {
mal.AddItem("Reply", "", 'r', mal.replyAction)
mal.AddItem("Mention Reply", "", 'R', mal.mentionReplyAction)
}
// If the referenced message exists, add a new action to select the reply.
if m.ReferencedMessage != nil {
mal.AddItem("Select Reply", "", 'm', mal.selectReplyAction)
}
// If the content of the message contains link(s), add the appropriate actions to the list.
links := linkRegex.FindAllString(m.Content, -1)
if len(links) != 0 {
mal.AddItem("Open Link", "", 'l', func() {
for _, l := range links {
go open.Run(l)
}
c.App.SetRoot(c.View, true)
c.App.SetFocus(c.MessagesPanel)
})
}
// If the message contains attachments, add the appropriate actions to the actions list.
if len(m.Attachments) != 0 {
mal.AddItem("Open Attachment", "", 'o', mal.openAttachmentAction)
mal.AddItem("Download Attachment", "", 'd', mal.downloadAttachmentAction)
}
// If the client user has the `MANAGE_MESSAGES` permission, add a new action to delete the message.
if hasPermission(c.State, c.ChannelsTree.SelectedChannel.ID, discord.PermissionManageMessages) {
mal.AddItem("Delete", "", 'd', mal.deleteAction)
}
mal.AddItem("Copy Content", "", 'c', mal.copyContentAction)
mal.AddItem("Copy ID", "", 'i', mal.copyIDAction)
mal.AddItem("Copy Link", "", 'k', mal.copyLinkAction)
mal.SetTitle("Press the Escape key to close")
mal.SetTitleAlign(tview.AlignLeft)
mal.SetBorder(true)
mal.SetBorderPadding(0, 0, 1, 1)
return mal
}
func (mal *MessageActionsList) replyAction() {
mal.core.MessageInput.SetTitle("Replying to " + mal.message.Author.Tag())
mal.core.App.SetRoot(mal.core.View, true)
mal.core.App.SetFocus(mal.core.MessageInput)
}
func (mal *MessageActionsList) mentionReplyAction() {
mal.core.MessageInput.SetTitle("[@] Replying to " + mal.message.Author.Tag())
mal.core.App.SetRoot(mal.core.View, true)
mal.core.App.SetFocus(mal.core.MessageInput)
}
func (mal *MessageActionsList) selectReplyAction() {
ms, err := mal.core.State.Cabinet.Messages(mal.message.ChannelID)
if err != nil {
return
}
mal.core.MessagesPanel.SelectedMessage, _ = findMessageByID(ms, mal.message.ReferencedMessage.ID)
mal.core.MessagesPanel.
Highlight(mal.message.ReferencedMessage.ID.String()).
ScrollToHighlight()
mal.core.App.SetRoot(mal.core.View, true)
mal.core.App.SetFocus(mal.core.MessagesPanel)
}
func (mal *MessageActionsList) openAttachmentAction() {
for _, a := range mal.message.Attachments {
cacheDirPath, _ := os.UserCacheDir()
f, err := os.Create(filepath.Join(cacheDirPath, a.Filename))
if err != nil {
return
}
defer f.Close()
resp, err := http.Get(a.URL)
if err != nil {
return
}
d, err := io.ReadAll(resp.Body)
if err != nil {
return
}
f.Write(d)
go open.Run(f.Name())
}
mal.core.App.SetRoot(mal.core.View, true)
mal.core.App.SetFocus(mal.core.MessagesPanel)
}
func (mal *MessageActionsList) downloadAttachmentAction() {
for _, a := range mal.message.Attachments {
path, err := os.UserHomeDir()
if err != nil {
path = os.TempDir()
}
path = filepath.Join(path, "Downloads", a.Filename)
f, err := os.Create(path)
if err != nil {
return
}
defer f.Close()
resp, err := http.Get(a.URL)
if err != nil {
return
}
d, err := io.ReadAll(resp.Body)
if err != nil {
return
}
f.Write(d)
}
mal.core.App.SetRoot(mal.core.View, true)
mal.core.App.SetFocus(mal.core.MessagesPanel)
}
func (mal *MessageActionsList) deleteAction() {
mal.core.MessagesPanel.Clear()
err := mal.core.State.MessageRemove(mal.message.ChannelID, mal.message.ID)
if err != nil {
return
}
err = mal.core.State.DeleteMessage(mal.message.ChannelID, mal.message.ID, "Unknown")
if err != nil {
return
}
// The returned slice will be sorted from latest to oldest.
ms, err := mal.core.State.Cabinet.Messages(mal.message.ChannelID)
if err != nil {
return
}
for i := len(ms) - 1; i >= 0; i-- {
_, err = mal.core.MessagesPanel.Write(buildMessage(mal.core, ms[i]))
if err != nil {
return
}
}
mal.core.App.SetRoot(mal.core.View, true)
mal.core.App.SetFocus(mal.core.MessagesPanel)
}
func (mal *MessageActionsList) copyContentAction() {
err := clipboard.WriteAll(mal.message.Content)
if err != nil {
return
}
mal.core.App.SetRoot(mal.core.View, true)
mal.core.App.SetFocus(mal.core.MessagesPanel)
}
func (mal *MessageActionsList) copyIDAction() {
err := clipboard.WriteAll(mal.message.ID.String())
if err != nil {
return
}
mal.core.App.SetRoot(mal.core.View, true)
mal.core.App.SetFocus(mal.core.MessagesPanel)
}
func (mal *MessageActionsList) copyLinkAction() {
err := clipboard.WriteAll(mal.message.URL())
if err != nil {
return
}
mal.core.App.SetRoot(mal.core.View, true)
mal.core.App.SetFocus(mal.core.MessagesPanel)
}

View file

@ -1,144 +0,0 @@
package ui
import (
"io"
"os"
"os/exec"
"strings"
"github.com/atotto/clipboard"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/utils/json/option"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type MessageInput struct {
*tview.InputField
core *Core
}
func NewMessageInput(c *Core) *MessageInput {
mi := &MessageInput{
InputField: tview.NewInputField(),
core: c,
}
mi.SetFieldBackgroundColor(tview.Styles.PrimitiveBackgroundColor)
mi.SetPlaceholder("Message...")
mi.SetPlaceholderStyle(tcell.StyleDefault.Background(tview.Styles.PrimitiveBackgroundColor))
mi.SetInputCapture(mi.inputCapture)
mi.SetTitleAlign(tview.AlignLeft)
mi.SetBorder(true)
mi.SetBorderPadding(0, 0, 1, 1)
return mi
}
func (mi *MessageInput) inputCapture(event *tcell.EventKey) *tcell.EventKey {
switch event.Name() {
case "Enter":
return mi.sendMessage()
case mi.core.Config.Keys.MessageInput.OpenExternalEditor:
return mi.openExternalEditor()
case mi.core.Config.Keys.MessageInput.PasteClipboard:
return mi.pasteClipboard()
case "Esc":
mi.
SetText("").
SetTitle("")
mi.core.MessagesPanel.SelectedMessage = -1
mi.core.MessagesPanel.Highlight()
return nil
}
return event
}
func (mi *MessageInput) sendMessage() *tcell.EventKey {
if mi.core.ChannelsTree.SelectedChannel == nil {
return nil
}
t := strings.TrimSpace(mi.GetText())
if t == "" {
return nil
}
ms, err := mi.core.State.Messages(mi.core.ChannelsTree.SelectedChannel.ID, mi.core.Config.MessagesLimit)
if err != nil {
return nil
}
if len(mi.core.MessagesPanel.GetHighlights()) != 0 {
mID, err := discord.ParseSnowflake(mi.core.MessagesPanel.GetHighlights()[0])
if err != nil {
return nil
}
_, m := findMessageByID(ms, discord.MessageID(mID))
d := api.SendMessageData{
Content: t,
Reference: m.Reference,
AllowedMentions: &api.AllowedMentions{RepliedUser: option.False},
}
// If the title of the message InputField widget has "[@]" as a prefix, send the message as a reply and mention the replied user.
if strings.HasPrefix(mi.GetTitle(), "[@]") {
d.AllowedMentions.RepliedUser = option.True
}
go mi.core.State.SendMessageComplex(m.ChannelID, d)
mi.core.MessagesPanel.SelectedMessage = -1
mi.core.MessagesPanel.Highlight()
mi.SetTitle("")
} else {
go mi.core.State.SendMessage(mi.core.ChannelsTree.SelectedChannel.ID, t)
}
mi.SetText("")
return nil
}
func (mi *MessageInput) pasteClipboard() *tcell.EventKey {
text, _ := clipboard.ReadAll()
text = mi.GetText() + text
mi.SetText(text)
return nil
}
func (mi *MessageInput) openExternalEditor() *tcell.EventKey {
e := os.Getenv("EDITOR")
if e == "" {
return nil
}
f, err := os.CreateTemp(os.TempDir(), "discordo-*.md")
if err != nil {
return nil
}
defer os.Remove(f.Name())
cmd := exec.Command(e, f.Name())
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
mi.core.App.Suspend(func() {
err = cmd.Run()
if err != nil {
return
}
})
b, err := io.ReadAll(f)
if err != nil {
return nil
}
mi.SetText(string(b))
return nil
}

View file

@ -1,150 +0,0 @@
package ui
import (
"github.com/diamondburned/arikawa/v3/discord"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type MessagesPanel struct {
*tview.TextView
// The index of the currently selected message. A negative index indicates that there is no currently selected message.
SelectedMessage int
core *Core
}
func NewMessagesPanel(c *Core) *MessagesPanel {
mp := &MessagesPanel{
TextView: tview.NewTextView(),
SelectedMessage: -1,
core: c,
}
mp.SetDynamicColors(true)
mp.SetRegions(true)
mp.SetWordWrap(true)
mp.SetInputCapture(mp.onInputCapture)
mp.SetChangedFunc(func() {
mp.core.App.Draw()
})
mp.SetTitle("Messages")
mp.SetTitleAlign(tview.AlignLeft)
mp.SetBorder(true)
mp.SetBorderPadding(0, 0, 1, 1)
return mp
}
func (mp *MessagesPanel) onInputCapture(e *tcell.EventKey) *tcell.EventKey {
if mp.core.ChannelsTree.SelectedChannel == nil {
return nil
}
// Messages should return messages ordered from latest to earliest.
ms, err := mp.core.State.Cabinet.Messages(mp.core.ChannelsTree.SelectedChannel.ID)
if err != nil || len(ms) == 0 {
return nil
}
// Defaults
switch e.Name() {
case mp.core.Config.Keys.MessagesPanel.OpenActionsList:
return mp.openMessageActionsList(ms)
case mp.core.Config.Keys.MessagesPanel.SelectPreviousMessage:
return mp.selectPreviousMessage(ms)
case mp.core.Config.Keys.MessagesPanel.SelectNextMessage:
return mp.selectNextMessage(ms)
case mp.core.Config.Keys.MessagesPanel.SelectFirstMessage:
return mp.selectFirstMessage(ms)
case mp.core.Config.Keys.MessagesPanel.SelectLastMessage:
return mp.selectLastMessage(ms)
case "Esc":
mp.SelectedMessage = -1
mp.core.App.SetFocus(mp.core.View)
mp.
Clear().
Highlight().
SetTitle("")
return nil
}
return e
}
func (mp *MessagesPanel) selectPreviousMessage(ms []discord.Message) *tcell.EventKey {
// If there are no highlighted regions, select the latest (last) message in the messages panel.
if len(mp.GetHighlights()) == 0 {
mp.SelectedMessage = 0
} else {
// If the selected message is the oldest (first) message, select the latest (last) message in the messages panel.
if mp.SelectedMessage == len(ms)-1 {
mp.SelectedMessage = 0
} else {
mp.SelectedMessage++
}
}
mp.Highlight(ms[mp.SelectedMessage].ID.String())
mp.ScrollToHighlight()
return nil
}
func (mp *MessagesPanel) selectNextMessage(ms []discord.Message) *tcell.EventKey {
// If there are no highlighted regions, select the latest (last) message in the messages panel.
if len(mp.GetHighlights()) == 0 {
mp.SelectedMessage = 0
} else {
// If the selected message is the latest (last) message, select the oldest (first) message in the messages panel.
if mp.SelectedMessage == 0 {
mp.SelectedMessage = len(ms) - 1
} else {
mp.SelectedMessage--
}
}
mp.
Highlight(ms[mp.SelectedMessage].ID.String()).
ScrollToHighlight()
return nil
}
func (mp *MessagesPanel) selectFirstMessage(ms []discord.Message) *tcell.EventKey {
mp.SelectedMessage = len(ms) - 1
mp.
Highlight(ms[mp.SelectedMessage].ID.String()).
ScrollToHighlight()
return nil
}
func (mp *MessagesPanel) selectLastMessage(ms []discord.Message) *tcell.EventKey {
mp.SelectedMessage = 0
mp.
Highlight(ms[mp.SelectedMessage].ID.String()).
ScrollToHighlight()
return nil
}
func (mp *MessagesPanel) openMessageActionsList(ms []discord.Message) *tcell.EventKey {
hs := mp.GetHighlights()
if len(hs) == 0 {
return nil
}
mID, err := discord.ParseSnowflake(hs[0])
if err != nil {
return nil
}
_, m := findMessageByID(ms, discord.MessageID(mID))
if m == nil {
return nil
}
actionsList := NewMessageActionsList(mp.core, m)
mp.core.App.SetRoot(actionsList, true)
return nil
}

148
ui/messages_view.go Normal file
View file

@ -0,0 +1,148 @@
package ui
import (
"github.com/diamondburned/arikawa/v3/discord"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type MessagesView struct {
*tview.TextView
// The index of the currently selected message. A negative index indicates that there is no currently selected message.
selectedMessage int
core *Core
}
func newMessagesView(c *Core) *MessagesView {
v := &MessagesView{
TextView: tview.NewTextView(),
selectedMessage: -1,
core: c,
}
v.SetDynamicColors(true)
v.SetRegions(true)
v.SetWordWrap(true)
v.SetInputCapture(v.onInputCapture)
v.SetChangedFunc(func() {
v.core.App.Draw()
})
v.SetTitle("Messages")
v.SetTitleAlign(tview.AlignLeft)
v.SetBorder(true)
v.SetBorderPadding(0, 0, 1, 1)
return v
}
func (v *MessagesView) onInputCapture(e *tcell.EventKey) *tcell.EventKey {
if v.core.ChannelsView.selectedChannel == nil {
return nil
}
// Messages should return messages ordered from latest to earliest.
ms, err := v.core.State.Cabinet.Messages(v.core.ChannelsView.selectedChannel.ID)
if err != nil || len(ms) == 0 {
return nil
}
// Defaults
switch e.Name() {
case v.core.Config.Keys.MessagesView.OpenActionsView:
return v.openActionsView(ms)
case v.core.Config.Keys.MessagesView.SelectPreviousMessage:
return v.selectPreviousMessage(ms)
case v.core.Config.Keys.MessagesView.SelectNextMessage:
return v.selectNextMessage(ms)
case v.core.Config.Keys.MessagesView.SelectFirstMessage:
return v.selectFirstMessage(ms)
case v.core.Config.Keys.MessagesView.SelectLastMessage:
return v.selectLastMessage(ms)
case "Esc":
v.selectedMessage = -1
v.core.App.SetFocus(v.core.View)
v.
Clear().
Highlight().
SetTitle("")
return nil
}
return e
}
func (v *MessagesView) selectPreviousMessage(ms []discord.Message) *tcell.EventKey {
// If there are no highlighted regions, select the latest (last) message.
if len(v.GetHighlights()) == 0 {
v.selectedMessage = 0
} else {
// If the selected message is the oldest (first) message, select the latest (last) message.
if v.selectedMessage == len(ms)-1 {
v.selectedMessage = 0
} else {
v.selectedMessage++
}
}
v.Highlight(ms[v.selectedMessage].ID.String())
v.ScrollToHighlight()
return nil
}
func (v *MessagesView) selectNextMessage(ms []discord.Message) *tcell.EventKey {
// If there are no highlighted regions, select the latest (last) message.
if len(v.GetHighlights()) == 0 {
v.selectedMessage = 0
} else {
// If the selected message is the latest (last) message, select the oldest (first) message.
if v.selectedMessage == 0 {
v.selectedMessage = len(ms) - 1
} else {
v.selectedMessage--
}
}
v.
Highlight(ms[v.selectedMessage].ID.String()).
ScrollToHighlight()
return nil
}
func (v *MessagesView) selectFirstMessage(ms []discord.Message) *tcell.EventKey {
v.selectedMessage = len(ms) - 1
v.
Highlight(ms[v.selectedMessage].ID.String()).
ScrollToHighlight()
return nil
}
func (v *MessagesView) selectLastMessage(ms []discord.Message) *tcell.EventKey {
v.selectedMessage = 0
v.
Highlight(ms[v.selectedMessage].ID.String()).
ScrollToHighlight()
return nil
}
func (v *MessagesView) openActionsView(ms []discord.Message) *tcell.EventKey {
hs := v.GetHighlights()
if len(hs) == 0 {
return nil
}
mID, err := discord.ParseSnowflake(hs[0])
if err != nil {
return nil
}
_, m := findMessageByID(ms, discord.MessageID(mID))
if m == nil {
return nil
}
actionsView := newActionsView(v.core, m)
v.core.App.SetRoot(actionsView, true)
return nil
}