Jelajahi Sumber

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
rigormorrtiss 4 tahun lalu
induk
melakukan
5d19c768fe
3 mengubah file dengan 139 tambahan dan 21 penghapusan
  1. 12 3
      README.md
  2. 121 17
      discordo.go
  3. 6 1
      ui/textviews.go

+ 12 - 3
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
 

+ 121 - 17
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
-			}
+		if selectedChannel == nil {
+			return nil
+		}
 
-			messageInputField.SetText("")
-			go session.ChannelMessageSend(selectedChannel.ID, t)
+		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,

+ 6 - 1
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)