chatview.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. package chat
  2. import (
  3. "log/slog"
  4. "sync"
  5. "github.com/ayn2op/discordo/internal/config"
  6. "github.com/ayn2op/discordo/internal/keyring"
  7. "github.com/ayn2op/discordo/internal/notifications"
  8. "github.com/ayn2op/discordo/internal/ui"
  9. "github.com/ayn2op/tview"
  10. "github.com/diamondburned/arikawa/v3/discord"
  11. "github.com/diamondburned/arikawa/v3/gateway"
  12. "github.com/diamondburned/ningen/v3"
  13. "github.com/diamondburned/ningen/v3/states/read"
  14. "github.com/gdamore/tcell/v3"
  15. )
  16. const (
  17. flexPageName = "flex"
  18. mentionsListPageName = "mentionsList"
  19. attachmentsListPageName = "attachmentsList"
  20. confirmModalPageName = "confirmModal"
  21. )
  22. type ChatView struct {
  23. *tview.Pages
  24. mainFlex *tview.Flex
  25. rightFlex *tview.Flex
  26. guildsTree *guildsTree
  27. messagesList *messagesList
  28. messageInput *messageInput
  29. selectedChannel *discord.Channel
  30. selectedChannelMu sync.RWMutex
  31. app *tview.Application
  32. cfg *config.Config
  33. state *ningen.State
  34. onLogout func()
  35. }
  36. func NewChatView(app *tview.Application, cfg *config.Config, onLogout func()) *ChatView {
  37. chatView := &ChatView{
  38. Pages: tview.NewPages(),
  39. mainFlex: tview.NewFlex(),
  40. rightFlex: tview.NewFlex(),
  41. app: app,
  42. cfg: cfg,
  43. onLogout: onLogout,
  44. }
  45. chatView.guildsTree = newGuildsTree(cfg, chatView)
  46. chatView.messagesList = newMessagesList(cfg, chatView)
  47. chatView.messageInput = newMessageInput(cfg, chatView)
  48. chatView.SetInputCapture(chatView.onInputCapture)
  49. chatView.buildLayout()
  50. return chatView
  51. }
  52. func (cv *ChatView) SelectedChannel() *discord.Channel {
  53. cv.selectedChannelMu.RLock()
  54. defer cv.selectedChannelMu.RUnlock()
  55. return cv.selectedChannel
  56. }
  57. func (cv *ChatView) SetSelectedChannel(channel *discord.Channel) {
  58. cv.selectedChannelMu.Lock()
  59. cv.selectedChannel = channel
  60. cv.selectedChannelMu.Unlock()
  61. }
  62. func (cv *ChatView) buildLayout() {
  63. cv.Clear()
  64. cv.rightFlex.Clear()
  65. cv.mainFlex.Clear()
  66. cv.rightFlex.
  67. SetDirection(tview.FlexRow).
  68. AddItem(cv.messagesList, 0, 1, false).
  69. AddItem(cv.messageInput, 3, 1, false)
  70. // The guilds tree is always focused first at start-up.
  71. cv.mainFlex.
  72. AddItem(cv.guildsTree, 0, 1, true).
  73. AddItem(cv.rightFlex, 0, 4, false)
  74. cv.AddAndSwitchToPage(flexPageName, cv.mainFlex, true)
  75. }
  76. func (cv *ChatView) toggleGuildsTree() {
  77. // The guilds tree is visible if the number of items is two.
  78. if cv.mainFlex.GetItemCount() == 2 {
  79. cv.mainFlex.RemoveItem(cv.guildsTree)
  80. if cv.guildsTree.HasFocus() {
  81. cv.app.SetFocus(cv.mainFlex)
  82. }
  83. } else {
  84. cv.buildLayout()
  85. cv.app.SetFocus(cv.guildsTree)
  86. }
  87. }
  88. func (cv *ChatView) focusGuildsTree() bool {
  89. // The guilds tree is not hidden if the number of items is two.
  90. if cv.mainFlex.GetItemCount() == 2 {
  91. cv.app.SetFocus(cv.guildsTree)
  92. return true
  93. }
  94. return false
  95. }
  96. func (cv *ChatView) focusMessageInput() bool {
  97. if !cv.messageInput.GetDisabled() {
  98. cv.app.SetFocus(cv.messageInput)
  99. return true
  100. }
  101. return false
  102. }
  103. func (cv *ChatView) focusPrevious() {
  104. switch cv.app.GetFocus() {
  105. case cv.guildsTree:
  106. cv.focusMessageInput()
  107. case cv.messagesList: // Handle both a.messagesList and a.flex as well as other edge cases (if there is).
  108. if ok := cv.focusGuildsTree(); !ok {
  109. cv.app.SetFocus(cv.messageInput)
  110. }
  111. case cv.messageInput:
  112. cv.app.SetFocus(cv.messagesList)
  113. }
  114. }
  115. func (cv *ChatView) focusNext() {
  116. switch cv.app.GetFocus() {
  117. case cv.guildsTree:
  118. cv.app.SetFocus(cv.messagesList)
  119. case cv.messagesList:
  120. cv.focusMessageInput()
  121. case cv.messageInput: // Handle both a.messageInput and a.flex as well as other edge cases (if there is).
  122. if ok := cv.focusGuildsTree(); !ok {
  123. cv.app.SetFocus(cv.messagesList)
  124. }
  125. }
  126. }
  127. func (cv *ChatView) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
  128. switch event.Name() {
  129. case cv.cfg.Keys.FocusGuildsTree:
  130. cv.messageInput.removeMentionsList()
  131. cv.focusGuildsTree()
  132. return nil
  133. case cv.cfg.Keys.FocusMessagesList:
  134. cv.messageInput.removeMentionsList()
  135. cv.app.SetFocus(cv.messagesList)
  136. return nil
  137. case cv.cfg.Keys.FocusMessageInput:
  138. cv.focusMessageInput()
  139. return nil
  140. case cv.cfg.Keys.FocusPrevious:
  141. cv.focusPrevious()
  142. return nil
  143. case cv.cfg.Keys.FocusNext:
  144. cv.focusNext()
  145. return nil
  146. case cv.cfg.Keys.Logout:
  147. if cv.onLogout != nil {
  148. cv.onLogout()
  149. }
  150. if err := keyring.DeleteToken(); err != nil {
  151. slog.Error("failed to delete token from keyring", "err", err)
  152. return nil
  153. }
  154. return nil
  155. case cv.cfg.Keys.ToggleGuildsTree:
  156. cv.toggleGuildsTree()
  157. return nil
  158. }
  159. return event
  160. }
  161. func (cv *ChatView) showConfirmModal(prompt string, buttons []string, onDone func(label string)) {
  162. previousFocus := cv.app.GetFocus()
  163. modal := tview.NewModal().
  164. SetText(prompt).
  165. AddButtons(buttons).
  166. SetDoneFunc(func(_ int, buttonLabel string) {
  167. cv.RemovePage(confirmModalPageName).SwitchToPage(flexPageName)
  168. cv.app.SetFocus(previousFocus)
  169. if onDone != nil {
  170. onDone(buttonLabel)
  171. }
  172. })
  173. cv.
  174. AddAndSwitchToPage(confirmModalPageName, ui.Centered(modal, 0, 0), true).
  175. ShowPage(flexPageName)
  176. }
  177. func (cv *ChatView) onReadUpdate(event *read.UpdateEvent) {
  178. var guildNode *tview.TreeNode
  179. cv.guildsTree.
  180. GetRoot().
  181. Walk(func(node, parent *tview.TreeNode) bool {
  182. switch node.GetReference() {
  183. case event.GuildID:
  184. node.SetTextStyle(cv.guildsTree.getGuildNodeStyle(event.GuildID))
  185. guildNode = node
  186. return false
  187. case event.ChannelID:
  188. // private channel
  189. if !event.GuildID.IsValid() {
  190. style := cv.guildsTree.getChannelNodeStyle(event.ChannelID)
  191. node.SetTextStyle(style)
  192. return false
  193. }
  194. }
  195. return true
  196. })
  197. if guildNode != nil {
  198. guildNode.Walk(func(node, parent *tview.TreeNode) bool {
  199. if node.GetReference() == event.ChannelID {
  200. node.SetTextStyle(cv.guildsTree.getChannelNodeStyle(event.ChannelID))
  201. return false
  202. }
  203. return true
  204. })
  205. }
  206. cv.app.Draw()
  207. }
  208. func (cv *ChatView) onReady(r *gateway.ReadyEvent) {
  209. dmNode := tview.NewTreeNode("Direct Messages")
  210. root := cv.guildsTree.
  211. GetRoot().
  212. ClearChildren().
  213. AddChild(dmNode)
  214. for _, folder := range r.UserSettings.GuildFolders {
  215. if folder.ID == 0 && len(folder.GuildIDs) == 1 {
  216. guild, err := cv.state.Cabinet.Guild(folder.GuildIDs[0])
  217. if err != nil {
  218. slog.Error(
  219. "failed to get guild from state",
  220. "guild_id",
  221. folder.GuildIDs[0],
  222. "err",
  223. err,
  224. )
  225. continue
  226. }
  227. cv.guildsTree.createGuildNode(root, *guild)
  228. } else {
  229. cv.guildsTree.createFolderNode(folder)
  230. }
  231. }
  232. cv.guildsTree.SetCurrentNode(root)
  233. cv.app.SetFocus(cv.guildsTree)
  234. cv.app.Draw()
  235. }
  236. func (cv *ChatView) onMessageCreate(message *gateway.MessageCreateEvent) {
  237. if selected := cv.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
  238. cv.messagesList.drawMessage(cv.messagesList, message.Message)
  239. cv.app.Draw()
  240. }
  241. if err := notifications.Notify(cv.state, message, cv.cfg); err != nil {
  242. slog.Error("failed to notify", "err", err, "channel_id", message.ChannelID, "message_id", message.ID)
  243. }
  244. }
  245. func (cv *ChatView) onMessageUpdate(message *gateway.MessageUpdateEvent) {
  246. if selected := cv.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
  247. cv.onMessageDelete(&gateway.MessageDeleteEvent{ID: message.ID, ChannelID: message.ChannelID, GuildID: message.GuildID})
  248. }
  249. }
  250. func (cv *ChatView) onMessageDelete(message *gateway.MessageDeleteEvent) {
  251. if selected := cv.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
  252. messages, err := cv.state.Cabinet.Messages(message.ChannelID)
  253. if err != nil {
  254. slog.Error("failed to get messages from state", "err", err, "channel_id", message.ChannelID)
  255. return
  256. }
  257. cv.messagesList.reset()
  258. cv.messagesList.drawMessages(messages)
  259. cv.app.Draw()
  260. }
  261. }
  262. func (cv *ChatView) onGuildMembersChunk(event *gateway.GuildMembersChunkEvent) {
  263. cv.messagesList.setFetchingChunk(false, uint(len(event.Members)))
  264. }
  265. func (cv *ChatView) onGuildMemberRemove(event *gateway.GuildMemberRemoveEvent) {
  266. cv.messageInput.cache.Invalidate(event.GuildID.String()+" "+event.User.Username, cv.state.MemberState.SearchLimit)
  267. }