emoji_picker.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. package chat
  2. import (
  3. "fmt"
  4. "log/slog"
  5. "github.com/ayn2op/discordo/internal/config"
  6. "github.com/ayn2op/tview"
  7. "github.com/ayn2op/tview/help"
  8. "github.com/ayn2op/tview/keybind"
  9. "github.com/ayn2op/tview/picker"
  10. "github.com/diamondburned/arikawa/v3/discord"
  11. "github.com/gdamore/tcell/v3"
  12. )
  13. const emojiPickerLayerName = "emojiPicker"
  14. type emojiPicker struct {
  15. *picker.Model
  16. chatView *Model
  17. browseMode bool
  18. favorites *emojiFavorites
  19. // items mirrors what was passed to SetItems, so we can look up by cursor.
  20. items []picker.Item
  21. targetMessageID discord.MessageID
  22. targetChannelID discord.ChannelID
  23. // cursor tracks the browse-mode position (0-based into items).
  24. cursor int
  25. }
  26. var _ help.KeyMap = (*emojiPicker)(nil)
  27. func newEmojiPicker(cfg *config.Config, chatView *Model) *emojiPicker {
  28. ep := &emojiPicker{
  29. Model: picker.NewModel(),
  30. chatView: chatView,
  31. favorites: loadEmojiFavorites(),
  32. }
  33. ConfigurePicker(ep.Model, cfg, "Emoji")
  34. return ep
  35. }
  36. // resetBrowse starts the emoji picker in browse (scroll) mode by default.
  37. func (ep *emojiPicker) resetBrowse() { ep.browseMode = true; ep.cursor = 0 }
  38. var commonEmoji = []struct {
  39. emoji string
  40. names string
  41. }{
  42. {"👍", "thumbsup thumbs_up +1 like"},
  43. {"👎", "thumbsdown thumbs_down -1 dislike"},
  44. {"❤️", "heart love red_heart"},
  45. {"😂", "joy laughing tears"},
  46. {"😭", "sob crying"},
  47. {"😊", "blush smile happy"},
  48. {"😍", "heart_eyes love"},
  49. {"🤣", "rofl rolling"},
  50. {"🙂", "slightly_smiling_face"},
  51. {"😔", "pensive sad"},
  52. {"🔥", "fire lit hot"},
  53. {"✅", "white_check_mark check yes"},
  54. {"❌", "x cross no"},
  55. {"👀", "eyes look"},
  56. {"🎉", "tada party celebration"},
  57. {"💀", "skull dead"},
  58. {"🤔", "thinking hmm"},
  59. {"😐", "neutral_face"},
  60. {"🤷", "shrug"},
  61. {"👋", "wave hello hi"},
  62. {"🙏", "pray please thanks"},
  63. {"💯", "100 hundred perfect"},
  64. {"😎", "sunglasses cool"},
  65. {"🥳", "partying_face party"},
  66. {"😢", "cry sad"},
  67. {"😅", "sweat_smile nervous"},
  68. {"🤗", "hugs hugging"},
  69. {"😤", "triumph angry"},
  70. {"🥺", "pleading_face"},
  71. {"💪", "muscle strong flex"},
  72. {"✨", "sparkles stars"},
  73. {"💜", "purple_heart"},
  74. {"💙", "blue_heart"},
  75. {"💚", "green_heart"},
  76. {"🧡", "orange_heart"},
  77. {"💛", "yellow_heart"},
  78. {"🖤", "black_heart"},
  79. {"🤍", "white_heart"},
  80. {"⭐", "star"},
  81. {"🌟", "star2 glowing_star"},
  82. {"😮", "open_mouth surprised"},
  83. {"😱", "scream shocked"},
  84. {"😈", "smiling_imp devil"},
  85. {"🤡", "clown clown_face"},
  86. {"🫡", "salute saluting_face"},
  87. {"🫠", "melting_face"},
  88. {"💤", "zzz sleep"},
  89. {"🤝", "handshake"},
  90. {"🏳️", "white_flag surrender"},
  91. }
  92. func (ep *emojiPicker) update() {
  93. items := make(picker.Items, 0, maxFavoriteEmoji+len(commonEmoji)+50)
  94. allItems := make(picker.Items, 0, len(commonEmoji)+50)
  95. for _, e := range commonEmoji {
  96. allItems = append(allItems, picker.Item{
  97. Text: e.emoji + " " + e.names[:min(len(e.names), indexOf(e.names, ' '))],
  98. FilterText: e.names,
  99. Reference: discord.APIEmoji(e.emoji),
  100. })
  101. }
  102. // Guild custom emoji
  103. selectedChannel := ep.chatView.SelectedChannel()
  104. if selectedChannel != nil && selectedChannel.GuildID.IsValid() {
  105. emojis, err := ep.chatView.state.Cabinet.Emojis(selectedChannel.GuildID)
  106. if err == nil {
  107. for _, emoji := range emojis {
  108. if !emoji.Available {
  109. continue
  110. }
  111. allItems = append(allItems, picker.Item{
  112. Text: ":" + emoji.Name + ":",
  113. FilterText: emoji.Name,
  114. Reference: emoji.APIString(),
  115. })
  116. }
  117. }
  118. }
  119. // Index for favorite lookup.
  120. byKey := make(map[string]picker.Item, len(allItems))
  121. for _, item := range allItems {
  122. byKey[emojiItemKey(item)] = item
  123. }
  124. // Prepend favorites.
  125. favSet := make(map[string]struct{})
  126. for _, key := range ep.favorites.list() {
  127. if item, ok := byKey[key]; ok {
  128. items = append(items, picker.Item{
  129. Text: "★ " + item.Text,
  130. FilterText: "favorite " + item.FilterText,
  131. Reference: item.Reference,
  132. })
  133. favSet[key] = struct{}{}
  134. }
  135. }
  136. for _, item := range allItems {
  137. if _, isFav := favSet[emojiItemKey(item)]; !isFav {
  138. items = append(items, item)
  139. }
  140. }
  141. ep.items = items
  142. ep.Model.SetItems(items)
  143. // Clamp cursor.
  144. if ep.cursor >= len(items) {
  145. ep.cursor = max(len(items)-1, 0)
  146. }
  147. }
  148. func emojiItemKey(item picker.Item) string {
  149. if ref, ok := item.Reference.(discord.APIEmoji); ok {
  150. return string(ref)
  151. }
  152. return ""
  153. }
  154. func indexOf(s string, c byte) int {
  155. for i := range len(s) {
  156. if s[i] == c {
  157. return i
  158. }
  159. }
  160. return len(s)
  161. }
  162. func (ep *emojiPicker) HandleEvent(event tview.Event) tview.Command {
  163. switch event := event.(type) {
  164. case *tview.KeyEvent:
  165. if ep.browseMode {
  166. return ep.handleBrowseKey(event)
  167. }
  168. if event.Key() == tcell.KeyEsc {
  169. ep.browseMode = true
  170. return nil
  171. }
  172. case *picker.SelectedEvent:
  173. apiEmoji, ok := event.Reference.(discord.APIEmoji)
  174. if !ok {
  175. return nil
  176. }
  177. channelID := ep.targetChannelID
  178. messageID := ep.targetMessageID
  179. ep.chatView.closeEmojiPicker()
  180. return func() tview.Event {
  181. if err := ep.chatView.state.React(channelID, messageID, apiEmoji); err != nil {
  182. slog.Error("failed to add reaction", "err", err, "emoji", fmt.Sprint(apiEmoji))
  183. }
  184. return nil
  185. }
  186. case *picker.CancelEvent:
  187. ep.chatView.closeEmojiPicker()
  188. return nil
  189. }
  190. return ep.Model.HandleEvent(event)
  191. }
  192. func (ep *emojiPicker) handleBrowseKey(event *tview.KeyEvent) tview.Command {
  193. switch {
  194. case event.Key() == tcell.KeyEsc:
  195. ep.browseMode = false
  196. ep.chatView.closeEmojiPicker()
  197. return nil
  198. case event.Key() == tcell.KeyRune && event.Str() == "i":
  199. ep.browseMode = false
  200. return nil
  201. case event.Key() == tcell.KeyRune && event.Str() == "j":
  202. ep.cursor = min(ep.cursor+1, max(len(ep.items)-1, 0))
  203. return ep.Model.HandleEvent(tcell.NewEventKey(tcell.KeyCtrlN, "", tcell.ModCtrl))
  204. case event.Key() == tcell.KeyRune && event.Str() == "k":
  205. ep.cursor = max(ep.cursor-1, 0)
  206. return ep.Model.HandleEvent(tcell.NewEventKey(tcell.KeyCtrlP, "", tcell.ModCtrl))
  207. case event.Key() == tcell.KeyRune && event.Str() == "g":
  208. ep.cursor = 0
  209. return ep.Model.HandleEvent(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone))
  210. case event.Key() == tcell.KeyRune && event.Str() == "G":
  211. ep.cursor = max(len(ep.items)-1, 0)
  212. return ep.Model.HandleEvent(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone))
  213. case event.Key() == tcell.KeyRune && event.Str() == "f":
  214. if ep.cursor >= 0 && ep.cursor < len(ep.items) {
  215. key := emojiItemKey(ep.items[ep.cursor])
  216. if key != "" {
  217. ep.favorites.toggle(key)
  218. ep.update()
  219. }
  220. }
  221. return nil
  222. case event.Key() == tcell.KeyEnter:
  223. return ep.Model.HandleEvent(event)
  224. }
  225. return nil
  226. }
  227. func (ep *emojiPicker) ShortHelp() []keybind.Keybind {
  228. cfg := ep.chatView.cfg.Keybinds.Picker
  229. return []keybind.Keybind{cfg.Up.Keybind, cfg.Down.Keybind, cfg.Select.Keybind, cfg.Cancel.Keybind}
  230. }
  231. func (ep *emojiPicker) FullHelp() [][]keybind.Keybind {
  232. cfg := ep.chatView.cfg.Keybinds.Picker
  233. return [][]keybind.Keybind{
  234. {cfg.Up.Keybind, cfg.Down.Keybind, cfg.Top.Keybind, cfg.Bottom.Keybind},
  235. {cfg.Select.Keybind, cfg.Cancel.Keybind},
  236. }
  237. }