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-13 23:08:13 +05:30
committed by GitHub
parent dab4d953ec
commit 0003800f05
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

+2
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 {}
@@ -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)
@@ -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]
@@ -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
+6 -6
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]
@@ -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))
@@ -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))
@@ -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}]})
@@ -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})
@@ -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))))
@@ -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))))))
@@ -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))
@@ -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"]])))
@@ -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
+1
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
+15 -10
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}
+6 -1
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))
+13 -1
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”"
}