|
|
@@ -10,6 +10,7 @@ import (
|
|
|
"github.com/ayn2op/tview/keybind"
|
|
|
"github.com/ayn2op/tview/picker"
|
|
|
"github.com/diamondburned/arikawa/v3/discord"
|
|
|
+ "github.com/gdamore/tcell/v3"
|
|
|
)
|
|
|
|
|
|
const emojiPickerLayerName = "emojiPicker"
|
|
|
@@ -18,22 +19,33 @@ 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}
|
|
|
+ ep := &emojiPicker{
|
|
|
+ Model: picker.NewModel(),
|
|
|
+ chatView: chatView,
|
|
|
+ favorites: loadEmojiFavorites(),
|
|
|
+ }
|
|
|
ConfigurePicker(ep.Model, cfg, "Emoji")
|
|
|
return ep
|
|
|
}
|
|
|
|
|
|
-func (ep *emojiPicker) resetBrowse() { ep.browseMode = false }
|
|
|
+// resetBrowse starts the emoji picker in browse (scroll) mode by default.
|
|
|
+func (ep *emojiPicker) resetBrowse() { ep.browseMode = true; ep.cursor = 0 }
|
|
|
|
|
|
-// commonEmoji returns a list of frequently used unicode emoji for the picker.
|
|
|
var commonEmoji = []struct {
|
|
|
emoji string
|
|
|
names string
|
|
|
@@ -81,7 +93,6 @@ var commonEmoji = []struct {
|
|
|
{"😮", "open_mouth surprised"},
|
|
|
{"😱", "scream shocked"},
|
|
|
{"😈", "smiling_imp devil"},
|
|
|
- {"💀", "skull"},
|
|
|
{"🤡", "clown clown_face"},
|
|
|
{"🫡", "salute saluting_face"},
|
|
|
{"🫠", "melting_face"},
|
|
|
@@ -91,17 +102,18 @@ var commonEmoji = []struct {
|
|
|
}
|
|
|
|
|
|
func (ep *emojiPicker) update() {
|
|
|
- items := make(picker.Items, 0, len(commonEmoji)+50)
|
|
|
+ items := make(picker.Items, 0, maxFavoriteEmoji+len(commonEmoji)+50)
|
|
|
|
|
|
+ allItems := make(picker.Items, 0, len(commonEmoji)+50)
|
|
|
for _, e := range commonEmoji {
|
|
|
- items = append(items, picker.Item{
|
|
|
+ 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),
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- // Add guild custom emoji if available
|
|
|
+ // Guild custom emoji
|
|
|
selectedChannel := ep.chatView.SelectedChannel()
|
|
|
if selectedChannel != nil && selectedChannel.GuildID.IsValid() {
|
|
|
emojis, err := ep.chatView.state.Cabinet.Emojis(selectedChannel.GuildID)
|
|
|
@@ -110,7 +122,7 @@ func (ep *emojiPicker) update() {
|
|
|
if !emoji.Available {
|
|
|
continue
|
|
|
}
|
|
|
- items = append(items, picker.Item{
|
|
|
+ allItems = append(allItems, picker.Item{
|
|
|
Text: ":" + emoji.Name + ":",
|
|
|
FilterText: emoji.Name,
|
|
|
Reference: emoji.APIString(),
|
|
|
@@ -119,7 +131,45 @@ func (ep *emojiPicker) update() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 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 {
|
|
|
@@ -134,8 +184,12 @@ func indexOf(s string, c byte) int {
|
|
|
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() }); handled {
|
|
|
- return cmd
|
|
|
+ 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)
|
|
|
@@ -160,6 +214,42 @@ func (ep *emojiPicker) HandleEvent(event tview.Event) tview.Command {
|
|
|
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}
|