package chat import ( "fmt" "log/slog" "strings" "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 // allItems is cached on picker open; only favorites change on toggle. allItems []picker.Item targetMessageID discord.MessageID targetChannelID discord.ChannelID } 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 } 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"}, } // update loads all emoji items (cached) and rebuilds with favorites. func (ep *emojiPicker) update() { if ep.allItems == nil { ep.loadItems() } ep.rebuildWithFavorites() } // loadItems builds the static emoji list (common + guild). Called once per picker open. func (ep *emojiPicker) loadItems() { items := make(picker.Items, 0, len(commonEmoji)+50) for _, e := range commonEmoji { firstName := e.names if i := strings.IndexByte(e.names, ' '); i != -1 { firstName = e.names[:i] } items = append(items, picker.Item{ Text: e.emoji + " " + firstName, FilterText: e.names, Reference: discord.APIEmoji(e.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 } items = append(items, picker.Item{ Text: ":" + emoji.Name + ":", FilterText: emoji.Name, Reference: emoji.APIString(), }) } } } ep.allItems = items } // rebuildWithFavorites prepends favorites to the cached item list. func (ep *emojiPicker) rebuildWithFavorites() { byKey := make(map[string]picker.Item, len(ep.allItems)) for _, item := range ep.allItems { byKey[emojiItemKey(item)] = item } items := make(picker.Items, 0, maxFavoriteEmoji+len(ep.allItems)) 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 ep.allItems { if _, isFav := favSet[emojiItemKey(item)]; !isFav { items = append(items, item) } } ep.Model.SetItems(items) } func emojiItemKey(item picker.Item) string { if ref, ok := item.Reference.(discord.APIEmoji); ok { return string(ref) } return "" } func (ep *emojiPicker) HandleEvent(event tview.Event) tview.Command { switch event := event.(type) { case *tview.KeyEvent: if cmd, handled := pickerBrowseHandleKey( event, &ep.browseMode, ep.Model, func() { ep.chatView.closeEmojiPicker() }, ep.handleFavoriteKey, ); handled { return cmd } case *picker.SelectedEvent: apiEmoji, ok := event.Reference.(discord.APIEmoji) if !ok { return nil } channelID := ep.targetChannelID messageID := ep.targetMessageID // Check if user already reacted with this emoji. alreadyReacted := false if msg, err := ep.chatView.state.Cabinet.Message(channelID, messageID); err == nil { for _, r := range msg.Reactions { if r.Me && r.Emoji.APIString() == apiEmoji { alreadyReacted = true break } } } ep.chatView.closeEmojiPicker() return func() tview.Event { if alreadyReacted { if err := ep.chatView.state.Unreact(channelID, messageID, apiEmoji); err != nil { slog.Error("failed to remove reaction", "err", err, "emoji", fmt.Sprint(apiEmoji)) } } else { if err := ep.chatView.state.React(channelID, messageID, apiEmoji); err != nil { slog.Error("failed to add reaction", "err", err, "emoji", fmt.Sprint(apiEmoji)) } } if cmd := ep.chatView.focusMessagesList(); cmd != nil { return cmd() } return nil } case *picker.CancelEvent: ep.chatView.closeEmojiPicker() return nil } return ep.Model.HandleEvent(event) } func (ep *emojiPicker) handleFavoriteKey(event *tview.KeyEvent) (tview.Command, bool) { if event.Key() != tcell.KeyRune || event.Str() != "f" { return nil, false } // Trigger a select to get the highlighted item's reference without // dispatching the event through the normal loop. cmd := ep.Model.HandleEvent(tcell.NewEventKey(tcell.KeyEnter, "", tcell.ModNone)) if cmd == nil { return nil, true } if sel, ok := cmd().(*picker.SelectedEvent); ok { key := emojiItemKey(sel.Item) if key != "" { ep.favorites.toggle(key) ep.update() } } return nil, true } func (ep *emojiPicker) ShortHelp() []keybind.Keybind { return pickerShortHelp(ep.chatView.cfg.Keybinds.Picker) } func (ep *emojiPicker) FullHelp() [][]keybind.Keybind { return pickerFullHelp(ep.chatView.cfg.Keybinds.Picker) }