view.go 7.6 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 View 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 NewView(app *tview.Application, cfg *config.Config, onLogout func()) *View {
  37. v := &View{
  38. Pages: tview.NewPages(),
  39. mainFlex: tview.NewFlex(),
  40. rightFlex: tview.NewFlex(),
  41. app: app,
  42. cfg: cfg,
  43. onLogout: onLogout,
  44. }
  45. v.guildsTree = newGuildsTree(cfg, v)
  46. v.messagesList = newMessagesList(cfg, v)
  47. v.messageInput = newMessageInput(cfg, v)
  48. v.SetInputCapture(v.onInputCapture)
  49. v.buildLayout()
  50. return v
  51. }
  52. func (v *View) SelectedChannel() *discord.Channel {
  53. v.selectedChannelMu.RLock()
  54. defer v.selectedChannelMu.RUnlock()
  55. return v.selectedChannel
  56. }
  57. func (v *View) SetSelectedChannel(channel *discord.Channel) {
  58. v.selectedChannelMu.Lock()
  59. v.selectedChannel = channel
  60. v.selectedChannelMu.Unlock()
  61. }
  62. func (v *View) buildLayout() {
  63. v.Clear()
  64. v.rightFlex.Clear()
  65. v.mainFlex.Clear()
  66. v.rightFlex.
  67. SetDirection(tview.FlexRow).
  68. AddItem(v.messagesList, 0, 1, false).
  69. AddItem(v.messageInput, 3, 1, false)
  70. // The guilds tree is always focused first at start-up.
  71. v.mainFlex.
  72. AddItem(v.guildsTree, 0, 1, true).
  73. AddItem(v.rightFlex, 0, 4, false)
  74. v.AddAndSwitchToPage(flexPageName, v.mainFlex, true)
  75. }
  76. func (v *View) toggleGuildsTree() {
  77. // The guilds tree is visible if the number of items is two.
  78. if v.mainFlex.GetItemCount() == 2 {
  79. v.mainFlex.RemoveItem(v.guildsTree)
  80. if v.guildsTree.HasFocus() {
  81. v.app.SetFocus(v.mainFlex)
  82. }
  83. } else {
  84. v.buildLayout()
  85. v.app.SetFocus(v.guildsTree)
  86. }
  87. }
  88. func (v *View) focusGuildsTree() bool {
  89. // The guilds tree is not hidden if the number of items is two.
  90. if v.mainFlex.GetItemCount() == 2 {
  91. v.app.SetFocus(v.guildsTree)
  92. return true
  93. }
  94. return false
  95. }
  96. func (v *View) focusMessageInput() bool {
  97. if !v.messageInput.GetDisabled() {
  98. v.app.SetFocus(v.messageInput)
  99. return true
  100. }
  101. return false
  102. }
  103. func (v *View) focusPrevious() {
  104. switch v.app.GetFocus() {
  105. case v.guildsTree:
  106. v.focusMessageInput()
  107. case v.messagesList: // Handle both a.messagesList and a.flex as well as other edge cases (if there is).
  108. if ok := v.focusGuildsTree(); !ok {
  109. v.app.SetFocus(v.messageInput)
  110. }
  111. case v.messageInput:
  112. v.app.SetFocus(v.messagesList)
  113. }
  114. }
  115. func (v *View) focusNext() {
  116. switch v.app.GetFocus() {
  117. case v.guildsTree:
  118. v.app.SetFocus(v.messagesList)
  119. case v.messagesList:
  120. v.focusMessageInput()
  121. case v.messageInput: // Handle both a.messageInput and a.flex as well as other edge cases (if there is).
  122. if ok := v.focusGuildsTree(); !ok {
  123. v.app.SetFocus(v.messagesList)
  124. }
  125. }
  126. }
  127. func (v *View) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
  128. switch event.Name() {
  129. case v.cfg.Keys.FocusGuildsTree:
  130. v.messageInput.removeMentionsList()
  131. v.focusGuildsTree()
  132. return nil
  133. case v.cfg.Keys.FocusMessagesList:
  134. v.messageInput.removeMentionsList()
  135. v.app.SetFocus(v.messagesList)
  136. return nil
  137. case v.cfg.Keys.FocusMessageInput:
  138. v.focusMessageInput()
  139. return nil
  140. case v.cfg.Keys.FocusPrevious:
  141. v.focusPrevious()
  142. return nil
  143. case v.cfg.Keys.FocusNext:
  144. v.focusNext()
  145. return nil
  146. case v.cfg.Keys.Logout:
  147. if v.onLogout != nil {
  148. v.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 v.cfg.Keys.ToggleGuildsTree:
  156. v.toggleGuildsTree()
  157. return nil
  158. }
  159. return event
  160. }
  161. func (v *View) showConfirmModal(prompt string, buttons []string, onDone func(label string)) {
  162. previousFocus := v.app.GetFocus()
  163. modal := tview.NewModal().
  164. SetText(prompt).
  165. AddButtons(buttons).
  166. SetDoneFunc(func(_ int, buttonLabel string) {
  167. v.RemovePage(confirmModalPageName).SwitchToPage(flexPageName)
  168. v.app.SetFocus(previousFocus)
  169. if onDone != nil {
  170. onDone(buttonLabel)
  171. }
  172. })
  173. v.
  174. AddAndSwitchToPage(confirmModalPageName, ui.Centered(modal, 0, 0), true).
  175. ShowPage(flexPageName)
  176. }
  177. func (v *View) onReadUpdate(event *read.UpdateEvent) {
  178. var guildNode *tview.TreeNode
  179. v.guildsTree.
  180. GetRoot().
  181. Walk(func(node, parent *tview.TreeNode) bool {
  182. switch node.GetReference() {
  183. case event.GuildID:
  184. node.SetTextStyle(v.guildsTree.getGuildNodeStyle(event.GuildID))
  185. guildNode = node
  186. return false
  187. case event.ChannelID:
  188. // private channel
  189. if !event.GuildID.IsValid() {
  190. style := v.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(v.guildsTree.getChannelNodeStyle(event.ChannelID))
  201. return false
  202. }
  203. return true
  204. })
  205. }
  206. v.app.Draw()
  207. }
  208. func (v *View) onReady(r *gateway.ReadyEvent) {
  209. dmNode := tview.NewTreeNode("Direct Messages")
  210. root := v.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 := v.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. v.guildsTree.createGuildNode(root, *guild)
  228. } else {
  229. v.guildsTree.createFolderNode(folder)
  230. }
  231. }
  232. v.guildsTree.SetCurrentNode(root)
  233. v.app.SetFocus(v.guildsTree)
  234. v.app.Draw()
  235. }
  236. func (v *View) onMessageCreate(message *gateway.MessageCreateEvent) {
  237. if selected := v.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
  238. v.messagesList.drawMessage(v.messagesList, message.Message)
  239. v.app.Draw()
  240. }
  241. if err := notifications.Notify(v.state, message, v.cfg); err != nil {
  242. slog.Error("failed to notify", "err", err, "channel_id", message.ChannelID, "message_id", message.ID)
  243. }
  244. }
  245. func (v *View) onMessageUpdate(message *gateway.MessageUpdateEvent) {
  246. if selected := v.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
  247. v.onMessageDelete(&gateway.MessageDeleteEvent{ID: message.ID, ChannelID: message.ChannelID, GuildID: message.GuildID})
  248. }
  249. }
  250. func (v *View) onMessageDelete(message *gateway.MessageDeleteEvent) {
  251. if selected := v.SelectedChannel(); selected != nil && selected.ID == message.ChannelID {
  252. messages, err := v.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. v.messagesList.reset()
  258. v.messagesList.drawMessages(messages)
  259. v.app.Draw()
  260. }
  261. }
  262. func (v *View) onGuildMembersChunk(event *gateway.GuildMembersChunkEvent) {
  263. v.messagesList.setFetchingChunk(false, uint(len(event.Members)))
  264. }
  265. func (v *View) onGuildMemberRemove(event *gateway.GuildMemberRemoveEvent) {
  266. v.messageInput.cache.Invalidate(event.GuildID.String()+" "+event.User.Username, v.state.MemberState.SearchLimit)
  267. }