messages_list.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762
  1. package chat
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "log/slog"
  8. "net/http"
  9. "os"
  10. "path/filepath"
  11. "slices"
  12. "strings"
  13. "sync"
  14. "time"
  15. "github.com/ayn2op/discordo/internal/clipboard"
  16. "github.com/ayn2op/discordo/internal/config"
  17. "github.com/ayn2op/discordo/internal/consts"
  18. "github.com/ayn2op/discordo/internal/markdown"
  19. "github.com/ayn2op/discordo/internal/ui"
  20. "github.com/ayn2op/tview"
  21. "github.com/diamondburned/arikawa/v3/api"
  22. "github.com/diamondburned/arikawa/v3/discord"
  23. "github.com/diamondburned/arikawa/v3/gateway"
  24. "github.com/diamondburned/arikawa/v3/state"
  25. "github.com/diamondburned/arikawa/v3/utils/json/option"
  26. "github.com/diamondburned/ningen/v3/discordmd"
  27. "github.com/gdamore/tcell/v3"
  28. "github.com/skratchdot/open-golang/open"
  29. "github.com/yuin/goldmark/ast"
  30. "github.com/yuin/goldmark/parser"
  31. "github.com/yuin/goldmark/text"
  32. )
  33. type messagesList struct {
  34. *tview.ScrollList
  35. cfg *config.Config
  36. chatView *View
  37. messages []discord.Message
  38. renderer *markdown.Renderer
  39. fetchingMembers struct {
  40. mu sync.Mutex
  41. value bool
  42. count uint
  43. done chan struct{}
  44. }
  45. }
  46. func newMessagesList(cfg *config.Config, chatView *View) *messagesList {
  47. ml := &messagesList{
  48. ScrollList: tview.NewScrollList(),
  49. cfg: cfg,
  50. chatView: chatView,
  51. renderer: markdown.NewRenderer(cfg.Theme.MessagesList),
  52. }
  53. ml.Box = ui.ConfigureBox(ml.Box, &cfg.Theme)
  54. ml.SetTitle("Messages")
  55. ml.SetBuilder(ml.buildItem)
  56. ml.SetTrackEnd(true)
  57. ml.SetInputCapture(ml.onInputCapture)
  58. return ml
  59. }
  60. func (ml *messagesList) reset() {
  61. ml.messages = nil
  62. ml.
  63. Clear().
  64. SetBuilder(ml.buildItem).
  65. SetTitle("")
  66. }
  67. func (ml *messagesList) setTitle(channel discord.Channel) {
  68. title := ui.ChannelToString(channel, ml.cfg.Icons)
  69. if topic := channel.Topic; topic != "" {
  70. title += " - " + topic
  71. }
  72. ml.SetTitle(title)
  73. }
  74. func (ml *messagesList) setMessages(messages []discord.Message) {
  75. ml.messages = slices.Clone(messages)
  76. slices.Reverse(ml.messages)
  77. }
  78. func (ml *messagesList) addMessage(message discord.Message) {
  79. ml.messages = append(ml.messages, message)
  80. }
  81. func (ml *messagesList) setMessage(index int, message discord.Message) {
  82. if index < 0 || index >= len(ml.messages) {
  83. return
  84. }
  85. ml.messages[index] = message
  86. }
  87. func (ml *messagesList) deleteMessage(index int) {
  88. if index < 0 || index >= len(ml.messages) {
  89. return
  90. }
  91. ml.messages = append(ml.messages[:index], ml.messages[index+1:]...)
  92. }
  93. func (ml *messagesList) clearSelection() {
  94. ml.SetCursor(-1)
  95. }
  96. func (ml *messagesList) buildItem(index int, cursor int) tview.ScrollListItem {
  97. if index < 0 || index >= len(ml.messages) {
  98. return nil
  99. }
  100. message := ml.messages[index]
  101. tv := tview.NewTextView().
  102. SetWrap(true).
  103. SetWordWrap(true).
  104. SetDynamicColors(true).
  105. SetText(ml.renderMessage(message))
  106. if index == cursor {
  107. tv.SetTextStyle(ml.cfg.Theme.MessagesList.SelectedMessageStyle.Style)
  108. } else {
  109. tv.SetTextStyle(ml.cfg.Theme.MessagesList.MessageStyle.Style)
  110. }
  111. return tv
  112. }
  113. func (ml *messagesList) renderMessage(message discord.Message) string {
  114. var b strings.Builder
  115. ml.writeMessage(&b, message)
  116. return b.String()
  117. }
  118. func (ml *messagesList) writeMessage(writer io.Writer, message discord.Message) {
  119. if ml.cfg.HideBlockedUsers {
  120. isBlocked := ml.chatView.state.UserIsBlocked(message.Author.ID)
  121. if isBlocked {
  122. io.WriteString(writer, "[:red:b]Blocked message[:-:-]")
  123. return
  124. }
  125. }
  126. // reset
  127. io.WriteString(writer, "[-:-:-]")
  128. switch message.Type {
  129. case discord.DefaultMessage:
  130. if message.Reference != nil && message.Reference.Type == discord.MessageReferenceTypeForward {
  131. ml.drawForwardedMessage(writer, message)
  132. } else {
  133. ml.drawDefaultMessage(writer, message)
  134. }
  135. case discord.GuildMemberJoinMessage:
  136. ml.drawTimestamps(writer, message.Timestamp)
  137. ml.drawAuthor(writer, message)
  138. io.WriteString(writer, "joined the server.")
  139. case discord.InlinedReplyMessage:
  140. ml.drawReplyMessage(writer, message)
  141. case discord.ChannelPinnedMessage:
  142. ml.drawPinnedMessage(writer, message)
  143. default:
  144. ml.drawTimestamps(writer, message.Timestamp)
  145. ml.drawAuthor(writer, message)
  146. }
  147. }
  148. func (ml *messagesList) formatTimestamp(ts discord.Timestamp) string {
  149. return ts.Time().In(time.Local).Format(ml.cfg.Timestamps.Format)
  150. }
  151. func (ml *messagesList) drawTimestamps(w io.Writer, ts discord.Timestamp) {
  152. io.WriteString(w, "[::d]")
  153. io.WriteString(w, ml.formatTimestamp(ts))
  154. io.WriteString(w, "[::D] ")
  155. }
  156. func (ml *messagesList) drawAuthor(w io.Writer, message discord.Message) {
  157. name := message.Author.DisplayOrUsername()
  158. foreground := tcell.ColorDefault
  159. // Webhooks do not have nicknames or roles.
  160. if message.GuildID.IsValid() && !message.WebhookID.IsValid() {
  161. member, err := ml.chatView.state.Cabinet.Member(message.GuildID, message.Author.ID)
  162. if err != nil {
  163. slog.Error("failed to get member from state", "guild_id", message.GuildID, "member_id", message.Author.ID, "err", err)
  164. } else {
  165. if member.Nick != "" {
  166. name = member.Nick
  167. }
  168. color, ok := state.MemberColor(member, func(id discord.RoleID) *discord.Role {
  169. r, _ := ml.chatView.state.Cabinet.Role(message.GuildID, id)
  170. return r
  171. })
  172. if ok {
  173. foreground = tcell.NewHexColor(int32(color))
  174. }
  175. }
  176. }
  177. fmt.Fprintf(w, "[%s::b]%s[-::B] ", foreground, name)
  178. }
  179. func (ml *messagesList) drawContent(w io.Writer, message discord.Message) {
  180. c := []byte(tview.Escape(message.Content))
  181. if ml.chatView.cfg.Markdown {
  182. ast := discordmd.ParseWithMessage(c, *ml.chatView.state.Cabinet, &message, false)
  183. ml.renderer.Render(w, c, ast)
  184. } else {
  185. w.Write(c) // write the content as is
  186. }
  187. }
  188. func (ml *messagesList) drawSnapshotContent(w io.Writer, message discord.MessageSnapshotMessage) {
  189. c := []byte(tview.Escape(message.Content))
  190. // discordmd doesn't support MessageSnapshotMessage, so we just use write it as is. todo?
  191. w.Write(c)
  192. }
  193. func (ml *messagesList) drawDefaultMessage(w io.Writer, message discord.Message) {
  194. if ml.cfg.Timestamps.Enabled {
  195. ml.drawTimestamps(w, message.Timestamp)
  196. }
  197. ml.drawAuthor(w, message)
  198. ml.drawContent(w, message)
  199. if message.EditedTimestamp.IsValid() {
  200. io.WriteString(w, " [::d](edited)[::D]")
  201. }
  202. for _, a := range message.Attachments {
  203. io.WriteString(w, "\n")
  204. fg := ml.cfg.Theme.MessagesList.AttachmentStyle.GetForeground()
  205. bg := ml.cfg.Theme.MessagesList.AttachmentStyle.GetBackground()
  206. if ml.cfg.ShowAttachmentLinks {
  207. fmt.Fprintf(w, "[%s:%s]%s:\n%s[-:-]", fg, bg, a.Filename, a.URL)
  208. } else {
  209. fmt.Fprintf(w, "[%s:%s]%s[-:-]", fg, bg, a.Filename)
  210. }
  211. }
  212. }
  213. func (ml *messagesList) drawForwardedMessage(w io.Writer, message discord.Message) {
  214. ml.drawTimestamps(w, message.Timestamp)
  215. ml.drawAuthor(w, message)
  216. fmt.Fprintf(w, "[::d]%s [::-]", ml.cfg.Theme.MessagesList.ForwardedIndicator)
  217. ml.drawSnapshotContent(w, message.MessageSnapshots[0].Message)
  218. fmt.Fprintf(w, " [::d](%s)[-:-:-] ", ml.formatTimestamp(message.MessageSnapshots[0].Message.Timestamp))
  219. }
  220. func (ml *messagesList) drawReplyMessage(w io.Writer, message discord.Message) {
  221. // indicator
  222. io.WriteString(w, "[::d]")
  223. io.WriteString(w, ml.cfg.Theme.MessagesList.ReplyIndicator)
  224. io.WriteString(w, " ")
  225. if m := message.ReferencedMessage; m != nil {
  226. m.GuildID = message.GuildID
  227. ml.drawAuthor(w, *m)
  228. ml.drawContent(w, *m)
  229. } else {
  230. io.WriteString(w, "Original message was deleted")
  231. }
  232. io.WriteString(w, "\n")
  233. // main
  234. ml.drawDefaultMessage(w, message)
  235. }
  236. func (ml *messagesList) drawPinnedMessage(w io.Writer, message discord.Message) {
  237. io.WriteString(w, message.Author.DisplayOrUsername())
  238. io.WriteString(w, " pinned a message.")
  239. }
  240. func (ml *messagesList) selectedMessage() (*discord.Message, error) {
  241. if len(ml.messages) == 0 {
  242. return nil, errors.New("no messages available")
  243. }
  244. cursor := ml.Cursor()
  245. if cursor == -1 || cursor >= len(ml.messages) {
  246. return nil, errors.New("no message is currently selected")
  247. }
  248. return &ml.messages[cursor], nil
  249. }
  250. func (ml *messagesList) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
  251. switch event.Name() {
  252. case ml.cfg.Keybinds.MessagesList.ScrollUp:
  253. ml.ScrollUp()
  254. return nil
  255. case ml.cfg.Keybinds.MessagesList.ScrollDown:
  256. ml.ScrollDown()
  257. return nil
  258. case ml.cfg.Keybinds.MessagesList.ScrollTop:
  259. ml.ScrollToStart()
  260. return nil
  261. case ml.cfg.Keybinds.MessagesList.ScrollBottom:
  262. ml.ScrollToEnd()
  263. return nil
  264. case ml.cfg.Keybinds.MessagesList.Cancel:
  265. ml.clearSelection()
  266. return nil
  267. case ml.cfg.Keybinds.MessagesList.SelectUp, ml.cfg.Keybinds.MessagesList.SelectDown, ml.cfg.Keybinds.MessagesList.SelectTop, ml.cfg.Keybinds.MessagesList.SelectBottom, ml.cfg.Keybinds.MessagesList.SelectReply:
  268. ml._select(event.Name())
  269. return nil
  270. case ml.cfg.Keybinds.MessagesList.YankID:
  271. ml.yankID()
  272. return nil
  273. case ml.cfg.Keybinds.MessagesList.YankContent:
  274. ml.yankContent()
  275. return nil
  276. case ml.cfg.Keybinds.MessagesList.YankURL:
  277. ml.yankURL()
  278. return nil
  279. case ml.cfg.Keybinds.MessagesList.Open:
  280. ml.open()
  281. return nil
  282. case ml.cfg.Keybinds.MessagesList.Reply:
  283. ml.reply(false)
  284. return nil
  285. case ml.cfg.Keybinds.MessagesList.ReplyMention:
  286. ml.reply(true)
  287. return nil
  288. case ml.cfg.Keybinds.MessagesList.Edit:
  289. ml.edit()
  290. return nil
  291. case ml.cfg.Keybinds.MessagesList.Delete:
  292. ml.delete()
  293. return nil
  294. case ml.cfg.Keybinds.MessagesList.DeleteConfirm:
  295. ml.confirmDelete()
  296. return nil
  297. }
  298. return event
  299. }
  300. func (ml *messagesList) _select(name string) {
  301. messages := ml.messages
  302. if len(messages) == 0 {
  303. return
  304. }
  305. cursor := ml.Cursor()
  306. switch name {
  307. case ml.cfg.Keybinds.MessagesList.SelectUp:
  308. switch {
  309. case cursor == -1:
  310. cursor = len(messages) - 1
  311. case cursor > 0:
  312. cursor--
  313. case cursor == 0:
  314. selectedChannel := ml.chatView.SelectedChannel()
  315. if selectedChannel == nil {
  316. return
  317. }
  318. channelID := selectedChannel.ID
  319. before := ml.messages[0].ID
  320. limit := uint(ml.cfg.MessagesLimit)
  321. messages, err := ml.chatView.state.MessagesBefore(channelID, before, limit)
  322. if err != nil {
  323. slog.Error("failed to fetch older messages", "err", err)
  324. return
  325. }
  326. if len(messages) == 0 {
  327. return
  328. }
  329. if guildID := selectedChannel.GuildID; guildID.IsValid() {
  330. ml.requestGuildMembers(guildID, messages)
  331. }
  332. older := slices.Clone(messages)
  333. slices.Reverse(older)
  334. ml.messages = slices.Concat(older, ml.messages)
  335. cursor = len(messages) - 1
  336. }
  337. case ml.cfg.Keybinds.MessagesList.SelectDown:
  338. switch {
  339. case cursor == -1:
  340. cursor = len(messages) - 1
  341. case cursor < len(messages)-1:
  342. cursor++
  343. }
  344. case ml.cfg.Keybinds.MessagesList.SelectTop:
  345. cursor = 0
  346. case ml.cfg.Keybinds.MessagesList.SelectBottom:
  347. cursor = len(messages) - 1
  348. case ml.cfg.Keybinds.MessagesList.SelectReply:
  349. if cursor == -1 || cursor >= len(messages) {
  350. return
  351. }
  352. if ref := messages[cursor].ReferencedMessage; ref != nil {
  353. refIdx := slices.IndexFunc(messages, func(m discord.Message) bool {
  354. return m.ID == ref.ID
  355. })
  356. if refIdx != -1 {
  357. cursor = refIdx
  358. }
  359. }
  360. }
  361. ml.SetCursor(cursor)
  362. }
  363. func (ml *messagesList) yankID() {
  364. msg, err := ml.selectedMessage()
  365. if err != nil {
  366. slog.Error("failed to get selected message", "err", err)
  367. return
  368. }
  369. go clipboard.Write(clipboard.FmtText, []byte(msg.ID.String()))
  370. }
  371. func (ml *messagesList) yankContent() {
  372. msg, err := ml.selectedMessage()
  373. if err != nil {
  374. slog.Error("failed to get selected message", "err", err)
  375. return
  376. }
  377. go clipboard.Write(clipboard.FmtText, []byte(msg.Content))
  378. }
  379. func (ml *messagesList) yankURL() {
  380. msg, err := ml.selectedMessage()
  381. if err != nil {
  382. slog.Error("failed to get selected message", "err", err)
  383. return
  384. }
  385. go clipboard.Write(clipboard.FmtText, []byte(msg.URL()))
  386. }
  387. func (ml *messagesList) open() {
  388. msg, err := ml.selectedMessage()
  389. if err != nil {
  390. slog.Error("failed to get selected message", "err", err)
  391. return
  392. }
  393. var urls []string
  394. if msg.Content != "" {
  395. urls = extractURLs(msg.Content)
  396. }
  397. if len(urls) == 0 && len(msg.Attachments) == 0 {
  398. return
  399. }
  400. if len(urls)+len(msg.Attachments) == 1 {
  401. if len(urls) == 1 {
  402. go ml.openURL(urls[0])
  403. } else {
  404. attachment := msg.Attachments[0]
  405. if strings.HasPrefix(attachment.ContentType, "image/") {
  406. go ml.openAttachment(msg.Attachments[0])
  407. } else {
  408. go ml.openURL(attachment.URL)
  409. }
  410. }
  411. } else {
  412. ml.showAttachmentsList(urls, msg.Attachments)
  413. }
  414. }
  415. func extractURLs(content string) []string {
  416. src := []byte(content)
  417. node := parser.NewParser(
  418. parser.WithBlockParsers(discordmd.BlockParsers()...),
  419. parser.WithInlineParsers(discordmd.InlineParserWithLink()...),
  420. ).Parse(text.NewReader(src))
  421. var urls []string
  422. ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
  423. if entering {
  424. switch n := n.(type) {
  425. case *ast.AutoLink:
  426. urls = append(urls, string(n.URL(src)))
  427. case *ast.Link:
  428. urls = append(urls, string(n.Destination))
  429. }
  430. }
  431. return ast.WalkContinue, nil
  432. })
  433. return urls
  434. }
  435. func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord.Attachment) {
  436. list := tview.NewList().
  437. SetWrapAround(true).
  438. SetHighlightFullLine(true).
  439. ShowSecondaryText(false).
  440. SetDoneFunc(func() {
  441. ml.chatView.RemovePage(attachmentsListPageName).SwitchToPage(flexPageName)
  442. ml.chatView.app.SetFocus(ml)
  443. })
  444. list.
  445. SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
  446. switch event.Name() {
  447. case ml.cfg.Keybinds.MessagesList.SelectUp:
  448. return tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone)
  449. case ml.cfg.Keybinds.MessagesList.SelectDown:
  450. return tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone)
  451. case ml.cfg.Keybinds.MessagesList.SelectTop:
  452. return tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone)
  453. case ml.cfg.Keybinds.MessagesList.SelectBottom:
  454. return tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone)
  455. }
  456. return event
  457. })
  458. list.Box = ui.ConfigureBox(list.Box, &ml.cfg.Theme)
  459. for i, a := range attachments {
  460. list.AddItem(a.Filename, "", rune('a'+i), func() {
  461. if strings.HasPrefix(a.ContentType, "image/") {
  462. go ml.openAttachment(a)
  463. } else {
  464. go ml.openURL(a.URL)
  465. }
  466. })
  467. }
  468. for i, u := range urls {
  469. list.AddItem(u, "", rune('1'+i), func() {
  470. go ml.openURL(u)
  471. })
  472. }
  473. ml.chatView.
  474. AddAndSwitchToPage(attachmentsListPageName, ui.Centered(list, 0, 0), true).
  475. ShowPage(flexPageName)
  476. }
  477. func (ml *messagesList) openAttachment(attachment discord.Attachment) {
  478. resp, err := http.Get(attachment.URL)
  479. if err != nil {
  480. slog.Error("failed to fetch the attachment", "err", err, "url", attachment.URL)
  481. return
  482. }
  483. defer resp.Body.Close()
  484. path := filepath.Join(consts.CacheDir(), "attachments")
  485. if err := os.MkdirAll(path, os.ModePerm); err != nil {
  486. slog.Error("failed to create attachments dir", "err", err, "path", path)
  487. return
  488. }
  489. path = filepath.Join(path, attachment.Filename)
  490. file, err := os.Create(path)
  491. if err != nil {
  492. slog.Error("failed to create attachment file", "err", err, "path", path)
  493. return
  494. }
  495. defer file.Close()
  496. if _, err := io.Copy(file, resp.Body); err != nil {
  497. slog.Error("failed to copy attachment to file", "err", err)
  498. return
  499. }
  500. if err := open.Start(path); err != nil {
  501. slog.Error("failed to open attachment file", "err", err, "path", path)
  502. return
  503. }
  504. }
  505. func (ml *messagesList) openURL(url string) {
  506. if err := open.Start(url); err != nil {
  507. slog.Error("failed to open URL", "err", err, "url", url)
  508. }
  509. }
  510. func (ml *messagesList) reply(mention bool) {
  511. message, err := ml.selectedMessage()
  512. if err != nil {
  513. slog.Error("failed to get selected message", "err", err)
  514. return
  515. }
  516. name := message.Author.DisplayOrUsername()
  517. if message.GuildID.IsValid() {
  518. member, err := ml.chatView.state.Cabinet.Member(message.GuildID, message.Author.ID)
  519. if err != nil {
  520. slog.Error("failed to get member from state", "guild_id", message.GuildID, "member_id", message.Author.ID, "err", err)
  521. } else {
  522. if member.Nick != "" {
  523. name = member.Nick
  524. }
  525. }
  526. }
  527. data := ml.chatView.messageInput.sendMessageData
  528. data.Reference = &discord.MessageReference{MessageID: message.ID}
  529. data.AllowedMentions = &api.AllowedMentions{RepliedUser: option.False}
  530. title := "Replying to "
  531. if mention {
  532. data.AllowedMentions.RepliedUser = option.True
  533. title = "[@] " + title
  534. }
  535. ml.chatView.messageInput.sendMessageData = data
  536. ml.chatView.messageInput.SetTitle(title + name)
  537. ml.chatView.app.SetFocus(ml.chatView.messageInput)
  538. }
  539. func (ml *messagesList) edit() {
  540. message, err := ml.selectedMessage()
  541. if err != nil {
  542. slog.Error("failed to get selected message", "err", err)
  543. return
  544. }
  545. me, err := ml.chatView.state.Cabinet.Me()
  546. if err != nil {
  547. slog.Error("failed to get client user (me)", "err", err)
  548. return
  549. }
  550. if message.Author.ID != me.ID {
  551. slog.Error("failed to edit message; not the author", "channel_id", message.ChannelID, "message_id", message.ID)
  552. return
  553. }
  554. ml.chatView.messageInput.SetTitle("Editing")
  555. ml.chatView.messageInput.edit = true
  556. ml.chatView.messageInput.SetText(message.Content, true)
  557. ml.chatView.app.SetFocus(ml.chatView.messageInput)
  558. }
  559. func (ml *messagesList) confirmDelete() {
  560. onChoice := func(choice string) {
  561. if choice == "Yes" {
  562. ml.delete()
  563. }
  564. }
  565. ml.chatView.showConfirmModal(
  566. "Are you sure you want to delete this message?",
  567. []string{"Yes", "No"},
  568. onChoice,
  569. )
  570. }
  571. func (ml *messagesList) delete() {
  572. msg, err := ml.selectedMessage()
  573. if err != nil {
  574. slog.Error("failed to get selected message", "err", err)
  575. return
  576. }
  577. if msg.GuildID.IsValid() {
  578. me, err := ml.chatView.state.Cabinet.Me()
  579. if err != nil {
  580. slog.Error("failed to get client user (me)", "err", err)
  581. return
  582. }
  583. if msg.Author.ID != me.ID && !ml.chatView.state.HasPermissions(msg.ChannelID, discord.PermissionManageMessages) {
  584. slog.Error("failed to delete message; missing relevant permissions", "channel_id", msg.ChannelID, "message_id", msg.ID)
  585. return
  586. }
  587. }
  588. selected := ml.chatView.SelectedChannel()
  589. if selected == nil {
  590. return
  591. }
  592. if err := ml.chatView.state.DeleteMessage(selected.ID, msg.ID, ""); err != nil {
  593. slog.Error("failed to delete message", "channel_id", selected.ID, "message_id", msg.ID, "err", err)
  594. return
  595. }
  596. if err := ml.chatView.state.MessageRemove(selected.ID, msg.ID); err != nil {
  597. slog.Error("failed to delete message", "channel_id", selected.ID, "message_id", msg.ID, "err", err)
  598. return
  599. }
  600. }
  601. func (ml *messagesList) requestGuildMembers(guildID discord.GuildID, messages []discord.Message) {
  602. usersToFetch := make([]discord.UserID, 0, len(messages))
  603. seen := make(map[discord.UserID]struct{}, len(messages))
  604. for _, message := range messages {
  605. // Do not fetch member for a webhook message.
  606. if message.WebhookID.IsValid() {
  607. continue
  608. }
  609. if member, _ := ml.chatView.state.Cabinet.Member(guildID, message.Author.ID); member == nil {
  610. userID := message.Author.ID
  611. if _, ok := seen[userID]; !ok {
  612. seen[userID] = struct{}{}
  613. usersToFetch = append(usersToFetch, userID)
  614. }
  615. }
  616. }
  617. if len(usersToFetch) > 0 {
  618. err := ml.chatView.state.SendGateway(context.TODO(), &gateway.RequestGuildMembersCommand{
  619. GuildIDs: []discord.GuildID{guildID},
  620. UserIDs: usersToFetch,
  621. })
  622. if err != nil {
  623. slog.Error("failed to request guild members", "guild_id", guildID, "err", err)
  624. return
  625. }
  626. ml.setFetchingChunk(true, 0)
  627. ml.waitForChunkEvent()
  628. }
  629. }
  630. func (ml *messagesList) setFetchingChunk(value bool, count uint) {
  631. ml.fetchingMembers.mu.Lock()
  632. defer ml.fetchingMembers.mu.Unlock()
  633. if ml.fetchingMembers.value == value {
  634. return
  635. }
  636. ml.fetchingMembers.value = value
  637. if value {
  638. ml.fetchingMembers.done = make(chan struct{})
  639. } else {
  640. ml.fetchingMembers.count = count
  641. close(ml.fetchingMembers.done)
  642. }
  643. }
  644. func (ml *messagesList) waitForChunkEvent() uint {
  645. ml.fetchingMembers.mu.Lock()
  646. if !ml.fetchingMembers.value {
  647. ml.fetchingMembers.mu.Unlock()
  648. return 0
  649. }
  650. ml.fetchingMembers.mu.Unlock()
  651. <-ml.fetchingMembers.done
  652. return ml.fetchingMembers.count
  653. }