Implement Emoji Picker (#17195)

This commit adds Emoji Picker in the app for usage in Message Composer and Wallet Account.

---------

Signed-off-by: Mohamed Javid <19339952+smohamedjavid@users.noreply.github.com>
This commit is contained in:
Mohamed Javid 2023-09-14 01:38:13 +08:00 committed by GitHub
parent dab4d953ec
commit 0003800f05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 526 additions and 28 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -420,6 +420,8 @@
"react-native-transparent-video" react-native-transparent-video
"react-native-orientation-locker" react-native-orientation-locker
"react-native-gifted-charts" react-native-gifted-charts
"../resources/data/emojis/en.json" (js/JSON.parse (slurp
"./resources/data/emojis/en.json"))
"../src/js/worklets/core.js" worklet-factory
"../src/js/worklets/shell/bottom_tabs.js" #js {}
"../src/js/worklets/shell/home_stack.js" #js {}

View File

@ -1,6 +1,10 @@
(ns quo2.components.dividers.divider-label.style
(:require [quo2.foundations.colors :as colors]))
(defn get-height
[tight?]
(if tight? 34 42))
(defn- get-border-color
[blur? theme]
(colors/theme-colors (if blur? colors/neutral-80-opa-5 colors/neutral-10)
@ -17,6 +21,7 @@
[blur? tight? chevron theme]
{:border-top-width 1
:border-top-color (get-border-color blur? theme)
:height (get-height tight?)
:padding-top (if tight? 6 14)
:padding-bottom 7
:padding-left (if (= :left chevron) 16 20)

View File

@ -1,8 +1,10 @@
(ns quo2.components.profile.showcase-nav.style
(:require [quo2.foundations.colors :as colors]))
(def height 56)
(def root-container
{:height 56})
{:height height})
(defn container
[state theme]

View File

@ -5,7 +5,7 @@
[react-native.core :as rn]))
(defn- render-button
[{:keys [icon id]} _ _ {:keys [state on-press active-id]}]
[{:keys [icon id]} index _ {:keys [state on-press active-id]}]
(let [active? (= id active-id)
button-type (if active? :grey :ghost)
scroll-state? (= state :scroll)]
@ -17,21 +17,21 @@
:icon-only? true
:on-press (fn []
(when on-press
(on-press id)))
(on-press id index)))
:container-style style/button-container}
icon]))
(defn- view-internal
[{:keys [theme container-style default-active state data on-press active-id]}]
[rn/view
{:style style/root-container
{:style (merge style/root-container container-style)
:accessibility-label :showcase-nav}
[rn/flat-list
{:data data
:key-fn str
:key-fn :id
:horizontal true
:shows-horizontal-scroll-indicator false
:content-container-style (merge (style/container state theme) container-style)
:content-container-style (style/container state theme)
:render-fn render-button
:render-data {:state state
:on-press on-press

View File

@ -6,15 +6,15 @@
(defn- wrap-render-fn
[f render-data]
(fn [data]
(reagent/as-element [f (.-item ^js data) (.-index ^js data)
(.-separators ^js data) render-data
(.-isActive ^js data) (.-drag ^js data)])))
(fn [^js data]
(reagent/as-element [f (.-item data) (.-index data)
(.-separators data) render-data
(.-isActive data) (.-drag data)])))
(defn- wrap-on-drag-end-fn
[f]
(fn [data]
(f (.-from ^js data) (.-to ^js data) (.-data ^js data))))
(fn [^js data]
(f (.-from data) (.-to data) (.-data data))))
(defn- wrap-key-fn
[f]

View File

@ -0,0 +1,22 @@
(ns status-im2.contexts.emoji-picker.constants
(:require [react-native.core :as rn]))
(def ^:const default-category :people)
(def ^:const search-debounce-ms 600)
(def ^:const emojis-per-row 7)
(def ^:const emoji-size 32)
(def ^:const emoji-row-padding-horizontal 20)
(def ^:const emoji-section-header-margin-bottom 6)
(def ^:const emoji-row-separator-height 16)
(def ^:const emoji-item-margin-right
(/ (- (:width (rn/get-window)) (* emoji-row-padding-horizontal 2) (* emoji-size emojis-per-row))
(- emojis-per-row 1)))
(def ^:const item-height (+ emoji-size emoji-row-separator-height))

View File

@ -0,0 +1,103 @@
(ns status-im2.contexts.emoji-picker.data
(:require [status-im2.contexts.emoji-picker.constants :as constants]
[utils.transforms :as transforms]))
;; Emoji data is pulled from the `emojibase` (https://emojibase.dev).
;;
;; The dataset constains `group` key which holds a number to reduce the file size.
;; It is then categorized below:
;; - smileys-emotion (0)
;; - people-body (1)
;; - animals-nature (3)
;; - food-drink (4)
;; - activity (6)
;; - travel-places (5)
;; - objects (7)
;; - symbols (8)
;; - flags (9)
;;
;; NOTE:
;; - smileys & emoticons (0) and people (1) are grouped in one section in the app
;; - The emoji components (https://symbl.cc/en/emoji/component) which are group 2 are not used
;; in the app and are removed from the dataset.
(def ^:const emoji-data (transforms/js->clj (js/require "../resources/data/emojis/en.json")))
(def ^:const group-smileys-emotion 0)
(def ^:const group-people-body 1)
(def ^:const group-animals-nature 3)
(def ^:const group-food-drink 4)
(def ^:const group-travel-places 5)
(def ^:const group-activity 6)
(def ^:const group-objects 7)
(def ^:const group-symbols 8)
(def ^:const group-flags 9)
(def ^:const categories
[{:title :t/emoji-people ;; 0 and 1
:icon :i/faces
:id :people
:data []}
{:title :t/emoji-nature ;; 3
:icon :i/nature
:id :nature
:data []}
{:title :t/emoji-food ;; 4
:icon :i/food
:id :food
:data []}
{:title :t/emoji-activity ;; 6
:icon :i/activity
:id :activity
:data []}
{:title :t/emoji-travel ;; 5
:icon :i/travel
:id :travel
:data []}
{:title :t/emoji-objects ;; 7
:icon :i/objects
:id :objects
:data []}
{:title :t/emoji-symbols ;; 8
:icon :i/symbols
:id :symbols
:data []}
{:title :t/emoji-flags ;; 9
:icon :i/flags
:id :flags
:data []}])
(defn emoji-group->category
[group]
(condp = group
group-smileys-emotion {:index 0 :id :people}
group-people-body {:index 0 :id :people}
group-animals-nature {:index 1 :id :nature}
group-food-drink {:index 2 :id :food}
group-travel-places {:index 4 :id :travel}
group-activity {:index 3 :id :activity}
group-objects {:index 5 :id :objects}
group-symbols {:index 6 :id :symbols}
group-flags {:index 7 :id :flags}
nil))
(def ^:private categorized-and-partitioned
(->> emoji-data
(reduce (fn [acc {:keys [group] :as emoji}]
(update-in acc [(-> (emoji-group->category group) :index) :data] conj emoji))
categories)
(reduce (fn [acc {:keys [data] :as item}]
(conj acc (assoc item :data (partition-all constants/emojis-per-row data))))
[])))
(def ^:const flatten-data
(mapcat (fn [{:keys [title id data]}]
(into [{:title title :id id :header? true}] data))
categorized-and-partitioned))
(def ^:private filter-section-header-index
(keep-indexed #(when (:header? %2) %1) flatten-data))
(defn get-section-header-index-in-data
[index]
(nth filter-section-header-index index))

View File

@ -0,0 +1,7 @@
(ns status-im2.contexts.emoji-picker.events
(:require [utils.re-frame :as rf]))
(rf/defn open-emoji-picker
{:events [:emoji-picker/open]}
[_ {:keys [on-select]}]
{:dispatch [:open-modal :emoji-picker {:on-select on-select}]})

View File

@ -0,0 +1,53 @@
(ns status-im2.contexts.emoji-picker.style
(:require [quo2.components.profile.showcase-nav.style :as showcase-nav.style]
[quo2.foundations.colors :as colors]
[react-native.safe-area :as safe-area]
[status-im2.contexts.emoji-picker.constants :as constants]))
(def flex-spacer {:flex 1})
(def category-nav-height (+ (safe-area/get-bottom) showcase-nav.style/height))
(def search-input-container
{:padding-horizontal 20
:padding-bottom 12})
(defn section-header
[theme]
{:background-color (colors/theme-colors colors/white colors/neutral-95 theme)
:z-index 1
:margin-bottom constants/emoji-section-header-margin-bottom})
(def emoji-row-container
{:padding-horizontal constants/emoji-row-padding-horizontal
:padding-bottom constants/emoji-row-separator-height
:flex-direction :row
:overflow :hidden})
(defn emoji-container
[last-item-on-row?]
(cond-> {:height constants/emoji-size
:width constants/emoji-size
:margin-right constants/emoji-item-margin-right}
last-item-on-row?
(dissoc :margin-right)))
(def list-container
{:padding-bottom showcase-nav.style/height})
(def empty-results
{:margin-top 100})
(def category-container
{:height category-nav-height
:overflow :hidden
:position :absolute
:left 0
:right 0
:bottom 0
:z-index 1})
(def category-blur-container
{:height category-nav-height
:overflow :hidden})

View File

@ -0,0 +1,17 @@
(ns status-im2.contexts.emoji-picker.utils
(:require [clojure.string :as string]
[status-im2.contexts.emoji-picker.constants :as constants]
[status-im2.contexts.emoji-picker.data :refer [emoji-data]]))
(defn search-emoji
[search-query]
(let [cleaned-query (string/lower-case (string/trim search-query))]
(->> (filter (fn [{:keys [label tags emoticon]}]
(or (string/includes? label cleaned-query)
(when emoticon
(if (vector? emoticon)
(some #(string/includes? (string/lower-case %) cleaned-query) emoticon)
(string/includes? (string/lower-case emoticon) cleaned-query)))
(some #(string/includes? % cleaned-query) tags)))
emoji-data)
(partition-all constants/emojis-per-row))))

View File

@ -0,0 +1,41 @@
(ns status-im2.contexts.emoji-picker.utils-test
(:require [cljs.test :refer [deftest is testing]]
[status-im2.contexts.emoji-picker.utils :as utils]))
(deftest emoji-search-test
(testing "search for emojis with name"
(let [search-input "dolphin"
expected `(({:group 3
:hexcode "1f42c"
:label "dolphin"
:tags ["flipper"]
:unicode "🐬"}))]
(is (= expected (utils/search-emoji search-input)))))
(testing "search for emojis with emoticon"
(let [search-input "<3"
expected `(({:group 0
:hexcode "2764"
:label "red heart"
:tags ["heart"]
:unicode "❤️"
:emoticon "<3"}))]
(is (= expected (utils/search-emoji search-input)))))
(testing "search for emojis with tag"
(let [search-input "tada"
expected `(({:group 6
:hexcode "1f389"
:label "party popper"
:tags ["celebration" "party" "popper" "tada"]
:unicode "🎉"}))]
(is (= expected (utils/search-emoji search-input)))))
(testing "search for emojis with tag"
(let [search-input "raven"
expected `(({:group 3
:hexcode "1f426-200d-2b1b"
:label "black bird"
:tags ["bird" "black" "crow" "raven" "rook"]
:unicode "🐦‍⬛"}))]
(is (= expected (utils/search-emoji search-input))))))

View File

@ -0,0 +1,208 @@
(ns status-im2.contexts.emoji-picker.view
(:require [clojure.string :as string]
[oops.core :as oops]
[quo2.core :as quo]
[quo2.foundations.colors :as colors]
[quo2.theme :as quo.theme]
[react-native.blur :as blur]
[react-native.core :as rn]
[react-native.gesture :as gesture]
[react-native.platform :as platform]
[reagent.core :as reagent]
[status-im2.contexts.emoji-picker.constants :as constants]
[status-im2.contexts.emoji-picker.data :as emoji-picker.data]
[status-im2.contexts.emoji-picker.style :as style]
[status-im2.contexts.emoji-picker.utils :as emoji-picker.utils]
[utils.debounce :as debounce]
[utils.i18n :as i18n]
[utils.re-frame :as rf]
[utils.transforms :as transforms]))
(defn- on-press-category
[{:keys [id index active-category scroll-ref]}]
(reset! active-category id)
(some-> ^js @scroll-ref
(.scrollToIndex #js
{:index (emoji-picker.data/get-section-header-index-in-data index)
:animated false})))
(defn- handle-on-viewable-items-changed
[{:keys [event active-category should-update-active-category?]}]
(when should-update-active-category?
(let [viewable-item (-> (oops/oget event "viewableItems")
transforms/js->clj
first
:item)
header? (and (map? viewable-item) (:header? viewable-item))
section-key (if header?
(:id viewable-item)
(:id (emoji-picker.data/emoji-group->category (-> viewable-item
first
:group))))]
(when (and (some? section-key) (not= @active-category section-key))
(reset! active-category section-key)))))
(defn- get-item-layout
[_ index]
#js
{:length constants/item-height
:offset (* constants/item-height index)
:index index})
(defn- section-header
[{:keys [title]} {:keys [theme]}]
[quo/divider-label
{:tight? false
:container-style (style/section-header theme)}
(i18n/label title)])
(defn- emoji-item
[{:keys [unicode] :as emoji} col-index on-select close]
(let [on-press (fn []
(when on-select
(on-select unicode emoji))
(close))
last-item-on-row? (= (inc col-index) constants/emojis-per-row)]
(fn []
[rn/pressable
{:style (style/emoji-container last-item-on-row?)
:on-press on-press}
[rn/text
{:style {:font-size constants/emoji-size}
:adjusts-font-size-to-fit true
:allow-font-scaling false}
unicode]])))
(defn- emoji-row
[row-data {:keys [on-select close]}]
(into [rn/view {:style style/emoji-row-container}]
(map-indexed
(fn [col-index {:keys [hexcode] :as emoji}]
^{:key hexcode}
[emoji-item emoji col-index on-select close])
row-data)))
(defn- render-item
[item _ _ render-data]
(if (:header? item)
[section-header item render-data]
[emoji-row item render-data]))
(defn- empty-result
[]
[quo/empty-state
{:title (i18n/label :t/emoji-no-results-title)
:description (i18n/label :t/emoji-no-results-description)
:placeholder? true
:container-style style/empty-results}])
(defn- render-list
[{:keys [theme filtered-data on-viewable-items-changed scroll-enabled on-scroll on-select
set-scroll-ref close]}]
(let [data (if filtered-data filtered-data emoji-picker.data/flatten-data)]
[gesture/flat-list
{:ref set-scroll-ref
:scroll-enabled @scroll-enabled
:data data
:initial-num-to-render 20
:max-to-render-per-batch 20
:render-fn render-item
:get-item-layout get-item-layout
:keyboard-dismiss-mode :on-drag
:keyboard-should-persist-taps :handled
:shows-vertical-scroll-indicator false
:on-scroll-to-index-failed identity
:empty-component [empty-result]
:on-scroll on-scroll
:render-data {:close close
:theme theme
:on-select on-select}
:content-container-style style/list-container
:viewability-config {:item-visible-percent-threshold 100
:minimum-view-time 200}
:on-viewable-items-changed on-viewable-items-changed}]))
(defn- footer
[{:keys [theme active-category scroll-ref]}]
(let [on-press (fn [id index]
(on-press-category
{:id id
:index index
:active-category active-category
:scroll-ref scroll-ref}))]
(fn []
[rn/view {:style style/category-container}
[blur/view
{:style style/category-blur-container
:blur-radius (if platform/android? 20 10)
:blur-amount (if platform/ios? 20 10)
:blur-type (quo.theme/theme-value (if platform/ios? :light :xlight) :dark theme)
:overlay-color (quo.theme/theme-value colors/white-70-blur colors/neutral-95-opa-70-blur theme)}
[quo/showcase-nav
{:state :scroll
:active-id @active-category
:data emoji-picker.data/categories
:on-press on-press}]]])))
(defn- clear
[{:keys [active-category filtered-data search-text]}]
(reset! active-category constants/default-category)
(reset! filtered-data nil)
(reset! search-text ""))
(defn- view-internal
[_]
(let [{:keys [on-select]} (rf/sub [:get-screen-params])
scroll-ref (atom nil)
set-scroll-ref #(reset! scroll-ref %)
search-text (reagent/atom "")
filtered-data (reagent/atom nil)
active-category (reagent/atom constants/default-category)
clear-states #(clear {:active-category active-category
:filtered-data filtered-data
:search-text search-text})
search-emojis (debounce/debounce
(fn []
(when (pos? (count @search-text))
(reset! filtered-data (emoji-picker.utils/search-emoji
@search-text))))
constants/search-debounce-ms)
on-change-text (fn [text]
(if (string/blank? text)
(clear-states)
(do
(reset! search-text text)
(search-emojis))))
on-viewable-items-changed (fn [event]
(handle-on-viewable-items-changed
{:event event
:active-category active-category
:should-update-active-category? (nil? @filtered-data)}))]
(fn [{:keys [theme] :as sheet-opts}]
(let [search-active? (pos? (count @search-text))]
[rn/keyboard-avoiding-view
{:style style/flex-spacer
:keyboard-vertical-offset 8}
[rn/view {:style style/flex-spacer}
[rn/view {:style style/search-input-container}
[quo/input
{:small? true
:placeholder (i18n/label :t/emoji-search-placeholder)
:icon-name :i/search
:value @search-text
:on-change-text on-change-text
:clearable? search-active?
:on-clear clear-states}]]
[render-list
(merge {:filtered-data @filtered-data
:set-scroll-ref set-scroll-ref
:on-select on-select
:on-viewable-items-changed on-viewable-items-changed}
sheet-opts)]
(when-not search-active?
[footer
{:theme theme
:active-category active-category
:scroll-ref scroll-ref}])]]))))
(def view (quo.theme/with-theme view-internal))

View File

@ -1,7 +1,8 @@
(ns status-im2.contexts.quo-preview.avatars.account-avatar
(:require [quo2.core :as quo]
[reagent.core :as reagent]
[status-im2.contexts.quo-preview.preview :as preview]))
[status-im2.contexts.quo-preview.preview :as preview]
[utils.re-frame :as rf]))
(def descriptor
[{:key :type
@ -36,5 +37,16 @@
:emoji "🍑"
:type :default})]
(fn []
[preview/preview-container {:state state :descriptor descriptor}
[quo/account-avatar @state]])))
[preview/preview-container
{:state state
:descriptor descriptor
:component-container-style {:align-items :center
:justify-content :center}}
[quo/account-avatar @state]
[quo/button
{:type :grey
:container-style {:margin-top 30}
:on-press #(rf/dispatch [:emoji-picker/open
{:on-select (fn [emoji]
(swap! state assoc :emoji emoji))}])}
"Open emoji picker"]])))

View File

@ -45,7 +45,9 @@
:type :grey
:background :photo
:icon-only? true
:on-press #(js/alert "pressed")
:on-press #(rf/dispatch [:emoji-picker/open
{:on-select (fn [selected-emoji]
(reset! emoji selected-emoji))}])
:container-style style/reaction-button-container} :i/reaction]]
[quo/title-input
{:color :red

View File

@ -10,6 +10,7 @@
status-im2.contexts.chat.events
status-im2.contexts.chat.photo-selector.events
status-im2.contexts.communities.overview.events
status-im2.contexts.emoji-picker.events
status-im2.contexts.onboarding.events
status-im2.contexts.profile.events
status-im2.contexts.shell.share.events

View File

@ -1,37 +1,38 @@
(ns status-im2.navigation.screens
(:require
[status-im.ui.screens.screens :as old-screens]
[status-im2.config :as config]
[status-im2.contexts.add-new-contact.views :as add-new-contact]
[status-im2.contexts.chat.camera.view :as camera-screen]
[status-im2.contexts.chat.group-details.view :as group-details]
[status-im2.contexts.chat.lightbox.view :as lightbox]
[status-im2.contexts.chat.messages.view :as chat]
[status-im2.contexts.chat.new-chat.view :as new-chat]
[status-im2.contexts.chat.photo-selector.view :as photo-selector]
[status-im2.contexts.chat.camera.view :as camera-screen]
[status-im2.contexts.communities.actions.request-to-join.view :as join-menu]
[status-im2.contexts.communities.discover.view :as communities.discover]
[status-im2.contexts.communities.overview.view :as communities.overview]
[status-im2.contexts.onboarding.intro.view :as intro]
[status-im2.contexts.emoji-picker.view :as emoji-picker]
[status-im2.contexts.onboarding.create-password.view :as create-password]
[status-im2.contexts.onboarding.create-profile.view :as create-profile]
[status-im2.contexts.onboarding.enable-biometrics.view :as enable-biometrics]
[status-im2.contexts.onboarding.enable-notifications.view :as enable-notifications]
[status-im2.contexts.onboarding.enter-seed-phrase.view :as enter-seed-phrase]
[status-im2.contexts.onboarding.generating-keys.view :as generating-keys]
[status-im2.contexts.onboarding.identifiers.view :as identifiers]
[status-im2.contexts.onboarding.welcome.view :as welcome]
[status-im2.contexts.onboarding.intro.view :as intro]
[status-im2.contexts.onboarding.new-to-status.view :as new-to-status]
[status-im2.contexts.onboarding.sign-in.view :as sign-in]
[status-im2.contexts.onboarding.generating-keys.view :as generating-keys]
[status-im2.contexts.onboarding.enter-seed-phrase.view :as enter-seed-phrase]
[status-im2.contexts.onboarding.syncing.results.view :as syncing-results]
[status-im2.contexts.onboarding.syncing.progress.view :as syncing-devices]
[status-im2.navigation.transitions :as transitions]
[status-im2.contexts.onboarding.syncing.results.view :as syncing-results]
[status-im2.contexts.onboarding.welcome.view :as welcome]
[status-im2.contexts.profile.profiles.view :as profiles]
[status-im2.contexts.quo-preview.main :as quo.preview]
[status-im2.contexts.shell.activity-center.view :as activity-center]
[status-im2.contexts.shell.jump-to.view :as shell]
[status-im2.contexts.shell.share.view :as share]
[status-im2.contexts.syncing.how-to-pair.view :as how-to-pair]
[status-im2.contexts.syncing.find-sync-code.view :as find-sync-code]
[status-im2.contexts.syncing.how-to-pair.view :as how-to-pair]
[status-im2.contexts.syncing.scan-sync-code-page.view :as scan-sync-code-page]
[status-im2.contexts.syncing.setup-syncing.view :as settings-setup-syncing]
[status-im2.contexts.syncing.syncing-devices-list.view :as settings-syncing]
@ -41,8 +42,8 @@
[status-im2.contexts.wallet.saved-address.view :as wallet-saved-address]
[status-im2.contexts.wallet.saved-addresses.view :as wallet-saved-addresses]
[status-im2.contexts.wallet.send.view :as wallet-send]
[status-im.ui.screens.screens :as old-screens]
[status-im2.navigation.options :as options]))
[status-im2.navigation.options :as options]
[status-im2.navigation.transitions :as transitions]))
(defn screens
[]
@ -233,6 +234,10 @@
:animations transitions/push-animations-for-transparent-background}
:component welcome/view}
{:name :emoji-picker
:options {:sheet? true}
:component emoji-picker/view}
{:name :wallet-accounts
:component wallet-accounts/view}

View File

@ -1,5 +1,6 @@
(ns utils.debounce
(:require [re-frame.core :as re-frame]))
(:require [goog.functions]
[re-frame.core :as re-frame]))
(def timeout (atom {}))
@ -30,3 +31,7 @@
(swap! chill assoc event-key true)
(js/setTimeout #(swap! chill assoc event-key false) duration-ms)
(re-frame/dispatch event))))
(defn debounce
[f duration-ms]
(goog.functions/debounce f duration-ms))

View File

@ -2306,5 +2306,17 @@
"mint": "Mint",
"via": "via",
"x-counter": "x{{counter}}",
"name-ens-or-address": "Name, ENS, or address"
"name-ens-or-address": "Name, ENS, or address",
"emoji-search-placeholder": "Search emojis",
"emoji-recent": "Recent",
"emoji-people": "People",
"emoji-nature": "Nature",
"emoji-food": "Food",
"emoji-activity": "Activity",
"emoji-travel": "Travel",
"emoji-objects": "Objects",
"emoji-symbols": "Symbols",
"emoji-flags": "Flags",
"emoji-no-results-title": "No emojis match your search",
"emoji-no-results-description": "Try something like “rainbow”"
}