emoji_picker.go 6.1 KB

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