messages_list.go 19 KB

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