| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240 |
- 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
- 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) 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)
- }
|