chatview.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. package cmd
  2. import (
  3. "log/slog"
  4. "github.com/ayn2op/discordo/internal/config"
  5. "github.com/ayn2op/discordo/internal/keyring"
  6. "github.com/ayn2op/discordo/internal/ui"
  7. "github.com/ayn2op/tview"
  8. "github.com/diamondburned/arikawa/v3/discord"
  9. "github.com/gdamore/tcell/v3"
  10. )
  11. const (
  12. flexPageName = "flex"
  13. mentionsListPageName = "mentionsList"
  14. attachmentsListPageName = "attachmentsList"
  15. confirmModalPageName = "confirmModal"
  16. )
  17. type chatView struct {
  18. *tview.Pages
  19. mainFlex *tview.Flex
  20. rightFlex *tview.Flex
  21. guildsTree *guildsTree
  22. messagesList *messagesList
  23. messageInput *messageInput
  24. selectedChannel *discord.Channel
  25. app *tview.Application
  26. cfg *config.Config
  27. }
  28. func newChatView(app *tview.Application, cfg *config.Config) *chatView {
  29. cv := &chatView{
  30. Pages: tview.NewPages(),
  31. mainFlex: tview.NewFlex(),
  32. rightFlex: tview.NewFlex(),
  33. guildsTree: newGuildsTree(cfg),
  34. messagesList: newMessagesList(cfg),
  35. messageInput: newMessageInput(cfg),
  36. app: app,
  37. cfg: cfg,
  38. }
  39. cv.SetInputCapture(cv.onInputCapture)
  40. cv.buildLayout()
  41. return cv
  42. }
  43. func (cv *chatView) buildLayout() {
  44. cv.Clear()
  45. cv.rightFlex.Clear()
  46. cv.mainFlex.Clear()
  47. cv.rightFlex.
  48. SetDirection(tview.FlexRow).
  49. AddItem(cv.messagesList, 0, 1, false).
  50. AddItem(cv.messageInput, 3, 1, false)
  51. // The guilds tree is always focused first at start-up.
  52. cv.mainFlex.
  53. AddItem(cv.guildsTree, 0, 1, true).
  54. AddItem(cv.rightFlex, 0, 4, false)
  55. cv.AddAndSwitchToPage(flexPageName, cv.mainFlex, true)
  56. }
  57. func (cv *chatView) toggleGuildsTree() {
  58. // The guilds tree is visible if the number of items is two.
  59. if cv.mainFlex.GetItemCount() == 2 {
  60. cv.mainFlex.RemoveItem(cv.guildsTree)
  61. if cv.guildsTree.HasFocus() {
  62. cv.app.SetFocus(cv.mainFlex)
  63. }
  64. } else {
  65. cv.buildLayout()
  66. cv.app.SetFocus(cv.guildsTree)
  67. }
  68. }
  69. func (cv *chatView) focusGuildsTree() bool {
  70. // The guilds tree is not hidden if the number of items is two.
  71. if cv.mainFlex.GetItemCount() == 2 {
  72. cv.app.SetFocus(cv.guildsTree)
  73. return true
  74. }
  75. return false
  76. }
  77. func (cv *chatView) focusMessageInput() bool {
  78. if !cv.messageInput.GetDisabled() {
  79. cv.app.SetFocus(cv.messageInput)
  80. return true
  81. }
  82. return false
  83. }
  84. func (cv *chatView) focusPrevious() {
  85. switch cv.app.GetFocus() {
  86. case cv.guildsTree:
  87. cv.focusMessageInput()
  88. case cv.messagesList: // Handle both a.messagesList and a.flex as well as other edge cases (if there is).
  89. if ok := cv.focusGuildsTree(); !ok {
  90. cv.app.SetFocus(cv.messageInput)
  91. }
  92. case cv.messageInput:
  93. cv.app.SetFocus(cv.messagesList)
  94. }
  95. }
  96. func (cv *chatView) focusNext() {
  97. switch cv.app.GetFocus() {
  98. case cv.guildsTree:
  99. cv.app.SetFocus(cv.messagesList)
  100. case cv.messagesList:
  101. cv.focusMessageInput()
  102. cv.app.SetFocus(cv.messageInput)
  103. case cv.messageInput: // Handle both a.messageInput and a.flex as well as other edge cases (if there is).
  104. if ok := cv.focusGuildsTree(); !ok {
  105. cv.app.SetFocus(cv.messagesList)
  106. }
  107. }
  108. }
  109. func (cv *chatView) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
  110. switch event.Name() {
  111. case cv.cfg.Keys.FocusGuildsTree:
  112. cv.messageInput.removeMentionsList()
  113. cv.focusGuildsTree()
  114. return nil
  115. case cv.cfg.Keys.FocusMessagesList:
  116. cv.messageInput.removeMentionsList()
  117. cv.app.SetFocus(cv.messagesList)
  118. return nil
  119. case cv.cfg.Keys.FocusMessageInput:
  120. cv.focusMessageInput()
  121. return nil
  122. case cv.cfg.Keys.FocusPrevious:
  123. cv.focusPrevious()
  124. return nil
  125. case cv.cfg.Keys.FocusNext:
  126. cv.focusNext()
  127. return nil
  128. case cv.cfg.Keys.Logout:
  129. app.quit()
  130. if err := keyring.DeleteToken(); err != nil {
  131. slog.Error("failed to delete token from keyring", "err", err)
  132. return nil
  133. }
  134. return nil
  135. case cv.cfg.Keys.ToggleGuildsTree:
  136. cv.toggleGuildsTree()
  137. return nil
  138. }
  139. return event
  140. }
  141. func (cv *chatView) showConfirmModal(prompt string, buttons []string, onDone func(label string)) {
  142. previousFocus := cv.app.GetFocus()
  143. modal := tview.NewModal().
  144. SetText(prompt).
  145. AddButtons(buttons).
  146. SetDoneFunc(func(_ int, buttonLabel string) {
  147. cv.RemovePage(confirmModalPageName).SwitchToPage(flexPageName)
  148. cv.app.SetFocus(previousFocus)
  149. if onDone != nil {
  150. onDone(buttonLabel)
  151. }
  152. })
  153. cv.
  154. AddAndSwitchToPage(confirmModalPageName, ui.Centered(modal, 0, 0), true).
  155. ShowPage(flexPageName)
  156. }