ui.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. package main
  2. import (
  3. "strings"
  4. "github.com/atotto/clipboard"
  5. "github.com/ayntgl/discordgo"
  6. "github.com/gdamore/tcell/v2"
  7. "github.com/rivo/tview"
  8. )
  9. var (
  10. app *tview.Application
  11. loginForm *tview.Form
  12. channelsTree *tview.TreeView
  13. messagesView *tview.TextView
  14. messageInputField *tview.InputField
  15. mainFlex *tview.Flex
  16. )
  17. func onAppInputCapture(e *tcell.EventKey) *tcell.EventKey {
  18. switch e.Name() {
  19. case conf.Keybindings.ChannelsTree.Focus:
  20. app.SetFocus(channelsTree)
  21. case conf.Keybindings.MessagesView.Focus:
  22. app.SetFocus(messagesView)
  23. case conf.Keybindings.MessageInputField.Focus:
  24. app.SetFocus(messageInputField)
  25. }
  26. return e
  27. }
  28. func onChannelsTreeSelected(n *tview.TreeNode) {
  29. selectedChannel = nil
  30. selectedMessage = nil
  31. messagesView.
  32. Clear().
  33. SetTitle("")
  34. messageInputField.SetText("")
  35. // Unhighlight the already-highlighted regions.
  36. messagesView.Highlight()
  37. if len(n.GetChildren()) != 0 || n.GetText() == "Direct Messages" {
  38. n.SetExpanded(!n.IsExpanded())
  39. } else {
  40. cID := n.GetReference().(string)
  41. c, err := session.State.Channel(cID)
  42. if err != nil {
  43. return
  44. }
  45. selectedChannel = c
  46. app.SetFocus(messageInputField)
  47. switch c.Type {
  48. case discordgo.ChannelTypeGuildText, discordgo.ChannelTypeGuildNews:
  49. title := generateChannelRepr(c)
  50. if c.Topic != "" {
  51. title += " - " + c.Topic
  52. }
  53. messagesView.SetTitle(title)
  54. case discordgo.ChannelTypeDM, discordgo.ChannelTypeGroupDM:
  55. messagesView.SetTitle(generateChannelRepr(c))
  56. }
  57. if strings.HasPrefix(n.GetText(), "[::b]") {
  58. n.SetText("[::d]" + generateChannelRepr(c) + "[::-]")
  59. }
  60. go func() {
  61. ms, err := session.ChannelMessages(cID, conf.GetMessagesLimit, "", "", "")
  62. if err != nil {
  63. return
  64. }
  65. for i := len(ms) - 1; i >= 0; i-- {
  66. selectedChannel.Messages = append(selectedChannel.Messages, ms[i])
  67. renderMessage(ms[i])
  68. }
  69. if len(ms) != 0 && isUnread(c) {
  70. session.ChannelMessageAck(c.ID, c.LastMessageID, "")
  71. }
  72. }()
  73. }
  74. }
  75. func createTopLevelChannelsTreeNodes(
  76. n *tview.TreeNode,
  77. cs []*discordgo.Channel,
  78. ) {
  79. for _, c := range cs {
  80. if (c.Type == discordgo.ChannelTypeGuildText || c.Type == discordgo.ChannelTypeGuildNews) &&
  81. (c.ParentID == "") {
  82. p, err := session.State.UserChannelPermissions(session.State.User.ID, c.ID)
  83. if err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
  84. continue
  85. }
  86. var tag string
  87. if isUnread(c) {
  88. tag = "[::b]"
  89. } else {
  90. tag = "[::d]"
  91. }
  92. cn := tview.NewTreeNode(tag + generateChannelRepr(c) + "[::-]").
  93. SetReference(c.ID)
  94. n.AddChild(cn)
  95. continue
  96. }
  97. }
  98. }
  99. func createCategoryChannelsTreeNodes(
  100. n *tview.TreeNode,
  101. cs []*discordgo.Channel,
  102. ) {
  103. CategoryLoop:
  104. for _, c := range cs {
  105. if c.Type == discordgo.ChannelTypeGuildCategory {
  106. p, err := session.State.UserChannelPermissions(session.State.User.ID, c.ID)
  107. if err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
  108. continue
  109. }
  110. for _, child := range cs {
  111. if child.ParentID == c.ID {
  112. cn := tview.NewTreeNode(c.Name).
  113. SetReference(c.ID)
  114. n.AddChild(cn)
  115. continue CategoryLoop
  116. }
  117. }
  118. cn := tview.NewTreeNode(c.Name).
  119. SetReference(c.ID)
  120. n.AddChild(cn)
  121. }
  122. }
  123. }
  124. func createSecondLevelChannelsTreeNodes(cs []*discordgo.Channel) {
  125. for _, c := range cs {
  126. if (c.Type == discordgo.ChannelTypeGuildText || c.Type == discordgo.ChannelTypeGuildNews) &&
  127. (c.ParentID != "") {
  128. p, err := session.State.UserChannelPermissions(session.State.User.ID, c.ID)
  129. if err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
  130. continue
  131. }
  132. var tag string
  133. if isUnread(c) {
  134. tag = "[::b]"
  135. } else {
  136. tag = "[::d]"
  137. }
  138. pn := getTreeNodeByReference(c.ParentID)
  139. if pn != nil {
  140. cn := tview.NewTreeNode(tag + generateChannelRepr(c) + "[::-]").
  141. SetReference(c.ID)
  142. pn.AddChild(cn)
  143. }
  144. }
  145. }
  146. }
  147. func getTreeNodeByReference(r interface{}) (mn *tview.TreeNode) {
  148. channelsTree.GetRoot().Walk(func(n, _ *tview.TreeNode) bool {
  149. if n.GetReference() == r {
  150. mn = n
  151. return false
  152. }
  153. return true
  154. })
  155. return
  156. }
  157. func onMessagesTextViewInputCapture(e *tcell.EventKey) *tcell.EventKey {
  158. if selectedChannel == nil {
  159. return nil
  160. }
  161. switch e.Name() {
  162. case conf.Keybindings.MessagesView.SelectPrevious:
  163. ms := selectedChannel.Messages
  164. if len(ms) == 0 {
  165. return nil
  166. }
  167. hs := messagesView.GetHighlights()
  168. if len(hs) == 0 {
  169. messagesView.
  170. Highlight(ms[len(ms)-1].ID).
  171. ScrollToHighlight()
  172. } else {
  173. idx, _ := findByMessageID(ms, hs[0])
  174. if idx == -1 || idx == 0 {
  175. return nil
  176. }
  177. messagesView.
  178. Highlight(ms[idx-1].ID).
  179. ScrollToHighlight()
  180. }
  181. return nil
  182. case conf.Keybindings.MessagesView.SelectNext:
  183. ms := selectedChannel.Messages
  184. if len(ms) == 0 {
  185. return nil
  186. }
  187. hs := messagesView.GetHighlights()
  188. if len(hs) == 0 {
  189. messagesView.
  190. Highlight(ms[len(ms)-1].ID).
  191. ScrollToHighlight()
  192. } else {
  193. idx, _ := findByMessageID(ms, hs[0])
  194. if idx == -1 || idx == len(ms)-1 {
  195. return nil
  196. }
  197. messagesView.
  198. Highlight(ms[idx+1].ID).
  199. ScrollToHighlight()
  200. }
  201. return nil
  202. case conf.Keybindings.MessagesView.SelectFirst:
  203. ms := selectedChannel.Messages
  204. if len(ms) == 0 {
  205. return nil
  206. }
  207. messagesView.
  208. Highlight(ms[0].ID).
  209. ScrollToHighlight()
  210. case conf.Keybindings.MessagesView.SelectLast:
  211. ms := selectedChannel.Messages
  212. if len(ms) == 0 {
  213. return nil
  214. }
  215. messagesView.
  216. Highlight(ms[len(ms)-1].ID).
  217. ScrollToHighlight()
  218. case conf.Keybindings.MessagesView.Reply:
  219. ms := selectedChannel.Messages
  220. if len(ms) == 0 {
  221. return nil
  222. }
  223. hs := messagesView.GetHighlights()
  224. if len(hs) == 0 {
  225. return nil
  226. }
  227. _, selectedMessage = findByMessageID(ms, hs[0])
  228. messageInputField.SetTitle(
  229. "Replying to " + selectedMessage.Author.Username,
  230. )
  231. app.SetFocus(messageInputField)
  232. case conf.Keybindings.MessagesView.ReplyMention:
  233. ms := selectedChannel.Messages
  234. if len(ms) == 0 {
  235. return nil
  236. }
  237. hs := messagesView.GetHighlights()
  238. if len(hs) == 0 {
  239. return nil
  240. }
  241. _, selectedMessage = findByMessageID(ms, hs[0])
  242. messageInputField.SetTitle("[@] Repling to " + selectedMessage.Author.Username)
  243. app.SetFocus(messageInputField)
  244. }
  245. return e
  246. }
  247. func onMessageInputFieldInputCapture(e *tcell.EventKey) *tcell.EventKey {
  248. // If the "Alt" modifier key is pressed, do not handle the event.
  249. if e.Modifiers() == tcell.ModAlt {
  250. return nil
  251. }
  252. switch e.Key() {
  253. case tcell.KeyEnter:
  254. if selectedChannel == nil {
  255. return nil
  256. }
  257. t := strings.TrimSpace(messageInputField.GetText())
  258. if t == "" {
  259. return nil
  260. }
  261. if selectedMessage != nil {
  262. d := &discordgo.MessageSend{
  263. Content: t,
  264. Reference: selectedMessage.Reference(),
  265. AllowedMentions: &discordgo.MessageAllowedMentions{RepliedUser: false},
  266. }
  267. if strings.HasPrefix(messageInputField.GetTitle(), "[@]") {
  268. d.AllowedMentions.RepliedUser = true
  269. } else {
  270. d.AllowedMentions.RepliedUser = false
  271. }
  272. go session.ChannelMessageSendComplex(selectedMessage.ChannelID, d)
  273. messageInputField.SetTitle("")
  274. selectedMessage = nil
  275. } else {
  276. go session.ChannelMessageSend(selectedChannel.ID, t)
  277. }
  278. messageInputField.SetText("")
  279. case tcell.KeyCtrlV:
  280. text, _ := clipboard.ReadAll()
  281. text = messageInputField.GetText() + text
  282. messageInputField.SetText(text)
  283. case tcell.KeyEscape:
  284. messageInputField.SetTitle("")
  285. selectedMessage = nil
  286. }
  287. return e
  288. }
  289. func newLoginForm(onLoginFormLoginButtonSelected func(), mfa bool) *tview.Form {
  290. w := tview.NewForm()
  291. w.
  292. AddButton("Login", onLoginFormLoginButtonSelected).
  293. SetButtonsAlign(tview.AlignCenter).
  294. SetBorder(true).
  295. SetBorderPadding(0, 0, 1, 0)
  296. if mfa {
  297. w.AddPasswordField("Code", "", 0, 0, nil)
  298. } else {
  299. w.
  300. AddInputField("Email", "", 0, nil, nil).
  301. AddPasswordField("Password", "", 0, 0, nil)
  302. }
  303. return w
  304. }