Rename inconsistent widget names (#224)
This commit is contained in:
parent
4b61bbf61d
commit
0bbe6bf93a
12 changed files with 796 additions and 799 deletions
|
|
@ -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
229
ui/actions_view.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -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
161
ui/channels_view.go
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
ui/core.go
90
ui/core.go
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
61
ui/guilds_view.go
Normal 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
144
ui/input_view.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
148
ui/messages_view.go
Normal 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
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue