emoji_picker.go 5.8 KB

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