From 5d19c768fec9ad404b695a448307543a5d9df23b Mon Sep 17 00:00:00 2001 From: rigormorrtiss Date: Tue, 31 Aug 2021 15:18:02 +0400 Subject: [PATCH] feat: implement selectable messages (#38) * feat: implement selectable messages * style: shorten long lines * feat: handle key down event for message select functionality * feat: add handlers for top and bottom navigation * style: shorten long lines * docs: add 's' to singular verb * docs: add missing period * fix: if selectedChannel is not initialized, do not handle key event * fix: prepend message to .Messages slice * fix: if selectedChannel is nil, do not handle events * feat: better default keybindings * fix: use != comparison operator --- README.md | 15 ++++-- discordo.go | 140 +++++++++++++++++++++++++++++++++++++++++------- ui/textviews.go | 7 ++- 3 files changed, 140 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index b0eaba5..71a50b2 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,18 @@ By default, Discordo utilizes OS-specific keyring to store credentials such as c ### Default Keybindings -- `Alt` + `g`: Sets the focus on the guilds TreeView. -- `Alt` + `m`: Sets the focus on the messages TextView. -- `Alt` + `i`: Sets the focus on the message InputField. +Global: + +- `Alt` + `1`: Sets the focus on the guilds TreeView. +- `Alt` + `2`: Sets the focus on the messages TextView. +- `Alt` + `3`: Sets the focus on the message InputField. + +TextView: + +- `k` or `Up`: Selects the message just before the currently selected message. +- `j` or `Down`: Selects the message just after the currently selected message. +- `g` or `Home`: Selects the first message rendered in the TextView. +- `G` or `End`: Selects the last message rendered in the TextView. ### Clipboard diff --git a/discordo.go b/discordo.go index 775f71b..947ef51 100644 --- a/discordo.go +++ b/discordo.go @@ -37,7 +37,10 @@ func main() { EnableMouse(config.Mouse). SetInputCapture(onAppInputCapture) guildsTreeView = ui.NewGuildsTreeView(onGuildsTreeViewSelected) - messagesTextView = ui.NewMessagesTextView(app) + messagesTextView = ui.NewMessagesTextView( + app, + onMessagesTextViewInputCapture, + ) messageInputField = ui.NewMessageInputField(onMessageInputFieldInputCapture) mainFlex = ui.NewMainFlex( guildsTreeView, @@ -73,29 +76,123 @@ func main() { func onAppInputCapture(e *tcell.EventKey) *tcell.EventKey { switch e.Name() { - case "Alt+Rune[g]": + case "Alt+Rune[1]": app.SetFocus(guildsTreeView) - case "Alt+Rune[m]": + case "Alt+Rune[2]": app.SetFocus(messagesTextView) - case "Alt+Rune[i]": + case "Alt+Rune[3]": app.SetFocus(messageInputField) } return e } +func onMessagesTextViewInputCapture(e *tcell.EventKey) *tcell.EventKey { + if selectedChannel == nil { + return nil + } + + switch { + case e.Key() == tcell.KeyUp || e.Rune() == 'k': // Up + ms := selectedChannel.Messages + hs := messagesTextView.GetHighlights() + // Initially, no message is highlighted/selected; highlight the last + // message in the TextView. + if len(hs) == 0 { + messagesTextView. + Highlight(selectedChannel.LastMessageID). + ScrollToHighlight() + } else { + // Find the index of the highlighted message in the + // *discordgo.Channel.Messages slice. + var idx int + for i, v := range ms { + if hs[0] == v.ID { + idx = i + } + } + // If the length of the *discordgo.Channel.Messages slice is + // equal to the index of the message just after highlighted + // message in the slice (this is the first-rendered message in + // the TextView), do not handle the event. + if len(ms) == idx+1 { + return nil + } + // Highlight the message just before the currently highlighted + // message. + messagesTextView. + Highlight(ms[idx+1].ID). + ScrollToHighlight() + } + + return nil + case e.Key() == tcell.KeyDown || e.Rune() == 'j': // Down + ms := selectedChannel.Messages + hs := messagesTextView.GetHighlights() + // Initially, no message is highlighted/selected; highlight the last + // message in the TextView. + if len(hs) == 0 { + messagesTextView. + Highlight(ms[0].ID). + ScrollToHighlight() + } else { + // Find the index of the highlighted message in the + // *discordgo.Channel.Messages slice. + var idx int + for i, v := range selectedChannel.Messages { + if v.Type == discordgo.MessageTypeDefault || v.Type == discordgo.MessageTypeReply { + if hs[0] == v.ID { + idx = i + } + } + } + // If the index of the highlighted message in the slice is equal + // to zero (this is the last-rendered message in the TextView), + // do not handle the event. + if idx == 0 { + return nil + } + // Highlight the message just after the currently highlighted + // message. + messagesTextView. + Highlight(ms[idx-1].ID). + ScrollToHighlight() + } + + return nil + case e.Key() == tcell.KeyHome || e.Rune() == 'g': // Top + ms := selectedChannel.Messages + // Highlight the last message in the selectedChannel.Messages slice + // (the first message rendered in the TextView). + messagesTextView. + Highlight(ms[len(ms)-1].ID). + ScrollToHighlight() + case e.Key() == tcell.KeyEnd || e.Rune() == 'G': // Bottom + ms := selectedChannel.Messages + // Highlight the first message in the selectedChannel.Messages slice + // (the last message rendered in the TextView). + messagesTextView. + Highlight(ms[0].ID). + ScrollToHighlight() + } + + return e +} + func onMessageInputFieldInputCapture(e *tcell.EventKey) *tcell.EventKey { switch e.Key() { case tcell.KeyEnter: - if selectedChannel != nil { - t := strings.TrimSpace(messageInputField.GetText()) - if t == "" { - return nil - } - - messageInputField.SetText("") - go session.ChannelMessageSend(selectedChannel.ID, t) + if selectedChannel == nil { + return nil } + + t := strings.TrimSpace(messageInputField.GetText()) + if t == "" { + return nil + } + + messageInputField.SetText("") + go session.ChannelMessageSend(selectedChannel.ID, t) case tcell.KeyCtrlV: text, _ := clipboard.ReadAll() text = messageInputField.GetText() + text @@ -157,13 +254,19 @@ func onSessionReady(_ *discordgo.Session, r *discordgo.Ready) { } func onSessionMessageCreate(_ *discordgo.Session, m *discordgo.MessageCreate) { - if selectedChannel != nil && selectedChannel.ID == m.ChannelID { - util.WriteMessage( - messagesTextView, - m.Message, - session.State.Ready.User.ID, - ) + if selectedChannel == nil || selectedChannel.ID != m.ChannelID { + return } + + selectedChannel.Messages = append( + []*discordgo.Message{m.Message}, + selectedChannel.Messages...) + + util.WriteMessage( + messagesTextView, + m.Message, + session.State.Ready.User.ID, + ) } func onGuildsTreeViewSelected(n *tview.TreeNode) { @@ -219,6 +322,7 @@ func onGuildsTreeViewSelected(n *tview.TreeNode) { func writeMessages(cID string) { msgs, _ := session.ChannelMessages(cID, config.GetMessagesLimit, "", "", "") + selectedChannel.Messages = msgs for i := len(msgs) - 1; i >= 0; i-- { util.WriteMessage( messagesTextView, diff --git a/ui/textviews.go b/ui/textviews.go index 8847a1a..c5fecb3 100644 --- a/ui/textviews.go +++ b/ui/textviews.go @@ -1,11 +1,15 @@ package ui import ( + "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) // NewMessagesTextView creates and returns a new messages textview. -func NewMessagesTextView(app *tview.Application) *tview.TextView { +func NewMessagesTextView( + app *tview.Application, + onMessagesTextViewInputCapture func(*tcell.EventKey) *tcell.EventKey, +) *tview.TextView { v := tview.NewTextView() v. SetRegions(true). @@ -15,6 +19,7 @@ func NewMessagesTextView(app *tview.Application) *tview.TextView { SetChangedFunc(func() { app.Draw() }). + SetInputCapture(onMessagesTextViewInputCapture). SetBorder(true). SetBorderPadding(0, 0, 1, 0). SetTitleAlign(tview.AlignLeft)