messages.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. package ui
  2. import (
  3. "io"
  4. "io/ioutil"
  5. "net/http"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "runtime"
  10. "strings"
  11. "github.com/atotto/clipboard"
  12. "github.com/ayntgl/discordgo"
  13. "github.com/ayntgl/discordo/discord"
  14. "github.com/gdamore/tcell/v2"
  15. "github.com/rivo/tview"
  16. )
  17. type MessagesTextView struct {
  18. *tview.TextView
  19. app *App
  20. }
  21. func NewMessagesTextView(app *App) *MessagesTextView {
  22. mtv := &MessagesTextView{
  23. TextView: tview.NewTextView(),
  24. app: app,
  25. }
  26. mtv.SetDynamicColors(true)
  27. mtv.SetRegions(true)
  28. mtv.SetWordWrap(true)
  29. mtv.SetChangedFunc(func() {
  30. mtv.app.Draw()
  31. })
  32. mtv.SetBorder(true)
  33. mtv.SetBorderPadding(0, 0, 1, 1)
  34. mtv.SetInputCapture(mtv.onInputCapture)
  35. return mtv
  36. }
  37. func (mtv *MessagesTextView) onInputCapture(e *tcell.EventKey) *tcell.EventKey {
  38. if mtv.app.SelectedChannel == nil {
  39. return nil
  40. }
  41. ms := mtv.app.SelectedChannel.Messages
  42. if len(ms) == 0 {
  43. return nil
  44. }
  45. switch e.Name() {
  46. case mtv.app.Config.Keybindings.SelectPreviousMessage:
  47. if len(mtv.app.MessagesTextView.GetHighlights()) == 0 {
  48. mtv.app.SelectedMessage = len(ms) - 1
  49. } else {
  50. mtv.app.SelectedMessage--
  51. if mtv.app.SelectedMessage < 0 {
  52. mtv.app.SelectedMessage = 0
  53. }
  54. }
  55. mtv.app.MessagesTextView.
  56. Highlight(ms[mtv.app.SelectedMessage].ID).
  57. ScrollToHighlight()
  58. return nil
  59. case mtv.app.Config.Keybindings.SelectNextMessage:
  60. if len(mtv.app.MessagesTextView.GetHighlights()) == 0 {
  61. mtv.app.SelectedMessage = len(ms) - 1
  62. } else {
  63. mtv.app.SelectedMessage++
  64. if mtv.app.SelectedMessage >= len(ms) {
  65. mtv.app.SelectedMessage = len(ms) - 1
  66. }
  67. }
  68. mtv.app.MessagesTextView.
  69. Highlight(ms[mtv.app.SelectedMessage].ID).
  70. ScrollToHighlight()
  71. return nil
  72. case mtv.app.Config.Keybindings.SelectFirstMessage:
  73. mtv.app.SelectedMessage = 0
  74. mtv.app.MessagesTextView.
  75. Highlight(ms[mtv.app.SelectedMessage].ID).
  76. ScrollToHighlight()
  77. return nil
  78. case mtv.app.Config.Keybindings.SelectLastMessage:
  79. mtv.app.SelectedMessage = len(ms) - 1
  80. mtv.app.MessagesTextView.
  81. Highlight(ms[mtv.app.SelectedMessage].ID).
  82. ScrollToHighlight()
  83. return nil
  84. case mtv.app.Config.Keybindings.OpenMessageActionsList:
  85. messageActionsList := tview.NewList()
  86. hs := mtv.app.MessagesTextView.GetHighlights()
  87. if len(hs) == 0 {
  88. return nil
  89. }
  90. _, m := discord.FindMessageByID(mtv.app.SelectedChannel.Messages, hs[0])
  91. if m == nil {
  92. return nil
  93. }
  94. if discord.HasPermission(mtv.app.Session.State, mtv.app.SelectedChannel.ID, discordgo.PermissionSendMessages) {
  95. messageActionsList.AddItem("Reply", "", 'r', nil)
  96. messageActionsList.AddItem("Mention Reply", "", 'R', nil)
  97. }
  98. if m.ReferencedMessage != nil {
  99. messageActionsList.AddItem("Select Reply", "", 'm', nil)
  100. }
  101. if len(m.Attachments) != 0 {
  102. messageActionsList.AddItem("Download Attachment", "", 'd', nil)
  103. messageActionsList.AddItem("Open Attachment", "", 'o', nil)
  104. }
  105. messageActionsList.
  106. ShowSecondaryText(false).
  107. AddItem("Copy Content", "", 'c', nil).
  108. AddItem("Copy ID", "", 'i', nil).
  109. SetDoneFunc(func() {
  110. mtv.app.
  111. SetRoot(mtv.app.MainFlex, true).
  112. SetFocus(mtv.app.MessagesTextView)
  113. }).
  114. SetSelectedFunc(func(_ int, mainText string, _ string, _ rune) {
  115. onMessageActionsListSelected(mtv.app, mainText, m)
  116. }).
  117. SetTitle("Press the Escape key to close").
  118. SetBorder(true)
  119. mtv.app.SetRoot(messageActionsList, true)
  120. return nil
  121. case "Esc":
  122. mtv.app.SelectedMessage = -1
  123. mtv.app.SetFocus(mtv.app.MainFlex)
  124. mtv.app.MessagesTextView.
  125. Clear().
  126. Highlight()
  127. return nil
  128. }
  129. return e
  130. }
  131. func onMessageActionsListSelected(app *App, mainText string, m *discordgo.Message) {
  132. switch mainText {
  133. case "Copy Content":
  134. if err := clipboard.WriteAll(m.Content); err != nil {
  135. return
  136. }
  137. app.SetRoot(app.MainFlex, false)
  138. case "Copy ID":
  139. if err := clipboard.WriteAll(m.ID); err != nil {
  140. return
  141. }
  142. app.SetRoot(app.MainFlex, false)
  143. case "Reply":
  144. app.MessageInputField.SetTitle("Replying to " + m.Author.String())
  145. app.
  146. SetRoot(app.MainFlex, false).
  147. SetFocus(app.MessageInputField)
  148. case "Mention Reply":
  149. app.MessageInputField.SetTitle("[@] Replying to " + m.Author.String())
  150. app.
  151. SetRoot(app.MainFlex, false).
  152. SetFocus(app.MessageInputField)
  153. case "Select Reply":
  154. app.SelectedMessage, _ = discord.FindMessageByID(app.SelectedChannel.Messages, m.ReferencedMessage.ID)
  155. app.MessagesTextView.
  156. Highlight(m.ReferencedMessage.ID).
  157. ScrollToHighlight()
  158. app.
  159. SetRoot(app.MainFlex, false).
  160. SetFocus(app.MessagesTextView)
  161. case "Download Attachment":
  162. for _, a := range m.Attachments {
  163. f, err := os.Create(filepath.Join(app.Config.General.AttachmentDownloadsDir, a.Filename))
  164. if err != nil {
  165. f.Close()
  166. return
  167. }
  168. defer f.Close()
  169. resp, err := http.Get(a.URL)
  170. if err != nil {
  171. return
  172. }
  173. d, err := ioutil.ReadAll(resp.Body)
  174. if err != nil {
  175. return
  176. }
  177. f.Write(d)
  178. }
  179. app.SetRoot(app.MainFlex, false)
  180. case "Open Attachment":
  181. for _, a := range m.Attachments {
  182. // We are caching the files, but files with the same name can still exist, so it ultimately does not matter
  183. cachePath, _ := os.UserCacheDir()
  184. f, err := os.Create(filepath.Join(cachePath, a.Filename))
  185. if err != nil {
  186. return
  187. }
  188. defer f.Close()
  189. resp, err := http.Get(a.URL)
  190. if err != nil {
  191. return
  192. }
  193. d, err := ioutil.ReadAll(resp.Body)
  194. if err != nil {
  195. return
  196. }
  197. f.Write(d)
  198. go Open(f.Name())
  199. }
  200. app.SetRoot(app.MainFlex, false)
  201. }
  202. }
  203. func Open(input string) {
  204. switch runtime.GOOS {
  205. case "windows":
  206. cmd := exec.Command("cmd", "/C start "+input)
  207. err := cmd.Run()
  208. if err != nil {
  209. return
  210. }
  211. case "darwin":
  212. cmd := exec.Command("open", input)
  213. err := cmd.Run()
  214. if err != nil {
  215. return
  216. }
  217. default: // Unix
  218. cmd := exec.Command("xdg-open", input)
  219. err := cmd.Run()
  220. if err != nil {
  221. return
  222. }
  223. }
  224. }
  225. type MessageInputField struct {
  226. *tview.InputField
  227. app *App
  228. }
  229. func NewMessageInputField(app *App) *MessageInputField {
  230. mi := &MessageInputField{
  231. InputField: tview.NewInputField(),
  232. app: app,
  233. }
  234. mi.SetFieldBackgroundColor(tview.Styles.PrimitiveBackgroundColor)
  235. mi.SetPlaceholder("Message...")
  236. mi.SetPlaceholderStyle(tcell.StyleDefault.Background(tview.Styles.PrimitiveBackgroundColor))
  237. mi.SetTitleAlign(tview.AlignLeft)
  238. mi.SetBorder(true)
  239. mi.SetBorderPadding(0, 0, 1, 1)
  240. mi.SetInputCapture(mi.onInputCapture)
  241. return mi
  242. }
  243. func (mi *MessageInputField) onInputCapture(e *tcell.EventKey) *tcell.EventKey {
  244. switch e.Name() {
  245. case "Enter":
  246. if mi.app.SelectedChannel == nil {
  247. return nil
  248. }
  249. t := strings.TrimSpace(mi.app.MessageInputField.GetText())
  250. if t == "" {
  251. return nil
  252. }
  253. if len(mi.app.MessagesTextView.GetHighlights()) != 0 {
  254. _, m := discord.FindMessageByID(mi.app.SelectedChannel.Messages, mi.app.MessagesTextView.GetHighlights()[0])
  255. d := &discordgo.MessageSend{
  256. Content: t,
  257. Reference: m.Reference(),
  258. AllowedMentions: &discordgo.MessageAllowedMentions{RepliedUser: false},
  259. }
  260. if strings.HasPrefix(mi.app.MessageInputField.GetTitle(), "[@]") {
  261. d.AllowedMentions.RepliedUser = true
  262. } else {
  263. d.AllowedMentions.RepliedUser = false
  264. }
  265. go mi.app.Session.ChannelMessageSendComplex(m.ChannelID, d)
  266. mi.app.SelectedMessage = -1
  267. mi.app.MessagesTextView.Highlight()
  268. mi.app.MessageInputField.SetTitle("")
  269. } else {
  270. go mi.app.Session.ChannelMessageSend(mi.app.SelectedChannel.ID, t)
  271. }
  272. mi.app.MessageInputField.SetText("")
  273. return nil
  274. case "Ctrl+V":
  275. text, _ := clipboard.ReadAll()
  276. text = mi.app.MessageInputField.GetText() + text
  277. mi.app.MessageInputField.SetText(text)
  278. return nil
  279. case "Esc":
  280. mi.app.MessageInputField.
  281. SetText("").
  282. SetTitle("")
  283. mi.app.SetFocus(mi.app.MainFlex)
  284. mi.app.SelectedMessage = -1
  285. mi.app.MessagesTextView.Highlight()
  286. return nil
  287. case mi.app.Config.Keybindings.OpenExternalEditor:
  288. e := os.Getenv("EDITOR")
  289. if e == "" {
  290. return nil
  291. }
  292. f, err := os.CreateTemp(os.TempDir(), "discordo-*.md")
  293. if err != nil {
  294. return nil
  295. }
  296. defer os.Remove(f.Name())
  297. cmd := exec.Command(e, f.Name())
  298. cmd.Stdin = os.Stdin
  299. cmd.Stdout = os.Stdout
  300. mi.app.Suspend(func() {
  301. err = cmd.Run()
  302. if err != nil {
  303. return
  304. }
  305. })
  306. b, err := io.ReadAll(f)
  307. if err != nil {
  308. return nil
  309. }
  310. mi.app.MessageInputField.SetText(string(b))
  311. return nil
  312. }
  313. return e
  314. }