ui.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. package main
  2. import (
  3. "sort"
  4. "strings"
  5. "github.com/atotto/clipboard"
  6. "github.com/ayntgl/discordgo"
  7. "github.com/ayntgl/discordo/config"
  8. "github.com/ayntgl/discordo/util"
  9. "github.com/gdamore/tcell/v2"
  10. "github.com/rivo/tview"
  11. )
  12. func onAppInputCapture(e *tcell.EventKey) *tcell.EventKey {
  13. if util.HasKeybinding(config.Keybindings.FocusChannelsTreeView, e.Name()) {
  14. app.SetFocus(channelsTreeView)
  15. return nil
  16. } else if util.HasKeybinding(config.Keybindings.FocusMessagesView, e.Name()) {
  17. app.SetFocus(messagesTextView)
  18. return nil
  19. } else if util.HasKeybinding(config.Keybindings.FocusMessageInputField, e.Name()) {
  20. app.SetFocus(messageInputField)
  21. return nil
  22. }
  23. return e
  24. }
  25. func onChannelsTreeSelected(n *tview.TreeNode) {
  26. selectedChannel = nil
  27. selectedMessage = 0
  28. messagesTextView.
  29. Clear().
  30. SetTitle("")
  31. messageInputField.SetText("")
  32. // Unhighlight the already-highlighted regions.
  33. messagesTextView.Highlight()
  34. id := n.GetReference()
  35. switch n.GetLevel() {
  36. case 1: // Guilds or Direct Messages
  37. if len(n.GetChildren()) == 0 {
  38. // If the reference of the selected `*TreeNode` is `nil`, it is the direct messages `*TreeNode`.
  39. if id == nil {
  40. cs := app.Session.State.PrivateChannels
  41. sort.Slice(cs, func(i, j int) bool {
  42. return cs[i].LastMessageID > cs[j].LastMessageID
  43. })
  44. for _, c := range cs {
  45. tag := "[::d]"
  46. if util.ChannelIsUnread(app.Session.State, c) {
  47. tag = "[::b]"
  48. }
  49. cn := tview.NewTreeNode(tag + util.ChannelToString(c) + "[::-]").
  50. SetReference(c.ID).
  51. Collapse()
  52. n.AddChild(cn)
  53. }
  54. } else {
  55. g, err := app.Session.State.Guild(id.(string))
  56. if err != nil {
  57. return
  58. }
  59. sort.Slice(g.Channels, func(i, j int) bool {
  60. return g.Channels[i].Position < g.Channels[j].Position
  61. })
  62. // Top-level channels
  63. createTopLevelChannelsNodes(channelsTreeView, app.Session.State, n, g.Channels)
  64. // Category channels
  65. createCategoryChannelsNodes(channelsTreeView, app.Session.State, n, g.Channels)
  66. // Second-level channels
  67. createSecondLevelChannelsNodes(channelsTreeView, app.Session.State, g.Channels)
  68. }
  69. }
  70. n.SetExpanded(!n.IsExpanded())
  71. default: // Channels
  72. c, err := app.Session.State.Channel(id.(string))
  73. if err != nil {
  74. return
  75. }
  76. selectedChannel = c
  77. app.SetFocus(messageInputField)
  78. switch c.Type {
  79. case discordgo.ChannelTypeGuildText, discordgo.ChannelTypeGuildNews:
  80. title := util.ChannelToString(c)
  81. if c.Topic != "" {
  82. title += " - " + c.Topic
  83. }
  84. messagesTextView.SetTitle(title)
  85. case discordgo.ChannelTypeDM, discordgo.ChannelTypeGroupDM:
  86. messagesTextView.SetTitle(util.ChannelToString(c))
  87. }
  88. if strings.HasPrefix(n.GetText(), "[::b]") {
  89. n.SetText("[::d]" + util.ChannelToString(c) + "[::-]")
  90. }
  91. go func() {
  92. ms, err := app.Session.ChannelMessages(c.ID, config.General.FetchMessagesLimit, "", "", "")
  93. if err != nil {
  94. return
  95. }
  96. for i := len(ms) - 1; i >= 0; i-- {
  97. selectedChannel.Messages = append(selectedChannel.Messages, ms[i])
  98. messagesTextView.Write(buildMessage(ms[i]))
  99. }
  100. // Scroll to the end of the text after the messages have been written to the TextView.
  101. messagesTextView.ScrollToEnd()
  102. if len(ms) != 0 && util.ChannelIsUnread(app.Session.State, c) {
  103. app.Session.ChannelMessageAck(c.ID, c.LastMessageID, "")
  104. }
  105. }()
  106. }
  107. }
  108. // createTopLevelChannelsNodes builds and creates `*tview.TreeNode`s for top-level (channels that have an empty parent ID and of type GUILD_TEXT, GUILD_NEWS) channels. If the client user does not have the VIEW_CHANNEL permission for a channel, the channel is excluded from the parent.
  109. func createTopLevelChannelsNodes(treeView *tview.TreeView, s *discordgo.State, n *tview.TreeNode, cs []*discordgo.Channel) {
  110. for _, c := range cs {
  111. if (c.Type == discordgo.ChannelTypeGuildText || c.Type == discordgo.ChannelTypeGuildNews) &&
  112. (c.ParentID == "") {
  113. if !util.HasPermission(s, c.ID, discordgo.PermissionViewChannel) {
  114. continue
  115. }
  116. n.AddChild(util.CreateChannelNode(s, c))
  117. continue
  118. }
  119. }
  120. }
  121. // createCategoryChannelsNodes builds and creates `*tview.TreeNode`s for category (type: GUILD_CATEGORY) channels. If the client user does not have the VIEW_CHANNEL permission for a channel, the channel is excluded from the parent.
  122. func createCategoryChannelsNodes(treeView *tview.TreeView, s *discordgo.State, n *tview.TreeNode, cs []*discordgo.Channel) {
  123. CategoryLoop:
  124. for _, c := range cs {
  125. if c.Type == discordgo.ChannelTypeGuildCategory {
  126. if !util.HasPermission(s, c.ID, discordgo.PermissionViewChannel) {
  127. continue
  128. }
  129. for _, child := range cs {
  130. if child.ParentID == c.ID {
  131. n.AddChild(util.CreateChannelNode(s, c))
  132. continue CategoryLoop
  133. }
  134. }
  135. n.AddChild(util.CreateChannelNode(s, c))
  136. }
  137. }
  138. }
  139. // createSecondLevelChannelsNodes builds and creates `*tview.TreeNode`s for second-level (channels that have a non-empty parent ID and of type GUILD_TEXT, GUILD_NEWS) channels. If the client user does not have the VIEW_CHANNEL permission for a channel, the channel is excluded from the parent.
  140. func createSecondLevelChannelsNodes(treeView *tview.TreeView, s *discordgo.State, cs []*discordgo.Channel) {
  141. for _, c := range cs {
  142. if (c.Type == discordgo.ChannelTypeGuildText || c.Type == discordgo.ChannelTypeGuildNews) &&
  143. (c.ParentID != "") {
  144. if !util.HasPermission(s, c.ID, discordgo.PermissionViewChannel) {
  145. continue
  146. }
  147. pn := util.GetNodeByReference(treeView, c.ParentID)
  148. if pn != nil {
  149. pn.AddChild(util.CreateChannelNode(s, c))
  150. }
  151. }
  152. }
  153. }
  154. func onMessagesViewInputCapture(e *tcell.EventKey) *tcell.EventKey {
  155. if selectedChannel == nil {
  156. return nil
  157. }
  158. ms := selectedChannel.Messages
  159. if len(ms) == 0 {
  160. return nil
  161. }
  162. if util.HasKeybinding(config.Keybindings.SelectPreviousMessage, e.Name()) {
  163. if len(messagesTextView.GetHighlights()) == 0 {
  164. selectedMessage = len(ms) - 1
  165. } else {
  166. selectedMessage--
  167. if selectedMessage < 0 {
  168. selectedMessage = 0
  169. }
  170. }
  171. messagesTextView.
  172. Highlight(ms[selectedMessage].ID).
  173. ScrollToHighlight()
  174. return nil
  175. } else if util.HasKeybinding(config.Keybindings.SelectNextMessage, e.Name()) {
  176. if len(messagesTextView.GetHighlights()) == 0 {
  177. selectedMessage = len(ms) - 1
  178. } else {
  179. selectedMessage++
  180. if selectedMessage >= len(ms) {
  181. selectedMessage = len(ms) - 1
  182. }
  183. }
  184. messagesTextView.
  185. Highlight(ms[selectedMessage].ID).
  186. ScrollToHighlight()
  187. return nil
  188. } else if util.HasKeybinding(config.Keybindings.SelectFirstMessage, e.Name()) {
  189. selectedMessage = 0
  190. messagesTextView.
  191. Highlight(ms[selectedMessage].ID).
  192. ScrollToHighlight()
  193. return nil
  194. } else if util.HasKeybinding(config.Keybindings.SelectLastMessage, e.Name()) {
  195. selectedMessage = len(ms) - 1
  196. messagesTextView.
  197. Highlight(ms[selectedMessage].ID).
  198. ScrollToHighlight()
  199. return nil
  200. } else if util.HasKeybinding(config.Keybindings.SelectMessageReference, e.Name()) {
  201. hs := messagesTextView.GetHighlights()
  202. if len(hs) == 0 {
  203. return nil
  204. }
  205. _, m := util.FindMessageByID(selectedChannel.Messages, hs[0])
  206. if m.ReferencedMessage != nil {
  207. selectedMessage, _ = util.FindMessageByID(selectedChannel.Messages, m.ReferencedMessage.ID)
  208. messagesTextView.
  209. Highlight(m.ReferencedMessage.ID).
  210. ScrollToHighlight()
  211. }
  212. return nil
  213. } else if util.HasKeybinding(config.Keybindings.ReplySelectedMessage, e.Name()) {
  214. hs := messagesTextView.GetHighlights()
  215. if len(hs) == 0 {
  216. return nil
  217. }
  218. _, m := util.FindMessageByID(selectedChannel.Messages, hs[0])
  219. messageInputField.SetTitle("Replying to " + m.Author.String())
  220. app.SetFocus(messageInputField)
  221. return nil
  222. } else if util.HasKeybinding(config.Keybindings.MentionReplySelectedMessage, e.Name()) {
  223. hs := messagesTextView.GetHighlights()
  224. if len(hs) == 0 {
  225. return nil
  226. }
  227. _, m := util.FindMessageByID(selectedChannel.Messages, hs[0])
  228. messageInputField.SetTitle("[@] Replying to " + m.Author.String())
  229. app.SetFocus(messageInputField)
  230. return nil
  231. } else if util.HasKeybinding(config.Keybindings.CopySelectedMessage, e.Name()) {
  232. hs := messagesTextView.GetHighlights()
  233. if len(hs) == 0 {
  234. return nil
  235. }
  236. _, m := util.FindMessageByID(selectedChannel.Messages, hs[0])
  237. err := clipboard.WriteAll(m.Content)
  238. if err != nil {
  239. return nil
  240. }
  241. return nil
  242. }
  243. return e
  244. }
  245. func onMessageInputFieldInputCapture(e *tcell.EventKey) *tcell.EventKey {
  246. switch e.Key() {
  247. case tcell.KeyEnter:
  248. if selectedChannel == nil {
  249. return nil
  250. }
  251. t := strings.TrimSpace(messageInputField.GetText())
  252. if t == "" {
  253. return nil
  254. }
  255. if len(messagesTextView.GetHighlights()) != 0 {
  256. _, m := util.FindMessageByID(selectedChannel.Messages, messagesTextView.GetHighlights()[0])
  257. d := &discordgo.MessageSend{
  258. Content: t,
  259. Reference: m.Reference(),
  260. AllowedMentions: &discordgo.MessageAllowedMentions{RepliedUser: false},
  261. }
  262. if strings.HasPrefix(messageInputField.GetTitle(), "[@]") {
  263. d.AllowedMentions.RepliedUser = true
  264. } else {
  265. d.AllowedMentions.RepliedUser = false
  266. }
  267. go app.Session.ChannelMessageSendComplex(m.ChannelID, d)
  268. selectedMessage = -1
  269. messagesTextView.Highlight()
  270. messageInputField.SetTitle("")
  271. } else {
  272. go app.Session.ChannelMessageSend(selectedChannel.ID, t)
  273. }
  274. messageInputField.SetText("")
  275. return nil
  276. case tcell.KeyCtrlV:
  277. text, _ := clipboard.ReadAll()
  278. text = messageInputField.GetText() + text
  279. messageInputField.SetText(text)
  280. return nil
  281. case tcell.KeyEscape:
  282. messageInputField.SetText("")
  283. messageInputField.SetTitle("")
  284. selectedMessage = -1
  285. messagesTextView.Highlight()
  286. return nil
  287. }
  288. return e
  289. }