ui.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. package main
  2. import (
  3. "sort"
  4. "strings"
  5. "github.com/atotto/clipboard"
  6. "github.com/ayntgl/discordgo"
  7. "github.com/gdamore/tcell/v2"
  8. "github.com/rivo/tview"
  9. )
  10. var (
  11. selectedChannel *discordgo.Channel
  12. selectedMessage int = -1
  13. )
  14. func onAppInputCapture(e *tcell.EventKey) *tcell.EventKey {
  15. if hasKeybinding(conf.Keybindings.FocusChannelsTree, e.Name()) {
  16. app.SetFocus(channelsTree)
  17. return nil
  18. } else if hasKeybinding(conf.Keybindings.FocusMessagesView, e.Name()) {
  19. app.SetFocus(messagesView)
  20. return nil
  21. } else if hasKeybinding(conf.Keybindings.FocusMessageInputField, e.Name()) {
  22. app.SetFocus(messageInputField)
  23. return nil
  24. }
  25. return e
  26. }
  27. func onChannelsTreeSelected(n *tview.TreeNode) {
  28. selectedChannel = nil
  29. selectedMessage = 0
  30. messagesView.
  31. Clear().
  32. SetTitle("")
  33. messageInputField.SetText("")
  34. // Unhighlight the already-highlighted regions.
  35. messagesView.Highlight()
  36. id := n.GetReference()
  37. switch n.GetLevel() {
  38. case 1: // Guilds or Direct Messages
  39. if len(n.GetChildren()) == 0 {
  40. // If the reference of the selected `*TreeNode` is `nil`, it is the direct messages `*TreeNode`.
  41. if id == nil {
  42. cs := session.State.PrivateChannels
  43. sort.Slice(cs, func(i, j int) bool {
  44. return cs[i].LastMessageID > cs[j].LastMessageID
  45. })
  46. for _, c := range cs {
  47. tag := "[::d]"
  48. if channelIsUnread(session.State, c) {
  49. tag = "[::b]"
  50. }
  51. cn := tview.NewTreeNode(tag + channelToString(c) + "[::-]").
  52. SetReference(c.ID).
  53. Collapse()
  54. n.AddChild(cn)
  55. }
  56. } else {
  57. g, err := session.State.Guild(id.(string))
  58. if err != nil {
  59. return
  60. }
  61. sort.Slice(g.Channels, func(i, j int) bool {
  62. return g.Channels[i].Position < g.Channels[j].Position
  63. })
  64. // Top-level channels
  65. createTopLevelChannelsNodes(channelsTree, session.State, n, g.Channels)
  66. // Category channels
  67. createCategoryChannelsNodes(channelsTree, session.State, n, g.Channels)
  68. // Second-level channels
  69. createSecondLevelChannelsNodes(channelsTree, session.State, g.Channels)
  70. }
  71. }
  72. n.SetExpanded(!n.IsExpanded())
  73. default: // Channels
  74. c, err := session.State.Channel(id.(string))
  75. if err != nil {
  76. return
  77. }
  78. selectedChannel = c
  79. app.SetFocus(messageInputField)
  80. switch c.Type {
  81. case discordgo.ChannelTypeGuildText, discordgo.ChannelTypeGuildNews:
  82. title := channelToString(c)
  83. if c.Topic != "" {
  84. title += " - " + c.Topic
  85. }
  86. messagesView.SetTitle(title)
  87. case discordgo.ChannelTypeDM, discordgo.ChannelTypeGroupDM:
  88. messagesView.SetTitle(channelToString(c))
  89. }
  90. if strings.HasPrefix(n.GetText(), "[::b]") {
  91. n.SetText("[::d]" + channelToString(c) + "[::-]")
  92. }
  93. go func() {
  94. ms, err := session.ChannelMessages(c.ID, conf.GetMessagesLimit, "", "", "")
  95. if err != nil {
  96. return
  97. }
  98. for i := len(ms) - 1; i >= 0; i-- {
  99. selectedChannel.Messages = append(selectedChannel.Messages, ms[i])
  100. messagesView.Write(buildMessage(ms[i]))
  101. }
  102. // Scroll to the end of the text after the messages have been written to the TextView.
  103. messagesView.ScrollToEnd()
  104. if len(ms) != 0 && channelIsUnread(session.State, c) {
  105. session.ChannelMessageAck(c.ID, c.LastMessageID, "")
  106. }
  107. }()
  108. }
  109. }
  110. // 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.
  111. func createTopLevelChannelsNodes(treeView *tview.TreeView, s *discordgo.State, n *tview.TreeNode, cs []*discordgo.Channel) {
  112. for _, c := range cs {
  113. if (c.Type == discordgo.ChannelTypeGuildText || c.Type == discordgo.ChannelTypeGuildNews) &&
  114. (c.ParentID == "") {
  115. if !hasPermission(s, c.ID, discordgo.PermissionViewChannel) {
  116. continue
  117. }
  118. n.AddChild(createChannelNode(s, c))
  119. continue
  120. }
  121. }
  122. }
  123. // 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.
  124. func createCategoryChannelsNodes(treeView *tview.TreeView, s *discordgo.State, n *tview.TreeNode, cs []*discordgo.Channel) {
  125. CategoryLoop:
  126. for _, c := range cs {
  127. if c.Type == discordgo.ChannelTypeGuildCategory {
  128. if !hasPermission(s, c.ID, discordgo.PermissionViewChannel) {
  129. continue
  130. }
  131. for _, child := range cs {
  132. if child.ParentID == c.ID {
  133. n.AddChild(createChannelNode(s, c))
  134. continue CategoryLoop
  135. }
  136. }
  137. n.AddChild(createChannelNode(s, c))
  138. }
  139. }
  140. }
  141. // 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.
  142. func createSecondLevelChannelsNodes(treeView *tview.TreeView, s *discordgo.State, cs []*discordgo.Channel) {
  143. for _, c := range cs {
  144. if (c.Type == discordgo.ChannelTypeGuildText || c.Type == discordgo.ChannelTypeGuildNews) &&
  145. (c.ParentID != "") {
  146. if !hasPermission(s, c.ID, discordgo.PermissionViewChannel) {
  147. continue
  148. }
  149. pn := getTreeNodeByReference(treeView, c.ParentID)
  150. if pn != nil {
  151. pn.AddChild(createChannelNode(s, c))
  152. }
  153. }
  154. }
  155. }
  156. func onMessagesViewInputCapture(e *tcell.EventKey) *tcell.EventKey {
  157. if selectedChannel == nil {
  158. return nil
  159. }
  160. ms := selectedChannel.Messages
  161. if len(ms) == 0 {
  162. return nil
  163. }
  164. if hasKeybinding(conf.Keybindings.SelectPreviousMessage, e.Name()) {
  165. if len(messagesView.GetHighlights()) == 0 {
  166. selectedMessage = len(ms) - 1
  167. } else {
  168. selectedMessage--
  169. if selectedMessage < 0 {
  170. selectedMessage = 0
  171. }
  172. }
  173. messagesView.
  174. Highlight(ms[selectedMessage].ID).
  175. ScrollToHighlight()
  176. return nil
  177. } else if hasKeybinding(conf.Keybindings.SelectNextMessage, e.Name()) {
  178. if len(messagesView.GetHighlights()) == 0 {
  179. selectedMessage = len(ms) - 1
  180. } else {
  181. selectedMessage++
  182. if selectedMessage >= len(ms) {
  183. selectedMessage = len(ms) - 1
  184. }
  185. }
  186. messagesView.
  187. Highlight(ms[selectedMessage].ID).
  188. ScrollToHighlight()
  189. return nil
  190. } else if hasKeybinding(conf.Keybindings.SelectFirstMessage, e.Name()) {
  191. selectedMessage = 0
  192. messagesView.
  193. Highlight(ms[selectedMessage].ID).
  194. ScrollToHighlight()
  195. return nil
  196. } else if hasKeybinding(conf.Keybindings.SelectLastMessage, e.Name()) {
  197. selectedMessage = len(ms) - 1
  198. messagesView.
  199. Highlight(ms[selectedMessage].ID).
  200. ScrollToHighlight()
  201. return nil
  202. } else if hasKeybinding(conf.Keybindings.SelectMessageReference, e.Name()) {
  203. hs := messagesView.GetHighlights()
  204. if len(hs) == 0 {
  205. return nil
  206. }
  207. _, m := findMessageByID(selectedChannel.Messages, hs[0])
  208. if m.ReferencedMessage != nil {
  209. selectedMessage, _ = findMessageByID(selectedChannel.Messages, m.ReferencedMessage.ID)
  210. messagesView.
  211. Highlight(m.ReferencedMessage.ID).
  212. ScrollToHighlight()
  213. }
  214. return nil
  215. } else if hasKeybinding(conf.Keybindings.ReplySelectedMessage, e.Name()) {
  216. hs := messagesView.GetHighlights()
  217. if len(hs) == 0 {
  218. return nil
  219. }
  220. _, m := findMessageByID(selectedChannel.Messages, hs[0])
  221. messageInputField.SetTitle("Replying to " + m.Author.String())
  222. app.SetFocus(messageInputField)
  223. return nil
  224. } else if hasKeybinding(conf.Keybindings.MentionReplySelectedMessage, e.Name()) {
  225. hs := messagesView.GetHighlights()
  226. if len(hs) == 0 {
  227. return nil
  228. }
  229. _, m := findMessageByID(selectedChannel.Messages, hs[0])
  230. messageInputField.SetTitle("[@] Replying to " + m.Author.String())
  231. app.SetFocus(messageInputField)
  232. return nil
  233. } else if hasKeybinding(conf.Keybindings.CopySelectedMessage, e.Name()) {
  234. hs := messagesView.GetHighlights()
  235. if len(hs) == 0 {
  236. return nil
  237. }
  238. _, m := findMessageByID(selectedChannel.Messages, hs[0])
  239. err := clipboard.WriteAll(m.Content)
  240. if err != nil {
  241. return nil
  242. }
  243. return nil
  244. }
  245. return e
  246. }
  247. func onMessageInputFieldInputCapture(e *tcell.EventKey) *tcell.EventKey {
  248. switch e.Key() {
  249. case tcell.KeyEnter:
  250. if selectedChannel == nil {
  251. return nil
  252. }
  253. t := strings.TrimSpace(messageInputField.GetText())
  254. if t == "" {
  255. return nil
  256. }
  257. if len(messagesView.GetHighlights()) != 0 {
  258. m := selectedChannel.Messages[selectedMessage]
  259. d := &discordgo.MessageSend{
  260. Content: t,
  261. Reference: m.Reference(),
  262. AllowedMentions: &discordgo.MessageAllowedMentions{RepliedUser: false},
  263. }
  264. if strings.HasPrefix(messageInputField.GetTitle(), "[@]") {
  265. d.AllowedMentions.RepliedUser = true
  266. } else {
  267. d.AllowedMentions.RepliedUser = false
  268. }
  269. go session.ChannelMessageSendComplex(m.ChannelID, d)
  270. selectedMessage = -1
  271. messagesView.Highlight()
  272. messageInputField.SetTitle("")
  273. } else {
  274. go session.ChannelMessageSend(selectedChannel.ID, t)
  275. }
  276. messageInputField.SetText("")
  277. return nil
  278. case tcell.KeyCtrlV:
  279. text, _ := clipboard.ReadAll()
  280. text = messageInputField.GetText() + text
  281. messageInputField.SetText(text)
  282. return nil
  283. case tcell.KeyEscape:
  284. messageInputField.SetText("")
  285. messageInputField.SetTitle("")
  286. selectedMessage = -1
  287. messagesView.Highlight()
  288. return nil
  289. }
  290. return e
  291. }
  292. func newLoginForm(onLoginFormLoginButtonSelected func(), mfa bool) *tview.Form {
  293. w := tview.NewForm()
  294. w.
  295. AddButton("Login", onLoginFormLoginButtonSelected).
  296. SetButtonsAlign(tview.AlignCenter).
  297. SetBorder(true).
  298. SetBorderPadding(0, 0, 1, 0)
  299. if mfa {
  300. w.AddPasswordField("Code", "", 0, 0, nil)
  301. } else {
  302. w.
  303. AddInputField("Email", "", 0, nil, nil).
  304. AddPasswordField("Password", "", 0, 0, nil)
  305. }
  306. return w
  307. }
  308. // getTreeNodeByReference walks the root `*TreeNode` of the given `*TreeView` *treeView* and returns the TreeNode whose reference is equal to the given reference *r*. If the `*TreeNode` is not found, `nil` is returned instead.
  309. func getTreeNodeByReference(treeView *tview.TreeView, r interface{}) (mn *tview.TreeNode) {
  310. treeView.GetRoot().Walk(func(n, _ *tview.TreeNode) bool {
  311. if n.GetReference() == r {
  312. mn = n
  313. return false
  314. }
  315. return true
  316. })
  317. return
  318. }
  319. // createChannelNode builds (encorporates unread channels in bold tag, otherwise dim, etc.) and returns a node according to the type of the given channel *c*.
  320. func createChannelNode(s *discordgo.State, c *discordgo.Channel) *tview.TreeNode {
  321. var cn *tview.TreeNode
  322. switch c.Type {
  323. case discordgo.ChannelTypeGuildText, discordgo.ChannelTypeGuildNews:
  324. tag := "[::d]"
  325. if channelIsUnread(s, c) {
  326. tag = "[::b]"
  327. }
  328. cn = tview.NewTreeNode(tag + channelToString(c) + "[::-]").
  329. SetReference(c.ID)
  330. case discordgo.ChannelTypeGuildCategory:
  331. cn = tview.NewTreeNode(c.Name).
  332. SetReference(c.ID)
  333. }
  334. return cn
  335. }
  336. // hasPermission returns a boolean that indicates whether the client user has the given permission *p* in the given channel ID *cID*.
  337. func hasPermission(s *discordgo.State, cID string, p int64) bool {
  338. perm, err := s.UserChannelPermissions(s.User.ID, cID)
  339. if err != nil {
  340. return false
  341. }
  342. return perm&p == p
  343. }
  344. func hasKeybinding(sl []string, s string) bool {
  345. for _, str := range sl {
  346. if str == s {
  347. return true
  348. }
  349. }
  350. return false
  351. }