| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264 |
- package chat
- import (
- "fmt"
- "log/slog"
- "github.com/ayn2op/discordo/internal/config"
- "github.com/ayn2op/tview"
- "github.com/ayn2op/tview/help"
- "github.com/ayn2op/tview/keybind"
- "github.com/ayn2op/tview/picker"
- "github.com/diamondburned/arikawa/v3/discord"
- "github.com/gdamore/tcell/v3"
- )
- const emojiPickerLayerName = "emojiPicker"
- type emojiPicker struct {
- *picker.Model
- chatView *Model
- browseMode bool
- favorites *emojiFavorites
- // items mirrors what was passed to SetItems, so we can look up by cursor.
- items []picker.Item
- targetMessageID discord.MessageID
- targetChannelID discord.ChannelID
- // cursor tracks the browse-mode position (0-based into items).
- cursor int
- }
- var _ help.KeyMap = (*emojiPicker)(nil)
- func newEmojiPicker(cfg *config.Config, chatView *Model) *emojiPicker {
- ep := &emojiPicker{
- Model: picker.NewModel(),
- chatView: chatView,
- favorites: loadEmojiFavorites(),
- }
- ConfigurePicker(ep.Model, cfg, "Emoji")
- return ep
- }
- // resetBrowse starts the emoji picker in browse (scroll) mode by default.
- func (ep *emojiPicker) resetBrowse() { ep.browseMode = true; ep.cursor = 0 }
- var commonEmoji = []struct {
- emoji string
- names string
- }{
- {"👍", "thumbsup thumbs_up +1 like"},
- {"👎", "thumbsdown thumbs_down -1 dislike"},
- {"❤️", "heart love red_heart"},
- {"😂", "joy laughing tears"},
- {"😭", "sob crying"},
- {"😊", "blush smile happy"},
- {"😍", "heart_eyes love"},
- {"🤣", "rofl rolling"},
- {"🙂", "slightly_smiling_face"},
- {"😔", "pensive sad"},
- {"🔥", "fire lit hot"},
- {"✅", "white_check_mark check yes"},
- {"❌", "x cross no"},
- {"👀", "eyes look"},
- {"🎉", "tada party celebration"},
- {"💀", "skull dead"},
- {"🤔", "thinking hmm"},
- {"😐", "neutral_face"},
- {"🤷", "shrug"},
- {"👋", "wave hello hi"},
- {"🙏", "pray please thanks"},
- {"💯", "100 hundred perfect"},
- {"😎", "sunglasses cool"},
- {"🥳", "partying_face party"},
- {"😢", "cry sad"},
- {"😅", "sweat_smile nervous"},
- {"🤗", "hugs hugging"},
- {"😤", "triumph angry"},
- {"🥺", "pleading_face"},
- {"💪", "muscle strong flex"},
- {"✨", "sparkles stars"},
- {"💜", "purple_heart"},
- {"💙", "blue_heart"},
- {"💚", "green_heart"},
- {"🧡", "orange_heart"},
- {"💛", "yellow_heart"},
- {"🖤", "black_heart"},
- {"🤍", "white_heart"},
- {"⭐", "star"},
- {"🌟", "star2 glowing_star"},
- {"😮", "open_mouth surprised"},
- {"😱", "scream shocked"},
- {"😈", "smiling_imp devil"},
- {"🤡", "clown clown_face"},
- {"🫡", "salute saluting_face"},
- {"🫠", "melting_face"},
- {"💤", "zzz sleep"},
- {"🤝", "handshake"},
- {"🏳️", "white_flag surrender"},
- }
- func (ep *emojiPicker) update() {
- items := make(picker.Items, 0, maxFavoriteEmoji+len(commonEmoji)+50)
- allItems := make(picker.Items, 0, len(commonEmoji)+50)
- for _, e := range commonEmoji {
- allItems = append(allItems, picker.Item{
- Text: e.emoji + " " + e.names[:min(len(e.names), indexOf(e.names, ' '))],
- FilterText: e.names,
- Reference: discord.APIEmoji(e.emoji),
- })
- }
- // Guild custom emoji
- selectedChannel := ep.chatView.SelectedChannel()
- if selectedChannel != nil && selectedChannel.GuildID.IsValid() {
- emojis, err := ep.chatView.state.Cabinet.Emojis(selectedChannel.GuildID)
- if err == nil {
- for _, emoji := range emojis {
- if !emoji.Available {
- continue
- }
- allItems = append(allItems, picker.Item{
- Text: ":" + emoji.Name + ":",
- FilterText: emoji.Name,
- Reference: emoji.APIString(),
- })
- }
- }
- }
- // Index for favorite lookup.
- byKey := make(map[string]picker.Item, len(allItems))
- for _, item := range allItems {
- byKey[emojiItemKey(item)] = item
- }
- // Prepend favorites.
- favSet := make(map[string]struct{})
- for _, key := range ep.favorites.list() {
- if item, ok := byKey[key]; ok {
- items = append(items, picker.Item{
- Text: "★ " + item.Text,
- FilterText: "favorite " + item.FilterText,
- Reference: item.Reference,
- })
- favSet[key] = struct{}{}
- }
- }
- for _, item := range allItems {
- if _, isFav := favSet[emojiItemKey(item)]; !isFav {
- items = append(items, item)
- }
- }
- ep.items = items
- ep.Model.SetItems(items)
- // Clamp cursor.
- if ep.cursor >= len(items) {
- ep.cursor = max(len(items)-1, 0)
- }
- }
- func emojiItemKey(item picker.Item) string {
- if ref, ok := item.Reference.(discord.APIEmoji); ok {
- return string(ref)
- }
- return ""
- }
- func indexOf(s string, c byte) int {
- for i := range len(s) {
- if s[i] == c {
- return i
- }
- }
- return len(s)
- }
- func (ep *emojiPicker) HandleEvent(event tview.Event) tview.Command {
- switch event := event.(type) {
- case *tview.KeyEvent:
- if ep.browseMode {
- return ep.handleBrowseKey(event)
- }
- if event.Key() == tcell.KeyEsc {
- ep.browseMode = true
- return nil
- }
- case *picker.SelectedEvent:
- apiEmoji, ok := event.Reference.(discord.APIEmoji)
- if !ok {
- return nil
- }
- channelID := ep.targetChannelID
- messageID := ep.targetMessageID
- ep.chatView.closeEmojiPicker()
- return func() tview.Event {
- if err := ep.chatView.state.React(channelID, messageID, apiEmoji); err != nil {
- slog.Error("failed to add reaction", "err", err, "emoji", fmt.Sprint(apiEmoji))
- }
- return nil
- }
- case *picker.CancelEvent:
- ep.chatView.closeEmojiPicker()
- return nil
- }
- return ep.Model.HandleEvent(event)
- }
- func (ep *emojiPicker) handleBrowseKey(event *tview.KeyEvent) tview.Command {
- switch {
- case event.Key() == tcell.KeyEsc:
- ep.browseMode = false
- ep.chatView.closeEmojiPicker()
- return nil
- case event.Key() == tcell.KeyRune && event.Str() == "i":
- ep.browseMode = false
- return nil
- case event.Key() == tcell.KeyRune && event.Str() == "j":
- ep.cursor = min(ep.cursor+1, max(len(ep.items)-1, 0))
- return ep.Model.HandleEvent(tcell.NewEventKey(tcell.KeyCtrlN, "", tcell.ModCtrl))
- case event.Key() == tcell.KeyRune && event.Str() == "k":
- ep.cursor = max(ep.cursor-1, 0)
- return ep.Model.HandleEvent(tcell.NewEventKey(tcell.KeyCtrlP, "", tcell.ModCtrl))
- case event.Key() == tcell.KeyRune && event.Str() == "g":
- ep.cursor = 0
- return ep.Model.HandleEvent(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone))
- case event.Key() == tcell.KeyRune && event.Str() == "G":
- ep.cursor = max(len(ep.items)-1, 0)
- return ep.Model.HandleEvent(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone))
- case event.Key() == tcell.KeyRune && event.Str() == "f":
- if ep.cursor >= 0 && ep.cursor < len(ep.items) {
- key := emojiItemKey(ep.items[ep.cursor])
- if key != "" {
- ep.favorites.toggle(key)
- ep.update()
- }
- }
- return nil
- case event.Key() == tcell.KeyEnter:
- return ep.Model.HandleEvent(event)
- }
- return nil
- }
- func (ep *emojiPicker) ShortHelp() []keybind.Keybind {
- cfg := ep.chatView.cfg.Keybinds.Picker
- return []keybind.Keybind{cfg.Up.Keybind, cfg.Down.Keybind, cfg.Select.Keybind, cfg.Cancel.Keybind}
- }
- func (ep *emojiPicker) FullHelp() [][]keybind.Keybind {
- cfg := ep.chatView.cfg.Keybinds.Picker
- return [][]keybind.Keybind{
- {cfg.Up.Keybind, cfg.Down.Keybind, cfg.Top.Keybind, cfg.Bottom.Keybind},
- {cfg.Select.Keybind, cfg.Cancel.Keybind},
- }
- }
|